def test_multitenant_authentication_not_allowed(): expected_tenant = "expected-tenant" expected_token = "***" def fake_check_output(command_line, **_): match = re.search("--tenant (.*)", command_line[-1]) assert match is None or match[1] == expected_tenant return json.dumps( { "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), "accessToken": expected_token, "subscription": "some-guid", "tenant": expected_token, "tokenType": "Bearer", } ) credential = AzureCliCredential() with mock.patch(CHECK_OUTPUT, fake_check_output): token = credential.get_token("scope") assert token.token == expected_token with mock.patch.dict( "os.environ", {EnvironmentVariables.AZURE_IDENTITY_DISABLE_MULTITENANTAUTH: "true"} ): token = credential.get_token("scope", tenant_id="un" + expected_tenant) assert token.token == expected_token
def test_multitenant_authentication(): default_tenant = "first-tenant" first_token = "***" second_tenant = "second-tenant" second_token = first_token * 2 def fake_check_output(command_line, **_): match = re.search("--tenant (.*)", command_line[-1]) tenant = match.groups()[0] if match else default_tenant assert tenant in (default_tenant, second_tenant), 'unexpected tenant "{}"'.format(tenant) return json.dumps( { "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), "accessToken": first_token if tenant == default_tenant else second_token, "subscription": "some-guid", "tenant": tenant, "tokenType": "Bearer", } ) credential = AzureCliCredential() with mock.patch(CHECK_OUTPUT, fake_check_output): token = credential.get_token("scope") assert token.token == first_token token = credential.get_token("scope", tenant_id=default_tenant) assert token.token == first_token token = credential.get_token("scope", tenant_id=second_tenant) assert token.token == second_token # should still default to the first tenant token = credential.get_token("scope") assert token.token == first_token
def _get_azcli_token(self, subscription: str = None) -> str: "retrieve token from azcli login" token = None tenant = self._authority if subscription is None else None self._current_authentication_method = self._current_authentication_method = AuthenticationMethod.azcli_login_subscription if subscription is not None else AuthenticationMethod.azcli_login try: from azure.identity import AzureCliCredential try: credential = AzureCliCredential() access_token = credential.get_token(self._resource) expires_datetime = datetime.fromtimestamp( access_token.expires_on) token = { 'accessToken': access_token.token, 'expiresOn': expires_datetime.strftime("%Y-%m-%d %H:%M:%S.%f"), 'tokenType': 'Bearer', } except: pass except [ImportError, ModuleNotFoundError]: raise AuthenticationError( "Azure CLI authentication requires 'azure-cli-core' to be installed." ) except: pass logger().debug( f"_MyAadHelper::_get_azcli_token {'failed' if token is None else 'succeeded'} to get token - subscription: '{subscription}', tenant: '{tenant}'" ) return token
class AzCliTokenProvider(CloudInfoTokenProvider): """AzCli Token Provider obtains a refresh token from the AzCli cache and uses it to authenticate with MSAL""" def __init__(self, kusto_uri: str, is_async: bool = False): super().__init__(kusto_uri, is_async) self._az_auth_context = None self._az_auth_context_async = None self._az_token = None @staticmethod def name() -> str: return "AzCliTokenProvider" def _context_impl(self) -> dict: return {"authority:": self.name()} def _init_impl(self): pass def _get_token_impl(self) -> Optional[dict]: try: if self._az_auth_context is None: self._az_auth_context = AzureCliCredential() self._az_token = self._az_auth_context.get_token(self._scopes[0]) return { TokenConstants.AZ_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.AZ_ACCESS_TOKEN: self._az_token.token } except Exception as e: raise KustoClientError( "Failed to obtain Az Cli token for '{0}'.\nPlease be sure AzCli version 2.3.0 and above is intalled.\n{1}" .format(self._kusto_uri, e)) async def _get_token_impl_async(self) -> Optional[dict]: try: if self._az_auth_context_async is None: self._az_auth_context_async = AsyncAzureCliCredential() self._az_token = await self._az_auth_context_async.get_token( self._scopes[0]) return { TokenConstants.AZ_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.AZ_ACCESS_TOKEN: self._az_token.token } except Exception as e: raise KustoClientError( "Failed to obtain Az Cli token for '{0}'.\nPlease be sure AzCli version 2.3.0 and above is installed.\n{1}" .format(self._kusto_uri, e)) def _get_token_from_cache_impl(self) -> Optional[dict]: if self._az_token is not None: # A token is considered valid if it is due to expire in no less than 10 minutes cur_time = time.time() if (self._az_token.expires_on - 600) > cur_time: return { TokenConstants.MSAL_TOKEN_TYPE: TokenConstants.BEARER_TYPE, TokenConstants.MSAL_ACCESS_TOKEN: self._az_token.token } return None
def _get_token(self): try: credential = AzureCliCredential() azureToken = credential.get_token(self.scope) except Exception as ex: raise RuntimeError(str(ex)) return azureToken.token
def test_multitenant_authentication_not_allowed(): """get_token(tenant_id=...) should raise when allow_multitenant_authentication is False (the default)""" expected_tenant = "expected-tenant" expected_token = "***" def fake_check_output(command_line, **_): match = re.search("--tenant (.*)", command_line[-1]) assert match is None or match[1] == expected_tenant return json.dumps({ "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), "accessToken": expected_token, "subscription": "some-guid", "tenant": expected_token, "tokenType": "Bearer", }) credential = AzureCliCredential() with mock.patch(CHECK_OUTPUT, fake_check_output): token = credential.get_token("scope") assert token.token == expected_token # specifying a tenant should get an error with pytest.raises(ClientAuthenticationError, match="allow_multitenant_authentication"): credential.get_token("scope", tenant_id="un" + expected_tenant) # ...unless the compat switch is enabled with mock.patch.dict( "os.environ", { EnvironmentVariables.AZURE_IDENTITY_ENABLE_LEGACY_TENANT_SELECTION: "true" }): token = credential.get_token("scope", tenant_id="un" + expected_tenant) assert ( token.token == expected_token ), "credential should ignore tenant_id kwarg when the compat switch is enabled"
class CredentialWrapper(BasicTokenAuthentication): def __init__(self, credential=None, resource_id="https://management.azure.com/.default", **kwargs): """Wrap any azure-identity credential to work with SDK that needs azure.common.credentials/msrestazure. Default resource is ARM (syntax of endpoint v2) :param credential: Any azure-identity credential (DefaultAzureCredential by default) :param str resource_id: The scope to use to get the token (default ARM) """ super(CredentialWrapper, self).__init__(None) self.credential = credential if credential is None: self.credential = AzureCliCredential() self._policy = BearerTokenCredentialPolicy(self.credential, resource_id, **kwargs) def _make_request(self): return PipelineRequest( HttpRequest( "CredentialWrapper", "https://fakeurl" ), PipelineContext(None) ) def set_token(self): """Ask the azure-core BearerTokenCredentialPolicy policy to get a token. Using the policy gives us for free the caching system of azure-core. We could make this code simpler by using private method, but by definition I can't assure they will be there forever, so mocking a fake call to the policy to extract the token, using 100% public API.""" request = self._make_request() self._policy.on_request(request) # Read Authorization, and get the second part after Bearer token = request.http_request.headers["Authorization"].split(" ", 1)[1] self.token = {"access_token": token} def signed_session(self, session=None): self.set_token() return super(CredentialWrapper, self).signed_session(session) def get_token(self, *scopes, **kwargs): # Pass get_token call to credential return self.credential.get_token(*scopes, **kwargs)
class AzureCredential: def __init__(self, cloud_endpoints, authorization_file=None, subscription_id_override=None): # type: (*str, *str) -> None if authorization_file: with open(authorization_file) as json_file: self._auth_params = json.load(json_file) else: self._auth_params = { 'client_id': os.environ.get(constants.ENV_CLIENT_ID), 'client_secret': os.environ.get(constants.ENV_CLIENT_SECRET), 'access_token': os.environ.get(constants.ENV_ACCESS_TOKEN), 'tenant_id': os.environ.get(constants.ENV_TENANT_ID), 'use_msi': bool(os.environ.get(constants.ENV_USE_MSI)), 'subscription_id': os.environ.get(constants.ENV_SUB_ID), 'keyvault_client_id': os.environ.get(constants.ENV_KEYVAULT_CLIENT_ID), 'keyvault_secret_id': os.environ.get(constants.ENV_KEYVAULT_SECRET_ID), 'enable_cli_auth': True } self._auth_params[ 'authority'] = cloud_endpoints.endpoints.active_directory keyvault_client_id = self._auth_params.get('keyvault_client_id') keyvault_secret_id = self._auth_params.get('keyvault_secret_id') # If user provided KeyVault secret, we will pull auth params information from it try: if keyvault_secret_id: self._auth_params.update( json.loads( get_keyvault_secret(keyvault_client_id, keyvault_secret_id))) except HTTPError as e: e.message = 'Failed to retrieve SP credential ' \ 'from Key Vault with client id: {0}'.format(keyvault_client_id) raise self._credential = None if self._auth_params.get('access_token') is not None: auth_name = 'Access Token' pass elif (self._auth_params.get('client_id') and self._auth_params.get('client_secret') and self._auth_params.get('tenant_id')): auth_name = 'Principal' self._credential = ClientSecretCredential( client_id=self._auth_params['client_id'], client_secret=self._auth_params['client_secret'], tenant_id=self._auth_params['tenant_id'], authority=self._auth_params['authority']) elif self._auth_params.get('use_msi'): auth_name = 'MSI' self._credential = ManagedIdentityCredential( client_id=self._auth_params.get('client_id')) elif self._auth_params.get('enable_cli_auth'): auth_name = 'Azure CLI' self._credential = AzureCliCredential() account_info, error = _run_command('az account show --output json') account_json = json.loads(account_info) self._auth_params['subscription_id'] = account_json['id'] self._auth_params['tenant_id'] = account_json['tenantId'] if error is not None: raise Exception('Unable to query TenantId and SubscriptionId') if subscription_id_override is not None: self._auth_params['subscription_id'] = subscription_id_override self._subscription_id = self._auth_params['subscription_id'] self._tenant_id = self._auth_params['tenant_id'] log.info('Authenticated [%s | %s%s]', auth_name, self.subscription_id, ' | Authorization File' if authorization_file else '') def get_token(self, *scopes, **kwargs): # Access Token is used only in tests realistically because # KeyVault, Storage and mgmt plane requires separate tokens. # TODO: Should we scope this to tests only? if (self._auth_params.get('access_token')): return AccessToken(self._auth_params['access_token'], expires_on=0) try: return self._credential.get_token(*scopes, **kwargs) except Exception as e: log.error('Failed to authenticate.\nMessage: {}'.format(e)) exit(1) # This is temporary until all SDKs we use are upgraded to Track 2 # List of legacy users: # - DNS # - Record Set (uses DNS SDK) # - Azure Graph def legacy_credentials(self, scope): # Track 2 SDKs use tuple token = self.get_token((scope + '.default')) return BasicTokenAuthentication(token={'access_token': token.token}) @property def tenant_id(self): # type: (None) -> str return self._tenant_id @property def auth_params(self): # type: (None) -> str return self._auth_params @property def subscription_id(self): # type: (None) -> str return self._subscription_id
class AzureCliAuthenticationContext(AzmetaAuthenticationContext): def __init__(self): self._credential = AzureCliCredential() def get_token(self, scopes: Union[Sequence[str], str]) -> AccessToken: return self._credential.get_token(scopes)