From ad8d6cef3732a0ced9270863c443b6b2306d15c5 Mon Sep 17 00:00:00 2001 From: Paul Van Eck Date: Sat, 21 Jun 2025 01:54:41 +0000 Subject: [PATCH 1/4] [Identity] Update AZURE_TOKEN_CREDENTIALs to allow specific creds Now users of DefaultAzureCredential can specify a specific credential in the DAC chain when using the AZURE_TOKEN_CREDENTIALS environment variable. Signed-off-by: Paul Van Eck --- sdk/identity/azure-identity/CHANGELOG.md | 4 +- .../azure/identity/_credentials/default.py | 102 +++++++++---- .../azure/identity/_internal/__init__.py | 2 + .../azure/identity/_internal/utils.py | 62 ++++++++ .../azure-identity/azure/identity/_version.py | 2 +- .../identity/aio/_credentials/default.py | 96 ++++++++---- sdk/identity/azure-identity/setup.py | 2 +- .../tests/test_token_credentials_env.py | 144 ++++++++++++++++++ .../tests/test_token_credentials_env_async.py | 144 ++++++++++++++++++ 9 files changed, 494 insertions(+), 64 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index ae38675266c8..8f316fd0bda6 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,9 +1,11 @@ # Release History -## 1.23.1 (Unreleased) +## 1.24.0b1 (Unreleased) ### Features Added +- Expanded the set of acceptable values for environment variable `AZURE_TOKEN_CREDENTIALS` to allow for selection of a specific credential in the `DefaultAzureCredential` chain. At runtime, only the specified credential will be used when acquiring tokens with `DefaultAzureCredential`. For example, setting `AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential` will make `DefaultAzureCredential` use only `WorkloadIdentityCredential`. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 8c2e6c1bcced..a65b2e5ca9a5 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -8,7 +8,7 @@ from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions, SupportsTokenInfo, TokenCredential from .._constants import EnvironmentVariables -from .._internal import get_default_authority, normalize_authority, within_dac +from .._internal import get_default_authority, normalize_authority, within_dac, process_credential_exclusions from .azure_powershell import AzurePowerShellCredential from .browser import InteractiveBrowserCredential from .chained import ChainedTokenCredential @@ -133,36 +133,76 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement process_timeout = kwargs.pop("process_timeout", 10) - token_credentials_env = os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS, "").strip().lower() - exclude_workload_identity_credential = kwargs.pop("exclude_workload_identity_credential", False) - 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) - exclude_visual_studio_code_credential = kwargs.pop("exclude_visual_studio_code_credential", True) - exclude_developer_cli_credential = kwargs.pop("exclude_developer_cli_credential", False) - exclude_cli_credential = kwargs.pop("exclude_cli_credential", False) - exclude_interactive_browser_credential = kwargs.pop("exclude_interactive_browser_credential", True) - exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False) - - if token_credentials_env == "dev": - # In dev mode, use only developer credentials - exclude_environment_credential = True - exclude_managed_identity_credential = True - exclude_workload_identity_credential = True - elif token_credentials_env == "prod": - # In prod mode, use only production credentials - exclude_shared_token_cache_credential = True - exclude_visual_studio_code_credential = True - exclude_cli_credential = True - exclude_developer_cli_credential = True - exclude_powershell_credential = True - exclude_interactive_browser_credential = True - elif token_credentials_env != "": - # If the environment variable is set to something other than dev or prod, raise an error - raise ValueError( - f"Invalid value for {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS}: {token_credentials_env}. " - "Valid values are 'dev' or 'prod'." - ) + # Define credential configuration mapping + credential_config = { + "environment": { + "exclude_param": "exclude_environment_credential", + "env_name": "environmentcredential", + "default_exclude": False, + }, + "workload_identity": { + "exclude_param": "exclude_workload_identity_credential", + "env_name": "workloadidentitycredential", + "default_exclude": False, + }, + "managed_identity": { + "exclude_param": "exclude_managed_identity_credential", + "env_name": "managedidentitycredential", + "default_exclude": False, + }, + "shared_token_cache": { + "exclude_param": "exclude_shared_token_cache_credential", + "env_name": "sharedtokencachecredential", + "default_exclude": False, + }, + "visual_studio_code": { + "exclude_param": "exclude_visual_studio_code_credential", + "env_name": "visualstudiocodecredential", + "default_exclude": True, + }, + "cli": { + "exclude_param": "exclude_cli_credential", + "env_name": "azureclicredential", + "default_exclude": False, + }, + "developer_cli": { + "exclude_param": "exclude_developer_cli_credential", + "env_name": "azuredeveloperclicredential", + "default_exclude": False, + }, + "powershell": { + "exclude_param": "exclude_powershell_credential", + "env_name": "azurepowershellcredential", + "default_exclude": False, + }, + "interactive_browser": { + "exclude_param": "exclude_interactive_browser_credential", + "env_name": "interactivebrowsercredential", + "default_exclude": True, + }, + } + + # Extract user-provided exclude flags and set defaults + exclude_flags = {} + user_excludes = {} + for cred_key, config in credential_config.items(): + param_name = cast(str, config["exclude_param"]) + user_excludes[cred_key] = kwargs.pop(param_name, None) + exclude_flags[cred_key] = config["default_exclude"] + + # Process AZURE_TOKEN_CREDENTIALS environment variable and apply user overrides + exclude_flags = process_credential_exclusions(credential_config, exclude_flags, user_excludes) + + # Extract individual exclude flags for backward compatibility + exclude_environment_credential = exclude_flags["environment"] + exclude_workload_identity_credential = exclude_flags["workload_identity"] + exclude_managed_identity_credential = exclude_flags["managed_identity"] + exclude_shared_token_cache_credential = exclude_flags["shared_token_cache"] + exclude_visual_studio_code_credential = exclude_flags["visual_studio_code"] + exclude_cli_credential = exclude_flags["cli"] + exclude_developer_cli_credential = exclude_flags["developer_cli"] + exclude_powershell_credential = exclude_flags["powershell"] + exclude_interactive_browser_credential = exclude_flags["interactive_browser"] credentials: List[SupportsTokenInfo] = [] within_dac.set(True) diff --git a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py index 6bc3387d0098..c0efa58d084a 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py @@ -11,6 +11,7 @@ from .utils import ( get_default_authority, normalize_authority, + process_credential_exclusions, resolve_tenant, validate_scope, validate_subscription, @@ -48,6 +49,7 @@ def _scopes_to_resource(*scopes) -> str: "get_default_authority", "InteractiveCredential", "normalize_authority", + "process_credential_exclusions", "resolve_tenant", "validate_scope", "validate_subscription", diff --git a/sdk/identity/azure-identity/azure/identity/_internal/utils.py b/sdk/identity/azure-identity/azure/identity/_internal/utils.py index 248e5ef2e2da..550217b07756 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/utils.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/utils.py @@ -133,3 +133,65 @@ def resolve_tenant( 'when creating the credential, or add "*" to additionally_allowed_tenants to allow ' "acquiring tokens for any tenant.".format(tenant_id) ) + + +def process_credential_exclusions(credential_config: dict, exclude_flags: dict, user_excludes: dict) -> dict: + """Process credential exclusions based on environment variable and user overrides. + + This method handles the AZURE_TOKEN_CREDENTIALS environment variable to determine + which credentials should be excluded from the credential chain, and then applies + any user-provided exclude overrides which take precedence over environment settings. + + :param credential_config: Configuration mapping for all available credentials, containing + exclude parameter names, environment names, and default exclude settings + :type credential_config: dict + :param exclude_flags: Dictionary of exclude flags for each credential (will be modified) + :type exclude_flags: dict + :param user_excludes: User-provided exclude overrides from constructor kwargs + :type user_excludes: dict + + :return: Dictionary of final exclude flags for each credential + :rtype: dict + + :raises ValueError: If token_credentials_env contains an invalid credential name + """ + # Handle AZURE_TOKEN_CREDENTIALS environment variable + token_credentials_env = os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS, "").strip().lower() + + if token_credentials_env == "dev": + # In dev mode, use only developer credentials + dev_credentials = {"cli", "developer_cli", "powershell", "shared_token_cache"} + for cred_key in credential_config: + exclude_flags[cred_key] = cred_key not in dev_credentials + elif token_credentials_env == "prod": + # In prod mode, use only production credentials + prod_credentials = {"environment", "workload_identity", "managed_identity"} + for cred_key in credential_config: + exclude_flags[cred_key] = cred_key not in prod_credentials + elif token_credentials_env: + # If a specific credential is specified, exclude all others except the specified one + valid_credentials = {config["env_name"] for config in credential_config.values()} + + if token_credentials_env not in valid_credentials: + valid_values = ["dev", "prod"] + sorted(valid_credentials) + raise ValueError( + f"Invalid value for {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS}: {token_credentials_env}. " + f"Valid values are: {', '.join(valid_values)}." + ) + + # Find which credential was selected and exclude all others + selected_cred_key = None + for cred_key, config in credential_config.items(): + if config["env_name"] == token_credentials_env: + selected_cred_key = cred_key + break + + for cred_key in credential_config: + exclude_flags[cred_key] = cred_key != selected_cred_key + + # Apply user-provided exclude flags (these override environment variable settings) + for cred_key, user_value in user_excludes.items(): + if user_value is not None: + exclude_flags[cred_key] = user_value + + return exclude_flags diff --git a/sdk/identity/azure-identity/azure/identity/_version.py b/sdk/identity/azure-identity/azure/identity/_version.py index 760523dec6db..7f08eadfa7d0 100644 --- a/sdk/identity/azure-identity/azure/identity/_version.py +++ b/sdk/identity/azure-identity/azure/identity/_version.py @@ -2,4 +2,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -VERSION = "1.23.1" +VERSION = "1.24.0b1" 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 08b3ac48bf01..3a8e8d9913b6 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -9,7 +9,7 @@ from azure.core.credentials import AccessToken, AccessTokenInfo, TokenRequestOptions from azure.core.credentials_async import AsyncTokenCredential, AsyncSupportsTokenInfo from ..._constants import EnvironmentVariables -from ..._internal import get_default_authority, normalize_authority, within_dac +from ..._internal import get_default_authority, normalize_authority, within_dac, process_credential_exclusions from .azure_cli import AzureCliCredential from .azd_cli import AzureDeveloperCliCredential from .azure_powershell import AzurePowerShellCredential @@ -89,7 +89,7 @@ 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.") @@ -125,34 +125,70 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement process_timeout = kwargs.pop("process_timeout", 10) - token_credentials_env = os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS, "").strip().lower() - exclude_workload_identity_credential = kwargs.pop("exclude_workload_identity_credential", False) - exclude_visual_studio_code_credential = kwargs.pop("exclude_visual_studio_code_credential", True) - exclude_developer_cli_credential = kwargs.pop("exclude_developer_cli_credential", False) - exclude_cli_credential = kwargs.pop("exclude_cli_credential", False) - 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) - exclude_powershell_credential = kwargs.pop("exclude_powershell_credential", False) - - if token_credentials_env == "dev": - # In dev mode, use only developer credentials - exclude_environment_credential = True - exclude_managed_identity_credential = True - exclude_workload_identity_credential = True - elif token_credentials_env == "prod": - # In prod mode, use only production credentials - exclude_shared_token_cache_credential = True - exclude_visual_studio_code_credential = True - exclude_cli_credential = True - exclude_developer_cli_credential = True - exclude_powershell_credential = True - elif token_credentials_env != "": - # If the environment variable is set to something other than dev or prod, raise an error - raise ValueError( - f"Invalid value for {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS}: {token_credentials_env}. " - "Valid values are 'dev' or 'prod'." - ) + # Define credential configuration mapping (async version) + credential_config = { + "environment": { + "exclude_param": "exclude_environment_credential", + "env_name": "environmentcredential", + "default_exclude": False, + }, + "workload_identity": { + "exclude_param": "exclude_workload_identity_credential", + "env_name": "workloadidentitycredential", + "default_exclude": False, + }, + "managed_identity": { + "exclude_param": "exclude_managed_identity_credential", + "env_name": "managedidentitycredential", + "default_exclude": False, + }, + "shared_token_cache": { + "exclude_param": "exclude_shared_token_cache_credential", + "env_name": "sharedtokencachecredential", + "default_exclude": False, + }, + "visual_studio_code": { + "exclude_param": "exclude_visual_studio_code_credential", + "env_name": "visualstudiocodecredential", + "default_exclude": True, + }, + "cli": { + "exclude_param": "exclude_cli_credential", + "env_name": "azureclicredential", + "default_exclude": False, + }, + "developer_cli": { + "exclude_param": "exclude_developer_cli_credential", + "env_name": "azuredeveloperclicredential", + "default_exclude": False, + }, + "powershell": { + "exclude_param": "exclude_powershell_credential", + "env_name": "azurepowershellcredential", + "default_exclude": False, + }, + } + + # Extract user-provided exclude flags and set defaults + exclude_flags = {} + user_excludes = {} + for cred_key, config in credential_config.items(): + param_name = cast(str, config["exclude_param"]) + user_excludes[cred_key] = kwargs.pop(param_name, None) + exclude_flags[cred_key] = config["default_exclude"] + + # Process AZURE_TOKEN_CREDENTIALS environment variable and apply user overrides + exclude_flags = process_credential_exclusions(credential_config, exclude_flags, user_excludes) + + # Extract individual exclude flags for backward compatibility + exclude_environment_credential = exclude_flags["environment"] + exclude_workload_identity_credential = exclude_flags["workload_identity"] + exclude_managed_identity_credential = exclude_flags["managed_identity"] + exclude_shared_token_cache_credential = exclude_flags["shared_token_cache"] + exclude_visual_studio_code_credential = exclude_flags["visual_studio_code"] + exclude_cli_credential = exclude_flags["cli"] + exclude_developer_cli_credential = exclude_flags["developer_cli"] + exclude_powershell_credential = exclude_flags["powershell"] credentials: List[AsyncSupportsTokenInfo] = [] within_dac.set(True) diff --git a/sdk/identity/azure-identity/setup.py b/sdk/identity/azure-identity/setup.py index 0248c249363b..1fbbd5a55de8 100644 --- a/sdk/identity/azure-identity/setup.py +++ b/sdk/identity/azure-identity/setup.py @@ -38,7 +38,7 @@ url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/identity/azure-identity", keywords="azure, azure sdk", classifiers=[ - "Development Status :: 5 - Production/Stable", + "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", diff --git a/sdk/identity/azure-identity/tests/test_token_credentials_env.py b/sdk/identity/azure-identity/tests/test_token_credentials_env.py index 10ac3783a1df..efdac07c28e7 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -116,3 +116,147 @@ def test_token_credentials_env_with_exclude(): actual_classes = {c.__class__ for c in credential.credentials} assert EnvironmentCredential not in actual_classes + + +def test_token_credentials_env_workload_identity_credential(): + """With AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential, only WorkloadIdentityCredential should be used""" + + with patch.dict( + "os.environ", + { + EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "WorkloadIdentityCredential", + EnvironmentVariables.AZURE_AUTHORITY_HOST: "https://login.microsoftonline.com", + EnvironmentVariables.AZURE_TENANT_ID: "tenant-id", + EnvironmentVariables.AZURE_CLIENT_ID: "client-id", + EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE: "/tmp/token", + }, + clear=False, + ): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only WorkloadIdentityCredential should be present + assert WorkloadIdentityCredential in actual_classes + assert len(actual_classes) == 1 + + # Verify other credentials are not present + assert EnvironmentCredential not in actual_classes + assert ManagedIdentityCredential not in actual_classes + assert AzureCliCredential not in actual_classes + + +def test_token_credentials_env_environment_credential(): + """With AZURE_TOKEN_CREDENTIALS=EnvironmentCredential, only EnvironmentCredential should be used""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "EnvironmentCredential"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only EnvironmentCredential should be present + assert EnvironmentCredential in actual_classes + assert len(actual_classes) == 1 + + # Verify other credentials are not present + assert WorkloadIdentityCredential not in actual_classes + assert ManagedIdentityCredential not in actual_classes + assert AzureCliCredential not in actual_classes + + +def test_token_credentials_env_managed_identity_credential(): + """With AZURE_TOKEN_CREDENTIALS=ManagedIdentityCredential, only ManagedIdentityCredential should be used""" + + with patch.dict( + "os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "ManagedIdentityCredential"}, clear=False + ): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only ManagedIdentityCredential should be present + assert ManagedIdentityCredential in actual_classes + assert len(actual_classes) == 1 + + # Verify other credentials are not present + assert EnvironmentCredential not in actual_classes + assert WorkloadIdentityCredential not in actual_classes + assert AzureCliCredential not in actual_classes + + +def test_token_credentials_env_azure_cli_credential(): + """With AZURE_TOKEN_CREDENTIALS=AzureCliCredential, only AzureCliCredential should be used""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "AzureCliCredential"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only AzureCliCredential should be present + assert AzureCliCredential in actual_classes + assert len(actual_classes) == 1 + + # Verify other credentials are not present + assert EnvironmentCredential not in actual_classes + assert ManagedIdentityCredential not in actual_classes + assert WorkloadIdentityCredential not in actual_classes + + +def test_token_credentials_env_specific_credential_case_insensitive(): + """Specific credential names should be case insensitive""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "environmentcredential"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only EnvironmentCredential should be present + assert EnvironmentCredential in actual_classes + assert len(actual_classes) == 1 + + +def test_token_credentials_env_invalid_specific_credential(): + """Invalid specific credential name should raise an error""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "InvalidCredential"}, clear=False): + with pytest.raises(ValueError) as exc_info: + credential = DefaultAzureCredential() + + error_msg = str(exc_info.value) + assert "Invalid value" in error_msg + + +def test_user_exclude_flags_override_env_var(): + """User-provided exclude flags should take precedence over AZURE_TOKEN_CREDENTIALS environment variable""" + + # Test case 1: env var says use specific credential, but user excludes it (should result in empty chain) + with patch.dict( + "os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "azurepowershellcredential"}, clear=False + ): + with pytest.raises(ValueError, match="at least one credential is required"): + DefaultAzureCredential(exclude_powershell_credential=True) + + # Test case 2: env var says use specific credential, user includes additional credential + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "environmentcredential"}, clear=False): + credential = DefaultAzureCredential(exclude_powershell_credential=False) + actual_classes = {c.__class__ for c in credential.credentials} + + # Both EnvironmentCredential and AzurePowerShellCredential should be present + assert EnvironmentCredential in actual_classes + assert AzurePowerShellCredential in actual_classes + + # Test case 3: env var says "dev" mode, user excludes a dev credential + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): + credential = DefaultAzureCredential(exclude_cli_credential=True) + actual_classes = {c.__class__ for c in credential.credentials} + + # AzureCliCredential should NOT be present despite "dev" mode + assert AzureCliCredential not in actual_classes + # Other dev credentials should still be present + assert AzurePowerShellCredential in actual_classes + assert AzureDeveloperCliCredential in actual_classes diff --git a/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py b/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py index 9db1193bd745..86d780f82ecf 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py @@ -116,3 +116,147 @@ def test_token_credentials_env_with_exclude(): actual_classes = {c.__class__ for c in credential.credentials} assert EnvironmentCredential not in actual_classes + + +def test_token_credentials_env_workload_identity_credential(): + """With AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential, only WorkloadIdentityCredential should be used""" + + with patch.dict( + "os.environ", + { + EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "WorkloadIdentityCredential", + EnvironmentVariables.AZURE_AUTHORITY_HOST: "https://login.microsoftonline.com", + EnvironmentVariables.AZURE_TENANT_ID: "tenant-id", + EnvironmentVariables.AZURE_CLIENT_ID: "client-id", + EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE: "/tmp/token", + }, + clear=False, + ): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only WorkloadIdentityCredential should be present + assert WorkloadIdentityCredential in actual_classes + assert len(actual_classes) == 1 + + # Verify other credentials are not present + assert EnvironmentCredential not in actual_classes + assert ManagedIdentityCredential not in actual_classes + assert AzureCliCredential not in actual_classes + + +def test_token_credentials_env_environment_credential(): + """With AZURE_TOKEN_CREDENTIALS=EnvironmentCredential, only EnvironmentCredential should be used""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "EnvironmentCredential"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only EnvironmentCredential should be present + assert EnvironmentCredential in actual_classes + assert len(actual_classes) == 1 + + # Verify other credentials are not present + assert WorkloadIdentityCredential not in actual_classes + assert ManagedIdentityCredential not in actual_classes + assert AzureCliCredential not in actual_classes + + +def test_token_credentials_env_managed_identity_credential(): + """With AZURE_TOKEN_CREDENTIALS=ManagedIdentityCredential, only ManagedIdentityCredential should be used""" + + with patch.dict( + "os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "ManagedIdentityCredential"}, clear=False + ): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only ManagedIdentityCredential should be present + assert ManagedIdentityCredential in actual_classes + assert len(actual_classes) == 1 + + # Verify other credentials are not present + assert EnvironmentCredential not in actual_classes + assert WorkloadIdentityCredential not in actual_classes + assert AzureCliCredential not in actual_classes + + +def test_token_credentials_env_azure_cli_credential(): + """With AZURE_TOKEN_CREDENTIALS=AzureCliCredential, only AzureCliCredential should be used""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "AzureCliCredential"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only AzureCliCredential should be present + assert AzureCliCredential in actual_classes + assert len(actual_classes) == 1 + + # Verify other credentials are not present + assert EnvironmentCredential not in actual_classes + assert ManagedIdentityCredential not in actual_classes + assert WorkloadIdentityCredential not in actual_classes + + +def test_token_credentials_env_specific_credential_case_insensitive(): + """Specific credential names should be case insensitive""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "environmentcredential"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # Only EnvironmentCredential should be present + assert EnvironmentCredential in actual_classes + assert len(actual_classes) == 1 + + +def test_token_credentials_env_invalid_specific_credential(): + """Invalid specific credential name should raise an error""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "InvalidCredential"}, clear=False): + with pytest.raises(ValueError) as exc_info: + credential = DefaultAzureCredential() + + error_msg = str(exc_info.value) + assert "Invalid value" in error_msg + + +def test_user_exclude_flags_override_env_var(): + """User-provided exclude flags should take precedence over AZURE_TOKEN_CREDENTIALS environment variable""" + + # Test case 1: env var says use specific credential, but user excludes it (should result in empty chain) + with patch.dict( + "os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "azurepowershellcredential"}, clear=False + ): + with pytest.raises(ValueError, match="at least one credential is required"): + DefaultAzureCredential(exclude_powershell_credential=True) + + # Test case 2: env var says use specific credential, user includes additional credential + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "environmentcredential"}, clear=False): + credential = DefaultAzureCredential(exclude_powershell_credential=False) + actual_classes = {c.__class__ for c in credential.credentials} + + # Both EnvironmentCredential and AzurePowerShellCredential should be present + assert EnvironmentCredential in actual_classes + assert AzurePowerShellCredential in actual_classes + + # Test case 3: env var says "dev" mode, user excludes a dev credential + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): + credential = DefaultAzureCredential(exclude_cli_credential=True) + actual_classes = {c.__class__ for c in credential.credentials} + + # AzureCliCredential should NOT be present despite "dev" mode + assert AzureCliCredential not in actual_classes + # Other dev credentials should still be present + assert AzurePowerShellCredential in actual_classes + assert AzureDeveloperCliCredential in actual_classes From 963ecb02e501c3d29adf88fd6420c1f8a8b6aee5 Mon Sep 17 00:00:00 2001 From: Paul Van Eck Date: Mon, 30 Jun 2025 18:13:01 +0000 Subject: [PATCH 2/4] Remove certain credentials from env-var usage Signed-off-by: Paul Van Eck --- .../azure/identity/_credentials/default.py | 2 -- .../azure-identity/azure/identity/_internal/utils.py | 4 ++-- .../azure/identity/aio/_credentials/default.py | 2 -- .../tests/test_token_credentials_env.py | 11 +++++++++++ .../tests/test_token_credentials_env_async.py | 11 +++++++++++ 5 files changed, 24 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 a65b2e5ca9a5..798afb842846 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -152,12 +152,10 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement }, "shared_token_cache": { "exclude_param": "exclude_shared_token_cache_credential", - "env_name": "sharedtokencachecredential", "default_exclude": False, }, "visual_studio_code": { "exclude_param": "exclude_visual_studio_code_credential", - "env_name": "visualstudiocodecredential", "default_exclude": True, }, "cli": { diff --git a/sdk/identity/azure-identity/azure/identity/_internal/utils.py b/sdk/identity/azure-identity/azure/identity/_internal/utils.py index 550217b07756..b18393965c05 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/utils.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/utils.py @@ -170,7 +170,7 @@ def process_credential_exclusions(credential_config: dict, exclude_flags: dict, exclude_flags[cred_key] = cred_key not in prod_credentials elif token_credentials_env: # If a specific credential is specified, exclude all others except the specified one - valid_credentials = {config["env_name"] for config in credential_config.values()} + valid_credentials = {config["env_name"] for config in credential_config.values() if "env_name" in config} if token_credentials_env not in valid_credentials: valid_values = ["dev", "prod"] + sorted(valid_credentials) @@ -182,7 +182,7 @@ def process_credential_exclusions(credential_config: dict, exclude_flags: dict, # Find which credential was selected and exclude all others selected_cred_key = None for cred_key, config in credential_config.items(): - if config["env_name"] == token_credentials_env: + if config.get("env_name") == token_credentials_env: selected_cred_key = cred_key break 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 3a8e8d9913b6..2445db573ed0 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -144,12 +144,10 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement }, "shared_token_cache": { "exclude_param": "exclude_shared_token_cache_credential", - "env_name": "sharedtokencachecredential", "default_exclude": False, }, "visual_studio_code": { "exclude_param": "exclude_visual_studio_code_credential", - "env_name": "visualstudiocodecredential", "default_exclude": True, }, "cli": { diff --git a/sdk/identity/azure-identity/tests/test_token_credentials_env.py b/sdk/identity/azure-identity/tests/test_token_credentials_env.py index efdac07c28e7..a90c949a0d93 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -230,6 +230,17 @@ def test_token_credentials_env_invalid_specific_credential(): error_msg = str(exc_info.value) assert "Invalid value" in error_msg + # Test case: specific credential that is not supported + # For example, SharedTokenCacheCredential is not supported in this context + with patch.dict( + "os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "sharedtokencachecredential"}, clear=True + ): + with pytest.raises(ValueError): + DefaultAzureCredential() + + error_msg = str(exc_info.value) + assert "Invalid value" in error_msg + def test_user_exclude_flags_override_env_var(): """User-provided exclude flags should take precedence over AZURE_TOKEN_CREDENTIALS environment variable""" diff --git a/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py b/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py index 86d780f82ecf..91e9a41735bf 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py @@ -230,6 +230,17 @@ def test_token_credentials_env_invalid_specific_credential(): error_msg = str(exc_info.value) assert "Invalid value" in error_msg + # Test case: specific credential that is not supported + # For example, SharedTokenCacheCredential is not supported in this context + with patch.dict( + "os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "sharedtokencachecredential"}, clear=True + ): + with pytest.raises(ValueError): + DefaultAzureCredential() + + error_msg = str(exc_info.value) + assert "Invalid value" in error_msg + def test_user_exclude_flags_override_env_var(): """User-provided exclude flags should take precedence over AZURE_TOKEN_CREDENTIALS environment variable""" From f2cfc617396077522b7f0e2b2023dada399fceb5 Mon Sep 17 00:00:00 2001 From: Paul Van Eck Date: Wed, 2 Jul 2025 18:59:08 +0000 Subject: [PATCH 3/4] update changelog + readme Signed-off-by: Paul Van Eck --- sdk/identity/azure-identity/CHANGELOG.md | 1 + sdk/identity/azure-identity/README.md | 25 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 8f316fd0bda6..a9e2e66c34a9 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features Added - Expanded the set of acceptable values for environment variable `AZURE_TOKEN_CREDENTIALS` to allow for selection of a specific credential in the `DefaultAzureCredential` chain. At runtime, only the specified credential will be used when acquiring tokens with `DefaultAzureCredential`. For example, setting `AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential` will make `DefaultAzureCredential` use only `WorkloadIdentityCredential`. + - Valid values are `EnvironmentCredential`, `WorkloadIdentityCredential`, `ManagedIdentityCredential`, `AzureCliCredential`, `AzurePowershellCredential`, `AzureDeveloperCliCredential`, and `InteractiveBrowserCredential`. ([#41709](https://github.com/Azure/azure-sdk-for-python/pull/41709)) ### Breaking Changes diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index 1670aa4d464e..e9af84d16367 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -297,6 +297,31 @@ variables: Configuration is attempted in the preceding order. For example, if values for a client secret and certificate are both present, the client secret is used. +### Configuring DefaultAzureCredential + +`DefaultAzureCredential` supports an additional environment variable to customize its behavior: + +|Variable name|Value +|-|- +|`AZURE_TOKEN_CREDENTIALS`|Credential name or group + +#### AZURE_TOKEN_CREDENTIALS values + +You can set `AZURE_TOKEN_CREDENTIALS` to one of the following values to control which credentials `DefaultAzureCredential` attempts to use: + +**Individual Credentials:** +- `EnvironmentCredential` - Only use environment variables for service principal authentication +- `WorkloadIdentityCredential` - Only use workload identity for Kubernetes authentication +- `ManagedIdentityCredential` - Only use managed identity authentication +- `AzureCliCredential` - Only use Azure CLI authentication +- `AzurePowerShellCredential` - Only use Azure PowerShell authentication +- `AzureDeveloperCliCredential` - Only use Azure Developer CLI authentication +- `InteractiveBrowserCredential` - Only use interactive browser authentication + +**Credential Groups:** +- `prod` - Use deployed service credentials: `EnvironmentCredential`, `WorkloadIdentityCredential`, and `ManagedIdentityCredential` +- `dev` - Use developer credentials: `SharedTokenCacheCredential`, `AzureCliCredential`, `AzurePowerShellCredential`, and `AzureDeveloperCliCredential` + ## Continuous Access Evaluation As of version 1.14.0, accessing resources protected by [Continuous Access Evaluation (CAE)][cae] is possible on a per-request basis. This behavior can be enabled by setting the `enable_cae` keyword argument to `True` in the credential's `get_token` method. CAE isn't supported for developer and managed identity credentials. From 8e9c61d74623056ea4230d619f49f63b3ceb8cc8 Mon Sep 17 00:00:00 2001 From: Paul Van Eck Date: Tue, 8 Jul 2025 22:20:23 +0000 Subject: [PATCH 4/4] Test and doc updates Signed-off-by: Paul Van Eck --- sdk/identity/azure-identity/CHANGELOG.md | 2 +- sdk/identity/azure-identity/README.md | 25 --------- .../tests/test_token_credentials_env.py | 56 ++++++------------- .../tests/test_token_credentials_env_async.py | 56 ++++++------------- 4 files changed, 35 insertions(+), 104 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index a9e2e66c34a9..bb69428b4167 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features Added - Expanded the set of acceptable values for environment variable `AZURE_TOKEN_CREDENTIALS` to allow for selection of a specific credential in the `DefaultAzureCredential` chain. At runtime, only the specified credential will be used when acquiring tokens with `DefaultAzureCredential`. For example, setting `AZURE_TOKEN_CREDENTIALS=WorkloadIdentityCredential` will make `DefaultAzureCredential` use only `WorkloadIdentityCredential`. - - Valid values are `EnvironmentCredential`, `WorkloadIdentityCredential`, `ManagedIdentityCredential`, `AzureCliCredential`, `AzurePowershellCredential`, `AzureDeveloperCliCredential`, and `InteractiveBrowserCredential`. ([#41709](https://github.com/Azure/azure-sdk-for-python/pull/41709)) + - Valid values are `EnvironmentCredential`, `WorkloadIdentityCredential`, `ManagedIdentityCredential`, `AzureCliCredential`, `AzurePowershellCredential`, `AzureDeveloperCliCredential`, and `InteractiveBrowserCredential`. ([#41709](https://github.com/Azure/azure-sdk-for-python/pull/41709)) ### Breaking Changes diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index e9af84d16367..1670aa4d464e 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -297,31 +297,6 @@ variables: Configuration is attempted in the preceding order. For example, if values for a client secret and certificate are both present, the client secret is used. -### Configuring DefaultAzureCredential - -`DefaultAzureCredential` supports an additional environment variable to customize its behavior: - -|Variable name|Value -|-|- -|`AZURE_TOKEN_CREDENTIALS`|Credential name or group - -#### AZURE_TOKEN_CREDENTIALS values - -You can set `AZURE_TOKEN_CREDENTIALS` to one of the following values to control which credentials `DefaultAzureCredential` attempts to use: - -**Individual Credentials:** -- `EnvironmentCredential` - Only use environment variables for service principal authentication -- `WorkloadIdentityCredential` - Only use workload identity for Kubernetes authentication -- `ManagedIdentityCredential` - Only use managed identity authentication -- `AzureCliCredential` - Only use Azure CLI authentication -- `AzurePowerShellCredential` - Only use Azure PowerShell authentication -- `AzureDeveloperCliCredential` - Only use Azure Developer CLI authentication -- `InteractiveBrowserCredential` - Only use interactive browser authentication - -**Credential Groups:** -- `prod` - Use deployed service credentials: `EnvironmentCredential`, `WorkloadIdentityCredential`, and `ManagedIdentityCredential` -- `dev` - Use developer credentials: `SharedTokenCacheCredential`, `AzureCliCredential`, `AzurePowerShellCredential`, and `AzureDeveloperCliCredential` - ## Continuous Access Evaluation As of version 1.14.0, accessing resources protected by [Continuous Access Evaluation (CAE)][cae] is possible on a per-request basis. This behavior can be enabled by setting the `enable_cae` keyword argument to `True` in the credential's `get_token` method. CAE isn't supported for developer and managed identity credentials. diff --git a/sdk/identity/azure-identity/tests/test_token_credentials_env.py b/sdk/identity/azure-identity/tests/test_token_credentials_env.py index a90c949a0d93..a50d8f62e05a 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -29,7 +29,7 @@ def test_token_credentials_env_dev(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # All dev credentials should be present (if supported) if SharedTokenCacheCredential.supported(): @@ -40,6 +40,9 @@ def test_token_credentials_env_dev(): assert AzureDeveloperCliCredential in actual_classes assert AzurePowerShellCredential in actual_classes + # Assert no duplicates + assert len(actual_classes) == len(set(actual_classes)) + # Production credentials should NOT be present for cred_class in prod_credentials: if cred_class == WorkloadIdentityCredential: @@ -60,18 +63,10 @@ def test_token_credentials_env_prod(): } with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "prod"}, clear=False): - # Print to verify the environment variable is set in the test - print(f"AZURE_TOKEN_CREDENTIALS={os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS)}") - credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} - - # Print which credentials are actually in the chain - print("Credentials in chain:") - for cls in actual_classes: - print(f" - {cls.__name__}") + actual_classes = [c.__class__ for c in credential.credentials] # Production credentials should be present assert EnvironmentCredential in actual_classes @@ -81,6 +76,9 @@ def test_token_credentials_env_prod(): if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): assert WorkloadIdentityCredential in actual_classes + # Assert no duplicates + assert len(actual_classes) == len(set(actual_classes)) + # Developer credentials should NOT be present for cred_class in dev_credentials: assert cred_class not in actual_classes @@ -93,7 +91,7 @@ def test_token_credentials_env_case_insensitive(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # EnvironmentCredential (prod) should not be present assert EnvironmentCredential not in actual_classes @@ -113,7 +111,7 @@ def test_token_credentials_env_invalid(): def test_token_credentials_env_with_exclude(): with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "prod"}, clear=False): credential = DefaultAzureCredential(exclude_environment_credential=True) - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] assert EnvironmentCredential not in actual_classes @@ -135,17 +133,12 @@ def test_token_credentials_env_workload_identity_credential(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only WorkloadIdentityCredential should be present assert WorkloadIdentityCredential in actual_classes assert len(actual_classes) == 1 - # Verify other credentials are not present - assert EnvironmentCredential not in actual_classes - assert ManagedIdentityCredential not in actual_classes - assert AzureCliCredential not in actual_classes - def test_token_credentials_env_environment_credential(): """With AZURE_TOKEN_CREDENTIALS=EnvironmentCredential, only EnvironmentCredential should be used""" @@ -154,17 +147,12 @@ def test_token_credentials_env_environment_credential(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only EnvironmentCredential should be present assert EnvironmentCredential in actual_classes assert len(actual_classes) == 1 - # Verify other credentials are not present - assert WorkloadIdentityCredential not in actual_classes - assert ManagedIdentityCredential not in actual_classes - assert AzureCliCredential not in actual_classes - def test_token_credentials_env_managed_identity_credential(): """With AZURE_TOKEN_CREDENTIALS=ManagedIdentityCredential, only ManagedIdentityCredential should be used""" @@ -175,17 +163,12 @@ def test_token_credentials_env_managed_identity_credential(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only ManagedIdentityCredential should be present assert ManagedIdentityCredential in actual_classes assert len(actual_classes) == 1 - # Verify other credentials are not present - assert EnvironmentCredential not in actual_classes - assert WorkloadIdentityCredential not in actual_classes - assert AzureCliCredential not in actual_classes - def test_token_credentials_env_azure_cli_credential(): """With AZURE_TOKEN_CREDENTIALS=AzureCliCredential, only AzureCliCredential should be used""" @@ -194,17 +177,12 @@ def test_token_credentials_env_azure_cli_credential(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only AzureCliCredential should be present assert AzureCliCredential in actual_classes assert len(actual_classes) == 1 - # Verify other credentials are not present - assert EnvironmentCredential not in actual_classes - assert ManagedIdentityCredential not in actual_classes - assert WorkloadIdentityCredential not in actual_classes - def test_token_credentials_env_specific_credential_case_insensitive(): """Specific credential names should be case insensitive""" @@ -213,7 +191,7 @@ def test_token_credentials_env_specific_credential_case_insensitive(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only EnvironmentCredential should be present assert EnvironmentCredential in actual_classes @@ -255,7 +233,7 @@ def test_user_exclude_flags_override_env_var(): # Test case 2: env var says use specific credential, user includes additional credential with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "environmentcredential"}, clear=False): credential = DefaultAzureCredential(exclude_powershell_credential=False) - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Both EnvironmentCredential and AzurePowerShellCredential should be present assert EnvironmentCredential in actual_classes @@ -264,7 +242,7 @@ def test_user_exclude_flags_override_env_var(): # Test case 3: env var says "dev" mode, user excludes a dev credential with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): credential = DefaultAzureCredential(exclude_cli_credential=True) - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # AzureCliCredential should NOT be present despite "dev" mode assert AzureCliCredential not in actual_classes diff --git a/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py b/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py index 91e9a41735bf..17cd9df98072 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py @@ -29,7 +29,7 @@ def test_token_credentials_env_dev(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # All dev credentials should be present (if supported) if SharedTokenCacheCredential.supported(): @@ -40,6 +40,9 @@ def test_token_credentials_env_dev(): assert AzureDeveloperCliCredential in actual_classes assert AzurePowerShellCredential in actual_classes + # Assert no duplicates + assert len(actual_classes) == len(set(actual_classes)) + # Production credentials should NOT be present for cred_class in prod_credentials: if cred_class == WorkloadIdentityCredential: @@ -60,18 +63,10 @@ def test_token_credentials_env_prod(): } with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "prod"}, clear=False): - # Print to verify the environment variable is set in the test - print(f"AZURE_TOKEN_CREDENTIALS={os.environ.get(EnvironmentVariables.AZURE_TOKEN_CREDENTIALS)}") - credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} - - # Print which credentials are actually in the chain - print("Credentials in chain:") - for cls in actual_classes: - print(f" - {cls.__name__}") + actual_classes = [c.__class__ for c in credential.credentials] # Production credentials should be present assert EnvironmentCredential in actual_classes @@ -81,6 +76,9 @@ def test_token_credentials_env_prod(): if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): assert WorkloadIdentityCredential in actual_classes + # Assert no duplicates + assert len(actual_classes) == len(set(actual_classes)) + # Developer credentials should NOT be present for cred_class in dev_credentials: assert cred_class not in actual_classes @@ -93,7 +91,7 @@ def test_token_credentials_env_case_insensitive(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # EnvironmentCredential (prod) should not be present assert EnvironmentCredential not in actual_classes @@ -113,7 +111,7 @@ def test_token_credentials_env_invalid(): def test_token_credentials_env_with_exclude(): with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "prod"}, clear=False): credential = DefaultAzureCredential(exclude_environment_credential=True) - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] assert EnvironmentCredential not in actual_classes @@ -135,17 +133,12 @@ def test_token_credentials_env_workload_identity_credential(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only WorkloadIdentityCredential should be present assert WorkloadIdentityCredential in actual_classes assert len(actual_classes) == 1 - # Verify other credentials are not present - assert EnvironmentCredential not in actual_classes - assert ManagedIdentityCredential not in actual_classes - assert AzureCliCredential not in actual_classes - def test_token_credentials_env_environment_credential(): """With AZURE_TOKEN_CREDENTIALS=EnvironmentCredential, only EnvironmentCredential should be used""" @@ -154,17 +147,12 @@ def test_token_credentials_env_environment_credential(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only EnvironmentCredential should be present assert EnvironmentCredential in actual_classes assert len(actual_classes) == 1 - # Verify other credentials are not present - assert WorkloadIdentityCredential not in actual_classes - assert ManagedIdentityCredential not in actual_classes - assert AzureCliCredential not in actual_classes - def test_token_credentials_env_managed_identity_credential(): """With AZURE_TOKEN_CREDENTIALS=ManagedIdentityCredential, only ManagedIdentityCredential should be used""" @@ -175,17 +163,12 @@ def test_token_credentials_env_managed_identity_credential(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only ManagedIdentityCredential should be present assert ManagedIdentityCredential in actual_classes assert len(actual_classes) == 1 - # Verify other credentials are not present - assert EnvironmentCredential not in actual_classes - assert WorkloadIdentityCredential not in actual_classes - assert AzureCliCredential not in actual_classes - def test_token_credentials_env_azure_cli_credential(): """With AZURE_TOKEN_CREDENTIALS=AzureCliCredential, only AzureCliCredential should be used""" @@ -194,17 +177,12 @@ def test_token_credentials_env_azure_cli_credential(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only AzureCliCredential should be present assert AzureCliCredential in actual_classes assert len(actual_classes) == 1 - # Verify other credentials are not present - assert EnvironmentCredential not in actual_classes - assert ManagedIdentityCredential not in actual_classes - assert WorkloadIdentityCredential not in actual_classes - def test_token_credentials_env_specific_credential_case_insensitive(): """Specific credential names should be case insensitive""" @@ -213,7 +191,7 @@ def test_token_credentials_env_specific_credential_case_insensitive(): credential = DefaultAzureCredential() # Get the actual credential classes in the chain - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Only EnvironmentCredential should be present assert EnvironmentCredential in actual_classes @@ -255,7 +233,7 @@ def test_user_exclude_flags_override_env_var(): # Test case 2: env var says use specific credential, user includes additional credential with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "environmentcredential"}, clear=False): credential = DefaultAzureCredential(exclude_powershell_credential=False) - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # Both EnvironmentCredential and AzurePowerShellCredential should be present assert EnvironmentCredential in actual_classes @@ -264,7 +242,7 @@ def test_user_exclude_flags_override_env_var(): # Test case 3: env var says "dev" mode, user excludes a dev credential with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): credential = DefaultAzureCredential(exclude_cli_credential=True) - actual_classes = {c.__class__ for c in credential.credentials} + actual_classes = [c.__class__ for c in credential.credentials] # AzureCliCredential should NOT be present despite "dev" mode assert AzureCliCredential not in actual_classes