Skip to content

Conversation

@atomassi
Copy link

@atomassi atomassi commented Dec 13, 2025

Closes #32533

Related command
az role assignment list
az role assignment delete

Description
Fix az role assignment list --role <guid> and az role assignment delete --role <guid> returning empty results when querying at non-subscription scopes:

  • Tenant (root) scope: /
  • Management group scope: /providers/Microsoft.Management/managementGroups/{mg-name}
  • Service group scope: /providers/Microsoft.Management/serviceGroups/{service-group-name}

Root Cause:
Role definition IDs can have two formats:

  • Tenant scope: /providers/Microsoft.Authorization/roleDefinitions/{guid}
  • Subscription scope: /subscriptions/{sub-id}/providers/Microsoft.Authorization/roleDefinitions/{guid}

When listing role assignments at tenant/management group/service group scope, the returned roleDefinitionId uses the tenant format, while the previous code resolved --role <guid> to the subscription format. The strict equality comparison then failed to match assignments.

if is_guid(role):
role_id = '/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/{}'.format(
definitions_client._config.subscription_id, role)

Then, _search_role_assignments performed a strict equality comparison:

if role:
role_id = _resolve_role_id(role, scope, definitions_client)
assignments = [ra for ra in assignments if ra.role_definition_id == role_id]

Example of the mismatch:

Source Format
Assignment's roleDefinitionId (from API at root/managed group/service group scope) /providers/Microsoft.Authorization/roleDefinitions/acdd72a7-...
Resolved role_id (from _resolve_role_id) /subscriptions/xxx/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-...

Changes:

  1. _resolve_role_id now returns tenant-format role definition IDs (/providers/Microsoft.Authorization/roleDefinitions/{guid}) when given a GUID, ensuring consistency across scopes. Also improved regex validation to reject malformed paths.
  2. Added _get_role_definition_id helper function that extracts the role definition GUID from a full resource ID as a uuid.UUID object for case-insensitive comparison (handling both tenant-format and subscription-format IDs). Returns None for invalid inputs.
  3. Role filtering in _search_role_assignments now compares extracted GUIDs (as UUID objects) instead of full resource IDs, so that assignments match regardless of whether they use tenant-format or subscription-format role definition IDs. Includes early return if the role ID couldn't be parsed to a valid GUID.

Testing Guide

# List role assignments at root (tenant) scope with a role GUID filter
az role assignment list --scope / --role acdd72a7-3385-48ef-bd42-f606fba81ae7

# List role assignments at management group scope
az role assignment list --scope /providers/Microsoft.Management/managementGroups/my-mg --role acdd72a7-3385-48ef-bd42-f606fba81ae7

# List role assignments at subscription scope (should still work as before)
az role assignment list --role Reader

# Delete role assignment at root scope with role filter
az role assignment delete --scope / --role acdd72a7-3385-48ef-bd42-f606fba81ae7 --assignee <object-id>

History Notes
[Role] az role assignment list/delete: Fix --role filter returning empty results at tenant, management group, and service group scopes


This checklist is used to make sure that common guidelines for a pull request are followed.

Copilot AI review requested due to automatic review settings December 13, 2025 17:20
@azure-client-tools-bot-prd
Copy link

azure-client-tools-bot-prd bot commented Dec 13, 2025

️✔️AzureCLI-FullTest
️✔️acr
️✔️latest
️✔️3.12
️✔️3.13
️✔️acs
️✔️latest
️✔️3.12
️✔️3.13
️✔️advisor
️✔️latest
️✔️3.12
️✔️3.13
️✔️ams
️✔️latest
️✔️3.12
️✔️3.13
️✔️apim
️✔️latest
️✔️3.12
️✔️3.13
️✔️appconfig
️✔️latest
️✔️3.12
️✔️3.13
️✔️appservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️aro
️✔️latest
️✔️3.12
️✔️3.13
️✔️backup
️✔️latest
️✔️3.12
️✔️3.13
️✔️batch
️✔️latest
️✔️3.12
️✔️3.13
️✔️batchai
️✔️latest
️✔️3.12
️✔️3.13
️✔️billing
️✔️latest
️✔️3.12
️✔️3.13
️✔️botservice
️✔️latest
️✔️3.12
️✔️3.13
️✔️cdn
️✔️latest
️✔️3.12
️✔️3.13
️✔️cloud
️✔️latest
️✔️3.12
️✔️3.13
️✔️cognitiveservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️compute_recommender
️✔️latest
️✔️3.12
️✔️3.13
️✔️computefleet
️✔️latest
️✔️3.12
️✔️3.13
️✔️config
️✔️latest
️✔️3.12
️✔️3.13
️✔️configure
️✔️latest
️✔️3.12
️✔️3.13
️✔️consumption
️✔️latest
️✔️3.12
️✔️3.13
️✔️container
️✔️latest
️✔️3.12
️✔️3.13
️✔️containerapp
️✔️latest
️✔️3.12
️✔️3.13
️✔️core
️✔️latest
️✔️3.12
️✔️3.13
️✔️cosmosdb
️✔️latest
️✔️3.12
️✔️3.13
️✔️databoxedge
️✔️latest
️✔️3.12
️✔️3.13
️✔️dls
️✔️latest
️✔️3.12
️✔️3.13
️✔️dms
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventgrid
️✔️latest
️✔️3.12
️✔️3.13
️✔️eventhubs
️✔️latest
️✔️3.12
️✔️3.13
️✔️feedback
️✔️latest
️✔️3.12
️✔️3.13
️✔️find
️✔️latest
️✔️3.12
️✔️3.13
️✔️hdinsight
️✔️latest
️✔️3.12
️✔️3.13
️✔️identity
️✔️latest
️✔️3.12
️✔️3.13
️✔️iot
️✔️latest
️✔️3.12
️✔️3.13
️✔️keyvault
️✔️latest
️✔️3.12
️✔️3.13
️✔️lab
️✔️latest
️✔️3.12
️✔️3.13
️✔️managedservices
️✔️latest
️✔️3.12
️✔️3.13
️✔️maps
️✔️latest
️✔️3.12
️✔️3.13
️✔️marketplaceordering
️✔️latest
️✔️3.12
️✔️3.13
️✔️monitor
️✔️latest
️✔️3.12
️✔️3.13
️✔️mysql
️✔️latest
️✔️3.12
️✔️3.13
️✔️netappfiles
️✔️latest
️✔️3.12
️✔️3.13
️✔️network
️✔️latest
️✔️3.12
️✔️3.13
️✔️policyinsights
️✔️latest
️✔️3.12
️✔️3.13
️✔️privatedns
️✔️latest
️✔️3.12
️✔️3.13
️✔️profile
️✔️latest
️✔️3.12
️✔️3.13
️✔️rdbms
️✔️latest
️✔️3.12
️✔️3.13
️✔️redis
️✔️latest
️✔️3.12
️✔️3.13
️✔️relay
️✔️latest
️✔️3.12
️✔️3.13
️✔️resource
️✔️latest
️✔️3.12
️✔️3.13
️✔️role
️✔️latest
️✔️3.12
️✔️3.13
️✔️search
️✔️latest
️✔️3.12
️✔️3.13
️✔️security
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicebus
️✔️latest
️✔️3.12
️✔️3.13
️✔️serviceconnector
️✔️latest
️✔️3.12
️✔️3.13
️✔️servicefabric
️✔️latest
️✔️3.12
️✔️3.13
️✔️signalr
️✔️latest
️✔️3.12
️✔️3.13
️✔️sql
️✔️latest
️✔️3.12
️✔️3.13
️✔️sqlvm
️✔️latest
️✔️3.12
️✔️3.13
️✔️storage
️✔️latest
️✔️3.12
️✔️3.13
️✔️synapse
️✔️latest
️✔️3.12
️✔️3.13
️✔️telemetry
️✔️latest
️✔️3.12
️✔️3.13
️✔️util
️✔️latest
️✔️3.12
️✔️3.13
️✔️vm
️✔️latest
️✔️3.12
️✔️3.13

@azure-client-tools-bot-prd
Copy link

azure-client-tools-bot-prd bot commented Dec 13, 2025

️✔️AzureCLI-BreakingChangeTest
️✔️Non Breaking Changes

@yonzhan
Copy link
Collaborator

yonzhan commented Dec 13, 2025

Thank you for your contribution! We will review the pull request and get back to you soon.

@github-actions
Copy link

The git hooks are available for azure-cli and azure-cli-extensions repos. They could help you run required checks before creating the PR.

Please sync the latest code with latest dev branch (for azure-cli) or main branch (for azure-cli-extensions).
After that please run the following commands to enable git hooks:

pip install azdev --upgrade
azdev setup -c <your azure-cli repo path> -r <your azure-cli-extensions repo path>

@microsoft-github-policy-service microsoft-github-policy-service bot added customer-reported Issues that are reported by GitHub users external to the Azure organization. Auto-Assign Auto assign by bot labels Dec 13, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a bug where az role assignment list --role <guid> and az role assignment delete --role <guid> returned empty results when querying at non-subscription scopes (tenant root, management groups, service groups). The root cause was a mismatch between tenant-format role definition IDs returned by the API and subscription-format IDs generated by the resolver.

Key changes:

  • Modified _resolve_role_id to return tenant-format role definition IDs for GUIDs
  • Changed role filtering logic from strict equality to endswith() comparison
  • Migrated tests from unittest to pytest and added comprehensive test coverage for multiple scope scenarios

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/azure-cli/azure/cli/command_modules/role/custom.py Updated _resolve_role_id to return tenant-format IDs for GUIDs; changed role filtering to use endswith() comparison; added function docstring
src/azure-cli/azure/cli/command_modules/role/tests/latest/test_role_custom.py Converted from unittest to pytest; added comprehensive test coverage for role ID resolution and role assignment filtering across different scopes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@atomassi atomassi force-pushed the atomassilli/32533 branch 2 times, most recently from 36c5ba3 to fb88971 Compare December 13, 2025 18:03
@ReaNAiveD
Copy link
Member

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 3 pipeline(s).

)

assert len(result) == 1
assert result[0].role_definition_id is not None
Copy link
Member

Choose a reason for hiding this comment

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

A new line is missing.

"""
if resource_id:
try:
return uuid.UUID(resource_id.rsplit('/', 1)[-1])
Copy link
Member

@jiasli jiasli Dec 19, 2025

Choose a reason for hiding this comment

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

We usually use azure.mgmt.core.tools.parse_resource_id to parse resource ID, but it seems it cannot parse a tenant-level resource:

from azure.mgmt.core.tools import parse_resource_id
print(parse_resource_id('/providers/Microsoft.Authorization/roleDefinitions/e89f72ce-a487-4671-bffa-5eaf57e59f58'))
print(parse_resource_id('/subscriptions/7e30e593-3cc2-47b7-8339-7d5eb15f8142/providers/Microsoft.Authorization/roleDefinitions/e89f72ce-a487-4671-bffa-5eaf57e59f58'))

Output:

{'name': '/providers/Microsoft.Authorization/roleDefinitions/e89f72ce-a487-4671-bffa-5eaf57e59f58'}
{'subscription': '7e30e593-3cc2-47b7-8339-7d5eb15f8142', 'namespace': 'Microsoft.Authorization', 'type': 'roleDefinitions', 'name': 'e89f72ce-a487-4671-bffa-5eaf57e59f58', 'children': '', 'resource_parent': '', 'resource_namespace': 'Microsoft.Authorization', 'resource_type': 'roleDefinitions', 'resource_name': 'e89f72ce-a487-4671-bffa-5eaf57e59f58'}

"""
# Check if it's a role definition resource ID: /providers/Microsoft.Authorization/roleDefinitions/<guid>
# optionally prefixed with /subscriptions/... The last segment must be a valid GUID.
if (re.match(r'(/subscriptions/[^/]+)?/providers/Microsoft.Authorization/roleDefinitions/[^/]+$', role, re.I) and
Copy link
Member

Choose a reason for hiding this comment

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

is_valid_resource_id cannot parse tenant-level resource either:

from azure.mgmt.core.tools import is_valid_resource_id
print(is_valid_resource_id('/providers/Microsoft.Authorization/roleDefinitions/e89f72ce-a487-4671-bffa-5eaf57e59f58'))
print(is_valid_resource_id('/subscriptions/7e30e593-3cc2-47b7-8339-7d5eb15f8142/providers/Microsoft.Authorization/roleDefinitions/e89f72ce-a487-4671-bffa-5eaf57e59f58'))

Output:

False
True

Copy link
Author

Choose a reason for hiding this comment

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

looks like only used for subscription scoped resources in this module

from azure.mgmt.core.tools import is_valid_resource_id
if scope.startswith('/subscriptions/') and not is_valid_resource_id(scope):
raise CLIError('Invalid scope. Please use --help to view the valid format.')

also it does not work for management groups and service groups, e.g.,

> python -c "from azure.mgmt.core.tools import is_valid_resource_id; rid='/providers/Microsoft.Management/managementGroups/mgmt1/providers/Microsoft.Authorization/roleAssignments/c4cb02d0-af03-4afc-a6ea-1a5af4e84b9b'; print(is_valid_resource_id(rid))"
False

@jiasli
Copy link
Member

jiasli commented Dec 19, 2025

I can confirm that the same role definition such as Reader/acdd72a7-3385-48ef-bd42-f606fba81ae7 have different ids when scope is different:

> az role definition list --scope / --name Reader
[
  {
    "assignableScopes": [
      "/"
    ],
    "createdBy": null,
    "createdOn": "2015-02-02T21:55:09.880642+00:00",
    "description": "View all resources, but does not allow you to make any changes.",
    "id": "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "name": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "permissions": [
      {
        "actions": [
          "*/read"
        ],
        "condition": null,
        "conditionVersion": null,
        "dataActions": [],
        "notActions": [],
        "notDataActions": []
      }
    ],
    "roleName": "Reader",
    "roleType": "BuiltInRole",
    "type": "Microsoft.Authorization/roleDefinitions",
    "updatedBy": null,
    "updatedOn": "2021-11-11T20:13:47.862868+00:00"
  }
]
> az role definition list --name Reader
[
  {
    "assignableScopes": [
      "/"
    ],
    "createdBy": null,
    "createdOn": "2015-02-02T21:55:09.880642+00:00",
    "description": "View all resources, but does not allow you to make any changes.",
    "id": "/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "name": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "permissions": [
      {
        "actions": [
          "*/read"
        ],
        "condition": null,
        "conditionVersion": null,
        "dataActions": [],
        "notActions": [],
        "notDataActions": []
      }
    ],
    "roleName": "Reader",
    "roleType": "BuiltInRole",
    "type": "Microsoft.Authorization/roleDefinitions",
    "updatedBy": null,
    "updatedOn": "2021-11-11T20:13:47.862868+00:00"
  }
]

"""
if resource_id:
try:
return uuid.UUID(resource_id.rsplit('/', 1)[-1])
Copy link
Member

Choose a reason for hiding this comment

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

There is no need to build a uuid.UUID object. String comparison is sufficient.

Copy link
Author

Choose a reason for hiding this comment

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

this is to

  1. check if valid
  2. deal with different guid formats (e.g, c4cb02d0-af03-4afc-a6ea-1a5af4e84b9b vs c4cb02d0af034afca6ea-1a5af4e84b9b) and case differences

mock_client = mock.Mock()
mock_client._config.subscription_id = '123'
test_role_id = 'b24988ac-6180-42a0-ab88-20f738123456'
@pytest.mark.parametrize("resource_id,expected", [
Copy link
Member

Choose a reason for hiding this comment

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

We currently don't use pytest features in order to keep max compatibility with unittest.

return role

if is_guid(role):
return f"/providers/Microsoft.Authorization/roleDefinitions/{role}"
Copy link
Member

@jiasli jiasli Dec 19, 2025

Choose a reason for hiding this comment

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

I assume this is only a dummy resource ID because the /providers/Microsoft.Authorization/roleDefinitions/ prefix will be stripped by _get_role_definition_id later. Perhaps we can make _resolve_role_id return the role name (GUID)?

Copy link
Author

Choose a reason for hiding this comment

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

this is because this method is also used by the create role assignment path and it needs a full resource ID (not just a guid).
Agree, this can just return the guid and the create can build the resource path internally

def _create_role_assignment(cli_ctx, role, assignee, resource_group_name=None, scope=None,
resolve_assignee=True, assignee_principal_type=None, description=None,
condition=None, condition_version=None, assignment_name=None):
"""Prepare scope, role ID and resolve object ID from Graph API."""
assignment_name = assignment_name or _gen_guid()
factory = _auth_client_factory(cli_ctx, scope)
assignments_client = factory.role_assignments
definitions_client = factory.role_definitions
scope = _build_role_scope(resource_group_name, scope,
assignments_client._config.subscription_id)
role_id = _resolve_role_id(role, scope, definitions_client)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Auto-Assign Auto assign by bot customer-reported Issues that are reported by GitHub users external to the Azure organization. RBAC az role

Projects

None yet

Development

Successfully merging this pull request may close these issues.

az role assignment list - does not return existing role assignment

4 participants