Skip to content

Commit fb88971

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

File tree

2 files changed

+42
-107
lines changed

2 files changed

+42
-107
lines changed

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -538,9 +538,15 @@ 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 ID formats depending on scope (e.g., tenant-scoped
543+
# /providers/Microsoft.Authorization/roleDefinitions/{guid} vs subscription-scoped
544+
# /subscriptions/{sub}/providers/Microsoft.Authorization/roleDefinitions/{guid}).
541545
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)]
546+
resource_id = _resolve_role_id(role, scope, definitions_client)
547+
role_id = _get_role_definition_id(resource_id)
548+
assignments = [ra for ra in assignments
549+
if _get_role_definition_id(ra.role_definition_id) == role_id]
544550

545551
# filter the assignee if "include_groups" is not provided because service side
546552
# does not accept filter "principalId eq and atScope()"
@@ -550,6 +556,11 @@ def _search_role_assignments(assignments_client, definitions_client,
550556
return assignments
551557

552558

559+
def _get_role_definition_id(resource_id):
560+
"""Extract the role definition ID from a role definition resource ID."""
561+
return resource_id.split('/')[-1] if resource_id else None
562+
563+
553564
def _build_role_scope(resource_group_name, scope, subscription_id):
554565
subscription_scope = '/subscriptions/' + subscription_id
555566
if scope:
@@ -567,10 +578,12 @@ def _build_role_scope(resource_group_name, scope, subscription_id):
567578

568579

569580
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')
581+
"""Resolve a role to its full role definition resource ID.
582+
583+
Accepts:
584+
- role definition resource ID (returned as-is)
585+
- role definition GUID
586+
- role name (e.g. 'Reader')
574587
"""
575588
if re.match(r'(/subscriptions/.+)?/providers/Microsoft.Authorization/roleDefinitions/', role, re.I):
576589
return role

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

Lines changed: 23 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,31 @@
55
import pytest
66
from unittest import mock
77

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

1012
# pylint: disable=line-too-long
1113

1214

15+
class TestGetRoleDefinitionId:
16+
"""Tests for _get_role_definition_id helper function."""
17+
18+
@pytest.mark.parametrize("resource_id,expected", [
19+
# Tenant-scoped format
20+
('/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7',
21+
'acdd72a7-3385-48ef-bd42-f606fba81ae7'),
22+
# Subscription-scoped format
23+
('/subscriptions/sub1/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c',
24+
'b24988ac-6180-42a0-ab88-20f7382dd24c'),
25+
# None returns None
26+
(None, None),
27+
])
28+
def test_extracts_guid_from_role_definition_id(self, resource_id, expected):
29+
"""Extracts the GUID from various role definition ID formats."""
30+
assert _get_role_definition_id(resource_id) == expected
31+
32+
1333
class TestResolveRoleId:
1434
"""Tests for _resolve_role_id function."""
1535

@@ -80,7 +100,7 @@ def _create_assignment(scope, role_definition_id, principal_id='principal-1'):
80100
@pytest.mark.parametrize("scope,role_def_format", [
81101
# Root scope with tenant-format role definition ID
82102
('/', '/providers/Microsoft.Authorization/roleDefinitions/{guid}'),
83-
# Management group scope with tenant-format role definition ID
103+
# Management group scope with tenant-format role definition ID
84104
('/providers/Microsoft.Management/managementGroups/my-mg',
85105
'/providers/Microsoft.Authorization/roleDefinitions/{guid}'),
86106
# Subscription scope with subscription-format role definition ID
@@ -123,104 +143,6 @@ def test_different_role_guid_does_not_match(self, mock_clients):
123143

124144
assert len(result) == 0
125145

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-
224146
def test_none_role_definition_id_is_skipped(self, mock_clients):
225147
"""Assignments with None role_definition_id are skipped when filtering by role."""
226148
assignments_client, definitions_client = mock_clients
@@ -238,4 +160,4 @@ def test_none_role_definition_id_is_skipped(self, mock_clients):
238160
)
239161

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

0 commit comments

Comments
 (0)