Skip to content

Commit 969b830

Browse files
author
Andrea Tomassilli
committed
clean
1 parent 203999e commit 969b830

File tree

2 files changed

+72
-108
lines changed

2 files changed

+72
-108
lines changed

src/azure-cli/azure/cli/command_modules/role/custom.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -538,9 +538,16 @@ def _search_role_assignments(assignments_client, definitions_client,
538538
ra.scope.lower() == scope.lower()
539539
)]
540540

541+
# Filter by role. Compare role definition IDs instead of full resource IDs because
542+
# the same role can have different resource ID formats depending on scope
541543
if role:
542-
role_id = _resolve_role_id(role, scope, definitions_client)
543-
assignments = [ra for ra in assignments if ra.role_definition_id and ra.role_definition_id.endswith(role_id)]
544+
resource_id = _resolve_role_id(role, scope, definitions_client)
545+
# If the role ID couldn't be parsed to a valid GUID, no assignments can match
546+
if not (role_id := _get_role_definition_id(resource_id)):
547+
return []
548+
549+
assignments = [ra for ra in assignments
550+
if _get_role_definition_id(ra.role_definition_id) == role_id]
544551

545552
# filter the assignee if "include_groups" is not provided because service side
546553
# does not accept filter "principalId eq and atScope()"
@@ -550,6 +557,19 @@ def _search_role_assignments(assignments_client, definitions_client,
550557
return assignments
551558

552559

560+
def _get_role_definition_id(resource_id):
561+
"""Extract the role definition GUID from a role definition resource ID.
562+
563+
Returns the GUID as a UUID object for case-insensitive comparison, or None if invalid.
564+
"""
565+
if resource_id:
566+
try:
567+
return uuid.UUID(resource_id.rsplit('/', 1)[-1])
568+
except ValueError:
569+
pass
570+
return None
571+
572+
553573
def _build_role_scope(resource_group_name, scope, subscription_id):
554574
subscription_scope = '/subscriptions/' + subscription_id
555575
if scope:
@@ -567,12 +587,17 @@ def _build_role_scope(resource_group_name, scope, subscription_id):
567587

568588

569589
def _resolve_role_id(role, scope, definitions_client):
570-
"""Resolve a role to its full role definition resource ID from
571-
- role definition resource ID (returned as-is)
572-
- role definition GUID
573-
- role name (e.g. 'Reader')
590+
"""Resolve a role to its full role definition resource ID.
591+
592+
Accepts:
593+
- role definition resource ID (returned as-is)
594+
- role definition GUID
595+
- role name (e.g. 'Reader')
574596
"""
575-
if re.match(r'(/subscriptions/.+)?/providers/Microsoft.Authorization/roleDefinitions/', role, re.I):
597+
# Check if it's a role definition resource ID: /providers/Microsoft.Authorization/roleDefinitions/<guid>
598+
# optionally prefixed with /subscriptions/... The last segment must be a valid GUID.
599+
if (re.match(r'(/subscriptions/[^/]+)?/providers/Microsoft.Authorization/roleDefinitions/[^/]+$', role, re.I)
600+
and is_guid(role.rsplit('/', 1)[-1])):
576601
return role
577602

578603
if is_guid(role):

src/azure-cli/azure/cli/command_modules/role/tests/latest/test_role_custom.py

Lines changed: 40 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,51 @@
22
# Copyright (c) Microsoft Corporation. All rights reserved.
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# --------------------------------------------------------------------------------------------
5+
import uuid
56
import pytest
67
from unittest import mock
78

8-
from azure.cli.command_modules.role.custom import _resolve_role_id, _search_role_assignments
9+
from azure.cli.command_modules.role.custom import (
10+
_resolve_role_id, _search_role_assignments, _get_role_definition_id
11+
)
912

1013
# pylint: disable=line-too-long
1114

1215

16+
class TestGetRoleDefinitionId:
17+
"""Tests for _get_role_definition_id helper function."""
18+
19+
@pytest.mark.parametrize("resource_id,expected", [
20+
# Tenant-scoped format
21+
('/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7',
22+
uuid.UUID('acdd72a7-3385-48ef-bd42-f606fba81ae7')),
23+
# Subscription-scoped format
24+
('/subscriptions/sub1/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c',
25+
uuid.UUID('b24988ac-6180-42a0-ab88-20f7382dd24c')),
26+
# None returns None
27+
(None, None),
28+
# Empty string returns None (not a valid GUID)
29+
('', None),
30+
# Invalid GUID in last segment returns None
31+
('/providers/Microsoft.Authorization/roleDefinitions/not-a-guid', None),
32+
# Malformed path with extra segments still extracts last segment if valid GUID
33+
('/some/path/acdd72a7-3385-48ef-bd42-f606fba81ae7',
34+
uuid.UUID('acdd72a7-3385-48ef-bd42-f606fba81ae7')),
35+
])
36+
def test_extracts_guid_from_role_definition_id(self, resource_id, expected):
37+
"""Extracts the GUID from various role definition ID formats."""
38+
result = _get_role_definition_id(resource_id)
39+
assert result == expected
40+
41+
def test_returns_uuid_for_case_insensitive_comparison(self):
42+
"""Returns UUID objects that compare equal regardless of case."""
43+
lower = _get_role_definition_id('/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7')
44+
upper = _get_role_definition_id('/providers/Microsoft.Authorization/roleDefinitions/ACDD72A7-3385-48EF-BD42-F606FBA81AE7')
45+
assert lower == upper
46+
assert isinstance(lower, uuid.UUID)
47+
assert isinstance(upper, uuid.UUID)
48+
49+
1350
class TestResolveRoleId:
1451
"""Tests for _resolve_role_id function."""
1552

@@ -80,7 +117,7 @@ def _create_assignment(scope, role_definition_id, principal_id='principal-1'):
80117
@pytest.mark.parametrize("scope,role_def_format", [
81118
# Root scope with tenant-format role definition ID
82119
('/', '/providers/Microsoft.Authorization/roleDefinitions/{guid}'),
83-
# Management group scope with tenant-format role definition ID
120+
# Management group scope with tenant-format role definition ID
84121
('/providers/Microsoft.Management/managementGroups/my-mg',
85122
'/providers/Microsoft.Authorization/roleDefinitions/{guid}'),
86123
# Subscription scope with subscription-format role definition ID
@@ -123,104 +160,6 @@ def test_different_role_guid_does_not_match(self, mock_clients):
123160

124161
assert len(result) == 0
125162

126-
def test_scope_comparison_is_case_insensitive(self, mock_clients):
127-
"""Scope matching is case insensitive."""
128-
assignments_client, definitions_client = mock_clients
129-
role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
130-
131-
assignments_client.list_for_scope.return_value = [
132-
self._create_assignment('/Subscriptions/SUB1/ResourceGroups/RG1', role_def_id),
133-
]
134-
135-
result = _search_role_assignments(
136-
assignments_client, definitions_client,
137-
scope='/subscriptions/sub1/resourcegroups/rg1',
138-
assignee_object_id=None, role=None,
139-
include_inherited=False, include_groups=False
140-
)
141-
142-
assert len(result) == 1
143-
144-
def test_include_inherited_returns_parent_scope_assignments(self, mock_clients):
145-
"""include_inherited=True returns assignments at and above the scope."""
146-
assignments_client, definitions_client = mock_clients
147-
role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
148-
149-
assignments_client.list_for_scope.return_value = [
150-
self._create_assignment('/', role_def_id, 'principal-1'),
151-
self._create_assignment('/subscriptions/sub1', role_def_id, 'principal-2'),
152-
]
153-
154-
result = _search_role_assignments(
155-
assignments_client, definitions_client,
156-
scope='/subscriptions/sub1',
157-
assignee_object_id=None, role=None,
158-
include_inherited=True, include_groups=False
159-
)
160-
161-
assert len(result) == 2
162-
163-
def test_include_inherited_false_filters_parent_scope(self, mock_clients):
164-
"""include_inherited=False filters out assignments at parent scopes."""
165-
assignments_client, definitions_client = mock_clients
166-
role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
167-
168-
assignments_client.list_for_scope.return_value = [
169-
self._create_assignment('/', role_def_id, 'principal-1'),
170-
self._create_assignment('/subscriptions/sub1', role_def_id, 'principal-2'),
171-
]
172-
173-
result = _search_role_assignments(
174-
assignments_client, definitions_client,
175-
scope='/subscriptions/sub1',
176-
assignee_object_id=None, role=None,
177-
include_inherited=False, include_groups=False
178-
)
179-
180-
assert len(result) == 1
181-
assert result[0].principal_id == 'principal-2'
182-
183-
def test_assignee_filter(self, mock_clients):
184-
"""assignee_object_id filters assignments by principal."""
185-
assignments_client, definitions_client = mock_clients
186-
role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
187-
188-
assignments_client.list_for_scope.return_value = [
189-
self._create_assignment('/subscriptions/sub1', role_def_id, 'principal-1'),
190-
self._create_assignment('/subscriptions/sub1', role_def_id, 'principal-2'),
191-
]
192-
193-
result = _search_role_assignments(
194-
assignments_client, definitions_client,
195-
scope='/subscriptions/sub1',
196-
assignee_object_id='principal-1', role=None,
197-
include_inherited=False, include_groups=False
198-
)
199-
200-
assert len(result) == 1
201-
assert result[0].principal_id == 'principal-1'
202-
203-
def test_no_scope_uses_subscription_api(self, mock_clients):
204-
"""When scope is None, list_for_subscription is called and all assignments returned."""
205-
assignments_client, definitions_client = mock_clients
206-
role_def_id = '/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7'
207-
208-
assignments_client.list_for_subscription.return_value = [
209-
self._create_assignment('/subscriptions/sub1', role_def_id, 'principal-1'),
210-
self._create_assignment('/subscriptions/sub1/resourceGroups/rg1', role_def_id, 'principal-2'),
211-
]
212-
213-
result = _search_role_assignments(
214-
assignments_client, definitions_client,
215-
scope=None,
216-
assignee_object_id=None, role=None,
217-
include_inherited=False, include_groups=False
218-
)
219-
220-
assert len(result) == 2
221-
assignments_client.list_for_subscription.assert_called_once()
222-
assignments_client.list_for_scope.assert_not_called()
223-
224163
def test_none_role_definition_id_is_skipped(self, mock_clients):
225164
"""Assignments with None role_definition_id are skipped when filtering by role."""
226165
assignments_client, definitions_client = mock_clients
@@ -238,4 +177,4 @@ def test_none_role_definition_id_is_skipped(self, mock_clients):
238177
)
239178

240179
assert len(result) == 1
241-
assert result[0].role_definition_id is not None
180+
assert result[0].role_definition_id is not None

0 commit comments

Comments
 (0)