async def test_get_token(): """The credential should parse the CLI's output to an AccessToken""" access_token = "access token" valid_seconds = 42 successful_output = json.dumps({ # expiresOn is a naive datetime representing valid_seconds from the epoch "expiresOn": datetime.fromtimestamp(valid_seconds).strftime("%Y-%m-%d %H:%M:%S.%f"), "accessToken": access_token, "subscription": "some-guid", "tenant": "some-guid", "tokenType": "Bearer", }) with mock.patch(SUBPROCESS_EXEC, mock_exec(successful_output)): credential = AzureCliCredential() token = await credential.get_token("scope") assert token.token == access_token assert type(token.expires_on) == int assert token.expires_on == valid_seconds
async def test_cannot_execute_shell(): """The credential should raise CredentialUnavailableError when the subprocess doesn't start""" with mock.patch(SUBPROCESS_EXEC, mock.Mock(side_effect=OSError())): with pytest.raises(CredentialUnavailableError): credential = AzureCliCredential() await credential.get_token("scope")
async def test_get_token(): """The credential should parse the CLI's output to an AccessToken""" access_token = "access token" expected_expires_on = 1602015811 successful_output = json.dumps({ "expiresOn": datetime.fromtimestamp(expected_expires_on).strftime( "%Y-%m-%d %H:%M:%S.%f"), "accessToken": access_token, "subscription": "some-guid", "tenant": "some-guid", "tokenType": "Bearer", }) with mock.patch(SUBPROCESS_EXEC, mock_exec(successful_output)): credential = AzureCliCredential() token = await credential.get_token("scope") assert token.token == access_token assert type(token.expires_on) == int assert token.expires_on == expected_expires_on
async def test_cli_not_installed_windows(): """The credential should raise CredentialUnavailableError when the CLI isn't installed""" output = "'az' is not recognized as an internal or external command, operable program or batch file." with mock.patch(SUBPROCESS_EXEC, mock_exec(output, return_code=1)): with pytest.raises(CredentialUnavailableError, match=CLI_NOT_FOUND): credential = AzureCliCredential() await credential.get_token("scope")
async def test_not_logged_in(): """When the CLI isn't logged in, the credential should raise CredentialUnavailableError""" output = "ERROR: Please run 'az login' to setup account." with mock.patch(SUBPROCESS_EXEC, mock_exec(output, return_code=1)): with pytest.raises(CredentialUnavailableError, match=NOT_LOGGED_IN): credential = AzureCliCredential() await credential.get_token("scope")
async def test_unexpected_error(): """When the CLI returns an unexpected error, the credential should raise an error containing the CLI's output""" output = "something went wrong" with mock.patch(SUBPROCESS_EXEC, mock_exec(output, return_code=42)): with pytest.raises(ClientAuthenticationError, match=output): credential = AzureCliCredential() await credential.get_token("scope")
async def test_cli_not_installed_linux(): """The credential should raise CredentialUnavailableError when the CLI isn't installed""" output = "/bin/sh: 1: az: not found" with mock.patch(SUBPROCESS_EXEC, mock_exec(output, return_code=127)): with pytest.raises(CredentialUnavailableError, match=CLI_NOT_FOUND): credential = AzureCliCredential() await credential.get_token("scope")
async def test_not_logged_in(): """When the CLI isn't logged in, the credential should raise an error containing the CLI's output""" output = "ERROR: Please run 'az login' to setup account." with mock.patch(SUBPROCESS_EXEC, mock_exec(output, return_code=1)): with pytest.raises(ClientAuthenticationError, match=output): credential = AzureCliCredential() await credential.get_token("scope")
async def get_subscriptions(cls) -> list[dict[str, str]]: res = [] auth = AzureCliCredential() async with SubscriptionClient(auth) as client: subs = client.subscriptions.list() async for sub in subs: res.append(sub.as_dict()) return res
async def test_subprocess_error_does_not_expose_token(output): """Errors from the subprocess shouldn't expose access tokens in CLI output""" with mock.patch(SUBPROCESS_EXEC, mock_exec(output, return_code=1)): with pytest.raises(ClientAuthenticationError) as ex: credential = AzureCliCredential() await credential.get_token("scope") assert "secret value" not in str(ex.value) assert "secret value" not in repr(ex.value)
async def test_parsing_error_does_not_expose_token(output): """Errors during CLI output parsing shouldn't expose access tokens in that output""" with mock.patch(SUBPROCESS_EXEC, mock_exec(output)): with pytest.raises(ClientAuthenticationError) as ex: credential = AzureCliCredential() await credential.get_token("scope") assert "secret value" not in str(ex.value) assert "secret value" not in repr(ex.value)
def _initialize_credentials(self): if self.subscription_id is not None \ and self.arm_base_url is not None: if self.vscode_tenant_id is None: self.vscode_tenant_id = self._get_tenant_id( arm_base_url=self.arm_base_url, subscription_id=self.subscription_id) if self.shared_cache_tenant_id is None: self.shared_cache_tenant_id = self._get_tenant_id( arm_base_url=self.arm_base_url, subscription_id=self.subscription_id) if self.interactive_browser_tenant_id is None: self.interactive_browser_tenant_id = self._get_tenant_id( arm_base_url=self.arm_base_url, subscription_id=self.subscription_id) credentials = [] # type: List[AsyncTokenCredential] if not self.exclude_token_file_credential: credentials.append(_TokenFileCredential()) if not self.exclude_environment_credential: credentials.append(EnvironmentCredential(authority=self.authority)) if not self.exclude_managed_identity_credential: credentials.append( ManagedIdentityCredential( client_id=self.managed_identity_client_id)) if not self.exclude_shared_token_cache_credential and SharedTokenCacheCredential.supported( ): try: # username and/or tenant_id are only required when the cache contains tokens for multiple identities shared_cache = SharedTokenCacheCredential( username=self.shared_cache_username, tenant_id=self.shared_cache_tenant_id, authority=self.authority) credentials.append(shared_cache) except Exception as ex: # pylint:disable=broad-except _LOGGER.info("Shared token cache is unavailable: '%s'", ex) if not self.exclude_visual_studio_code_credential: credentials.append( VisualStudioCodeCredential(tenant_id=self.vscode_tenant_id)) if not self.exclude_cli_credential: credentials.append(AzureCliCredential()) if not self.exclude_powershell_credential: credentials.append(AzurePowerShellCredential()) if not self.exclude_interactive_browser_credential: credentials.append( InteractiveBrowserCredential( tenant_id=self.interactive_browser_tenant_id)) if not self.exclude_device_code_credential: credentials.append( DeviceCodeCredential( tenant_id=self.interactive_browser_tenant_id)) self.credentials = credentials
async def test_windows_fallback(): """The credential should fall back to the sync implementation when not using ProactorEventLoop on Windows""" sync_get_token = mock.Mock() with mock.patch("azure.identity.aio._credentials.azure_cli._SyncAzureCliCredential") as fallback: fallback.return_value = mock.Mock(get_token=sync_get_token) with mock.patch(AzureCliCredential.__module__ + ".asyncio.get_event_loop"): # asyncio.get_event_loop now returns Mock, i.e. never ProactorEventLoop credential = AzureCliCredential() await credential.get_token("scope") assert sync_get_token.call_count == 1
async 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 = "***" async def fake_exec(*args, **_): match = re.search("--tenant (.*)", args[-1]) assert match is None or match[1] == expected_tenant output = json.dumps({ "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), "accessToken": expected_token, "subscription": "some-guid", "tenant": expected_token, "tokenType": "Bearer", }).encode() return mock.Mock( communicate=mock.Mock(return_value=get_completed_future((output, b""))), returncode=0) credential = AzureCliCredential() with mock.patch(SUBPROCESS_EXEC, fake_exec): token = await credential.get_token("scope") assert token.token == expected_token # specifying a tenant should get an error with pytest.raises(ClientAuthenticationError, match="allow_multitenant_authentication"): await 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 = await 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"
async def test_allow_multitenant_authentication(): """When allow_multitenant_authentication is True, the credential should respect get_token(tenant_id=...)""" default_tenant = "first-tenant" first_token = "***" second_tenant = "second-tenant" second_token = first_token * 2 async def fake_exec(*args, **_): match = re.search("--tenant (.*)", args[-1]) tenant = match[1] if match else default_tenant assert tenant in ( default_tenant, second_tenant), 'unexpected tenant "{}"'.format(tenant) output = 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", }).encode() return mock.Mock( communicate=mock.Mock(return_value=get_completed_future((output, b""))), returncode=0) credential = AzureCliCredential(allow_multitenant_authentication=True) with mock.patch(SUBPROCESS_EXEC, fake_exec): token = await credential.get_token("scope") assert token.token == first_token token = await credential.get_token("scope", tenant_id=default_tenant) assert token.token == first_token token = await credential.get_token("scope", tenant_id=second_tenant) assert token.token == second_token # should still default to the first tenant token = await credential.get_token("scope") assert token.token == first_token
async def test_multitenant_authentication_not_allowed(): expected_tenant = "expected-tenant" expected_token = "***" async def fake_exec(*args, **_): match = re.search("--tenant (.*)", args[-1]) assert match is None or match[1] == expected_tenant output = json.dumps({ "expiresOn": datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), "accessToken": expected_token, "subscription": "some-guid", "tenant": expected_token, "tokenType": "Bearer", }).encode() return mock.Mock( communicate=mock.Mock(return_value=get_completed_future((output, b""))), returncode=0) credential = AzureCliCredential() with mock.patch(SUBPROCESS_EXEC, fake_exec): token = await credential.get_token("scope") assert token.token == expected_token with mock.patch.dict("os.environ", { EnvironmentVariables.AZURE_IDENTITY_DISABLE_MULTITENANTAUTH: "true" }): token = await credential.get_token("scope", tenant_id="un" + expected_tenant) assert token.token == expected_token
async def test_context_manager(): """The credential must be a context manager, although it does nothing as one because it has no transport""" async with AzureCliCredential(): pass
def __init__(self, subscription): self.subscription = subscription self.auth = AzureCliCredential()