Skip to content
Merged
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
14 changes: 10 additions & 4 deletions src/azure-cli/azure/cli/command_modules/role/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,8 @@
text: az role assignment delete --role Reader --scope /subscriptions/00000000-0000-0000-0000-000000000000
- name: Delete all role assignments of an assignee at the subscription scope.
text: az role assignment delete --assignee 00000000-0000-0000-0000-000000000000 --scope /subscriptions/00000000-0000-0000-0000-000000000000
- name: Delete all role assignments of an assignee (with its object ID) at the subscription scope.
text: az role assignment delete --assignee-object-id 00000000-0000-0000-0000-000000000000 --scope /subscriptions/00000000-0000-0000-0000-000000000000
"""

helps['role assignment list'] = """
Expand All @@ -796,12 +798,16 @@
Delete classic administrators who no longer need access or assign an Azure RBAC role for fine-grained access
control. Learn more: https://go.microsoft.com/fwlink/?linkid=2238474
examples:
- name: List all role assignments with "Reader" role at the subscription scope.
- name: List role assignments at the subscription scope.
text: az role assignment list --scope /subscriptions/00000000-0000-0000-0000-000000000000
- name: List role assignments at the subscription scope, without filling roleDefinitionName property.
text: az role assignment list --scope /subscriptions/00000000-0000-0000-0000-000000000000 --fill-role-definition-name false
- name: List role assignments with "Reader" role at the subscription scope.
text: az role assignment list --role Reader --scope /subscriptions/00000000-0000-0000-0000-000000000000
- name: List all role assignments of an assignee at the subscription scope.
- name: List role assignments of an assignee at the subscription scope.
text: az role assignment list --assignee 00000000-0000-0000-0000-000000000000 --scope /subscriptions/00000000-0000-0000-0000-000000000000
- name: List all role assignments with "Reader" role at the subscription scope, without filling principalName property
text: az role assignment list --role Reader --scope /subscriptions/00000000-0000-0000-0000-000000000000 --fill-principal-name false
- name: List role assignments of an assignee (with its object ID) at the subscription scope, without filling principalName property. This command does not query Microsoft Graph.
text: az role assignment list --assignee-object-id 00000000-0000-0000-0000-000000000000 --scope /subscriptions/00000000-0000-0000-0000-000000000000 --fill-principal-name false
"""

helps['role assignment list-changelogs'] = """
Expand Down
11 changes: 7 additions & 4 deletions src/azure-cli/azure/cli/command_modules/role/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,11 @@ def load_arguments(self, _):
c.argument('include_inherited', action='store_true', help='include assignments applied on parent scopes')
c.argument('can_delegate', action='store_true', help='when set, the assignee will be able to create further role assignments to the same role')
c.argument('assignee', help='represent a user, group, or service principal. supported format: object id, user sign-in name, or service principal name')
c.argument('assignee_object_id', help="Use this parameter instead of '--assignee' to bypass Graph API invocation in case of insufficient privileges. "
"This parameter only works with object ids for users, groups, service principals, and "
"managed identities. For managed identities use the principal id. For service principals, "
"use the object id and not the app id.")
c.argument('assignee_object_id',
help="The assignee's object ID (also known as principal ID). "
"Use this argument instead of '--assignee' to bypass Microsoft Graph query in case "
"the logged-in account has no permission or the machine has no network access to query "
"Microsoft Graph.")
c.argument('ids', nargs='+', help='space-separated role assignment ids')
c.argument('include_classic_administrators', arg_type=get_three_state_flag(),
help='list default role assignments for subscription classic administrators, aka co-admins')
Expand All @@ -350,6 +351,8 @@ def load_arguments(self, _):
c.argument('fill_role_definition_name', arg_type=get_three_state_flag(),
help="Fill roleDefinitionName property in addition to roleDefinitionId. This operation is "
"expensive. If you encounter performance issue, set this flag to false.")
c.argument('include_groups', action='store_true',
help='Include extra assignments to the groups of which the user is a member (transitively).')

time_help = 'The {} of the query in the format of %Y-%m-%dT%H:%M:%SZ, e.g. 2000-12-31T12:59:59Z. Defaults to {}'
with self.argument_context('role assignment list-changelogs') as c:
Expand Down
48 changes: 30 additions & 18 deletions src/azure-cli/azure/cli/command_modules/role/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def create_role_assignment(cmd, role, scope,
assignment_name=assignment_name)
except Exception as ex: # pylint: disable=broad-except
if _error_caused_by_role_assignment_exists(ex): # for idempotent
return list_role_assignments(cmd, assignee=assignee, role=role, scope=scope)[0]
return list_role_assignments(cmd, assignee_object_id=object_id, role=role, scope=scope)[0]
Copy link
Copy Markdown
Member Author

@jiasli jiasli Apr 8, 2025

Choose a reason for hiding this comment

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

Using object_id instead of assignee saves one Graph query for az role assignment create.

raise


Expand All @@ -232,14 +232,19 @@ def _create_role_assignment(cli_ctx, role, assignee, resource_group_name=None, s
condition=condition, condition_version=condition_version)


def list_role_assignments(cmd, assignee=None, role=None, resource_group_name=None, # pylint: disable=too-many-locals
scope=None, include_inherited=False,
def list_role_assignments(cmd, # pylint: disable=too-many-locals, too-many-branches
assignee=None, assignee_object_id=None,
role=None,
resource_group_name=None, scope=None,
include_inherited=False,
show_all=False, include_groups=False, include_classic_administrators=False,
fill_role_definition_name=True, fill_principal_name=True):
'''
:param include_groups: include extra assignments to the groups of which the user is a
member(transitively).
'''
Comment on lines -239 to -242
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

include_groups should be declared in _params.py, not here. #602 (comment)

if assignee and assignee_object_id:
raise CLIError('Usage error: Provide only one of --assignee or --assignee-object-id.')
if assignee_object_id and include_classic_administrators:
raise CLIError('Usage error: --assignee-object-id cannot be used with --include-classic-administrators. '
'Use --assignee instead.')
Comment on lines +244 to +246
Copy link
Copy Markdown
Member Author

@jiasli jiasli Mar 27, 2025

Choose a reason for hiding this comment

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

Classic admin assignments use email_address (UPN), instead of principal ID:

co_admins = [x for x in co_admins if assignee == x.email_address.lower()]

When --assignee-object-id is provided, it is necessary to convert object ID to UPN:

if is_guid(assignee):
try:
result = _get_object_stubs(graph_client, [assignee])
if not result:
return []
assignee = _get_displayable_name(result[0]).lower()
except ValueError:
pass

This will trigger Graph call which is against the initial purpose.

Moreover, as --include-classic-administrator will be removed soon (#29470). Updating the legacy code is worthless.


if include_classic_administrators:
logger.warning(CLASSIC_ADMINISTRATOR_WARNING)

Expand All @@ -256,8 +261,10 @@ def list_role_assignments(cmd, assignee=None, role=None, resource_group_name=Non
scope = _build_role_scope(resource_group_name, scope,
definitions_client._config.subscription_id)

if assignee and not assignee_object_id:
assignee_object_id = _resolve_object_id(cmd.cli_ctx, assignee, fallback_to_object_id=True)
assignments = _search_role_assignments(cmd.cli_ctx, assignments_client, definitions_client,
scope, assignee, role,
scope, assignee_object_id, role,
include_inherited, include_groups)

results = todict(assignments) if assignments else []
Expand Down Expand Up @@ -522,13 +529,19 @@ def _get_displayable_name(graph_object):
return graph_object['displayName'] or ''


def delete_role_assignments(cmd, ids=None, assignee=None, role=None, resource_group_name=None,
scope=None, include_inherited=False,
def delete_role_assignments(cmd, ids=None,
assignee=None, assignee_object_id=None,
role=None,
resource_group_name=None, scope=None,
include_inherited=False,
yes=None): # pylint: disable=unused-argument
# yes is currently a no-op
if not any((ids, assignee, role, resource_group_name, scope)):
if not any((ids, assignee, assignee_object_id, role, resource_group_name, scope)):
raise ArgumentUsageError('Please provide at least one of these arguments: '
'--ids, --assignee, --role, --resource-group, --scope')
'--ids, --assignee, --assignee-object-id, --role, --resource-group, --scope')

if assignee and assignee_object_id:
raise CLIError('Usage error: Provide only one of --assignee or --assignee-object-id.')

factory = _auth_client_factory(cmd.cli_ctx, scope)
assignments_client = factory.role_assignments
Expand Down Expand Up @@ -559,8 +572,11 @@ def delete_role_assignments(cmd, ids=None, assignee=None, role=None, resource_gr

scope = _build_role_scope(resource_group_name, scope,
assignments_client._config.subscription_id)
# Delay resolving object ID, because if ids are provided, no need to resolve
if assignee and not assignee_object_id:
assignee_object_id = _resolve_object_id(cmd.cli_ctx, assignee, fallback_to_object_id=True)
assignments = _search_role_assignments(cmd.cli_ctx, assignments_client, definitions_client,
scope, assignee, role, include_inherited,
scope, assignee_object_id, role, include_inherited,
include_groups=False)

if assignments:
Expand All @@ -571,11 +587,7 @@ def delete_role_assignments(cmd, ids=None, assignee=None, role=None, resource_gr


def _search_role_assignments(cli_ctx, assignments_client, definitions_client,
scope, assignee, role, include_inherited, include_groups):
assignee_object_id = None
if assignee:
assignee_object_id = _resolve_object_id(cli_ctx, assignee, fallback_to_object_id=True)

scope, assignee_object_id, role, include_inherited, include_groups):
# https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-list-rest
# "atScope()" and "principalId eq '{value}'" query cannot be used together (API limitation).
# always use "scope" if provided, so we can get assignments beyond subscription e.g. management groups
Expand Down
Loading