diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 16ba5dd35901..6ef20be0fb71 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,6 +1,12 @@ # Release History -## 1.4.0b7 (Unreleased) +## 1.4.0b7 (2020-07-22) +- `DefaultAzureCredential` has a new optional keyword argument, +`visual_studio_code_tenant_id`, which sets the tenant the credential should +authenticate in when authenticating as the Azure user signed in to Visual +Studio Code. +- Renamed `AuthenticationRecord.deserialize` positional parameter `json_string` +to `data`. ## 1.4.0b6 (2020-07-07) diff --git a/sdk/identity/azure-identity/azure/identity/__init__.py b/sdk/identity/azure-identity/azure/identity/__init__.py index 5f408028a4c0..2e3f9d639a24 100644 --- a/sdk/identity/azure-identity/azure/identity/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/__init__.py @@ -6,7 +6,7 @@ from ._auth_record import AuthenticationRecord from ._exceptions import AuthenticationRequiredError, CredentialUnavailableError -from ._constants import KnownAuthorities +from ._constants import AzureAuthorityHosts, KnownAuthorities from ._credentials import ( AzureCliCredential, AuthorizationCodeCredential, @@ -26,9 +26,10 @@ __all__ = [ "AuthenticationRecord", - "AzureCliCredential", "AuthenticationRequiredError", "AuthorizationCodeCredential", + "AzureAuthorityHosts", + "AzureCliCredential", "CertificateCredential", "ChainedTokenCredential", "ClientSecretCredential", diff --git a/sdk/identity/azure-identity/azure/identity/_auth_record.py b/sdk/identity/azure-identity/azure/identity/_auth_record.py index 968e5e8a5588..2ae09877d101 100644 --- a/sdk/identity/azure-identity/azure/identity/_auth_record.py +++ b/sdk/identity/azure-identity/azure/identity/_auth_record.py @@ -43,11 +43,14 @@ def username(self): return self._username @classmethod - def deserialize(cls, json_string): + def deserialize(cls, data): # type: (str) -> AuthenticationRecord - """Deserialize a record from JSON""" + """Deserialize a record. - deserialized = json.loads(json_string) + :param str data: a serialized record + """ + + deserialized = json.loads(data) return cls( authority=deserialized["authority"], @@ -59,7 +62,10 @@ def deserialize(cls, json_string): def serialize(self): # type: () -> str - """Serialize the record to JSON""" + """Serialize the record. + + :rtype: str + """ record = { "authority": self._authority, diff --git a/sdk/identity/azure-identity/azure/identity/_constants.py b/sdk/identity/azure-identity/azure/identity/_constants.py index 4d217d7dc716..8bfb28d7adfa 100644 --- a/sdk/identity/azure-identity/azure/identity/_constants.py +++ b/sdk/identity/azure-identity/azure/identity/_constants.py @@ -11,13 +11,17 @@ DEFAULT_TOKEN_REFRESH_RETRY_DELAY = 30 -class KnownAuthorities: +class AzureAuthorityHosts: AZURE_CHINA = "login.chinacloudapi.cn" AZURE_GERMANY = "login.microsoftonline.de" AZURE_GOVERNMENT = "login.microsoftonline.us" AZURE_PUBLIC_CLOUD = "login.microsoftonline.com" +class KnownAuthorities(AzureAuthorityHosts): + """Alias of :class:`AzureAuthorityHosts`""" + + class EnvironmentVariables: AZURE_CLIENT_ID = "AZURE_CLIENT_ID" AZURE_CLIENT_SECRET = "AZURE_CLIENT_SECRET" diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/authorization_code.py b/sdk/identity/azure-identity/azure/identity/_credentials/authorization_code.py index c535d0371f13..b00a15145e7d 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/authorization_code.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/authorization_code.py @@ -26,7 +26,7 @@ class AuthorizationCodeCredential(object): :param str redirect_uri: The application's redirect URI. Must match the URI used to request the authorization code. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword str client_secret: One of the application's client secrets. Required only for web apps and web APIs. """ diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/browser.py b/sdk/identity/azure-identity/azure/identity/_credentials/browser.py index 50c8b754922d..cf860f5b39f1 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/browser.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/browser.py @@ -30,7 +30,7 @@ class InteractiveBrowserCredential(InteractiveCredential): https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword str tenant_id: an Azure Active Directory tenant ID. Defaults to the 'organizations' tenant, which can authenticate work or school accounts. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py index f0f032d49c9a..35c81b2e3da5 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py @@ -20,7 +20,7 @@ class CertificateCredential(CertificateCredentialBase): :param str certificate_path: path to a PEM-encoded certificate file including the private key. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword password: The certificate's password. If a unicode string, it will be encoded as UTF-8. If the certificate requires a different encoding, pass appropriately encoded bytes instead. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py index 94da7b0655a9..a327416cd731 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py @@ -24,7 +24,7 @@ class ClientSecretCredential(ClientSecretCredentialBase): :param str client_secret: one of the service principal's client secrets :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache. Defaults to False. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 7282630aa80f..3be7a513bbd4 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -46,7 +46,7 @@ class DefaultAzureCredential(ChainedTokenCredential): This default behavior is configurable with keyword arguments. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. Managed identities ignore this because they reside in a single cloud. :keyword bool exclude_cli_credential: Whether to exclude the Azure CLI from the credential. Defaults to **False**. :keyword bool exclude_environment_credential: Whether to exclude a service principal configured by environment @@ -66,6 +66,8 @@ class DefaultAzureCredential(ChainedTokenCredential): Defaults to the value of environment variable AZURE_USERNAME, if any. :keyword str shared_cache_tenant_id: Preferred tenant for :class:`~azure.identity.SharedTokenCacheCredential`. Defaults to the value of environment variable AZURE_TENANT_ID, if any. + :keyword str visual_studio_code_tenant_id: Tenant ID to use when authenticating with + :class:`~azure.identity.VSCodeCredential`. """ def __init__(self, **kwargs): @@ -82,6 +84,10 @@ def __init__(self, **kwargs): "shared_cache_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID) ) + vscode_tenant_id = kwargs.pop( + "visual_studio_code_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID) + ) + exclude_environment_credential = kwargs.pop("exclude_environment_credential", False) exclude_managed_identity_credential = kwargs.pop("exclude_managed_identity_credential", False) exclude_shared_token_cache_credential = kwargs.pop("exclude_shared_token_cache_credential", False) @@ -104,7 +110,7 @@ def __init__(self, **kwargs): except Exception as ex: # pylint:disable=broad-except _LOGGER.info("Shared token cache is unavailable: '%s'", ex) if not exclude_visual_studio_code_credential: - credentials.append(VSCodeCredential()) + credentials.append(VSCodeCredential(tenant_id=vscode_tenant_id)) if not exclude_cli_credential: credentials.append(AzureCliCredential()) if not exclude_interactive_browser_credential: diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py b/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py index fc0b0a78d99d..87fc9e738a31 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py @@ -32,7 +32,7 @@ class DeviceCodeCredential(InteractiveCredential): :param str client_id: the application's ID :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword str tenant_id: an Azure Active Directory tenant ID. Defaults to the 'organizations' tenant, which can authenticate work or school accounts. **Required for single-tenant applications.** diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py index 27b7ef30d074..741dcc30bf03 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py @@ -27,7 +27,7 @@ class SharedTokenCacheCredential(SharedTokenCacheBase): contains tokens for multiple identities. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword str tenant_id: an Azure Active Directory tenant ID. Used to select an account when the cache contains tokens for multiple identities. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py b/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py index d2a7a80c342a..1c6c1b3561d6 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py @@ -29,7 +29,7 @@ class UsernamePasswordCredential(InteractiveCredential): :param str password: the user's password :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword str tenant_id: tenant ID or a domain associated with a tenant. If not provided, defaults to the 'organizations' tenant, which supports only Azure Active Directory work or school accounts. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py b/sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py index 76273368a5ab..39484f4f677b 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py @@ -24,12 +24,22 @@ class VSCodeCredential(object): - """Authenticates by redeeming a refresh token previously saved by VS Code""" + """Authenticates as the Azure user signed in to Visual Studio Code. + + :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` + defines authorities for other clouds. + :keyword str tenant_id: ID of the tenant the credential should authenticate in. Defaults to the "organizations" + tenant, which supports only Azure Active Directory work or school accounts. + """ def __init__(self, **kwargs): # type: (**Any) -> None - self._client = kwargs.pop("_client", None) or AadClient("organizations", AZURE_VSCODE_CLIENT_ID, **kwargs) self._refresh_token = None + self._client = kwargs.pop("_client", None) + if not self._client: + tenant_id = kwargs.pop("tenant_id", None) or "organizations" + self._client = AadClient(tenant_id, AZURE_VSCODE_CLIENT_ID, **kwargs) @log_get_token("VSCodeCredential") def get_token(self, *scopes, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/authorization_code.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/authorization_code.py index 7528fbb05516..2701716fe4d9 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/authorization_code.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/authorization_code.py @@ -27,7 +27,7 @@ class AuthorizationCodeCredential(AsyncCredentialBase): :param str redirect_uri: The application's redirect URI. Must match the URI used to request the authorization code. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword str client_secret: One of the application's client secrets. Required only for web apps and web APIs. """ diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py index 180d9cba11e5..2842d32b918d 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py @@ -22,7 +22,7 @@ class CertificateCredential(CertificateCredentialBase, AsyncCredentialBase): :param str certificate_path: path to a PEM-encoded certificate file including the private key :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword password: The certificate's password. If a unicode string, it will be encoded as UTF-8. If the certificate requires a different encoding, pass appropriately encoded bytes instead. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py index f6249dc61ec0..bbc0aa98e472 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py @@ -22,7 +22,7 @@ class ClientSecretCredential(AsyncCredentialBase, ClientSecretCredentialBase): :param str client_secret: one of the service principal's client secrets :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache. Defaults to False. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py index fb1c066390b5..f5a1082b2b43 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -39,7 +39,7 @@ class DefaultAzureCredential(ChainedTokenCredential): This default behavior is configurable with keyword arguments. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. Managed identities ignore this because they reside in a single cloud. :keyword bool exclude_cli_credential: Whether to exclude the Azure CLI from the credential. Defaults to **False**. :keyword bool exclude_environment_credential: Whether to exclude a service principal configured by environment @@ -54,6 +54,8 @@ class DefaultAzureCredential(ChainedTokenCredential): Defaults to the value of environment variable AZURE_USERNAME, if any. :keyword str shared_cache_tenant_id: Preferred tenant for :class:`~azure.identity.SharedTokenCacheCredential`. Defaults to the value of environment variable AZURE_TENANT_ID, if any. + :keyword str visual_studio_code_tenant_id: Tenant ID to use when authenticating with + :class:`~azure.identity.VSCodeCredential`. """ def __init__(self, **kwargs: "Any") -> None: @@ -65,6 +67,10 @@ def __init__(self, **kwargs: "Any") -> None: "shared_cache_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID) ) + vscode_tenant_id = kwargs.pop( + "visual_studio_code_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID) + ) + exclude_visual_studio_code_credential = kwargs.pop("exclude_visual_studio_code_credential", False) exclude_cli_credential = kwargs.pop("exclude_cli_credential", False) exclude_environment_credential = kwargs.pop("exclude_environment_credential", False) @@ -87,7 +93,7 @@ def __init__(self, **kwargs: "Any") -> None: # transitive dependency pywin32 doesn't support 3.8 (https://github.com/mhammond/pywin32/issues/1431) _LOGGER.info("Shared token cache is unavailable: '%s'", ex) if not exclude_visual_studio_code_credential: - credentials.append(VSCodeCredential()) + credentials.append(VSCodeCredential(tenant_id=vscode_tenant_id)) if not exclude_cli_credential: credentials.append(AzureCliCredential()) diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py index 7b7ef7c28590..a737b8bcecfc 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py @@ -25,7 +25,7 @@ class SharedTokenCacheCredential(SharedTokenCacheBase, AsyncCredentialBase): may contain tokens for multiple identities. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', - the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. :keyword str tenant_id: an Azure Active Directory tenant ID. Used to select an account when the cache contains tokens for multiple identities. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py index eb235bb19776..c27247aedce3 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py @@ -18,11 +18,21 @@ class VSCodeCredential(AsyncCredentialBase): - """Authenticates by redeeming a refresh token previously saved by VS Code""" + """Authenticates as the Azure user signed in to Visual Studio Code. + + :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` + defines authorities for other clouds. + :keyword str tenant_id: ID of the tenant the credential should authenticate in. Defaults to the "organizations" + tenant, which supports only Azure Active Directory work or school accounts. + """ def __init__(self, **kwargs: "Any") -> None: - self._client = kwargs.pop("_client", None) or AadClient("organizations", AZURE_VSCODE_CLIENT_ID, **kwargs) self._refresh_token = None + self._client = kwargs.pop("_client", None) + if not self._client: + tenant_id = kwargs.pop("tenant_id", None) or "organizations" + self._client = AadClient(tenant_id, AZURE_VSCODE_CLIENT_ID, **kwargs) async def __aenter__(self): if self._client: diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index ac38d9e3d417..5da1366af0e4 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -221,6 +221,30 @@ def test_shared_cache_username(): assert token.token == expected_access_token +def test_vscode_tenant_id(): + """the credential should allow configuring a tenant ID for VSCodeCredential by kwarg or environment""" + + expected_args = {"tenant_id": "the-tenant"} + + with patch(DefaultAzureCredential.__module__ + ".VSCodeCredential") as mock_credential: + DefaultAzureCredential(visual_studio_code_tenant_id=expected_args["tenant_id"]) + mock_credential.assert_called_once_with(**expected_args) + + # tenant id can also be specified in $AZURE_TENANT_ID + with patch.dict(os.environ, {EnvironmentVariables.AZURE_TENANT_ID: expected_args["tenant_id"]}, clear=True): + with patch(DefaultAzureCredential.__module__ + ".VSCodeCredential") as mock_credential: + DefaultAzureCredential() + mock_credential.assert_called_once_with(**expected_args) + + # keyword argument should override environment variable + with patch.dict( + os.environ, {EnvironmentVariables.AZURE_TENANT_ID: "not-" + expected_args["tenant_id"]}, clear=True + ): + with patch(DefaultAzureCredential.__module__ + ".VSCodeCredential") as mock_credential: + DefaultAzureCredential(visual_studio_code_tenant_id=expected_args["tenant_id"]) + mock_credential.assert_called_once_with(**expected_args) + + @patch(DefaultAzureCredential.__module__ + ".SharedTokenCacheCredential") def test_default_credential_shared_cache_use(mock_credential): mock_credential.supported = Mock(return_value=False) @@ -265,7 +289,7 @@ def test_interactive_browser_tenant_id(): def validate_tenant_id(credential): assert len(credential.call_args_list) == 1, "InteractiveBrowserCredential should be instantiated once" _, kwargs = credential.call_args - assert kwargs == {'tenant_id': tenant_id} + assert kwargs == {"tenant_id": tenant_id} with patch(DefaultAzureCredential.__module__ + ".InteractiveBrowserCredential") as mock_credential: DefaultAzureCredential(exclude_interactive_browser_credential=False, interactive_browser_tenant_id=tenant_id) @@ -280,5 +304,7 @@ def validate_tenant_id(credential): # keyword argument should override environment variable with patch.dict(os.environ, {EnvironmentVariables.AZURE_TENANT_ID: "not-" + tenant_id}, clear=True): with patch(DefaultAzureCredential.__module__ + ".InteractiveBrowserCredential") as mock_credential: - DefaultAzureCredential(exclude_interactive_browser_credential=False, interactive_browser_tenant_id=tenant_id) + DefaultAzureCredential( + exclude_interactive_browser_credential=False, interactive_browser_tenant_id=tenant_id + ) validate_tenant_id(mock_credential) diff --git a/sdk/identity/azure-identity/tests/test_default_async.py b/sdk/identity/azure-identity/tests/test_default_async.py index 16417eec2325..c345a8f44844 100644 --- a/sdk/identity/azure-identity/tests/test_default_async.py +++ b/sdk/identity/azure-identity/tests/test_default_async.py @@ -206,6 +206,30 @@ async def test_shared_cache_username(): assert token.token == expected_access_token +def test_vscode_tenant_id(): + """the credential should allow configuring a tenant ID for VSCodeCredential by kwarg or environment""" + + expected_args = {"tenant_id": "the-tenant"} + + with patch(DefaultAzureCredential.__module__ + ".VSCodeCredential") as mock_credential: + DefaultAzureCredential(visual_studio_code_tenant_id=expected_args["tenant_id"]) + mock_credential.assert_called_once_with(**expected_args) + + # tenant id can also be specified in $AZURE_TENANT_ID + with patch.dict(os.environ, {EnvironmentVariables.AZURE_TENANT_ID: expected_args["tenant_id"]}, clear=True): + with patch(DefaultAzureCredential.__module__ + ".VSCodeCredential") as mock_credential: + DefaultAzureCredential() + mock_credential.assert_called_once_with(**expected_args) + + # keyword argument should override environment variable + with patch.dict( + os.environ, {EnvironmentVariables.AZURE_TENANT_ID: "not-" + expected_args["tenant_id"]}, clear=True + ): + with patch(DefaultAzureCredential.__module__ + ".VSCodeCredential") as mock_credential: + DefaultAzureCredential(visual_studio_code_tenant_id=expected_args["tenant_id"]) + mock_credential.assert_called_once_with(**expected_args) + + @pytest.mark.asyncio async def test_default_credential_shared_cache_use(): with patch(DefaultAzureCredential.__module__ + ".SharedTokenCacheCredential") as mock_credential: diff --git a/sdk/identity/azure-identity/tests/test_vscode_credential.py b/sdk/identity/azure-identity/tests/test_vscode_credential.py index 6f22ad7ad8d6..ed5a0f5af235 100644 --- a/sdk/identity/azure-identity/tests/test_vscode_credential.py +++ b/sdk/identity/azure-identity/tests/test_vscode_credential.py @@ -7,9 +7,11 @@ from azure.core.credentials import AccessToken from azure.identity import CredentialUnavailableError, VSCodeCredential from azure.core.pipeline.policies import SansIOHTTPPolicy +from azure.identity._constants import EnvironmentVariables from azure.identity._internal.user_agent import USER_AGENT from azure.identity._credentials.vscode_credential import get_credentials import pytest +from six.moves.urllib_parse import urlparse from helpers import build_aad_response, mock_response, Request, validating_transport @@ -50,6 +52,39 @@ def test_user_agent(): credential.get_token("scope") +@pytest.mark.parametrize("authority", ("localhost", "https://localhost")) +def test_request_url(authority): + """the credential should accept an authority, with or without scheme, as an argument or environment variable""" + + tenant_id = "expected_tenant" + access_token = "***" + parsed_authority = urlparse(authority) + expected_netloc = parsed_authority.netloc or authority # "localhost" parses to netloc "", path "localhost" + expected_refresh_token = "refresh-token" + + def mock_send(request, **kwargs): + actual = urlparse(request.url) + assert actual.scheme == "https" + assert actual.netloc == expected_netloc + assert actual.path.startswith("/" + tenant_id) + assert request.body["refresh_token"] == expected_refresh_token + return mock_response(json_payload={"token_type": "Bearer", "expires_in": 42, "access_token": access_token}) + + credential = VSCodeCredential( + tenant_id=tenant_id, transport=mock.Mock(send=mock_send), authority=authority + ) + with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value=expected_refresh_token): + token = credential.get_token("scope") + assert token.token == access_token + + # authority can be configured via environment variable + with mock.patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): + credential = VSCodeCredential(tenant_id=tenant_id, transport=mock.Mock(send=mock_send)) + with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value=expected_refresh_token): + credential.get_token("scope") + assert token.token == access_token + + def test_credential_unavailable_error(): with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value=None): credential = VSCodeCredential() diff --git a/sdk/identity/azure-identity/tests/test_vscode_credential_async.py b/sdk/identity/azure-identity/tests/test_vscode_credential_async.py index 5207c73641fe..1a6dd7059cf6 100644 --- a/sdk/identity/azure-identity/tests/test_vscode_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_vscode_credential_async.py @@ -3,12 +3,13 @@ # Licensed under the MIT License. # ------------------------------------ from unittest import mock +from urllib.parse import urlparse -import sys from azure.core.credentials import AccessToken from azure.identity import CredentialUnavailableError -from azure.identity.aio import VSCodeCredential +from azure.identity._constants import EnvironmentVariables from azure.identity._internal.user_agent import USER_AGENT +from azure.identity.aio import VSCodeCredential from azure.core.pipeline.policies import SansIOHTTPPolicy import pytest @@ -50,6 +51,38 @@ async def test_user_agent(): await credential.get_token("scope") +@pytest.mark.asyncio +@pytest.mark.parametrize("authority", ("localhost", "https://localhost")) +async def test_request_url(authority): + """the credential should accept an authority, with or without scheme, as an argument or environment variable""" + + tenant_id = "expected_tenant" + access_token = "***" + parsed_authority = urlparse(authority) + expected_netloc = parsed_authority.netloc or authority # "localhost" parses to netloc "", path "localhost" + expected_refresh_token = "refresh-token" + + async def mock_send(request, **kwargs): + actual = urlparse(request.url) + assert actual.scheme == "https" + assert actual.netloc == expected_netloc + assert actual.path.startswith("/" + tenant_id) + assert request.body["refresh_token"] == expected_refresh_token + return mock_response(json_payload={"token_type": "Bearer", "expires_in": 42, "access_token": access_token}) + + credential = VSCodeCredential(tenant_id=tenant_id, transport=mock.Mock(send=mock_send), authority=authority) + with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value=expected_refresh_token): + token = await credential.get_token("scope") + assert token.token == access_token + + # authority can be configured via environment variable + with mock.patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): + credential = VSCodeCredential(tenant_id=tenant_id, transport=mock.Mock(send=mock_send)) + with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value=expected_refresh_token): + await credential.get_token("scope") + assert token.token == access_token + + @pytest.mark.asyncio async def test_credential_unavailable_error(): with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value=None):