diff --git a/src/azure-cli-core/azure/cli/core/_identity.py b/src/azure-cli-core/azure/cli/core/_identity.py index 4c3c4518098..5d9bec75081 100644 --- a/src/azure-cli-core/azure/cli/core/_identity.py +++ b/src/azure-cli-core/azure/cli/core/_identity.py @@ -23,6 +23,7 @@ ) _CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46' +_DEFAULT_SCOPES = ('https://management.core.windows.net/.default',) logger = get_logger(__name__) _SERVICE_PRINCIPAL_ID = 'servicePrincipalId' @@ -64,10 +65,13 @@ class Identity: CLOUD_SHELL_IDENTITY_UNIQUE_NAME = "unique_name" - def __init__(self, authority=None, tenant_id=None, client_id=None, **kwargs): + def __init__(self, authority=None, tenant_id=None, client_id=None, scopes=None, **kwargs): self.authority = authority self.tenant_id = tenant_id or "organizations" self.client_id = client_id or _CLIENT_ID + self.scopes = scopes or _DEFAULT_SCOPES + if self.scopes and not isinstance(self.scopes, (list, tuple)): + self.scopes = (self.scopes,) self._cred_cache = kwargs.pop('cred_cache', None) self.allow_unencrypted = kwargs.pop('allow_unencrypted', True) @@ -102,9 +106,10 @@ def login_with_interactive_browser(self): client_id=self.client_id, enable_persistent_cache=True, allow_unencrypted_cache=self.allow_unencrypted) - auth_record = credential.authenticate() + auth_record = credential.authenticate(scopes=self.scopes) # todo: remove after ADAL token deprecation - self._cred_cache.add_credential(credential) + if self._cred_cache: + self._cred_cache.add_credential(credential) return credential, auth_record def login_with_device_code(self): @@ -113,6 +118,7 @@ def prompt_callback(verification_uri, user_code, _): # expires_on is discarded logger.warning("To sign in, use a web browser to open the page %s and enter the code %s to authenticate.", verification_uri, user_code) + try: credential = DeviceCodeCredential(authority=self.authority, tenant_id=self.tenant_id, @@ -121,9 +127,10 @@ def prompt_callback(verification_uri, user_code, _): prompt_callback=prompt_callback, allow_unencrypted_cache=self.allow_unencrypted) - auth_record = credential.authenticate() + auth_record = credential.authenticate(scopes=self.scopes) # todo: remove after ADAL token deprecation - self._cred_cache.add_credential(credential) + if self._cred_cache: + self._cred_cache.add_credential(credential) return credential, auth_record except ValueError as ex: logger.debug('Device code authentication failed: %s', str(ex)) @@ -141,10 +148,11 @@ def login_with_username_password(self, username, password): password=password, enable_persistent_cache=True, allow_unencrypted_cache=self.allow_unencrypted) - auth_record = credential.authenticate() + auth_record = credential.authenticate(scopes=self.scopes) # todo: remove after ADAL token deprecation - self._cred_cache.add_credential(credential) + if self._cred_cache: + self._cred_cache.add_credential(credential) return credential, auth_record def login_with_service_principal_secret(self, client_id, client_secret): @@ -155,7 +163,8 @@ def login_with_service_principal_secret(self, client_id, client_secret): entry = sp_auth.get_entry_to_persist() self._msal_store.save_service_principal_cred(entry) # backward compatible with ADAL, to be deprecated - self._cred_cache.save_service_principal_cred(entry) + if self._cred_cache: + self._cred_cache.save_service_principal_cred(entry) credential = ClientSecretCredential(self.tenant_id, client_id, client_secret, authority=self.authority) return credential @@ -168,20 +177,21 @@ def login_with_service_principal_certificate(self, client_id, certificate_path): entry = sp_auth.get_entry_to_persist() self._msal_store.save_service_principal_cred(entry) # backward compatible with ADAL, to be deprecated - self._cred_cache.save_service_principal_cred(entry) + if self._cred_cache: + self._cred_cache.save_service_principal_cred(entry) # TODO: support use_cert_sn_issuer in CertificateCredential credential = CertificateCredential(self.tenant_id, client_id, certificate_path, authority=self.authority) return credential - def login_with_managed_identity(self, resource, identity_id=None): # pylint: disable=too-many-statements + def login_with_managed_identity(self, identity_id=None): # pylint: disable=too-many-statements from msrestazure.tools import is_valid_resource_id from requests import HTTPError from azure.core.exceptions import ClientAuthenticationError credential = None id_type = None - scope = resource.rstrip('/') + '/.default' + if identity_id: # Try resource ID if is_valid_resource_id(identity_id): @@ -192,7 +202,7 @@ def login_with_managed_identity(self, resource, identity_id=None): # pylint: di try: # Try client ID credential = ManagedIdentityCredential(client_id=identity_id) - credential.get_token(scope) + credential.get_token(*self.scopes) id_type = self.MANAGED_IDENTITY_CLIENT_ID authenticated = True except ClientAuthenticationError as e: @@ -226,7 +236,7 @@ def login_with_managed_identity(self, resource, identity_id=None): # pylint: di else: credential = ManagedIdentityCredential() - decoded = self._decode_managed_identity_token(credential, resource) + decoded = self._decode_managed_identity_token(credential) resource_id = decoded.get('xms_mirid') # User-assigned identity has resourceID as # /subscriptions/xxx/resourcegroups/xxx/providers/Microsoft.ManagedIdentity/userAssignedIdentities/xxx @@ -248,9 +258,9 @@ def login_with_managed_identity(self, resource, identity_id=None): # pylint: di return credential, managed_identity_info - def login_in_cloud_shell(self, resource): + def login_in_cloud_shell(self): credential = ManagedIdentityCredential() - decoded = self._decode_managed_identity_token(credential, resource) + decoded = self._decode_managed_identity_token(credential) cloud_shell_identity_info = { self.MANAGED_IDENTITY_TENANT_ID: decoded['tid'], @@ -260,12 +270,10 @@ def login_in_cloud_shell(self, resource): logger.warning('Using Cloud Shell Managed Identity: %s', json.dumps(cloud_shell_identity_info)) return credential, cloud_shell_identity_info - @staticmethod - def _decode_managed_identity_token(credential, resource): + def _decode_managed_identity_token(credential): # As Managed Identity doesn't have ID token, we need to get an initial access token and extract info from it - # The resource is only used for acquiring the initial access token - scope = resource.rstrip('/') + '/.default' - token = credential.get_token(scope) + # The scopes is only used for acquiring the initial access token + token = credential.get_token(*self.scopes) from msal.oauth2cli.oidc import decode_part access_token = token.token diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index e049e720f3a..86ac0362b5f 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -97,17 +97,23 @@ def _get_cloud_console_token_endpoint(): class Profile: def __init__(self, storage=None, auth_ctx_factory=None, use_global_creds_cache=True, - async_persist=True, cli_ctx=None): + async_persist=True, cli_ctx=None, scopes=None, client_id=None, store_adal_cache=True): from azure.cli.core import get_default_cli self.cli_ctx = cli_ctx or get_default_cli() self._storage = storage or ACCOUNT - self._management_resource_uri = self.cli_ctx.cloud.endpoints.management - self._ad_resource_uri = self.cli_ctx.cloud.endpoints.active_directory_resource_id + self._scopes = scopes + if self._scopes and not isinstance(self._scopes, (list, tuple)): + self._scopes = (self._scopes,) + self._client_id = client_id + self._authority = self.cli_ctx.cloud.endpoints.active_directory.replace('https://', '') - self._ad = self.cli_ctx.cloud.endpoints.active_directory - self._adal_cache = ADALCredentialCache(cli_ctx=self.cli_ctx) + + if store_adal_cache: + self._adal_cache = ADALCredentialCache(cli_ctx=self.cli_ctx) + else: + self._adal_cache = None # pylint: disable=too-many-branches,too-many-statements def login(self, @@ -124,7 +130,7 @@ def login(self, credential = None auth_record = None - identity = Identity(self._authority, tenant, cred_cache=self._adal_cache, + identity = Identity(self._authority, tenant, self._client_id, self._get_scopes(), cred_cache=self._adal_cache, allow_unencrypted=self.cli_ctx.config .getboolean('core', 'allow_fallback_to_plaintext', fallback=True) ) @@ -195,7 +201,8 @@ def login(self, self._set_subscriptions(consolidated) # todo: remove after ADAL token deprecation - self._adal_cache.persist_cached_creds() + if self._adal_cache: + self._adal_cache.persist_cached_creds() # use deepcopy as we don't want to persist these changes to file. return deepcopy(consolidated) @@ -206,9 +213,8 @@ def login_with_managed_identity(self, identity_id=None, allow_no_subscriptions=N # Managed identities for Azure resources is the new name for the service formerly known as # Managed Service Identity (MSI). - resource = self.cli_ctx.cloud.endpoints.active_directory_resource_id - identity = Identity() - credential, mi_info = identity.login_with_managed_identity(resource, identity_id) + identity = Identity(scopes=self._get_scopes()) + credential, mi_info = identity.login_with_managed_identity(identity_id) tenant = mi_info[Identity.MANAGED_IDENTITY_TENANT_ID] if find_subscriptions: @@ -249,9 +255,8 @@ def login_with_managed_identity(self, identity_id=None, allow_no_subscriptions=N def login_in_cloud_shell(self, allow_no_subscriptions=None, find_subscriptions=True): # TODO: deprecate allow_no_subscriptions - resource = self.cli_ctx.cloud.endpoints.active_directory_resource_id - identity = Identity() - credential, identity_info = identity.login_in_cloud_shell(resource) + identity = Identity(scopes=self._get_scopes()) + credential, identity_info = identity.login_in_cloud_shell() tenant = identity_info[Identity.MANAGED_IDENTITY_TENANT_ID] if find_subscriptions: @@ -274,6 +279,12 @@ def login_in_cloud_shell(self, allow_no_subscriptions=None, find_subscriptions=T self._set_subscriptions(consolidated) return deepcopy(consolidated) + def _get_scopes(self): + if self._scopes: + return self._scopes + else: + return (self.cli_ctx.cloud.endpoints.active_directory_resource_id.rstrip('/') + '/.default',) + def _normalize_properties(self, user, subscriptions, is_service_principal, cert_sn_issuer_auth=None, user_assigned_identity_id=None, home_account_id=None, managed_identity_info=None): import sys @@ -548,8 +559,7 @@ def _create_identity_credential(self, account, aux_tenant_id=None): identity_type, identity_id = Profile._try_parse_msi_account_name(account) tenant_id = aux_tenant_id if aux_tenant_id else account[_TENANT_ID] - authority = self.cli_ctx.cloud.endpoints.active_directory.replace('https://', '') - identity = Identity(authority, tenant_id, cred_cache=self._adal_cache) + identity = Identity(self._authority, tenant_id, self._client_id, cred_cache=self._adal_cache) if identity_type is None: if in_cloud_console() and account[_USER_ENTITY].get(_CLOUD_SHELL_ID): @@ -576,8 +586,13 @@ def get_login_credentials(self, resource=None, subscription_id=None, aux_subscri if aux_tenants and aux_subscriptions: raise CLIError("Please specify only one of aux_subscriptions and aux_tenants, not both") + if resource and self._scopes: + raise CLIError("Please specify only one of resource and scopes, not both") + if not self._scopes: + resource = resource or self.cli_ctx.cloud.endpoints.active_directory_resource_id + account = self.get_subscription(subscription_id) - resource = resource or self.cli_ctx.cloud.endpoints.active_directory_resource_id + external_tenants_info = [] if aux_tenants: external_tenants_info = [tenant for tenant in aux_tenants if tenant != account[_TENANT_ID]] @@ -594,7 +609,8 @@ def get_login_credentials(self, resource=None, subscription_id=None, aux_subscri from azure.cli.core.authentication import AuthenticationWrapper auth_object = AuthenticationWrapper(identity_credential, external_credentials=external_credentials if external_credentials else None, - resource=resource) + resource=resource, + scopes=self._scopes) return (auth_object, str(account[_SUBSCRIPTION_ID]), str(account[_TENANT_ID])) diff --git a/src/azure-cli-core/azure/cli/core/authentication.py b/src/azure-cli-core/azure/cli/core/authentication.py index 2abc9d86a05..d501d6ac901 100644 --- a/src/azure-cli-core/azure/cli/core/authentication.py +++ b/src/azure-cli-core/azure/cli/core/authentication.py @@ -25,7 +25,8 @@ def _create_scopes(resource): scope = resource + '/.default' else: scope = resource.rstrip('/') + '/.default' - return scope + # Return the single scope in tuple format + return (scope,) class AuthenticationWrapper(Authentication): @@ -35,14 +36,19 @@ def __init__(self, credential, **kwargs): # _external_credentials and _resource are only needed in Track1 SDK self._external_credentials = kwargs.pop("external_credentials", None) self._resource = kwargs.pop("resource", None) + self._scopes = kwargs.pop("scopes", None) def _get_token(self, *scopes): external_tenant_tokens = [] + + if not scopes: + scopes = self._scopes if not scopes: if self._resource: - scopes = [_create_scopes(self._resource)] + scopes = _create_scopes(self._resource) else: raise CLIError("Unexpected error: Resource or Scope need be specified to get access token") + try: token = self._credential.get_token(*scopes) if self._external_credentials: diff --git a/src/azure-cli-core/azure/cli/core/commands/client_factory.py b/src/azure-cli-core/azure/cli/core/commands/client_factory.py index 4010adecf2d..ef79f035a8e 100644 --- a/src/azure-cli-core/azure/cli/core/commands/client_factory.py +++ b/src/azure-cli-core/azure/cli/core/commands/client_factory.py @@ -137,15 +137,22 @@ def _get_mgmt_service_client(cli_ctx, api_version=None, base_url_bound=True, resource=None, + scopes=None, sdk_profile=None, aux_subscriptions=None, aux_tenants=None, **kwargs): from azure.cli.core._profile import Profile logger.debug('Getting management service client client_type=%s', client_type.__name__) - resource = resource or cli_ctx.cloud.endpoints.active_directory_resource_id - profile = Profile(cli_ctx=cli_ctx) - cred, subscription_id, _ = profile.get_login_credentials(subscription_id=subscription_id, resource=resource, + + if not scopes: + resource = resource or cli_ctx.cloud.endpoints.active_directory_resource_id + + client_id = kwargs.pop('client_id', None) + + profile = Profile(cli_ctx=cli_ctx, scopes=scopes, client_id=client_id) + cred, subscription_id, _ = profile.get_login_credentials(subscription_id=subscription_id, + resource=resource, aux_subscriptions=aux_subscriptions, aux_tenants=aux_tenants) @@ -158,6 +165,9 @@ def _get_mgmt_service_client(cli_ctx, client_kwargs['profile'] = sdk_profile if kwargs: client_kwargs.update(kwargs) + + if scopes: + client_kwargs['credential_scopes'] = scopes if is_track2(client_type): client_kwargs.update(configure_common_settings_track2(cli_ctx))