Skip to content

Commit 3646512

Browse files
author
Andrea Tomassilli
committed
change
1 parent 63606b2 commit 3646512

File tree

2 files changed

+167
-33
lines changed

2 files changed

+167
-33
lines changed

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

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ def _search_role_assignments(assignments_client, definitions_client,
540540

541541
if role:
542542
role_id = _resolve_role_id(role, scope, definitions_client)
543-
assignments = [ra for ra in assignments if ra.role_definition_id == role_id]
543+
assignments = [ra for ra in assignments if ra.role_definition_id.endswith(role_id)]
544544

545545
# filter the assignee if "include_groups" is not provided because service side
546546
# does not accept filter "principalId eq and atScope()"
@@ -567,24 +567,25 @@ def _build_role_scope(resource_group_name, scope, subscription_id):
567567

568568

569569
def _resolve_role_id(role, scope, definitions_client):
570-
role_id = None
571-
if re.match(r'/subscriptions/.+/providers/Microsoft.Authorization/roleDefinitions/',
572-
role, re.I):
573-
role_id = role
574-
else:
575-
if is_guid(role):
576-
role_id = '/subscriptions/{}/providers/Microsoft.Authorization/roleDefinitions/{}'.format(
577-
definitions_client._config.subscription_id, role)
578-
if not role_id: # retrieve role id
579-
role_defs = list(definitions_client.list(scope, "roleName eq '{}'".format(role)))
580-
if not role_defs:
581-
raise CLIError("Role '{}' doesn't exist.".format(role))
582-
if len(role_defs) > 1:
583-
ids = [r.id for r in role_defs]
584-
err = "More than one role matches the given name '{}'. Please pick a value from '{}'"
585-
raise CLIError(err.format(role, ids))
586-
role_id = role_defs[0].id
587-
return role_id
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')
574+
"""
575+
if re.match(r'(/subscriptions/.+)?/providers/Microsoft.Authorization/roleDefinitions/', role, re.I):
576+
return role
577+
578+
if is_guid(role):
579+
return f"/providers/Microsoft.Authorization/roleDefinitions/{role}"
580+
581+
role_defs = list(definitions_client.list(scope, "roleName eq '{}'".format(role)))
582+
if not role_defs:
583+
raise CLIError("Role '{}' doesn't exist.".format(role))
584+
if len(role_defs) > 1:
585+
ids = [r.id for r in role_defs]
586+
err = "More than one role matches the given name '{}'. Please pick a value from '{}'"
587+
raise CLIError(err.format(role, ids))
588+
return role_defs[0].id
588589

589590

590591
def create_application(cmd, client, display_name, identifier_uris=None,

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

Lines changed: 147 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,160 @@
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 unittest
5+
import pytest
66
from unittest import mock
77

8-
from azure.cli.command_modules.role.custom import _resolve_role_id
8+
from azure.cli.command_modules.role.custom import _resolve_role_id, _search_role_assignments
99

1010
# pylint: disable=line-too-long
1111

1212

13-
class TestRoleCustomCommands(unittest.TestCase):
13+
class TestResolveRoleId:
14+
"""Tests for _resolve_role_id function."""
1415

15-
def test_resolve_role_id(self, ):
16-
mock_client = mock.Mock()
17-
mock_client._config.subscription_id = '123'
18-
test_role_id = 'b24988ac-6180-42a0-ab88-20f738123456'
16+
@pytest.fixture
17+
def mock_client(self):
18+
client = mock.Mock()
19+
client._config.subscription_id = '00000000-0000-0000-0000-000000000000'
20+
return client
1921

20-
# action(using a logical name)
21-
result = _resolve_role_id(test_role_id, 'foobar', mock_client)
22+
@pytest.mark.parametrize("role_input,expected_output", [
23+
# GUID returns tenant format
24+
('b24988ac-6180-42a0-ab88-20f738123456',
25+
'/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f738123456'),
26+
# Subscription-scoped ID returned as-is
27+
('/subscriptions/sub1/providers/Microsoft.Authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456',
28+
'/subscriptions/sub1/providers/Microsoft.Authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456'),
29+
# Tenant-scoped ID returned as-is
30+
('/providers/Microsoft.Authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456',
31+
'/providers/Microsoft.Authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456'),
32+
])
33+
def test_resolve_role_id_formats(self, mock_client, role_input, expected_output):
34+
"""Role IDs (GUID, subscription-scoped, tenant-scoped) are resolved correctly."""
35+
result = _resolve_role_id(role_input, '/subscriptions/sub1', mock_client)
36+
assert result == expected_output
2237

23-
# assert
24-
self.assertEqual('/subscriptions/123/providers/Microsoft.Authorization/roleDefinitions/{}'.format(test_role_id), result)
38+
def test_role_name_queries_api(self, mock_client):
39+
"""Role name triggers API lookup and returns the role definition ID from API."""
40+
mock_role_def = mock.Mock()
41+
mock_role_def.id = '/subscriptions/123/providers/Microsoft.Authorization/roleDefinitions/acdd72a7'
42+
mock_client.list.return_value = [mock_role_def]
2543

26-
# action (using a full id)
27-
test_full_id = '/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272123456/providers/microsoft.authorization/roleDefinitions/5370bbf4-6b73-4417-969b-8f2e6e123456'
28-
self.assertEqual(test_full_id, _resolve_role_id(test_full_id, 'foobar', mock_client))
44+
result = _resolve_role_id('Reader', '/subscriptions/123', mock_client)
45+
46+
assert result == mock_role_def.id
47+
mock_client.list.assert_called_once_with('/subscriptions/123', "roleName eq 'Reader'")
48+
49+
@pytest.mark.parametrize("api_response,error_contains", [
50+
([], "doesn't exist"), # Not found
51+
([mock.Mock(id='id1'), mock.Mock(id='id2')], "More than one role"), # Multiple matches
52+
])
53+
def test_role_name_error_cases(self, mock_client, api_response, error_contains):
54+
"""Role name lookup raises CLIError for not found or multiple matches."""
55+
from knack.util import CLIError
56+
mock_client.list.return_value = api_response
57+
58+
with pytest.raises(CLIError, match=error_contains):
59+
_resolve_role_id('SomeRole', '/subscriptions/123', mock_client)
60+
61+
62+
class TestSearchRoleAssignments:
63+
"""Tests for _search_role_assignments function, focusing on role filtering."""
64+
65+
@pytest.fixture
66+
def mock_clients(self):
67+
assignments_client = mock.Mock()
68+
definitions_client = mock.Mock()
69+
definitions_client._config.subscription_id = '00000000-0000-0000-0000-000000000000'
70+
return assignments_client, definitions_client
71+
72+
@staticmethod
73+
def _create_assignment(scope, role_definition_id, principal_id='principal-1'):
74+
assignment = mock.Mock()
75+
assignment.scope = scope
76+
assignment.role_definition_id = role_definition_id
77+
assignment.principal_id = principal_id
78+
return assignment
79+
80+
@pytest.mark.parametrize("scope,role_def_format", [
81+
# Root scope with tenant-format role definition ID
82+
('/', '/providers/Microsoft.Authorization/roleDefinitions/{guid}'),
83+
# Management group scope with tenant-format role definition ID
84+
('/providers/Microsoft.Management/managementGroups/my-mg',
85+
'/providers/Microsoft.Authorization/roleDefinitions/{guid}'),
86+
# Subscription scope with subscription-format role definition ID
87+
('/subscriptions/00000000-0000-0000-0000-000000000000',
88+
'/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/{guid}'),
89+
])
90+
def test_guid_filter_matches_across_scopes(self, mock_clients, scope, role_def_format):
91+
"""GUID filter matches assignments at various scopes with different roleDefinitionId formats."""
92+
assignments_client, definitions_client = mock_clients
93+
role_guid = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
94+
role_def_id = role_def_format.format(guid=role_guid)
95+
96+
assignments_client.list_for_scope.return_value = [
97+
self._create_assignment(scope, role_def_id),
98+
]
99+
100+
result = _search_role_assignments(
101+
assignments_client, definitions_client,
102+
scope=scope, assignee_object_id=None, role=role_guid,
103+
include_inherited=False, include_groups=False
104+
)
105+
106+
assert len(result) == 1
107+
108+
def test_different_role_guid_does_not_match(self, mock_clients):
109+
"""Assignments with different role GUIDs are filtered out."""
110+
assignments_client, definitions_client = mock_clients
111+
filter_guid = 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
112+
other_guid = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
113+
114+
assignments_client.list_for_scope.return_value = [
115+
self._create_assignment('/', f'/providers/Microsoft.Authorization/roleDefinitions/{other_guid}'),
116+
]
117+
118+
result = _search_role_assignments(
119+
assignments_client, definitions_client,
120+
scope='/', assignee_object_id=None, role=filter_guid,
121+
include_inherited=False, include_groups=False
122+
)
123+
124+
assert len(result) == 0
125+
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

0 commit comments

Comments
 (0)