From 802f58179f1ce79b7c49dda7503ed0ba9c2ae4b6 Mon Sep 17 00:00:00 2001 From: Jiashuo Li <4003950+jiasli@users.noreply.github.com> Date: Fri, 15 Oct 2021 15:43:03 +0800 Subject: [PATCH] {Role} Keep `--sdk-auth`, for now (#19872) --- src/azure-cli-core/azure/cli/core/_profile.py | 52 ++++++++++++++ .../azure/cli/core/auth/identity.py | 4 ++ .../azure/cli/core/tests/test_profile.py | 69 +++++++++++++++++++ .../cli/command_modules/profile/__init__.py | 2 +- .../azure/cli/command_modules/role/_params.py | 3 +- 5 files changed, 127 insertions(+), 3 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index fd948429a67..8fcd997e248 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -643,6 +643,58 @@ def refresh_accounts(self): self._set_subscriptions(result, merge=False) + def get_sp_auth_info(self, subscription_id=None, name=None, password=None, cert_file=None): + """Generate a JSON for --sdk-auth argument when used in: + - az ad sp create-for-rbac --sdk-auth + - az account show --sdk-auth + """ + from collections import OrderedDict + account = self.get_subscription(subscription_id) + + # is the credential created through command like 'create-for-rbac'? + result = OrderedDict() + if name and (password or cert_file): + result['clientId'] = name + if password: + result['clientSecret'] = password + else: + result['clientCertificate'] = cert_file + result['subscriptionId'] = subscription_id or account[_SUBSCRIPTION_ID] + else: # has logged in through cli + user_type = account[_USER_ENTITY].get(_USER_TYPE) + if user_type == _SERVICE_PRINCIPAL: + client_id = account[_USER_ENTITY][_USER_NAME] + result['clientId'] = client_id + identity = Identity(tenant_id=account[_TENANT_ID]) + sp_entry = identity.get_service_principal_entry(client_id) + + from .auth.msal_authentication import _CLIENT_SECRET, _CERTIFICATE + secret = sp_entry.get(_CLIENT_SECRET) + if secret: + result['clientSecret'] = secret + else: + # we can output 'clientCertificateThumbprint' if asked + result['clientCertificate'] = sp_entry.get(_CERTIFICATE) + result['subscriptionId'] = account[_SUBSCRIPTION_ID] + else: + raise CLIError('SDK Auth file is only applicable when authenticated using a service principal') + + result[_TENANT_ID] = account[_TENANT_ID] + endpoint_mappings = OrderedDict() # use OrderedDict to control the output sequence + endpoint_mappings['active_directory'] = 'activeDirectoryEndpointUrl' + endpoint_mappings['resource_manager'] = 'resourceManagerEndpointUrl' + endpoint_mappings['active_directory_graph_resource_id'] = 'activeDirectoryGraphResourceId' + endpoint_mappings['sql_management'] = 'sqlManagementEndpointUrl' + endpoint_mappings['gallery'] = 'galleryEndpointUrl' + endpoint_mappings['management'] = 'managementEndpointUrl' + from azure.cli.core.cloud import CloudEndpointNotSetException + for e in endpoint_mappings: + try: + result[endpoint_mappings[e]] = getattr(get_active_cloud(self.cli_ctx).endpoints, e) + except CloudEndpointNotSetException: + result[endpoint_mappings[e]] = None + return result + def get_installation_id(self): installation_id = self._storage.get(_INSTALLATION_ID) if not installation_id: diff --git a/src/azure-cli-core/azure/cli/core/auth/identity.py b/src/azure-cli-core/azure/cli/core/auth/identity.py index 37e065b0334..7b39b72ac25 100644 --- a/src/azure-cli-core/azure/cli/core/auth/identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/identity.py @@ -184,6 +184,10 @@ def get_service_principal_credential(self, client_id): sp_auth = ServicePrincipalAuth(entry) return ServicePrincipalCredential(sp_auth, **self._msal_app_kwargs) + def get_service_principal_entry(self, client_id): + """This method is only used by --sdk-auth. DO NOT use it elsewhere.""" + return self._msal_secret_store.load_entry(client_id, self.tenant_id) + def get_managed_identity_credential(self, client_id=None): raise NotImplementedError diff --git a/src/azure-cli-core/azure/cli/core/tests/test_profile.py b/src/azure-cli-core/azure/cli/core/tests/test_profile.py index 3b5b5abdd01..bcdff38a32e 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_profile.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_profile.py @@ -1368,6 +1368,75 @@ def test_login_common_tenant_mfa_warning(self, get_user_credential_mock, create_ # With pytest, use -o log_cli=True to manually check the log + # Tests for get_sp_auth_info + def test_get_auth_info_fail_on_user_account(self): + cli = DummyCli() + storage_mock = {'subscriptions': None} + profile = Profile(cli_ctx=cli, storage=storage_mock) + + consolidated = profile._normalize_properties(self.user1, + [self.subscription1], + False) + profile._set_subscriptions(consolidated) + + # testing dump of existing logged in account + self.assertRaises(CLIError, profile.get_sp_auth_info) + + @mock.patch('azure.cli.core.auth.identity.Identity.get_service_principal_entry', autospec=True) + @mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True) + @mock.patch('azure.cli.core.auth.identity.Identity.get_service_principal_credential', autospec=True) + @mock.patch('azure.cli.core.auth.identity.Identity.login_with_service_principal', autospec=True) + def test_get_auth_info_for_logged_in_service_principal(self, login_with_service_principal_mock, + get_service_principal_credential_mock, + create_subscription_client_mock, + get_service_principal_entry_mock): + cli = DummyCli() + mock_subscription_client = mock.MagicMock() + mock_subscription_client.tenants.list.return_value = [TenantStub(self.tenant_id)] + mock_subscription_client.subscriptions.list.return_value = [deepcopy(self.subscription1_raw)] + create_subscription_client_mock.return_value = mock_subscription_client + + storage_mock = {'subscriptions': []} + profile = Profile(cli_ctx=cli, storage=storage_mock) + profile._management_resource_uri = 'https://management.core.windows.net/' + profile.login(False, '1234', 'my-secret', True, self.tenant_id, use_device_code=False, + allow_no_subscriptions=False) + # action + get_service_principal_entry_mock.return_value = { + "tenant": self.tenant_id, + "client_id": '1234', + "client_secret": 'my-secret' + } + extended_info = profile.get_sp_auth_info() + # assert + self.assertEqual(self.id1.split('/')[-1], extended_info['subscriptionId']) + self.assertEqual('1234', extended_info['clientId']) + self.assertEqual('my-secret', extended_info['clientSecret']) + self.assertEqual('https://login.microsoftonline.com', extended_info['activeDirectoryEndpointUrl']) + self.assertEqual('https://management.azure.com/', extended_info['resourceManagerEndpointUrl']) + + def test_get_auth_info_for_newly_created_service_principal(self): + cli = DummyCli() + storage_mock = {'subscriptions': []} + profile = Profile(cli_ctx=cli, storage=storage_mock) + consolidated = profile._normalize_properties(self.user1, [self.subscription1], False) + profile._set_subscriptions(consolidated) + + # certificate + extended_info = profile.get_sp_auth_info(name='1234', cert_file='/tmp/123.pem') + + self.assertEqual(self.id1.split('/')[-1], extended_info['subscriptionId']) + self.assertEqual(self.tenant_id, extended_info['tenantId']) + self.assertEqual('1234', extended_info['clientId']) + self.assertEqual('/tmp/123.pem', extended_info['clientCertificate']) + self.assertIsNone(extended_info.get('clientSecret', None)) + self.assertEqual('https://login.microsoftonline.com', extended_info['activeDirectoryEndpointUrl']) + self.assertEqual('https://management.azure.com/', extended_info['resourceManagerEndpointUrl']) + + # secret + extended_info = profile.get_sp_auth_info(name='1234', password='very_secret') + self.assertEqual('very_secret', extended_info['clientSecret']) + class FileHandleStub(object): # pylint: disable=too-few-public-methods diff --git a/src/azure-cli/azure/cli/command_modules/profile/__init__.py b/src/azure-cli/azure/cli/command_modules/profile/__init__.py index 4c59661a05a..9e268d7c375 100644 --- a/src/azure-cli/azure/cli/command_modules/profile/__init__.py +++ b/src/azure-cli/azure/cli/command_modules/profile/__init__.py @@ -75,7 +75,7 @@ def load_arguments(self, command): with self.argument_context('account show') as c: c.argument('show_auth_for_sdk', options_list=['--sdk-auth'], action='store_true', - deprecate_info=c.deprecate(target='--sdk-auth', expiration='3.0.0'), + deprecate_info=c.deprecate(target='--sdk-auth'), help='Output result to a file compatible with Azure SDK auth. Only applicable when authenticating with a Service Principal.') with self.argument_context('account get-access-token') as c: 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 80daa42a94f..02eef165040 100644 --- a/src/azure-cli/azure/cli/command_modules/role/_params.py +++ b/src/azure-cli/azure/cli/command_modules/role/_params.py @@ -90,8 +90,7 @@ def load_arguments(self, _): help='Skip creating the default assignment, which allows the service principal to access resources under the current subscription. ' 'When specified, --scopes will be ignored. You may use `az role assignment create` to create ' 'role assignments for this service principal later.') - c.argument('show_auth_for_sdk', options_list='--sdk-auth', - deprecate_info=c.deprecate(target='--sdk-auth', expiration='3.0.0'), + c.argument('show_auth_for_sdk', options_list='--sdk-auth', deprecate_info=c.deprecate(target='--sdk-auth'), help='output result in compatible with Azure SDK auth file', arg_type=get_three_state_flag()) with self.argument_context('ad sp owner list') as c: