From 5555a55bccf1e37af66380bbe895c054c3e8e330 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Fri, 31 Jan 2025 16:49:03 -0800 Subject: [PATCH 01/19] Added AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST support --- sdk/identity/azure-identity/CHANGELOG.md | 1 + .../azure/identity/_credentials/default.py | 105 ++++++++++++------ .../identity/aio/_credentials/default.py | 75 +++++++------ .../azure-identity/tests/test_default.py | 75 +++++++++++++ .../tests/test_default_async.py | 16 +++ 5 files changed, 204 insertions(+), 68 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 831b8518e227..b4d3cf1f171f 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features Added - Added `subscription` parameter to `AzureCliCredential` to specify the subscription to use when authenticating with the Azure CLI. ([#37994](https://github.com/Azure/azure-sdk-for-python/pull/37994)) +- Added support for environment variable `AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST`. ### Breaking Changes diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 426fad01e9ca..024e31cdf223 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -23,6 +23,19 @@ _LOGGER = logging.getLogger(__name__) +VALID_CREDENTIALS = [ + "DEVELOPER_CLI", + "WORKLOAD_IDENTITY", + "CLI", + "ENVIRONMENT", + "MANAGED_IDENTITY", + "POWERSHELL", + "VISUAL_STUDIO_CODE", + "SHARED_CACHE", + "INTERACTIVE_BROWSER", +] + + class DefaultAzureCredential(ChainedTokenCredential): """A credential capable of handling most Azure SDK authentication scenarios. For more information, See `Usage guidance for DefaultAzureCredential @@ -48,22 +61,10 @@ class DefaultAzureCredential(ChainedTokenCredential): :keyword str authority: Authority of a Microsoft Entra 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. Managed identities ignore this because they reside in a single cloud. - :keyword bool exclude_workload_identity_credential: Whether to exclude the workload identity from the credential. - Defaults to **False**. - :keyword bool exclude_developer_cli_credential: Whether to exclude the Azure Developer CLI - from the credential. Defaults to **False**. - :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 - variables from the credential. Defaults to **False**. - :keyword bool exclude_managed_identity_credential: Whether to exclude managed identity from the credential. - Defaults to **False**. - :keyword bool exclude_powershell_credential: Whether to exclude Azure PowerShell. Defaults to **False**. - :keyword bool exclude_visual_studio_code_credential: Whether to exclude stored credential from VS Code. - Defaults to **True**. - :keyword bool exclude_shared_token_cache_credential: Whether to exclude the shared token cache. Defaults to - **False**. - :keyword bool exclude_interactive_browser_credential: Whether to exclude interactive browser authentication (see - :class:`~azure.identity.InteractiveBrowserCredential`). Defaults to **True**. + :keyword str default_credential_allow_list: A semicolon-separated list of credential names. + The default is to try all available credentials. If this is set, only the credentials in the list are tried. + e.g. "ENVIRONMENT;CLI;MANAGED_IDENTITY" will only try EnvironmentCredential, AzureCliCredential, and + ManagedIdentityCredential. :keyword str interactive_browser_tenant_id: Tenant ID to use when authenticating a user through :class:`~azure.identity.InteractiveBrowserCredential`. Defaults to the value of environment variable AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants. @@ -100,7 +101,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement if "tenant_id" in kwargs: raise TypeError("'tenant_id' is not supported in DefaultAzureCredential.") - authority = kwargs.pop("authority", None) + authority: Optional[str] = kwargs.pop("authority", None) vscode_tenant_id = kwargs.pop( "visual_studio_code_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID) @@ -144,45 +145,58 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False) credentials: List[SupportsTokenInfo] = [] + avail_credentials = {} within_dac.set(True) + if not exclude_environment_credential: - credentials.append(EnvironmentCredential(authority=authority, _within_dac=True, **kwargs)) + env_cred = EnvironmentCredential(authority=authority, _within_dac=True, **kwargs) + avail_credentials["ENVIRONMENT"] = env_cred + credentials.append(env_cred) if not exclude_workload_identity_credential: if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): client_id = workload_identity_client_id - credentials.append( - WorkloadIdentityCredential( - client_id=cast(str, client_id), - tenant_id=workload_identity_tenant_id, - token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE], - **kwargs - ) + workload_cred = WorkloadIdentityCredential( + client_id=cast(str, client_id), + tenant_id=workload_identity_tenant_id, + token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE], + **kwargs, ) + avail_credentials["WORKLOAD_IDENTITY"] = workload_cred + credentials.append(workload_cred) if not exclude_managed_identity_credential: - credentials.append( - ManagedIdentityCredential( - client_id=managed_identity_client_id, - _exclude_workload_identity_credential=exclude_workload_identity_credential, - **kwargs - ) + mi_cred = ManagedIdentityCredential( + client_id=managed_identity_client_id, + _exclude_workload_identity_credential=exclude_workload_identity_credential, + **kwargs, ) + avail_credentials["MANAGED_IDENTITY"] = mi_cred + credentials.append(mi_cred) if not 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=shared_cache_username, tenant_id=shared_cache_tenant_id, authority=authority, **kwargs ) + avail_credentials["SHARED_CACHE"] = shared_cache credentials.append(shared_cache) 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(VisualStudioCodeCredential(**vscode_args)) + vscode_cred = VisualStudioCodeCredential(**vscode_args) + avail_credentials["VISUAL_STUDIO_CODE"] = vscode_cred + credentials.append(vscode_cred) if not exclude_cli_credential: - credentials.append(AzureCliCredential(process_timeout=process_timeout)) + cli_cred = AzureCliCredential(process_timeout=process_timeout) + avail_credentials["CLI"] = cli_cred + credentials.append(cli_cred) if not exclude_powershell_credential: - credentials.append(AzurePowerShellCredential(process_timeout=process_timeout)) + ps_cred = AzurePowerShellCredential(process_timeout=process_timeout) + avail_credentials["POWERSHELL"] = ps_cred + credentials.append(ps_cred) if not exclude_developer_cli_credential: - credentials.append(AzureDeveloperCliCredential(process_timeout=process_timeout)) + dev_cli_cred = AzureDeveloperCliCredential(process_timeout=process_timeout) + avail_credentials["DEVELOPER_CLI"] = dev_cli_cred + credentials.append(dev_cli_cred) if not exclude_interactive_browser_credential: if interactive_browser_client_id: credentials.append( @@ -192,6 +206,12 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement ) else: credentials.append(InteractiveBrowserCredential(tenant_id=interactive_browser_tenant_id, **kwargs)) + default_credential_allow_list = kwargs.pop( + "default_credential_allow_list", os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") + ) + if default_credential_allow_list: + default_credential_allow_list = default_credential_allow_list.upper() + credentials = parse_azure_dac(default_credential_allow_list, avail_credentials) within_dac.set(False) super(DefaultAzureCredential, self).__init__(*credentials) @@ -256,3 +276,18 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = token_info = cast(SupportsTokenInfo, super()).get_token_info(*scopes, options=options) within_dac.set(False) return token_info + + +def parse_azure_dac(az_dac, avail_credentials): + striped_az_dac = az_dac.strip() + if striped_az_dac.endswith(";"): + striped_az_dac = striped_az_dac[:-1] + creds = [cred.strip() for cred in striped_az_dac.split(";")] + credentials = [] + + for cred in creds: + if cred not in VALID_CREDENTIALS: + raise ValueError(f"Invalid credential '{cred}' in AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") + credentials.append(avail_credentials.get(cred)) + + return credentials 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 2b048735458f..3dc0e86fd05a 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -10,6 +10,7 @@ from azure.core.credentials_async import AsyncTokenCredential, AsyncSupportsTokenInfo from ..._constants import EnvironmentVariables from ..._internal import get_default_authority, normalize_authority, within_dac +from ..._credentials.default import parse_azure_dac from .azure_cli import AzureCliCredential from .azd_cli import AzureDeveloperCliCredential from .azure_powershell import AzurePowerShellCredential @@ -48,20 +49,10 @@ class DefaultAzureCredential(ChainedTokenCredential): :keyword str authority: Authority of a Microsoft Entra 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. Managed identities ignore this because they reside in a single cloud. - :keyword bool exclude_workload_identity_credential: Whether to exclude the workload identity from the credential. - Defaults to **False**. - :keyword bool exclude_developer_cli_credential: Whether to exclude the Azure Developer CLI - from the credential. Defaults to **False**. - :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 - variables from the credential. Defaults to **False**. - :keyword bool exclude_powershell_credential: Whether to exclude Azure PowerShell. Defaults to **False**. - :keyword bool exclude_visual_studio_code_credential: Whether to exclude stored credential from VS Code. - Defaults to **True**. - :keyword bool exclude_managed_identity_credential: Whether to exclude managed identity from the credential. - Defaults to **False**. - :keyword bool exclude_shared_token_cache_credential: Whether to exclude the shared token cache. Defaults to - **False**. + :keyword str default_credential_allow_list: A semicolon-separated list of credential names. + The default is to try all available credentials. If this is set, only the credentials in the list are tried. + e.g. "ENVIRONMENT;CLI;MANAGED_IDENTITY" will only try EnvironmentCredential, AzureCliCredential, and + ManagedIdentityCredential. :keyword str managed_identity_client_id: The client ID of a user-assigned managed identity. Defaults to the value of the environment variable AZURE_CLIENT_ID, if any. If not specified, a system-assigned identity will be used. :keyword str workload_identity_client_id: The client ID of an identity assigned to the pod. Defaults to the value @@ -93,7 +84,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement if "tenant_id" in kwargs: raise TypeError("'tenant_id' is not supported in DefaultAzureCredential.") - authority = kwargs.pop("authority", None) + authority: Optional[str] = kwargs.pop("authority", None) vscode_tenant_id = kwargs.pop( "visual_studio_code_tenant_id", os.environ.get(EnvironmentVariables.AZURE_TENANT_ID) @@ -135,45 +126,63 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False) credentials: List[AsyncSupportsTokenInfo] = [] + avail_credentials = {} within_dac.set(True) if not exclude_environment_credential: - credentials.append(EnvironmentCredential(authority=authority, _within_dac=True, **kwargs)) + env_cred = EnvironmentCredential(authority=authority, _within_dac=True, **kwargs) + avail_credentials["ENVIRONMENT"] = env_cred + credentials.append(env_cred) if not exclude_workload_identity_credential: if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): client_id = workload_identity_client_id - credentials.append( - WorkloadIdentityCredential( - client_id=cast(str, client_id), - tenant_id=workload_identity_tenant_id, - token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE], - **kwargs - ) - ) - if not exclude_managed_identity_credential: - credentials.append( - ManagedIdentityCredential( - client_id=managed_identity_client_id, - _exclude_workload_identity_credential=exclude_workload_identity_credential, + workload_cred = WorkloadIdentityCredential( + client_id=cast(str, client_id), + tenant_id=workload_identity_tenant_id, + token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE], **kwargs ) + avail_credentials["WORKLOAD_IDENTITY"] = workload_cred + credentials.append(workload_cred) + if not exclude_managed_identity_credential: + mi_cred = ManagedIdentityCredential( + client_id=managed_identity_client_id, + _exclude_workload_identity_credential=exclude_workload_identity_credential, + **kwargs ) + avail_credentials["MANAGED_IDENTITY"] = mi_cred + credentials.append(mi_cred) if not 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=shared_cache_username, tenant_id=shared_cache_tenant_id, authority=authority, **kwargs ) + avail_credentials["SHARED_CACHE"] = shared_cache credentials.append(shared_cache) 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(VisualStudioCodeCredential(**vscode_args)) + vscode_cred = VisualStudioCodeCredential(**vscode_args) + avail_credentials["VISUAL_STUDIO_CODE"] = vscode_cred + credentials.append(vscode_cred) if not exclude_cli_credential: - credentials.append(AzureCliCredential(process_timeout=process_timeout)) + cli_cred = AzureCliCredential(process_timeout=process_timeout) + avail_credentials["CLI"] = cli_cred + credentials.append(cli_cred) if not exclude_powershell_credential: - credentials.append(AzurePowerShellCredential(process_timeout=process_timeout)) + ps_cred = AzurePowerShellCredential(process_timeout=process_timeout) + avail_credentials["POWERSHELL"] = ps_cred + credentials.append(ps_cred) if not exclude_developer_cli_credential: - credentials.append(AzureDeveloperCliCredential(process_timeout=process_timeout)) + dev_cli_cred = AzureDeveloperCliCredential(process_timeout=process_timeout) + avail_credentials["DEVELOPER_CLI"] = dev_cli_cred + credentials.append(dev_cli_cred) + default_credential_allow_list = kwargs.pop( + "default_credential_allow_list", os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") + ) + if default_credential_allow_list: + default_credential_allow_list = default_credential_allow_list.upper() + credentials = parse_azure_dac(default_credential_allow_list, avail_credentials) within_dac.set(False) super().__init__(*credentials) diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index b77bfbcbad20..d054eb0276c3 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -9,6 +9,7 @@ AzureCliCredential, AzureDeveloperCliCredential, AzurePowerShellCredential, + EnvironmentCredential, CredentialUnavailableError, DefaultAzureCredential, InteractiveBrowserCredential, @@ -18,6 +19,7 @@ from azure.identity._constants import EnvironmentVariables from azure.identity._credentials.azure_cli import AzureCliCredential from azure.identity._credentials.azd_cli import AzureDeveloperCliCredential +from azure.identity._credentials.default import parse_azure_dac from azure.identity._credentials.managed_identity import ManagedIdentityCredential import pytest from urllib.parse import urlparse @@ -432,3 +434,76 @@ def test_validate_cloud_shell_credential_in_dac(): DefaultAzureCredential(identity_config={"client_id": "foo"}) DefaultAzureCredential(identity_config={"object_id": "foo"}) DefaultAzureCredential(identity_config={"resource_id": "foo"}) + + +def test_valid_allow_list(): + avail_credentials = { + "ENVIRONMENT": "MockEnvironmentCredential", + "MANAGED_IDENTITY": "MockManagedIdentityCredential", + "CLI": "MockAzureCliCredential", + "WORKLOAD_IDENTITY": "MockWorkloadIdentityCredential", + "DEVELOPER_CLI": "MockAzureDeveloperCliCredential", + } + az_dac = "ENVIRONMENT;CLI;MANAGED_IDENTITY" + credentials = parse_azure_dac(az_dac, avail_credentials) + expected_credentials = [ + "MockEnvironmentCredential", + "MockAzureCliCredential", + "MockManagedIdentityCredential", + ] + assert credentials == expected_credentials + + +def test_invalid_credential_in_allow_list(): + avail_credentials = { + "ENVIRONMENT": "MockEnvironmentCredential", + } + az_dac = "ENVIRONMENT;INVALID_CREDENTIAL" + with pytest.raises(ValueError): + parse_azure_dac(az_dac, avail_credentials) + + +def test_allow_list_with_trailing_semicolon(): + avail_credentials = { + "ENVIRONMENT": "MockEnvironmentCredential", + "MANAGED_IDENTITY": "MockManagedIdentityCredential", + "CLI": "MockAzureCliCredential", + } + az_dac = "CLI;MANAGED_IDENTITY;" + credentials = parse_azure_dac(az_dac, avail_credentials) + expected_credentials = [ + "MockAzureCliCredential", + "MockManagedIdentityCredential", + ] + assert credentials == expected_credentials + + +def test_allow_list_with_extra_spaces(): + avail_credentials = { + "ENVIRONMENT": "MockEnvironmentCredential", + "MANAGED_IDENTITY": "MockManagedIdentityCredential", + "CLI": "MockAzureCliCredential", + } + az_dac = " ENVIRONMENT ; CLI ; MANAGED_IDENTITY " + credentials = parse_azure_dac(az_dac, avail_credentials) + expected_credentials = [ + "MockEnvironmentCredential", + "MockAzureCliCredential", + "MockManagedIdentityCredential", + ] + assert credentials == expected_credentials + + +def test_default_azure_credential_constructor_env_var(): + with patch("os.environ", {"AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST": "ENVIRONMENT;CLI"}): + credential = DefaultAzureCredential() + assert len(credential.credentials) == 2 + assert isinstance(credential.credentials[0], EnvironmentCredential) + assert isinstance(credential.credentials[1], AzureCliCredential) + + +def test_default_azure_credential_constructor_with_param(): + credential = DefaultAzureCredential(default_credential_allow_list="ENVIRONMENT;CLI") + assert len(credential.credentials) == 2 + assert isinstance(credential.credentials[0], EnvironmentCredential) + assert isinstance(credential.credentials[1], AzureCliCredential) diff --git a/sdk/identity/azure-identity/tests/test_default_async.py b/sdk/identity/azure-identity/tests/test_default_async.py index 18a942b705a8..6ea8ff1bdbce 100644 --- a/sdk/identity/azure-identity/tests/test_default_async.py +++ b/sdk/identity/azure-identity/tests/test_default_async.py @@ -12,6 +12,7 @@ AzurePowerShellCredential, AzureCliCredential, AzureDeveloperCliCredential, + EnvironmentCredential, DefaultAzureCredential, ManagedIdentityCredential, SharedTokenCacheCredential, @@ -344,3 +345,18 @@ def test_validate_cloud_shell_credential_in_dac(): DefaultAzureCredential(identity_config={"client_id": "foo"}) DefaultAzureCredential(identity_config={"object_id": "foo"}) DefaultAzureCredential(identity_config={"resource_id": "foo"}) + + +def test_default_azure_credential_constructor_env_var(): + with patch("os.environ", {"AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST": "ENVIRONMENT;CLI"}): + credential = DefaultAzureCredential() + assert len(credential.credentials) == 2 + assert isinstance(credential.credentials[0], EnvironmentCredential) + assert isinstance(credential.credentials[1], AzureCliCredential) + + +def test_default_azure_credential_constructor_with_param(): + credential = DefaultAzureCredential(default_credential_allow_list="ENVIRONMENT;CLI") + assert len(credential.credentials) == 2 + assert isinstance(credential.credentials[0], EnvironmentCredential) + assert isinstance(credential.credentials[1], AzureCliCredential) From 695ae3fae7ff248b5232d56622c1137b7d16de5b Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Fri, 31 Jan 2025 17:22:13 -0800 Subject: [PATCH 02/19] updates --- .../azure-identity/azure/identity/_credentials/default.py | 7 ++++--- .../azure/identity/aio/_credentials/default.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 024e31cdf223..281d96675b5e 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -4,7 +4,7 @@ # ------------------------------------ import logging import os -from typing import List, Any, Optional, cast +from typing import List, Any, Optional, cast, Dict from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions, SupportsTokenInfo, TokenCredential from .._constants import EnvironmentVariables @@ -97,7 +97,8 @@ class DefaultAzureCredential(ChainedTokenCredential): :caption: Create a DefaultAzureCredential. """ - def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statements, too-many-locals + def __init__(self, **kwargs: Any) -> None: + # pylint: disable=too-many-statements, too-many-locals if "tenant_id" in kwargs: raise TypeError("'tenant_id' is not supported in DefaultAzureCredential.") @@ -145,7 +146,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False) credentials: List[SupportsTokenInfo] = [] - avail_credentials = {} + avail_credentials: Dict[str, Any] = {} within_dac.set(True) if not exclude_environment_credential: 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 3dc0e86fd05a..c70cdc41820d 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -4,7 +4,7 @@ # ------------------------------------ import logging import os -from typing import List, Optional, Any, cast +from typing import List, Optional, Any, cast, Dict from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions from azure.core.credentials_async import AsyncTokenCredential, AsyncSupportsTokenInfo @@ -80,7 +80,8 @@ class DefaultAzureCredential(ChainedTokenCredential): :caption: Create a DefaultAzureCredential. """ - def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statements + def __init__(self, **kwargs: Any) -> None: + # pylint: disable=too-many-statements, too-many-locals if "tenant_id" in kwargs: raise TypeError("'tenant_id' is not supported in DefaultAzureCredential.") @@ -126,7 +127,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False) credentials: List[AsyncSupportsTokenInfo] = [] - avail_credentials = {} + avail_credentials: Dict[str, Any] = {} within_dac.set(True) if not exclude_environment_credential: env_cred = EnvironmentCredential(authority=authority, _within_dac=True, **kwargs) From 60a629018e94fad64a92d7a770f296a8c2a64f08 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Mon, 3 Feb 2025 10:14:48 -0800 Subject: [PATCH 03/19] update --- .../azure-identity/azure/identity/_credentials/default.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 281d96675b5e..af9ae446c628 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -30,7 +30,6 @@ "ENVIRONMENT", "MANAGED_IDENTITY", "POWERSHELL", - "VISUAL_STUDIO_CODE", "SHARED_CACHE", "INTERACTIVE_BROWSER", ] From 7902f423fc02ec6b87800c1c4112ffa26299cd96 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Mon, 3 Feb 2025 13:37:23 -0800 Subject: [PATCH 04/19] updates --- sdk/identity/azure-identity/CHANGELOG.md | 2 +- .../azure-identity/azure/identity/_credentials/default.py | 4 ++-- sdk/identity/azure-identity/tests/test_default.py | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index b4d3cf1f171f..4e80c9a553ca 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features Added - Added `subscription` parameter to `AzureCliCredential` to specify the subscription to use when authenticating with the Azure CLI. ([#37994](https://github.com/Azure/azure-sdk-for-python/pull/37994)) -- Added support for environment variable `AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST`. +- Added support for environment variable `AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST` and argument `default_credential_allow_list` for customizing the behavior of `DefaultAzureCredential`. ([#39520](https://github.com/Azure/azure-sdk-for-python/pull/39520)) ### Breaking Changes diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index af9ae446c628..6ac3985fb646 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -31,7 +31,6 @@ "MANAGED_IDENTITY", "POWERSHELL", "SHARED_CACHE", - "INTERACTIVE_BROWSER", ] @@ -63,7 +62,8 @@ class DefaultAzureCredential(ChainedTokenCredential): :keyword str default_credential_allow_list: A semicolon-separated list of credential names. The default is to try all available credentials. If this is set, only the credentials in the list are tried. e.g. "ENVIRONMENT;CLI;MANAGED_IDENTITY" will only try EnvironmentCredential, AzureCliCredential, and - ManagedIdentityCredential. + ManagedIdentityCredential. All valid credential names are "DEVELOPER_CLI", "WORKLOAD_IDENTITY", "CLI", + "ENVIRONMENT", "MANAGED_IDENTITY", "POWERSHELL" and "SHARED_CACHE". :keyword str interactive_browser_tenant_id: Tenant ID to use when authenticating a user through :class:`~azure.identity.InteractiveBrowserCredential`. Defaults to the value of environment variable AZURE_TENANT_ID, if any. If unspecified, users will authenticate in their home tenants. diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index d054eb0276c3..75b19e712e14 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -502,6 +502,14 @@ def test_default_azure_credential_constructor_env_var(): assert isinstance(credential.credentials[1], AzureCliCredential) +def test_default_azure_credential_constructor_env_var_lowercases(): + with patch("os.environ", {"AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST": "environment;cli"}): + credential = DefaultAzureCredential() + assert len(credential.credentials) == 2 + assert isinstance(credential.credentials[0], EnvironmentCredential) + assert isinstance(credential.credentials[1], AzureCliCredential) + + def test_default_azure_credential_constructor_with_param(): credential = DefaultAzureCredential(default_credential_allow_list="ENVIRONMENT;CLI") assert len(credential.credentials) == 2 From a57b9e31580c0cbd1141cdff2daf4496807b4623 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Mon, 3 Feb 2025 13:39:21 -0800 Subject: [PATCH 05/19] update --- .../azure-identity/azure/identity/aio/_credentials/default.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 c70cdc41820d..6894b601d693 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -52,7 +52,8 @@ class DefaultAzureCredential(ChainedTokenCredential): :keyword str default_credential_allow_list: A semicolon-separated list of credential names. The default is to try all available credentials. If this is set, only the credentials in the list are tried. e.g. "ENVIRONMENT;CLI;MANAGED_IDENTITY" will only try EnvironmentCredential, AzureCliCredential, and - ManagedIdentityCredential. + ManagedIdentityCredential. All valid credential names are "DEVELOPER_CLI", "WORKLOAD_IDENTITY", "CLI", + "ENVIRONMENT", "MANAGED_IDENTITY", "POWERSHELL" and "SHARED_CACHE". :keyword str managed_identity_client_id: The client ID of a user-assigned managed identity. Defaults to the value of the environment variable AZURE_CLIENT_ID, if any. If not specified, a system-assigned identity will be used. :keyword str workload_identity_client_id: The client ID of an identity assigned to the pod. Defaults to the value From f0fd2da4fa342cc534e2e7444ce38dafae2be62b Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Mon, 3 Feb 2025 13:47:02 -0800 Subject: [PATCH 06/19] update --- sdk/identity/azure-identity/tests/test_default_async.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sdk/identity/azure-identity/tests/test_default_async.py b/sdk/identity/azure-identity/tests/test_default_async.py index 6ea8ff1bdbce..5b2d4f1e3142 100644 --- a/sdk/identity/azure-identity/tests/test_default_async.py +++ b/sdk/identity/azure-identity/tests/test_default_async.py @@ -355,6 +355,14 @@ def test_default_azure_credential_constructor_env_var(): assert isinstance(credential.credentials[1], AzureCliCredential) +def test_default_azure_credential_constructor_env_var_lowercases(): + with patch("os.environ", {"AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST": "environment;cli"}): + credential = DefaultAzureCredential() + assert len(credential.credentials) == 2 + assert isinstance(credential.credentials[0], EnvironmentCredential) + assert isinstance(credential.credentials[1], AzureCliCredential) + + def test_default_azure_credential_constructor_with_param(): credential = DefaultAzureCredential(default_credential_allow_list="ENVIRONMENT;CLI") assert len(credential.credentials) == 2 From 4e65311b43b6709f75502fc864d167da0b5e6738 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 4 Feb 2025 14:22:28 -0800 Subject: [PATCH 07/19] Update sdk/identity/azure-identity/azure/identity/_credentials/default.py Co-authored-by: Paul Van Eck --- .../azure-identity/azure/identity/_credentials/default.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 6ac3985fb646..f07c130463d4 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -287,7 +287,10 @@ def parse_azure_dac(az_dac, avail_credentials): for cred in creds: if cred not in VALID_CREDENTIALS: - raise ValueError(f"Invalid credential '{cred}' in AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") + raise ValueError( + f"Invalid credential '{cred}' in AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST. " + f"Valid credentials are {', '.join(VALID_CREDENTIALS)}." + ) credentials.append(avail_credentials.get(cred)) return credentials From 06f28cc634bc09a55804046f3a1acc3632591009 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 4 Feb 2025 14:24:46 -0800 Subject: [PATCH 08/19] Update sdk/identity/azure-identity/azure/identity/_credentials/default.py Co-authored-by: Paul Van Eck --- .../azure-identity/azure/identity/_credentials/default.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index f07c130463d4..944ba6385d8b 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -291,6 +291,8 @@ def parse_azure_dac(az_dac, avail_credentials): f"Invalid credential '{cred}' in AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST. " f"Valid credentials are {', '.join(VALID_CREDENTIALS)}." ) - credentials.append(avail_credentials.get(cred)) + credential = avail_credentials.get(cred) + if credential: + credentials.append(credential) return credentials From 3adfdadee52ffe474d560a51bc922ab33a4d99a9 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 4 Feb 2025 14:31:03 -0800 Subject: [PATCH 09/19] update --- sdk/identity/azure-identity/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index b2b398223f43..83bb69470505 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -285,6 +285,12 @@ Not all credentials require this configuration. Credentials that authenticate th [DefaultAzureCredential][default_cred_ref] and [EnvironmentCredential][environment_cred_ref] can be configured with environment variables. Each type of authentication requires values for specific variables: +### DefaultAzureCredential + +|Variable name|Value +|-|- +|`AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST`|Specifies a semicolon-separated list of credential names that will be utilized by DefaultAzureCredential. + ### Service principal with secret |Variable name|Value From b84b39c5f5a01807e5e9e1ac1d7f996d5161adfa Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Wed, 5 Feb 2025 14:41:12 -0800 Subject: [PATCH 10/19] Update sdk/identity/azure-identity/README.md Co-authored-by: Paul Van Eck --- sdk/identity/azure-identity/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index 83bb69470505..1ea9462fd286 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -289,7 +289,7 @@ variables: |Variable name|Value |-|- -|`AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST`|Specifies a semicolon-separated list of credential names that will be utilized by DefaultAzureCredential. +|`AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST`|Specifies a semicolon-separated list of credential names that will be utilized by DefaultAzureCredential. Valid credential names are "DEVELOPER_CLI", "WORKLOAD_IDENTITY", "CLI", "ENVIRONMENT", "MANAGED_IDENTITY", "POWERSHELL" and "SHARED_CACHE". ### Service principal with secret From 3b620d4e8a02395d21abf9fd1981bb8db8d52863 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Thu, 6 Feb 2025 10:28:23 -0800 Subject: [PATCH 11/19] Update sdk/identity/azure-identity/README.md Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> --- sdk/identity/azure-identity/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index 1ea9462fd286..7ce275e51d6c 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -289,7 +289,7 @@ variables: |Variable name|Value |-|- -|`AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST`|Specifies a semicolon-separated list of credential names that will be utilized by DefaultAzureCredential. Valid credential names are "DEVELOPER_CLI", "WORKLOAD_IDENTITY", "CLI", "ENVIRONMENT", "MANAGED_IDENTITY", "POWERSHELL" and "SHARED_CACHE". +|`AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST`|Specifies a semicolon-separated list of credential names that will be utilized by DefaultAzureCredential. Valid credential names are `DEVELOPER_CLI`, `WORKLOAD_IDENTITY`, `CLI`, `ENVIRONMENT`, `MANAGED_IDENTITY`, `POWERSHELL` and `SHARED_CACHE`. ### Service principal with secret From f10a81b72f8b26e9bedec3ba6540eafb43f05452 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Thu, 6 Feb 2025 14:27:16 -0800 Subject: [PATCH 12/19] raise if the specified cred is excluded. --- .../azure/identity/_credentials/default.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 944ba6385d8b..8ab14064e057 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -23,17 +23,6 @@ _LOGGER = logging.getLogger(__name__) -VALID_CREDENTIALS = [ - "DEVELOPER_CLI", - "WORKLOAD_IDENTITY", - "CLI", - "ENVIRONMENT", - "MANAGED_IDENTITY", - "POWERSHELL", - "SHARED_CACHE", -] - - class DefaultAzureCredential(ChainedTokenCredential): """A credential capable of handling most Azure SDK authentication scenarios. For more information, See `Usage guidance for DefaultAzureCredential @@ -286,10 +275,10 @@ def parse_azure_dac(az_dac, avail_credentials): credentials = [] for cred in creds: - if cred not in VALID_CREDENTIALS: + if cred not in avail_credentials: raise ValueError( - f"Invalid credential '{cred}' in AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST. " - f"Valid credentials are {', '.join(VALID_CREDENTIALS)}." + f"The credential '{cred}' in AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST is invalid or excluded. " + f"Available credentials are {', '.join(avail_credentials)}." ) credential = avail_credentials.get(cred) if credential: From 7531346b1dfb3bdcb5b5735f2dfeed931cea325c Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Thu, 6 Feb 2025 15:57:50 -0800 Subject: [PATCH 13/19] update --- .../azure/identity/_credentials/default.py | 24 ++++++++++++------- .../identity/aio/_credentials/default.py | 20 +++++++++------- .../azure-identity/tests/test_default.py | 16 ++++++++----- .../tests/test_default_async.py | 2 +- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 8ab14064e057..84a721402448 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -48,9 +48,9 @@ class DefaultAzureCredential(ChainedTokenCredential): :keyword str authority: Authority of a Microsoft Entra 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. Managed identities ignore this because they reside in a single cloud. - :keyword str default_credential_allow_list: A semicolon-separated list of credential names. + :keyword str[] default_credential_allow_list: A list of credential names. The default is to try all available credentials. If this is set, only the credentials in the list are tried. - e.g. "ENVIRONMENT;CLI;MANAGED_IDENTITY" will only try EnvironmentCredential, AzureCliCredential, and + e.g. ["ENVIRONMENT","CLI","MANAGED_IDENTITY"] will only try EnvironmentCredential, AzureCliCredential, and ManagedIdentityCredential. All valid credential names are "DEVELOPER_CLI", "WORKLOAD_IDENTITY", "CLI", "ENVIRONMENT", "MANAGED_IDENTITY", "POWERSHELL" and "SHARED_CACHE". :keyword str interactive_browser_tenant_id: Tenant ID to use when authenticating a user through @@ -195,12 +195,14 @@ def __init__(self, **kwargs: Any) -> None: ) else: credentials.append(InteractiveBrowserCredential(tenant_id=interactive_browser_tenant_id, **kwargs)) - default_credential_allow_list = kwargs.pop( - "default_credential_allow_list", os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") - ) - if default_credential_allow_list: - default_credential_allow_list = default_credential_allow_list.upper() - credentials = parse_azure_dac(default_credential_allow_list, avail_credentials) + cred_types = kwargs.pop("default_credential_allow_list", None) # type: ignore + if cred_types is None: + default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") + if default_credential_allow_list: + default_credential_allow_list = default_credential_allow_list.upper() + cred_types = parse_azure_dac(default_credential_allow_list) + if cred_types: + credentials = resolve_credentials(cred_types, avail_credentials) within_dac.set(False) super(DefaultAzureCredential, self).__init__(*credentials) @@ -267,11 +269,15 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = return token_info -def parse_azure_dac(az_dac, avail_credentials): +def parse_azure_dac(az_dac): striped_az_dac = az_dac.strip() if striped_az_dac.endswith(";"): striped_az_dac = striped_az_dac[:-1] creds = [cred.strip() for cred in striped_az_dac.split(";")] + return creds + + +def resolve_credentials(creds, avail_credentials): credentials = [] for cred in creds: 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 6894b601d693..84936f786243 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -10,7 +10,7 @@ from azure.core.credentials_async import AsyncTokenCredential, AsyncSupportsTokenInfo from ..._constants import EnvironmentVariables from ..._internal import get_default_authority, normalize_authority, within_dac -from ..._credentials.default import parse_azure_dac +from ..._credentials.default import parse_azure_dac, resolve_credentials from .azure_cli import AzureCliCredential from .azd_cli import AzureDeveloperCliCredential from .azure_powershell import AzurePowerShellCredential @@ -49,9 +49,9 @@ class DefaultAzureCredential(ChainedTokenCredential): :keyword str authority: Authority of a Microsoft Entra 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. Managed identities ignore this because they reside in a single cloud. - :keyword str default_credential_allow_list: A semicolon-separated list of credential names. + :keyword str[] default_credential_allow_list: A list of credential names. The default is to try all available credentials. If this is set, only the credentials in the list are tried. - e.g. "ENVIRONMENT;CLI;MANAGED_IDENTITY" will only try EnvironmentCredential, AzureCliCredential, and + e.g. ["ENVIRONMENT","CLI","MANAGED_IDENTITY"] will only try EnvironmentCredential, AzureCliCredential, and ManagedIdentityCredential. All valid credential names are "DEVELOPER_CLI", "WORKLOAD_IDENTITY", "CLI", "ENVIRONMENT", "MANAGED_IDENTITY", "POWERSHELL" and "SHARED_CACHE". :keyword str managed_identity_client_id: The client ID of a user-assigned managed identity. Defaults to the value @@ -179,12 +179,14 @@ def __init__(self, **kwargs: Any) -> None: dev_cli_cred = AzureDeveloperCliCredential(process_timeout=process_timeout) avail_credentials["DEVELOPER_CLI"] = dev_cli_cred credentials.append(dev_cli_cred) - default_credential_allow_list = kwargs.pop( - "default_credential_allow_list", os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") - ) - if default_credential_allow_list: - default_credential_allow_list = default_credential_allow_list.upper() - credentials = parse_azure_dac(default_credential_allow_list, avail_credentials) + cred_types = kwargs.pop("default_credential_allow_list", None) # type: ignore + if cred_types is None: + default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") + if default_credential_allow_list: + default_credential_allow_list = default_credential_allow_list.upper() + cred_types = parse_azure_dac(default_credential_allow_list) + if cred_types: + credentials = resolve_credentials(cred_types, avail_credentials) within_dac.set(False) super().__init__(*credentials) diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index 75b19e712e14..3cf9d316e69d 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -19,7 +19,7 @@ from azure.identity._constants import EnvironmentVariables from azure.identity._credentials.azure_cli import AzureCliCredential from azure.identity._credentials.azd_cli import AzureDeveloperCliCredential -from azure.identity._credentials.default import parse_azure_dac +from azure.identity._credentials.default import parse_azure_dac, resolve_credentials from azure.identity._credentials.managed_identity import ManagedIdentityCredential import pytest from urllib.parse import urlparse @@ -445,7 +445,8 @@ def test_valid_allow_list(): "DEVELOPER_CLI": "MockAzureDeveloperCliCredential", } az_dac = "ENVIRONMENT;CLI;MANAGED_IDENTITY" - credentials = parse_azure_dac(az_dac, avail_credentials) + cred_types = parse_azure_dac(az_dac) + credentials = resolve_credentials(cred_types, avail_credentials) expected_credentials = [ "MockEnvironmentCredential", "MockAzureCliCredential", @@ -459,8 +460,9 @@ def test_invalid_credential_in_allow_list(): "ENVIRONMENT": "MockEnvironmentCredential", } az_dac = "ENVIRONMENT;INVALID_CREDENTIAL" + cred_types = parse_azure_dac(az_dac) with pytest.raises(ValueError): - parse_azure_dac(az_dac, avail_credentials) + credentials = resolve_credentials(cred_types, avail_credentials) def test_allow_list_with_trailing_semicolon(): @@ -470,7 +472,8 @@ def test_allow_list_with_trailing_semicolon(): "CLI": "MockAzureCliCredential", } az_dac = "CLI;MANAGED_IDENTITY;" - credentials = parse_azure_dac(az_dac, avail_credentials) + cred_types = parse_azure_dac(az_dac) + credentials = resolve_credentials(cred_types, avail_credentials) expected_credentials = [ "MockAzureCliCredential", "MockManagedIdentityCredential", @@ -485,7 +488,8 @@ def test_allow_list_with_extra_spaces(): "CLI": "MockAzureCliCredential", } az_dac = " ENVIRONMENT ; CLI ; MANAGED_IDENTITY " - credentials = parse_azure_dac(az_dac, avail_credentials) + cred_types = parse_azure_dac(az_dac) + credentials = resolve_credentials(cred_types, avail_credentials) expected_credentials = [ "MockEnvironmentCredential", "MockAzureCliCredential", @@ -511,7 +515,7 @@ def test_default_azure_credential_constructor_env_var_lowercases(): def test_default_azure_credential_constructor_with_param(): - credential = DefaultAzureCredential(default_credential_allow_list="ENVIRONMENT;CLI") + credential = DefaultAzureCredential(default_credential_allow_list=["ENVIRONMENT","CLI"]) assert len(credential.credentials) == 2 assert isinstance(credential.credentials[0], EnvironmentCredential) assert isinstance(credential.credentials[1], AzureCliCredential) diff --git a/sdk/identity/azure-identity/tests/test_default_async.py b/sdk/identity/azure-identity/tests/test_default_async.py index 5b2d4f1e3142..c5166c7fb3f2 100644 --- a/sdk/identity/azure-identity/tests/test_default_async.py +++ b/sdk/identity/azure-identity/tests/test_default_async.py @@ -364,7 +364,7 @@ def test_default_azure_credential_constructor_env_var_lowercases(): def test_default_azure_credential_constructor_with_param(): - credential = DefaultAzureCredential(default_credential_allow_list="ENVIRONMENT;CLI") + credential = DefaultAzureCredential(default_credential_allow_list=["ENVIRONMENT","CLI"]) assert len(credential.credentials) == 2 assert isinstance(credential.credentials[0], EnvironmentCredential) assert isinstance(credential.credentials[1], AzureCliCredential) From 7c4ebfd97c9141e2486a211f03aef91b2e17c939 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Thu, 6 Feb 2025 16:50:57 -0800 Subject: [PATCH 14/19] update --- sdk/identity/azure-identity/tests/test_default.py | 2 +- sdk/identity/azure-identity/tests/test_default_async.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index 3cf9d316e69d..9c3988e8736b 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -515,7 +515,7 @@ def test_default_azure_credential_constructor_env_var_lowercases(): def test_default_azure_credential_constructor_with_param(): - credential = DefaultAzureCredential(default_credential_allow_list=["ENVIRONMENT","CLI"]) + credential = DefaultAzureCredential(default_credential_allow_list=["ENVIRONMENT", "CLI"]) assert len(credential.credentials) == 2 assert isinstance(credential.credentials[0], EnvironmentCredential) assert isinstance(credential.credentials[1], AzureCliCredential) diff --git a/sdk/identity/azure-identity/tests/test_default_async.py b/sdk/identity/azure-identity/tests/test_default_async.py index c5166c7fb3f2..8ebb8eee86a2 100644 --- a/sdk/identity/azure-identity/tests/test_default_async.py +++ b/sdk/identity/azure-identity/tests/test_default_async.py @@ -364,7 +364,7 @@ def test_default_azure_credential_constructor_env_var_lowercases(): def test_default_azure_credential_constructor_with_param(): - credential = DefaultAzureCredential(default_credential_allow_list=["ENVIRONMENT","CLI"]) + credential = DefaultAzureCredential(default_credential_allow_list=["ENVIRONMENT", "CLI"]) assert len(credential.credentials) == 2 assert isinstance(credential.credentials[0], EnvironmentCredential) assert isinstance(credential.credentials[1], AzureCliCredential) From 8f3414b8c79a57be8d4b5447825731baff2266cb Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Fri, 7 Feb 2025 08:32:11 -0800 Subject: [PATCH 15/19] Update sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py Co-authored-by: Paul Van Eck --- .../azure-identity/azure/identity/aio/_credentials/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 84936f786243..f061ba378b44 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -49,7 +49,7 @@ class DefaultAzureCredential(ChainedTokenCredential): :keyword str authority: Authority of a Microsoft Entra 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. Managed identities ignore this because they reside in a single cloud. - :keyword str[] default_credential_allow_list: A list of credential names. + :keyword list[str] default_credential_allow_list: A list of credential names. The default is to try all available credentials. If this is set, only the credentials in the list are tried. e.g. ["ENVIRONMENT","CLI","MANAGED_IDENTITY"] will only try EnvironmentCredential, AzureCliCredential, and ManagedIdentityCredential. All valid credential names are "DEVELOPER_CLI", "WORKLOAD_IDENTITY", "CLI", From fcbe93f1786fac144154b8ff24531fa19efa278e Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Fri, 7 Feb 2025 08:57:42 -0800 Subject: [PATCH 16/19] updates --- .../azure/identity/_credentials/default.py | 52 ++++++++++--------- .../identity/aio/_credentials/default.py | 31 +++++++---- .../azure-identity/tests/test_default.py | 34 +++++++++--- 3 files changed, 74 insertions(+), 43 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 84a721402448..9f671d1b05af 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -48,7 +48,7 @@ class DefaultAzureCredential(ChainedTokenCredential): :keyword str authority: Authority of a Microsoft Entra 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. Managed identities ignore this because they reside in a single cloud. - :keyword str[] default_credential_allow_list: A list of credential names. + :keyword list[str] default_credential_allow_list: A list of credential names. The default is to try all available credentials. If this is set, only the credentials in the list are tried. e.g. ["ENVIRONMENT","CLI","MANAGED_IDENTITY"] will only try EnvironmentCredential, AzureCliCredential, and ManagedIdentityCredential. All valid credential names are "DEVELOPER_CLI", "WORKLOAD_IDENTITY", "CLI", @@ -134,14 +134,17 @@ def __init__(self, **kwargs: Any) -> None: exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False) credentials: List[SupportsTokenInfo] = [] + valid_credentials: List[str] = [] avail_credentials: Dict[str, Any] = {} within_dac.set(True) if not exclude_environment_credential: + valid_credentials.append("ENVIRONMENT") env_cred = EnvironmentCredential(authority=authority, _within_dac=True, **kwargs) avail_credentials["ENVIRONMENT"] = env_cred credentials.append(env_cred) if not exclude_workload_identity_credential: + valid_credentials.append("WORKLOAD_IDENTITY") if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): client_id = workload_identity_client_id workload_cred = WorkloadIdentityCredential( @@ -153,6 +156,7 @@ def __init__(self, **kwargs: Any) -> None: avail_credentials["WORKLOAD_IDENTITY"] = workload_cred credentials.append(workload_cred) if not exclude_managed_identity_credential: + valid_credentials.append("MANAGED_IDENTITY") mi_cred = ManagedIdentityCredential( client_id=managed_identity_client_id, _exclude_workload_identity_credential=exclude_workload_identity_credential, @@ -160,29 +164,34 @@ def __init__(self, **kwargs: Any) -> None: ) avail_credentials["MANAGED_IDENTITY"] = mi_cred credentials.append(mi_cred) - if not 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=shared_cache_username, tenant_id=shared_cache_tenant_id, authority=authority, **kwargs - ) - avail_credentials["SHARED_CACHE"] = shared_cache - credentials.append(shared_cache) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.info("Shared token cache is unavailable: '%s'", ex) + if not exclude_shared_token_cache_credential: + valid_credentials.append("SHARED_CACHE") + if SharedTokenCacheCredential.supported(): + try: + # username and/or tenant_id are only required when the cache contains tokens for multiple identities + shared_cache = SharedTokenCacheCredential( + username=shared_cache_username, tenant_id=shared_cache_tenant_id, authority=authority, **kwargs + ) + avail_credentials["SHARED_CACHE"] = shared_cache + credentials.append(shared_cache) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.info("Shared token cache is unavailable: '%s'", ex) if not exclude_visual_studio_code_credential: vscode_cred = VisualStudioCodeCredential(**vscode_args) avail_credentials["VISUAL_STUDIO_CODE"] = vscode_cred credentials.append(vscode_cred) if not exclude_cli_credential: + valid_credentials.append("CLI") cli_cred = AzureCliCredential(process_timeout=process_timeout) avail_credentials["CLI"] = cli_cred credentials.append(cli_cred) if not exclude_powershell_credential: + valid_credentials.append("POWERSHELL") ps_cred = AzurePowerShellCredential(process_timeout=process_timeout) avail_credentials["POWERSHELL"] = ps_cred credentials.append(ps_cred) if not exclude_developer_cli_credential: + valid_credentials.append("DEVELOPER_CLI") dev_cli_cred = AzureDeveloperCliCredential(process_timeout=process_timeout) avail_credentials["DEVELOPER_CLI"] = dev_cli_cred credentials.append(dev_cli_cred) @@ -200,7 +209,7 @@ def __init__(self, **kwargs: Any) -> None: default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") if default_credential_allow_list: default_credential_allow_list = default_credential_allow_list.upper() - cred_types = parse_azure_dac(default_credential_allow_list) + cred_types = parse_azure_dac(default_credential_allow_list, valid_credentials) if cred_types: credentials = resolve_credentials(cred_types, avail_credentials) within_dac.set(False) @@ -269,25 +278,20 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = return token_info -def parse_azure_dac(az_dac): +def parse_azure_dac(az_dac, valid_credentials): striped_az_dac = az_dac.strip() if striped_az_dac.endswith(";"): striped_az_dac = striped_az_dac[:-1] creds = [cred.strip() for cred in striped_az_dac.split(";")] - return creds - - -def resolve_credentials(creds, avail_credentials): - credentials = [] - for cred in creds: - if cred not in avail_credentials: + if cred not in valid_credentials: raise ValueError( f"The credential '{cred}' in AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST is invalid or excluded. " - f"Available credentials are {', '.join(avail_credentials)}." + f"Available credentials are {', '.join(valid_credentials)}." ) - credential = avail_credentials.get(cred) - if credential: - credentials.append(credential) + return creds + +def resolve_credentials(creds, avail_credentials): + credentials = [avail_credentials[cred] for cred in creds if cred in avail_credentials] return credentials 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 f061ba378b44..840b9ccbe087 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -128,13 +128,16 @@ def __init__(self, **kwargs: Any) -> None: exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False) credentials: List[AsyncSupportsTokenInfo] = [] + valid_credentials: List[str] = [] avail_credentials: Dict[str, Any] = {} within_dac.set(True) if not exclude_environment_credential: + valid_credentials.append("ENVIRONMENT") env_cred = EnvironmentCredential(authority=authority, _within_dac=True, **kwargs) avail_credentials["ENVIRONMENT"] = env_cred credentials.append(env_cred) if not exclude_workload_identity_credential: + valid_credentials.append("WORKLOAD_IDENTITY") if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): client_id = workload_identity_client_id workload_cred = WorkloadIdentityCredential( @@ -146,6 +149,7 @@ def __init__(self, **kwargs: Any) -> None: avail_credentials["WORKLOAD_IDENTITY"] = workload_cred credentials.append(workload_cred) if not exclude_managed_identity_credential: + valid_credentials.append("MANAGED_IDENTITY") mi_cred = ManagedIdentityCredential( client_id=managed_identity_client_id, _exclude_workload_identity_credential=exclude_workload_identity_credential, @@ -153,29 +157,34 @@ def __init__(self, **kwargs: Any) -> None: ) avail_credentials["MANAGED_IDENTITY"] = mi_cred credentials.append(mi_cred) - if not 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=shared_cache_username, tenant_id=shared_cache_tenant_id, authority=authority, **kwargs - ) - avail_credentials["SHARED_CACHE"] = shared_cache - credentials.append(shared_cache) - except Exception as ex: # pylint:disable=broad-except - _LOGGER.info("Shared token cache is unavailable: '%s'", ex) + if not exclude_shared_token_cache_credential: + valid_credentials.append("SHARED_CACHE") + if SharedTokenCacheCredential.supported(): + try: + # username and/or tenant_id are only required when the cache contains tokens for multiple identities + shared_cache = SharedTokenCacheCredential( + username=shared_cache_username, tenant_id=shared_cache_tenant_id, authority=authority, **kwargs + ) + avail_credentials["SHARED_CACHE"] = shared_cache + credentials.append(shared_cache) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.info("Shared token cache is unavailable: '%s'", ex) if not exclude_visual_studio_code_credential: vscode_cred = VisualStudioCodeCredential(**vscode_args) avail_credentials["VISUAL_STUDIO_CODE"] = vscode_cred credentials.append(vscode_cred) if not exclude_cli_credential: + valid_credentials.append("CLI") cli_cred = AzureCliCredential(process_timeout=process_timeout) avail_credentials["CLI"] = cli_cred credentials.append(cli_cred) if not exclude_powershell_credential: + valid_credentials.append("POWERSHELL") ps_cred = AzurePowerShellCredential(process_timeout=process_timeout) avail_credentials["POWERSHELL"] = ps_cred credentials.append(ps_cred) if not exclude_developer_cli_credential: + valid_credentials.append("DEVELOPER_CLI") dev_cli_cred = AzureDeveloperCliCredential(process_timeout=process_timeout) avail_credentials["DEVELOPER_CLI"] = dev_cli_cred credentials.append(dev_cli_cred) @@ -184,7 +193,7 @@ def __init__(self, **kwargs: Any) -> None: default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") if default_credential_allow_list: default_credential_allow_list = default_credential_allow_list.upper() - cred_types = parse_azure_dac(default_credential_allow_list) + cred_types = parse_azure_dac(default_credential_allow_list, valid_credentials) if cred_types: credentials = resolve_credentials(cred_types, avail_credentials) within_dac.set(False) diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index 9c3988e8736b..f2b040590040 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -437,6 +437,13 @@ def test_validate_cloud_shell_credential_in_dac(): def test_valid_allow_list(): + valid_credentials = [ + "ENVIRONMENT", + "MANAGED_IDENTITY", + "CLI", + "WORKLOAD_IDENTITY", + "DEVELOPER_CLI", + ] avail_credentials = { "ENVIRONMENT": "MockEnvironmentCredential", "MANAGED_IDENTITY": "MockManagedIdentityCredential", @@ -445,7 +452,7 @@ def test_valid_allow_list(): "DEVELOPER_CLI": "MockAzureDeveloperCliCredential", } az_dac = "ENVIRONMENT;CLI;MANAGED_IDENTITY" - cred_types = parse_azure_dac(az_dac) + cred_types = parse_azure_dac(az_dac, valid_credentials) credentials = resolve_credentials(cred_types, avail_credentials) expected_credentials = [ "MockEnvironmentCredential", @@ -456,23 +463,27 @@ def test_valid_allow_list(): def test_invalid_credential_in_allow_list(): - avail_credentials = { - "ENVIRONMENT": "MockEnvironmentCredential", - } + valid_credentials = ["ENVIRONMENT"] az_dac = "ENVIRONMENT;INVALID_CREDENTIAL" - cred_types = parse_azure_dac(az_dac) with pytest.raises(ValueError): - credentials = resolve_credentials(cred_types, avail_credentials) + cred_types = parse_azure_dac(az_dac, valid_credentials) def test_allow_list_with_trailing_semicolon(): + valid_credentials = [ + "ENVIRONMENT", + "MANAGED_IDENTITY", + "CLI", + "WORKLOAD_IDENTITY", + "DEVELOPER_CLI", + ] avail_credentials = { "ENVIRONMENT": "MockEnvironmentCredential", "MANAGED_IDENTITY": "MockManagedIdentityCredential", "CLI": "MockAzureCliCredential", } az_dac = "CLI;MANAGED_IDENTITY;" - cred_types = parse_azure_dac(az_dac) + cred_types = parse_azure_dac(az_dac, valid_credentials) credentials = resolve_credentials(cred_types, avail_credentials) expected_credentials = [ "MockAzureCliCredential", @@ -482,13 +493,20 @@ def test_allow_list_with_trailing_semicolon(): def test_allow_list_with_extra_spaces(): + valid_credentials = [ + "ENVIRONMENT", + "MANAGED_IDENTITY", + "CLI", + "WORKLOAD_IDENTITY", + "DEVELOPER_CLI", + ] avail_credentials = { "ENVIRONMENT": "MockEnvironmentCredential", "MANAGED_IDENTITY": "MockManagedIdentityCredential", "CLI": "MockAzureCliCredential", } az_dac = " ENVIRONMENT ; CLI ; MANAGED_IDENTITY " - cred_types = parse_azure_dac(az_dac) + cred_types = parse_azure_dac(az_dac, valid_credentials) credentials = resolve_credentials(cred_types, avail_credentials) expected_credentials = [ "MockEnvironmentCredential", From 7e4a763148b36107c77c7ca7a00da3595f548ff0 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Fri, 7 Feb 2025 09:02:36 -0800 Subject: [PATCH 17/19] update --- .../azure-identity/azure/identity/_credentials/default.py | 2 +- .../azure-identity/azure/identity/aio/_credentials/default.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 9f671d1b05af..cd7c3357820f 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -204,7 +204,7 @@ def __init__(self, **kwargs: Any) -> None: ) else: credentials.append(InteractiveBrowserCredential(tenant_id=interactive_browser_tenant_id, **kwargs)) - cred_types = kwargs.pop("default_credential_allow_list", None) # type: ignore + cred_types: Optional[list[str]] = kwargs.pop("default_credential_allow_list", None) if cred_types is None: default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") if default_credential_allow_list: 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 840b9ccbe087..7a3df776dfb6 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -188,7 +188,7 @@ def __init__(self, **kwargs: Any) -> None: dev_cli_cred = AzureDeveloperCliCredential(process_timeout=process_timeout) avail_credentials["DEVELOPER_CLI"] = dev_cli_cred credentials.append(dev_cli_cred) - cred_types = kwargs.pop("default_credential_allow_list", None) # type: ignore + cred_types: Optional[list[str]] = kwargs.pop("default_credential_allow_list", None) if cred_types is None: default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") if default_credential_allow_list: From dd0829ce61c1f9624778619334ff3bf197f0b548 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Fri, 7 Feb 2025 10:03:28 -0800 Subject: [PATCH 18/19] updates --- .../azure/identity/_credentials/default.py | 19 ++++++++++++------- .../identity/aio/_credentials/default.py | 4 ++-- .../azure-identity/tests/test_default.py | 18 +++++++++++------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index cd7c3357820f..88df4a041feb 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -209,9 +209,9 @@ def __init__(self, **kwargs: Any) -> None: default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") if default_credential_allow_list: default_credential_allow_list = default_credential_allow_list.upper() - cred_types = parse_azure_dac(default_credential_allow_list, valid_credentials) + cred_types = parse_azure_dac(default_credential_allow_list) if cred_types: - credentials = resolve_credentials(cred_types, avail_credentials) + credentials = resolve_credentials(cred_types, valid_credentials, avail_credentials) within_dac.set(False) super(DefaultAzureCredential, self).__init__(*credentials) @@ -278,20 +278,25 @@ def get_token_info(self, *scopes: str, options: Optional[TokenRequestOptions] = return token_info -def parse_azure_dac(az_dac, valid_credentials): +def parse_azure_dac(az_dac): striped_az_dac = az_dac.strip() if striped_az_dac.endswith(";"): striped_az_dac = striped_az_dac[:-1] creds = [cred.strip() for cred in striped_az_dac.split(";")] + return creds + + +def resolve_credentials(creds, valid_credentials, avail_credentials): + credentials = [] + for cred in creds: if cred not in valid_credentials: raise ValueError( f"The credential '{cred}' in AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST is invalid or excluded. " f"Available credentials are {', '.join(valid_credentials)}." ) - return creds - + credential = avail_credentials.get(cred) + if credential: + credentials.append(credential) -def resolve_credentials(creds, avail_credentials): - credentials = [avail_credentials[cred] for cred in creds if cred in avail_credentials] return credentials 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 7a3df776dfb6..a867b569c4db 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -193,9 +193,9 @@ def __init__(self, **kwargs: Any) -> None: default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") if default_credential_allow_list: default_credential_allow_list = default_credential_allow_list.upper() - cred_types = parse_azure_dac(default_credential_allow_list, valid_credentials) + cred_types = parse_azure_dac(default_credential_allow_list) if cred_types: - credentials = resolve_credentials(cred_types, avail_credentials) + credentials = resolve_credentials(cred_types, valid_credentials, avail_credentials) within_dac.set(False) super().__init__(*credentials) diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index f2b040590040..ed931f5bf0e5 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -452,8 +452,8 @@ def test_valid_allow_list(): "DEVELOPER_CLI": "MockAzureDeveloperCliCredential", } az_dac = "ENVIRONMENT;CLI;MANAGED_IDENTITY" - cred_types = parse_azure_dac(az_dac, valid_credentials) - credentials = resolve_credentials(cred_types, avail_credentials) + cred_types = parse_azure_dac(az_dac) + credentials = resolve_credentials(cred_types, valid_credentials, avail_credentials) expected_credentials = [ "MockEnvironmentCredential", "MockAzureCliCredential", @@ -464,9 +464,13 @@ def test_valid_allow_list(): def test_invalid_credential_in_allow_list(): valid_credentials = ["ENVIRONMENT"] + avail_credentials = { + "ENVIRONMENT": "MockEnvironmentCredential", + } az_dac = "ENVIRONMENT;INVALID_CREDENTIAL" + cred_types = parse_azure_dac(az_dac) with pytest.raises(ValueError): - cred_types = parse_azure_dac(az_dac, valid_credentials) + credentials = resolve_credentials(cred_types, valid_credentials, avail_credentials) def test_allow_list_with_trailing_semicolon(): @@ -483,8 +487,8 @@ def test_allow_list_with_trailing_semicolon(): "CLI": "MockAzureCliCredential", } az_dac = "CLI;MANAGED_IDENTITY;" - cred_types = parse_azure_dac(az_dac, valid_credentials) - credentials = resolve_credentials(cred_types, avail_credentials) + cred_types = parse_azure_dac(az_dac) + credentials = resolve_credentials(cred_types, valid_credentials, avail_credentials) expected_credentials = [ "MockAzureCliCredential", "MockManagedIdentityCredential", @@ -506,8 +510,8 @@ def test_allow_list_with_extra_spaces(): "CLI": "MockAzureCliCredential", } az_dac = " ENVIRONMENT ; CLI ; MANAGED_IDENTITY " - cred_types = parse_azure_dac(az_dac, valid_credentials) - credentials = resolve_credentials(cred_types, avail_credentials) + cred_types = parse_azure_dac(az_dac) + credentials = resolve_credentials(cred_types, valid_credentials, avail_credentials) expected_credentials = [ "MockEnvironmentCredential", "MockAzureCliCredential", From 70b881c05aa58bde11bff35cb2645167221fce86 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Fri, 7 Feb 2025 10:04:55 -0800 Subject: [PATCH 19/19] update --- .../azure-identity/azure/identity/_credentials/default.py | 2 +- .../azure-identity/azure/identity/aio/_credentials/default.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 88df4a041feb..f08c3daa19e0 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -204,7 +204,7 @@ def __init__(self, **kwargs: Any) -> None: ) else: credentials.append(InteractiveBrowserCredential(tenant_id=interactive_browser_tenant_id, **kwargs)) - cred_types: Optional[list[str]] = kwargs.pop("default_credential_allow_list", None) + cred_types: Optional[List[str]] = kwargs.pop("default_credential_allow_list", None) if cred_types is None: default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") if default_credential_allow_list: 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 a867b569c4db..75b2317a0f9d 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -188,7 +188,7 @@ def __init__(self, **kwargs: Any) -> None: dev_cli_cred = AzureDeveloperCliCredential(process_timeout=process_timeout) avail_credentials["DEVELOPER_CLI"] = dev_cli_cred credentials.append(dev_cli_cred) - cred_types: Optional[list[str]] = kwargs.pop("default_credential_allow_list", None) + cred_types: Optional[List[str]] = kwargs.pop("default_credential_allow_list", None) if cred_types is None: default_credential_allow_list = os.environ.get("AZURE_DEFAULT_CREDENTIAL_ALLOW_LIST") if default_credential_allow_list: