From dca625d37b4099c57ad7c5a5fa9707fda55296cf Mon Sep 17 00:00:00 2001 From: Jonathan Ruttle Date: Wed, 29 Jul 2026 08:50:58 +0100 Subject: [PATCH] Support agent principal types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c68fc8b-b668-4d31-bff0-292712a310b2 --- .../azure/cli/command_modules/role/_params.py | 21 ++++++----- .../azure/cli/command_modules/role/custom.py | 13 +++++-- .../role/tests/latest/test_role_custom.py | 37 ++++++++++++++++++- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/role/_params.py b/src/azure-cli/azure/cli/command_modules/role/_params.py index 83b5499f0b3..49c035d940e 100644 --- a/src/azure-cli/azure/cli/command_modules/role/_params.py +++ b/src/azure-cli/azure/cli/command_modules/role/_params.py @@ -5,6 +5,8 @@ # pylint: disable=line-too-long +from enum import Enum + from knack.arguments import CLIArgumentType from azure.cli.core.commands.parameters import get_enum_type, get_three_state_flag @@ -20,6 +22,15 @@ JSON_PROPERTY_HELP = "Should be JSON file path or in-line JSON string. See examples for details" +class RoleAssignmentPrincipalType(str, Enum): + agent_service_principal = "AgentServicePrincipal" + agent_user = "AgentUser" + user = "User" + group = "Group" + service_principal = "ServicePrincipal" + foreign_group = "ForeignGroup" + + # pylint: disable=too-many-statements def load_arguments(self, _): with self.argument_context('ad') as c: @@ -371,15 +382,7 @@ def load_arguments(self, _): # As only 'User', 'Group' or 'ServicePrincipal' are allowed values, the REST spec contains invalid values # (like MSI) which are used only internally by the service. So hide them. # https://github.com/Azure/azure-rest-api-specs/blob/962013a1cf9bf5b87e3aad75a14c7dd620acda62/specification/authorization/resource-manager/Microsoft.Authorization/preview/2020-04-01-preview/authorization-RoleAssignmentsCalls.json#L508-L522 - from enum import Enum - - class PrincipalType(str, Enum): - user = "User" - group = "Group" - service_principal = "ServicePrincipal" - foreign_group = "ForeignGroup" - - c.argument('assignee_principal_type', arg_type=get_enum_type(PrincipalType), + c.argument('assignee_principal_type', arg_type=get_enum_type(RoleAssignmentPrincipalType), help='use with --assignee-object-id to avoid errors caused by propagation latency in Microsoft Graph') with self.argument_context('role assignment update') as c: diff --git a/src/azure-cli/azure/cli/command_modules/role/custom.py b/src/azure-cli/azure/cli/command_modules/role/custom.py index 279ad0d219b..b0926b35d96 100644 --- a/src/azure-cli/azure/cli/command_modules/role/custom.py +++ b/src/azure-cli/azure/cli/command_modules/role/custom.py @@ -160,12 +160,16 @@ def _deny_assignment_to_dict(da): USER = 'User' SERVICE_PRINCIPAL = 'ServicePrincipal' GROUP = 'Group' +AGENT_USER = 'AgentUser' +AGENT_SERVICE_PRINCIPAL = 'AgentServicePrincipal' # Map Graph '@odata.type' to ARM RBAC's principalType ODATA_TYPE_TO_PRINCIPAL_TYPE = { '#microsoft.graph.user': USER, '#microsoft.graph.servicePrincipal': SERVICE_PRINCIPAL, - '#microsoft.graph.group': GROUP + '#microsoft.graph.group': GROUP, + '#microsoft.graph.agentUser': AGENT_USER, + '#microsoft.graph.agentIdentity': AGENT_SERVICE_PRINCIPAL } # Object ID property name @@ -1859,13 +1863,16 @@ def _resolve_object_id_and_type(cli_ctx, assignee, fallback_to_object_id=False): if assignee.find('@') >= 0: # looks like a user principal name result = list(client.user_list(filter="userPrincipalName eq '{}'".format(assignee))) if result: - return result[0][ID], USER + principal_type = _odata_type_to_arm_principal_type(result[0].get('@odata.type')) or USER + return result[0][ID], principal_type # Try resolving as service principal result = list(client.service_principal_list( filter="servicePrincipalNames/any(c:c eq '{}')".format(assignee))) if result: - return result[0][ID], SERVICE_PRINCIPAL + principal_type = _odata_type_to_arm_principal_type( + result[0].get('@odata.type')) or SERVICE_PRINCIPAL + return result[0][ID], principal_type # Try resolving as object ID if is_guid(assignee): # assume an object id, let us verify it diff --git a/src/azure-cli/azure/cli/command_modules/role/tests/latest/test_role_custom.py b/src/azure-cli/azure/cli/command_modules/role/tests/latest/test_role_custom.py index 8e62ee971df..6c479c02f26 100644 --- a/src/azure-cli/azure/cli/command_modules/role/tests/latest/test_role_custom.py +++ b/src/azure-cli/azure/cli/command_modules/role/tests/latest/test_role_custom.py @@ -5,13 +5,48 @@ import unittest from unittest import mock -from azure.cli.command_modules.role.custom import _resolve_role_id +from azure.cli.command_modules.role._params import RoleAssignmentPrincipalType +from azure.cli.command_modules.role.custom import ( + ODATA_TYPE_TO_PRINCIPAL_TYPE, + _resolve_object_id_and_type, + _resolve_role_id, +) # pylint: disable=line-too-long class TestRoleCustomCommands(unittest.TestCase): + def test_agent_principal_types(self): + self.assertEqual(RoleAssignmentPrincipalType.agent_user.value, 'AgentUser') + self.assertEqual(RoleAssignmentPrincipalType.agent_service_principal.value, 'AgentServicePrincipal') + self.assertEqual(ODATA_TYPE_TO_PRINCIPAL_TYPE['#microsoft.graph.agentUser'], 'AgentUser') + self.assertEqual( + ODATA_TYPE_TO_PRINCIPAL_TYPE['#microsoft.graph.agentIdentity'], + 'AgentServicePrincipal') + + @mock.patch('azure.cli.command_modules.role.custom._graph_client_factory') + def test_resolve_agent_principal_types_by_name(self, graph_client_factory): + graph_client = graph_client_factory.return_value + graph_client.user_list.return_value = [{ + 'id': 'agent-user-id', + '@odata.type': '#microsoft.graph.agentUser', + }] + + self.assertEqual( + _resolve_object_id_and_type(mock.Mock(), 'agent@contoso.com'), + ('agent-user-id', 'AgentUser')) + + graph_client.user_list.return_value = [] + graph_client.service_principal_list.return_value = [{ + 'id': 'agent-identity-id', + '@odata.type': '#microsoft.graph.agentIdentity', + }] + + self.assertEqual( + _resolve_object_id_and_type(mock.Mock(), 'agent-identity-app-id'), + ('agent-identity-id', 'AgentServicePrincipal')) + def test_resolve_role_id(self, ): mock_client = mock.Mock() mock_client._config.subscription_id = '123'