Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/azure-cli-core/azure/cli/core/commands/arm.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ def resolve_role_id(cli_ctx, role, scope):
except ValueError:
pass
if not role_id: # retrieve role id
role_defs = list(client.list(scope, "roleName eq '{}'".format(role)))
role_defs = list(client.list(scope, filter="roleName eq '{}'".format(role)))
if not role_defs:
raise CLIError("Role '{}' doesn't exist.".format(role))
if len(role_defs) > 1:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def resolve_role_id(role, scope, definitions_client):
pass
if not role_id: # retrieve role id
role_defs = list(definitions_client.list(
scope, "roleName eq '{}'".format(role)))
scope, filter="roleName eq '{}'".format(role)))
if len(role_defs) == 0:
raise AzCLIError("Role '{}' doesn't exist.".format(role))
if len(role_defs) > 1:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def _resolve_role_id(cli_ctx, role, scope, definitions_client):
role_id = '/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/{}'.format(
subscription_id, role)
if not role_id: # retrieve role id
role_defs = list(definitions_client.list(scope, "roleName eq '{}'".format(role)))
role_defs = list(definitions_client.list(scope, filter="roleName eq '{}'".format(role)))

if not role_defs:
raise CLIError("Role '{}' doesn't exist.".format(role))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ def _create_role_assignment(cli_ctx, role, assignee, scope=None):
definitions_client = auth_client.role_definitions

assignment_name = uuid.uuid4()
role_defs = list(definitions_client.list(scope, "roleName eq '{}'".format(role)))
role_defs = list(definitions_client.list(scope, filter="roleName eq '{}'".format(role)))
role_id = role_defs[0].id

api_version = supported_api_version(cli_ctx, resource_type=ResourceType.MGMT_AUTHORIZATION, max_api='2015-07-01')
Expand Down
90 changes: 90 additions & 0 deletions src/azure-cli/azure/cli/command_modules/role/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,96 @@
short-summary: List changelogs for role assignments.
"""

helps['role deny-assignment'] = """
type: group
short-summary: Manage deny assignments.
long-summary: >-
Deny assignments block users from performing specific Azure resource actions even if a role assignment
grants them access. User-assigned deny assignments can be created to deny write, delete, and action
operations at a given scope while excluding specific principals.
"""

helps['role deny-assignment list'] = """
type: command
short-summary: List deny assignments.
examples:
- name: List deny assignments at the subscription scope.
text: az role deny-assignment list --scope /subscriptions/00000000-0000-0000-0000-000000000000
- name: List all deny assignments in the current subscription.
text: az role deny-assignment list
- name: List deny assignments at a resource group scope.
text: az role deny-assignment list --scope /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myGroup
"""

helps['role deny-assignment show'] = """
type: command
short-summary: Get a deny assignment.
examples:
- name: Show a deny assignment by its fully qualified ID.
text: >-
az role deny-assignment show
--id /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/denyAssignments/00000000-0000-0000-0000-000000000001
- name: Show a deny assignment by name and scope.
text: >-
az role deny-assignment show
--name 00000000-0000-0000-0000-000000000001
--scope /subscriptions/00000000-0000-0000-0000-000000000000
"""

helps['role deny-assignment create'] = """
type: command
short-summary: Create a user-assigned deny assignment.
long-summary: >-
Creates a deny assignment that blocks specific actions at the given scope. Two modes are supported:
(1) Everyone mode (default) — denies actions for all principals, requiring at least one excluded principal;
(2) Per-principal mode — denies actions for a specific User or ServicePrincipal specified via --principal-id.
DataActions are not supported, DoNotApplyToChildScopes is not supported, read actions (*/read) are not
permitted, and Group type principals are not allowed.
examples:
- name: Create a deny assignment blocking role assignment writes for everyone, excluding a service principal.
text: >-
az role deny-assignment create
--name "Block role assignment changes"
--scope /subscriptions/00000000-0000-0000-0000-000000000000
--actions "Microsoft.Authorization/roleAssignments/write" "Microsoft.Authorization/roleAssignments/delete"
--exclude-principal-ids 00000000-0000-0000-0000-000000000001
--exclude-principal-types ServicePrincipal
- name: Create a deny assignment targeting a specific user.
text: >-
az role deny-assignment create
--name "Deny resource deletion for user"
--scope /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myGroup
--actions "*/delete"
--principal-id 00000000-0000-0000-0000-000000000001
--principal-type User
- name: Create a deny assignment targeting a specific service principal with exclusions.
text: >-
az role deny-assignment create
--name "Deny write actions for app"
--scope /subscriptions/00000000-0000-0000-0000-000000000000
--actions "*/write"
--principal-id 00000000-0000-0000-0000-000000000001
--principal-type ServicePrincipal
--exclude-principal-ids 00000000-0000-0000-0000-000000000002
--exclude-principal-types ServicePrincipal
--description "Block write operations for this application"
"""

helps['role deny-assignment delete'] = """
type: command
short-summary: Delete a user-assigned deny assignment.
examples:
- name: Delete a deny assignment by its fully qualified ID.
text: >-
az role deny-assignment delete
--id /subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/denyAssignments/00000000-0000-0000-0000-000000000001
- name: Delete a deny assignment by name and scope.
text: >-
az role deny-assignment delete
--name 00000000-0000-0000-0000-000000000001
--scope /subscriptions/00000000-0000-0000-0000-000000000000
"""

helps['role definition'] = """
type: group
short-summary: Manage role definitions.
Expand Down
54 changes: 54 additions & 0 deletions src/azure-cli/azure/cli/command_modules/role/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,60 @@ class PrincipalType(str, Enum):
with self.argument_context('role assignment delete') as c:
c.argument('yes', options_list=['--yes', '-y'], action='store_true', help='Currently no-op.')

with self.argument_context('role deny-assignment') as c:
c.argument('scope', help='Scope at which the deny assignment applies. '
'For example, /subscriptions/00000000-0000-0000-0000-000000000000 or '
'/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myGroup')
c.argument('deny_assignment_name', options_list=['--name', '-n'],
help='The display name of the deny assignment.')
Comment on lines +397 to +398
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deny_assignment_name is defined at the role deny-assignment group level, which makes --name/-n show up for subcommands like list even though list_deny_assignments doesn't accept that parameter. If a user supplies --name on list, the handler will receive an unexpected kwarg and fail. Recommend removing deny_assignment_name from the group context and defining --name only on show/create/delete where it is supported.

Suggested change
c.argument('deny_assignment_name', options_list=['--name', '-n'],
help='The display name of the deny assignment.')

Copilot uses AI. Check for mistakes.

with self.argument_context('role deny-assignment list') as c:
c.argument('filter_str', options_list=['--filter'],
help='OData filter expression to apply. For example, '
'"atScope()" to list at the current scope, or '
'"gdprExportPrincipalId eq \'{objectId}\'" to list for a specific principal.')

with self.argument_context('role deny-assignment show') as c:
c.argument('deny_assignment_id', options_list=['--id'],
help='The fully qualified ID of the deny assignment including scope, '
'e.g. /subscriptions/{id}/providers/Microsoft.Authorization/denyAssignments/{denyAssignmentId}')
c.argument('deny_assignment_name', options_list=['--name', '-n'],
help='The name (GUID) of the deny assignment.')

with self.argument_context('role deny-assignment create') as c:
c.argument('deny_assignment_name', options_list=['--name', '-n'],
help='The display name of the deny assignment.')
c.argument('description', help='Description of the deny assignment.')
c.argument('actions', nargs='+',
help='Space-separated list of actions to deny, e.g. '
'"Microsoft.Authorization/roleAssignments/write". '
'Note: read actions (*/read) are not permitted for user-assigned deny assignments.')
c.argument('not_actions', nargs='+',
help='Space-separated list of actions to exclude from the deny.')
c.argument('principal_id', options_list=['--principal-id'],
help='The object ID of a specific User or ServicePrincipal to deny. '
'If omitted, the deny assignment applies to Everyone (all principals) and '
'--exclude-principal-ids is required. Group principals are not permitted.')
c.argument('principal_type', options_list=['--principal-type'],
arg_type=get_enum_type(['User', 'ServicePrincipal']),
help='The type of the principal specified by --principal-id. '
'Required when --principal-id is provided. Accepted values: User, ServicePrincipal.')
c.argument('exclude_principal_ids', nargs='+', options_list=['--exclude-principal-ids'],
help='Space-separated list of principal object IDs to exclude from the deny. '
'Required when no --principal-id is specified (Everyone mode). '
'Optional when --principal-id is specified.')
c.argument('exclude_principal_types', nargs='+', options_list=['--exclude-principal-types'],
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--exclude-principal-types is documented as having accepted values, but the argument doesn't enforce them. To keep validation consistent with role assignment create --assignee-principal-type, use arg_type=get_enum_type([...]) (or an Enum) so invalid values are caught client-side with a clear error.

Suggested change
c.argument('exclude_principal_types', nargs='+', options_list=['--exclude-principal-types'],
c.argument('exclude_principal_types', nargs='+', options_list=['--exclude-principal-types'],
arg_type=get_enum_type(['User', 'Group', 'ServicePrincipal']),

Copilot uses AI. Check for mistakes.
help='Space-separated list of principal types corresponding to --exclude-principal-ids. '
'Accepted values: User, Group, ServicePrincipal.')
c.argument('assignment_name', options_list=['--assignment-name'],
help='A GUID for the deny assignment. If omitted, a new GUID is generated.')

with self.argument_context('role deny-assignment delete') as c:
c.argument('deny_assignment_id', options_list=['--id'],
help='The fully qualified ID of the deny assignment to delete.')
c.argument('deny_assignment_name', options_list=['--name', '-n'],
help='The name (GUID) of the deny assignment to delete.')

with self.argument_context('role definition') as c:
c.argument('custom_role_only', arg_type=get_three_state_flag(), help='custom roles only(vs. build-in ones)')
c.argument('role_definition', help="json formatted content which defines the new role.")
Expand Down
12 changes: 12 additions & 0 deletions src/azure-cli/azure/cli/command_modules/role/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ def transform_assignment_list(result):
('Scope', r['scope'])]) for r in result]


def transform_deny_assignment_list(result):
return [OrderedDict([('Name', r.get('denyAssignmentName', '')),
('Id', r.get('name', '')),
('Scope', r.get('scope', ''))]) for r in result]


def get_graph_object_transformer(object_type):
selected_keys_for_type = {
'app': ('displayName', 'id', 'appId', 'createdDateTime'),
Expand Down Expand Up @@ -78,6 +84,12 @@ def load_command_table(self, _):
g.custom_command('update', 'update_role_assignment')
g.custom_command('list-changelogs', 'list_role_assignment_change_logs')

with self.command_group('role deny-assignment') as g:
g.custom_command('list', 'list_deny_assignments', table_transformer=transform_deny_assignment_list)
g.custom_show_command('show', 'show_deny_assignment')
g.custom_command('create', 'create_deny_assignment')
g.custom_command('delete', 'delete_deny_assignment', confirmation=True)

with self.command_group('ad app', client_factory=get_graph_client, exception_handler=graph_err_handler) as g:
g.custom_command('create', 'create_application')
g.custom_command('delete', 'delete_application')
Expand Down
Loading
Loading