Skip to content

Commit 967983c

Browse files
authored
[Profile] az login: Add --client-id, --object-id and --resource-id for authenticating with user-assigned managed identity (#30525)
1 parent 97d6708 commit 967983c

File tree

6 files changed

+81
-20
lines changed

6 files changed

+81
-20
lines changed

src/azure-cli-core/azure/cli/core/_profile.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060

6161
_AZ_LOGIN_MESSAGE = "Please run 'az login' to setup account."
6262

63+
MANAGED_IDENTITY_ID_WARNING = (
64+
"Passing the managed identity ID with --username is deprecated and will be removed in a future release. "
65+
"Please use --client-id, --object-id or --resource-id instead."
66+
)
67+
6368

6469
def load_subscriptions(cli_ctx, all_clouds=False, refresh=False):
6570
profile = Profile(cli_ctx=cli_ctx)
@@ -219,7 +224,8 @@ def login(self,
219224
self._set_subscriptions(consolidated)
220225
return deepcopy(consolidated)
221226

222-
def login_with_managed_identity(self, identity_id=None, allow_no_subscriptions=None):
227+
def login_with_managed_identity(self, identity_id=None, client_id=None, object_id=None, resource_id=None,
228+
allow_no_subscriptions=None):
223229
if _on_azure_arc():
224230
return self.login_with_managed_identity_azure_arc(
225231
identity_id=identity_id, allow_no_subscriptions=allow_no_subscriptions)
@@ -229,7 +235,28 @@ def login_with_managed_identity(self, identity_id=None, allow_no_subscriptions=N
229235
from azure.cli.core.auth.adal_authentication import MSIAuthenticationWrapper
230236
resource = self.cli_ctx.cloud.endpoints.active_directory_resource_id
231237

232-
if identity_id:
238+
id_arg_count = len([arg for arg in (client_id, object_id, resource_id, identity_id) if arg])
239+
if id_arg_count > 1:
240+
raise CLIError('Usage error: Provide only one of --client-id, --object-id, --resource-id, or --username.')
241+
242+
if id_arg_count == 0:
243+
identity_type = MsiAccountTypes.system_assigned
244+
msi_creds = MSIAuthenticationWrapper(resource=resource)
245+
elif client_id:
246+
identity_type = MsiAccountTypes.user_assigned_client_id
247+
identity_id = client_id
248+
msi_creds = MSIAuthenticationWrapper(resource=resource, client_id=client_id)
249+
elif object_id:
250+
identity_type = MsiAccountTypes.user_assigned_object_id
251+
identity_id = object_id
252+
msi_creds = MSIAuthenticationWrapper(resource=resource, object_id=object_id)
253+
elif resource_id:
254+
identity_type = MsiAccountTypes.user_assigned_resource_id
255+
identity_id = resource_id
256+
msi_creds = MSIAuthenticationWrapper(resource=resource, msi_res_id=resource_id)
257+
# The old way of re-using the same --username for 3 types of ID
258+
elif identity_id:
259+
logger.warning(MANAGED_IDENTITY_ID_WARNING)
233260
if is_valid_resource_id(identity_id):
234261
msi_creds = MSIAuthenticationWrapper(resource=resource, msi_res_id=identity_id)
235262
identity_type = MsiAccountTypes.user_assigned_resource_id
@@ -260,10 +287,6 @@ def login_with_managed_identity(self, identity_id=None, allow_no_subscriptions=N
260287
if not authenticated:
261288
raise CLIError('Failed to connect to MSI, check your managed service identity id.')
262289

263-
else:
264-
identity_type = MsiAccountTypes.system_assigned
265-
msi_creds = MSIAuthenticationWrapper(resource=resource)
266-
267290
token_entry = msi_creds.token
268291
token = token_entry['access_token']
269292
logger.info('MSI: token was retrieved. Now trying to initialize local accounts...')

src/azure-cli-core/azure/cli/core/tests/test_profile.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ def test_login_in_cloud_shell(self, cloud_shell_credential_mock, create_subscrip
498498

499499
@mock.patch('requests.get', autospec=True)
500500
@mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True)
501-
def test_find_subscriptions_in_vm_with_mi_system_assigned(self, create_subscription_client_mock, mock_get):
501+
def test_login_with_mi_system_assigned(self, create_subscription_client_mock, mock_get):
502502
mock_subscription_client = mock.MagicMock()
503503
mock_subscription_client.subscriptions.list.return_value = [deepcopy(self.subscription1_raw)]
504504
create_subscription_client_mock.return_value = mock_subscription_client
@@ -531,7 +531,7 @@ def test_find_subscriptions_in_vm_with_mi_system_assigned(self, create_subscript
531531

532532
@mock.patch('requests.get', autospec=True)
533533
@mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True)
534-
def test_find_subscriptions_in_vm_with_mi_no_subscriptions(self, create_subscription_client_mock, mock_get):
534+
def test_login_with_mi_no_subscriptions(self, create_subscription_client_mock, mock_get):
535535
mock_subscription_client = mock.MagicMock()
536536
mock_subscription_client.subscriptions.list.return_value = []
537537
create_subscription_client_mock.return_value = mock_subscription_client
@@ -566,8 +566,7 @@ def test_find_subscriptions_in_vm_with_mi_no_subscriptions(self, create_subscrip
566566

567567
@mock.patch('requests.get', autospec=True)
568568
@mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True)
569-
def test_find_subscriptions_in_vm_with_mi_user_assigned_with_client_id(self, create_subscription_client_mock,
570-
mock_get):
569+
def test_login_with_mi_user_assigned_client_id(self, create_subscription_client_mock, mock_get):
571570
mock_subscription_client = mock.MagicMock()
572571
mock_subscription_client.subscriptions.list.return_value = [deepcopy(self.subscription1_raw)]
573572
create_subscription_client_mock.return_value = mock_subscription_client
@@ -587,6 +586,19 @@ def test_find_subscriptions_in_vm_with_mi_user_assigned_with_client_id(self, cre
587586
good_response.content = encoded_test_token
588587
mock_get.return_value = good_response
589588

589+
subscriptions = profile.login_with_managed_identity(client_id=test_client_id)
590+
591+
self.assertEqual(len(subscriptions), 1)
592+
s = subscriptions[0]
593+
self.assertEqual(s['name'], self.display_name1)
594+
self.assertEqual(s['id'], self.id1.split('/')[-1])
595+
self.assertEqual(s['tenantId'], self.test_mi_tenant)
596+
597+
self.assertEqual(s['user']['name'], 'userAssignedIdentity')
598+
self.assertEqual(s['user']['type'], 'servicePrincipal')
599+
self.assertEqual(s['user']['assignedIdentityInfo'], 'MSIClient-{}'.format(test_client_id))
600+
601+
# Old way of using identity_id
590602
subscriptions = profile.login_with_managed_identity(identity_id=test_client_id)
591603

592604
self.assertEqual(len(subscriptions), 1)
@@ -601,7 +613,7 @@ def test_find_subscriptions_in_vm_with_mi_user_assigned_with_client_id(self, cre
601613

602614
@mock.patch('azure.cli.core.auth.adal_authentication.MSIAuthenticationWrapper', autospec=True)
603615
@mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True)
604-
def test_find_subscriptions_in_vm_with_mi_user_assigned_with_object_id(self, create_subscription_client_mock,
616+
def test_login_with_mi_user_assigned_object_id(self, create_subscription_client_mock,
605617
mock_msi_auth):
606618
mock_subscription_client = mock.MagicMock()
607619
mock_subscription_client.subscriptions.list.return_value = [deepcopy(self.subscription1_raw)]
@@ -632,6 +644,14 @@ def set_token(self):
632644
mock_msi_auth.side_effect = AuthStub
633645
test_object_id = '54826b22-38d6-4fb2-bad9-b7b93a3e9999'
634646

647+
subscriptions = profile.login_with_managed_identity(object_id=test_object_id)
648+
649+
s = subscriptions[0]
650+
self.assertEqual(s['user']['name'], 'userAssignedIdentity')
651+
self.assertEqual(s['user']['type'], 'servicePrincipal')
652+
self.assertEqual(s['user']['assignedIdentityInfo'], 'MSIObject-{}'.format(test_object_id))
653+
654+
# Old way of using identity_id
635655
subscriptions = profile.login_with_managed_identity(identity_id=test_object_id)
636656

637657
s = subscriptions[0]
@@ -641,7 +661,7 @@ def set_token(self):
641661

642662
@mock.patch('requests.get', autospec=True)
643663
@mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True)
644-
def test_find_subscriptions_in_vm_with_mi_user_assigned_with_res_id(self, create_subscription_client_mock,
664+
def test_login_with_mi_user_assigned_resource_id(self, create_subscription_client_mock,
645665
mock_get):
646666

647667
mock_subscription_client = mock.MagicMock()
@@ -665,6 +685,14 @@ def test_find_subscriptions_in_vm_with_mi_user_assigned_with_res_id(self, create
665685
good_response.content = encoded_test_token
666686
mock_get.return_value = good_response
667687

688+
subscriptions = profile.login_with_managed_identity(resource_id=test_res_id)
689+
690+
s = subscriptions[0]
691+
self.assertEqual(s['user']['name'], 'userAssignedIdentity')
692+
self.assertEqual(s['user']['type'], 'servicePrincipal')
693+
self.assertEqual(subscriptions[0]['user']['assignedIdentityInfo'], 'MSIResource-{}'.format(test_res_id))
694+
695+
# Old way of using identity_id
668696
subscriptions = profile.login_with_managed_identity(identity_id=test_res_id)
669697

670698
s = subscriptions[0]

src/azure-cli/azure/cli/command_modules/profile/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ def load_arguments(self, command):
7575
# Managed identity
7676
c.argument('identity', options_list=('-i', '--identity'), action='store_true',
7777
help="Log in using managed identity", arg_group='Managed Identity')
78+
c.argument('client_id',
79+
help="Client ID of the user-assigned managed identity", arg_group='Managed Identity')
80+
c.argument('object_id',
81+
help="Object ID of the user-assigned managed identity", arg_group='Managed Identity')
82+
c.argument('resource_id',
83+
help="Resource ID of the user-assigned managed identity", arg_group='Managed Identity')
7884

7985
with self.argument_context('logout') as c:
8086
c.argument('username', help='account user, if missing, logout the current active account')

src/azure-cli/azure/cli/command_modules/profile/_help.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@
4343
text: az login --service-principal --username APP_ID --certificate /path/to/cert.pem --tenant TENANT_ID
4444
- name: Log in with a system-assigned managed identity.
4545
text: az login --identity
46-
- name: Log in with a user-assigned managed identity. You must specify the client ID, object ID or resource ID of the user-assigned managed identity with --username.
47-
text: az login --identity --username 00000000-0000-0000-0000-000000000000
46+
- name: Log in with a user-assigned managed identity's client ID.
47+
text: az login --identity --client-id 00000000-0000-0000-0000-000000000000
48+
- name: Log in with a user-assigned managed identity's resource ID.
49+
text: az login --identity --resource-id /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyResourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/MyIdentity
4850
"""
4951

5052
helps['account'] = """

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,14 @@ def account_clear(cmd):
114114
profile.logout_all()
115115

116116

117-
# pylint: disable=inconsistent-return-statements, too-many-branches
117+
# pylint: disable=too-many-branches, too-many-locals
118118
def login(cmd, username=None, password=None, tenant=None, scopes=None, allow_no_subscriptions=False,
119119
# Device code flow
120120
use_device_code=False,
121121
# Service principal
122122
service_principal=None, certificate=None, use_cert_sn_issuer=None, client_assertion=None,
123123
# Managed identity
124-
identity=False):
124+
identity=False, client_id=None, object_id=None, resource_id=None):
125125
"""Log in to access Azure subscriptions"""
126126

127127
# quick argument usage check
@@ -143,7 +143,9 @@ def login(cmd, username=None, password=None, tenant=None, scopes=None, allow_no_
143143
if identity:
144144
if in_cloud_console():
145145
return profile.login_in_cloud_shell()
146-
return profile.login_with_managed_identity(username, allow_no_subscriptions)
146+
return profile.login_with_managed_identity(
147+
identity_id=username, client_id=client_id, object_id=object_id, resource_id=resource_id,
148+
allow_no_subscriptions=allow_no_subscriptions)
147149
if in_cloud_console(): # tell users they might not need login
148150
logger.warning(_CLOUD_CONSOLE_LOGIN_WARNING)
149151

src/azure-cli/azure/cli/command_modules/profile/tests/latest/test_profile_custom.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ def test_get_raw_token(self, get_raw_token_mock):
8888
get_raw_token_mock.assert_called_with(mock.ANY, None, None, None, tenant_id)
8989

9090
@mock.patch('azure.cli.command_modules.profile.custom.Profile', autospec=True)
91-
def test_get_login(self, profile_mock):
91+
def test_login_with_mi(self, profile_mock):
9292
invoked = []
9393

94-
def test_login(msi_port, identity_id=None):
94+
def login_with_managed_identity_mock(*args, **kwargs):
9595
invoked.append(True)
9696

9797
# mock the instance
9898
profile_instance = mock.MagicMock()
99-
profile_instance.login_with_managed_identity = test_login
99+
profile_instance.login_with_managed_identity = login_with_managed_identity_mock
100100
# mock the constructor
101101
profile_mock.return_value = profile_instance
102102

0 commit comments

Comments
 (0)