From e687121d633658c3405a29b9942f013d32682558 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 08:22:43 -0700 Subject: [PATCH 01/12] Added `AZURE_TOKEN_CREDENTIALS` environment variable support --- sdk/identity/azure-identity/CHANGELOG.md | 4 +- .../azure/identity/_constants.py | 1 + .../azure/identity/_credentials/default.py | 15 ++ .../azure-identity/azure/identity/_version.py | 2 +- .../identity/aio/_credentials/default.py | 14 ++ .../tests/test_token_credentials_env.py | 129 ++++++++++++++++++ .../tests/test_token_credentials_env_async.py | 129 ++++++++++++++++++ 7 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 sdk/identity/azure-identity/tests/test_token_credentials_env.py create mode 100644 sdk/identity/azure-identity/tests/test_token_credentials_env_async.py diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index cbc3c3a8276c..ea2c288e923e 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,9 +1,11 @@ # Release History -## 1.22.1 (Unreleased) +## 1.23.0 (Unreleased) ### Features Added +- Added `AZURE_TOKEN_CREDENTIALS` environment variable to `DefaultAzureCredential` to allow for choosing groups of credentials. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/identity/azure-identity/azure/identity/_constants.py b/sdk/identity/azure-identity/azure/identity/_constants.py index 92d56a7d3401..ff0d47929ffb 100644 --- a/sdk/identity/azure-identity/azure/identity/_constants.py +++ b/sdk/identity/azure-identity/azure/identity/_constants.py @@ -64,4 +64,5 @@ class EnvironmentVariables: AZURE_REGIONAL_AUTHORITY_NAME = "AZURE_REGIONAL_AUTHORITY_NAME" AZURE_FEDERATED_TOKEN_FILE = "AZURE_FEDERATED_TOKEN_FILE" + AZURE_TOKEN_CREDENTIALS = "AZURE_TOKEN_CREDENTIALS" WORKLOAD_IDENTITY_VARS = (AZURE_AUTHORITY_HOST, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index c4eea4fb915a..5cdc034c504d 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -133,6 +133,7 @@ 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, "").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) @@ -143,6 +144,20 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement 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 + credentials: List[SupportsTokenInfo] = [] within_dac.set(True) if not exclude_environment_credential: diff --git a/sdk/identity/azure-identity/azure/identity/_version.py b/sdk/identity/azure-identity/azure/identity/_version.py index 6d10094d199d..ab116a1aed66 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.22.1" +VERSION = "1.23.0" 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 53ad1c45630e..11912cd30b97 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -125,6 +125,7 @@ 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, "").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) @@ -134,6 +135,19 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement 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 + credentials: List[AsyncSupportsTokenInfo] = [] within_dac.set(True) if not exclude_environment_credential: diff --git a/sdk/identity/azure-identity/tests/test_token_credentials_env.py b/sdk/identity/azure-identity/tests/test_token_credentials_env.py new file mode 100644 index 000000000000..7bcf14d826ab --- /dev/null +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -0,0 +1,129 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import os +from unittest.mock import patch + +import pytest + +from azure.identity import ( + AzureCliCredential, + AzureDeveloperCliCredential, + AzurePowerShellCredential, + DefaultAzureCredential, + EnvironmentCredential, + ManagedIdentityCredential, + SharedTokenCacheCredential, + WorkloadIdentityCredential, +) +from azure.identity._constants import EnvironmentVariables + + +def test_token_credentials_env_dev(): + """With AZURE_TOKEN_CREDENTIALS=dev, DefaultAzureCredential should use only developer credentials""" + + dev_credentials = { + SharedTokenCacheCredential, + AzureCliCredential, + AzureDeveloperCliCredential, + AzurePowerShellCredential, + } + + prod_credentials = { + EnvironmentCredential, + WorkloadIdentityCredential, + ManagedIdentityCredential + } + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # All dev credentials should be present (if supported) + if SharedTokenCacheCredential.supported(): + assert SharedTokenCacheCredential in actual_classes + + # Other developer credentials should be present + assert AzureCliCredential in actual_classes + assert AzureDeveloperCliCredential in actual_classes + assert AzurePowerShellCredential in actual_classes + + # Production credentials should NOT be present + for cred_class in prod_credentials: + if cred_class == WorkloadIdentityCredential: + # Skip this check unless env vars are set + if not all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): + continue + assert cred_class not in actual_classes + + +def test_token_credentials_env_prod(): + """With AZURE_TOKEN_CREDENTIALS=prod, DefaultAzureCredential should use only production credentials""" + + dev_credentials = { + SharedTokenCacheCredential, + AzureCliCredential, + AzureDeveloperCliCredential, + AzurePowerShellCredential, + } + + 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__}") + + # Production credentials should be present + assert EnvironmentCredential in actual_classes + assert ManagedIdentityCredential in actual_classes + + # Check WorkloadIdentityCredential only if env vars are set + if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): + assert WorkloadIdentityCredential in actual_classes + + # Developer credentials should NOT be present + for cred_class in dev_credentials: + assert cred_class not in actual_classes + + +def test_token_credentials_env_case_insensitive(): + """AZURE_TOKEN_CREDENTIALS should be case insensitive""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "DeV"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # EnvironmentCredential (prod) should not be present + assert EnvironmentCredential not in actual_classes + + # AzureCliCredential (dev) should be present + assert AzureCliCredential in actual_classes + + +def test_token_credentials_env_invalid(): + """Invalid AZURE_TOKEN_CREDENTIALS value should use default behavior""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): + credential = DefaultAzureCredential() + + # Compare with default credential chain + default_credential = DefaultAzureCredential() + + # Classes should be the same + actual_classes = {c.__class__ for c in credential.credentials} + default_classes = {c.__class__ for c in default_credential.credentials} + + assert actual_classes == default_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 new file mode 100644 index 000000000000..d5a8d0d9b27c --- /dev/null +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env_async.py @@ -0,0 +1,129 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import os +from unittest.mock import patch + +import pytest + +from azure.identity.aio import ( + AzureCliCredential, + AzureDeveloperCliCredential, + AzurePowerShellCredential, + DefaultAzureCredential, + EnvironmentCredential, + ManagedIdentityCredential, + SharedTokenCacheCredential, + WorkloadIdentityCredential, +) +from azure.identity._constants import EnvironmentVariables + + +def test_token_credentials_env_dev(): + """With AZURE_TOKEN_CREDENTIALS=dev, DefaultAzureCredential should use only developer credentials""" + + dev_credentials = { + SharedTokenCacheCredential, + AzureCliCredential, + AzureDeveloperCliCredential, + AzurePowerShellCredential, + } + + prod_credentials = { + EnvironmentCredential, + WorkloadIdentityCredential, + ManagedIdentityCredential + } + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # All dev credentials should be present (if supported) + if SharedTokenCacheCredential.supported(): + assert SharedTokenCacheCredential in actual_classes + + # Other developer credentials should be present + assert AzureCliCredential in actual_classes + assert AzureDeveloperCliCredential in actual_classes + assert AzurePowerShellCredential in actual_classes + + # Production credentials should NOT be present + for cred_class in prod_credentials: + if cred_class == WorkloadIdentityCredential: + # Skip this check unless env vars are set + if not all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): + continue + assert cred_class not in actual_classes + + +def test_token_credentials_env_prod(): + """With AZURE_TOKEN_CREDENTIALS=prod, DefaultAzureCredential should use only production credentials""" + + dev_credentials = { + SharedTokenCacheCredential, + AzureCliCredential, + AzureDeveloperCliCredential, + AzurePowerShellCredential, + } + + 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__}") + + # Production credentials should be present + assert EnvironmentCredential in actual_classes + assert ManagedIdentityCredential in actual_classes + + # Check WorkloadIdentityCredential only if env vars are set + if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): + assert WorkloadIdentityCredential in actual_classes + + # Developer credentials should NOT be present + for cred_class in dev_credentials: + assert cred_class not in actual_classes + + +def test_token_credentials_env_case_insensitive(): + """AZURE_TOKEN_CREDENTIALS should be case insensitive""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "DeV"}, clear=False): + credential = DefaultAzureCredential() + + # Get the actual credential classes in the chain + actual_classes = {c.__class__ for c in credential.credentials} + + # EnvironmentCredential (prod) should not be present + assert EnvironmentCredential not in actual_classes + + # AzureCliCredential (dev) should be present + assert AzureCliCredential in actual_classes + + +def test_token_credentials_env_invalid(): + """Invalid AZURE_TOKEN_CREDENTIALS value should use default behavior""" + + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): + credential = DefaultAzureCredential() + + # Compare with default credential chain + default_credential = DefaultAzureCredential() + + # Classes should be the same + actual_classes = {c.__class__ for c in credential.credentials} + default_classes = {c.__class__ for c in default_credential.credentials} + + assert actual_classes == default_classes From eb3c461380d6c9e89f50ad8419eb7ee2cceb0b58 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 08:28:27 -0700 Subject: [PATCH 02/12] add trimming --- .../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 5cdc034c504d..7d4e8f3be4a6 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -133,7 +133,7 @@ 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, "").lower() + 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) 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 11912cd30b97..cf8c776ce165 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -125,7 +125,7 @@ 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, "").lower() + 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) From b3bc48f1f40a64658a259c9452762ba71989f20a Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 08:30:04 -0700 Subject: [PATCH 03/12] update --- .../tests/test_token_credentials_env.py | 56 +++++++++---------- .../tests/test_token_credentials_env_async.py | 56 +++++++++---------- 2 files changed, 52 insertions(+), 60 deletions(-) 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 7bcf14d826ab..cef8e71db85b 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -9,7 +9,7 @@ from azure.identity import ( AzureCliCredential, - AzureDeveloperCliCredential, + AzureDeveloperCliCredential, AzurePowerShellCredential, DefaultAzureCredential, EnvironmentCredential, @@ -22,39 +22,35 @@ def test_token_credentials_env_dev(): """With AZURE_TOKEN_CREDENTIALS=dev, DefaultAzureCredential should use only developer credentials""" - + dev_credentials = { SharedTokenCacheCredential, AzureCliCredential, AzureDeveloperCliCredential, AzurePowerShellCredential, } - - prod_credentials = { - EnvironmentCredential, - WorkloadIdentityCredential, - ManagedIdentityCredential - } - + + prod_credentials = {EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential} + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): credential = DefaultAzureCredential() - + # Get the actual credential classes in the chain actual_classes = {c.__class__ for c in credential.credentials} - + # All dev credentials should be present (if supported) if SharedTokenCacheCredential.supported(): assert SharedTokenCacheCredential in actual_classes - + # Other developer credentials should be present assert AzureCliCredential in actual_classes assert AzureDeveloperCliCredential in actual_classes assert AzurePowerShellCredential in actual_classes - + # Production credentials should NOT be present for cred_class in prod_credentials: if cred_class == WorkloadIdentityCredential: - # Skip this check unless env vars are set + # Skip this check unless env vars are set if not all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): continue assert cred_class not in actual_classes @@ -62,36 +58,36 @@ def test_token_credentials_env_dev(): def test_token_credentials_env_prod(): """With AZURE_TOKEN_CREDENTIALS=prod, DefaultAzureCredential should use only production credentials""" - + dev_credentials = { SharedTokenCacheCredential, AzureCliCredential, AzureDeveloperCliCredential, AzurePowerShellCredential, } - + 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__}") - + # Production credentials should be present assert EnvironmentCredential in actual_classes assert ManagedIdentityCredential in actual_classes - + # Check WorkloadIdentityCredential only if env vars are set if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): assert WorkloadIdentityCredential in actual_classes - + # Developer credentials should NOT be present for cred_class in dev_credentials: assert cred_class not in actual_classes @@ -99,31 +95,31 @@ def test_token_credentials_env_prod(): def test_token_credentials_env_case_insensitive(): """AZURE_TOKEN_CREDENTIALS should be case insensitive""" - + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "DeV"}, clear=False): credential = DefaultAzureCredential() - + # Get the actual credential classes in the chain actual_classes = {c.__class__ for c in credential.credentials} - + # EnvironmentCredential (prod) should not be present assert EnvironmentCredential not in actual_classes - + # AzureCliCredential (dev) should be present assert AzureCliCredential in actual_classes def test_token_credentials_env_invalid(): """Invalid AZURE_TOKEN_CREDENTIALS value should use default behavior""" - + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): credential = DefaultAzureCredential() - + # Compare with default credential chain default_credential = DefaultAzureCredential() - + # Classes should be the same actual_classes = {c.__class__ for c in credential.credentials} default_classes = {c.__class__ for c in default_credential.credentials} - + assert actual_classes == default_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 d5a8d0d9b27c..e7ce61066946 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 @@ -9,7 +9,7 @@ from azure.identity.aio import ( AzureCliCredential, - AzureDeveloperCliCredential, + AzureDeveloperCliCredential, AzurePowerShellCredential, DefaultAzureCredential, EnvironmentCredential, @@ -22,39 +22,35 @@ def test_token_credentials_env_dev(): """With AZURE_TOKEN_CREDENTIALS=dev, DefaultAzureCredential should use only developer credentials""" - + dev_credentials = { SharedTokenCacheCredential, AzureCliCredential, AzureDeveloperCliCredential, AzurePowerShellCredential, } - - prod_credentials = { - EnvironmentCredential, - WorkloadIdentityCredential, - ManagedIdentityCredential - } - + + prod_credentials = {EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential} + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): credential = DefaultAzureCredential() - + # Get the actual credential classes in the chain actual_classes = {c.__class__ for c in credential.credentials} - + # All dev credentials should be present (if supported) if SharedTokenCacheCredential.supported(): assert SharedTokenCacheCredential in actual_classes - + # Other developer credentials should be present assert AzureCliCredential in actual_classes assert AzureDeveloperCliCredential in actual_classes assert AzurePowerShellCredential in actual_classes - + # Production credentials should NOT be present for cred_class in prod_credentials: if cred_class == WorkloadIdentityCredential: - # Skip this check unless env vars are set + # Skip this check unless env vars are set if not all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): continue assert cred_class not in actual_classes @@ -62,36 +58,36 @@ def test_token_credentials_env_dev(): def test_token_credentials_env_prod(): """With AZURE_TOKEN_CREDENTIALS=prod, DefaultAzureCredential should use only production credentials""" - + dev_credentials = { SharedTokenCacheCredential, AzureCliCredential, AzureDeveloperCliCredential, AzurePowerShellCredential, } - + 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__}") - + # Production credentials should be present assert EnvironmentCredential in actual_classes assert ManagedIdentityCredential in actual_classes - + # Check WorkloadIdentityCredential only if env vars are set if all(os.environ.get(var) for var in EnvironmentVariables.WORKLOAD_IDENTITY_VARS): assert WorkloadIdentityCredential in actual_classes - + # Developer credentials should NOT be present for cred_class in dev_credentials: assert cred_class not in actual_classes @@ -99,31 +95,31 @@ def test_token_credentials_env_prod(): def test_token_credentials_env_case_insensitive(): """AZURE_TOKEN_CREDENTIALS should be case insensitive""" - + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "DeV"}, clear=False): credential = DefaultAzureCredential() - + # Get the actual credential classes in the chain actual_classes = {c.__class__ for c in credential.credentials} - + # EnvironmentCredential (prod) should not be present assert EnvironmentCredential not in actual_classes - + # AzureCliCredential (dev) should be present assert AzureCliCredential in actual_classes def test_token_credentials_env_invalid(): """Invalid AZURE_TOKEN_CREDENTIALS value should use default behavior""" - + with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): credential = DefaultAzureCredential() - + # Compare with default credential chain default_credential = DefaultAzureCredential() - + # Classes should be the same actual_classes = {c.__class__ for c in credential.credentials} default_classes = {c.__class__ for c in default_credential.credentials} - + assert actual_classes == default_classes From d592b06b56482d69c1827824e5d5e867c61e25de Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 10:44:52 -0700 Subject: [PATCH 04/12] updates --- sdk/identity/azure-identity/CHANGELOG.md | 2 ++ .../azure/identity/_credentials/default.py | 10 ++++++++-- .../azure/identity/aio/_credentials/default.py | 10 ++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index ea2c288e923e..56505a93c8a6 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -5,6 +5,8 @@ ### Features Added - Added `AZURE_TOKEN_CREDENTIALS` environment variable to `DefaultAzureCredential` to allow for choosing groups of credentials. + - `prod` for `EnvironmentCredential`, `WorkloadIdentityCredential`, and `ManagedIdentityCredential`. + - `dev` for `SharedTokenCredential`, `AureCliCredential`, `AzurePowershellCredential`, and `AzureDeveloperCliCredential`. ### 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 7d4e8f3be4a6..8c2e6c1bcced 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -157,6 +157,12 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement 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'." + ) credentials: List[SupportsTokenInfo] = [] within_dac.set(True) @@ -170,7 +176,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement client_id=cast(str, client_id), tenant_id=workload_identity_tenant_id, token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE], - **kwargs + **kwargs, ) ) if not exclude_managed_identity_credential: @@ -178,7 +184,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement ManagedIdentityCredential( client_id=managed_identity_client_id, _exclude_workload_identity_credential=exclude_workload_identity_credential, - **kwargs + **kwargs, ) ) if not exclude_shared_token_cache_credential and SharedTokenCacheCredential.supported(): 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 cf8c776ce165..08b3ac48bf01 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py @@ -147,6 +147,12 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement 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'." + ) credentials: List[AsyncSupportsTokenInfo] = [] within_dac.set(True) @@ -160,7 +166,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement client_id=cast(str, client_id), tenant_id=workload_identity_tenant_id, token_file_path=os.environ[EnvironmentVariables.AZURE_FEDERATED_TOKEN_FILE], - **kwargs + **kwargs, ) ) if not exclude_managed_identity_credential: @@ -168,7 +174,7 @@ def __init__(self, **kwargs: Any) -> None: # pylint: disable=too-many-statement ManagedIdentityCredential( client_id=managed_identity_client_id, _exclude_workload_identity_credential=exclude_workload_identity_credential, - **kwargs + **kwargs, ) ) if not exclude_shared_token_cache_credential and SharedTokenCacheCredential.supported(): From 7e669a49520fb0eb5c2534d07349ab93659680b1 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 11:05:45 -0700 Subject: [PATCH 05/12] udpate tests --- .../tests/test_token_credentials_env.py | 12 ++---------- .../tests/test_token_credentials_env_async.py | 12 ++---------- 2 files changed, 4 insertions(+), 20 deletions(-) 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 cef8e71db85b..e7dca8739fe8 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -113,13 +113,5 @@ def test_token_credentials_env_invalid(): """Invalid AZURE_TOKEN_CREDENTIALS value should use default behavior""" with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): - credential = DefaultAzureCredential() - - # Compare with default credential chain - default_credential = DefaultAzureCredential() - - # Classes should be the same - actual_classes = {c.__class__ for c in credential.credentials} - default_classes = {c.__class__ for c in default_credential.credentials} - - assert actual_classes == default_classes + with pytest.raises(ValueError): + credential = DefaultAzureCredential() 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 e7ce61066946..5ae00a16e5d7 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 @@ -113,13 +113,5 @@ def test_token_credentials_env_invalid(): """Invalid AZURE_TOKEN_CREDENTIALS value should use default behavior""" with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): - credential = DefaultAzureCredential() - - # Compare with default credential chain - default_credential = DefaultAzureCredential() - - # Classes should be the same - actual_classes = {c.__class__ for c in credential.credentials} - default_classes = {c.__class__ for c in default_credential.credentials} - - assert actual_classes == default_classes + with pytest.raises(ValueError): + credential = DefaultAzureCredential() From 81aeaf39a0fb7bbb47480c1915a03ba3f9eb0ffc Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 11:32:39 -0700 Subject: [PATCH 06/12] fix typo --- sdk/identity/azure-identity/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 56505a93c8a6..73c31fce72a9 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -6,7 +6,7 @@ - Added `AZURE_TOKEN_CREDENTIALS` environment variable to `DefaultAzureCredential` to allow for choosing groups of credentials. - `prod` for `EnvironmentCredential`, `WorkloadIdentityCredential`, and `ManagedIdentityCredential`. - - `dev` for `SharedTokenCredential`, `AureCliCredential`, `AzurePowershellCredential`, and `AzureDeveloperCliCredential`. + - `dev` for `SharedTokenCredential`, `AzureCliCredential`, `AzurePowershellCredential`, and `AzureDeveloperCliCredential`. ### Breaking Changes From 5beaf8b3d366dddc044798ee48f528f09231b19c Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 11:41:33 -0700 Subject: [PATCH 07/12] Update sdk/identity/azure-identity/tests/test_token_credentials_env_async.py Co-authored-by: Paul Van Eck --- .../azure-identity/tests/test_token_credentials_env_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5ae00a16e5d7..f8df522bfaa7 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 @@ -110,7 +110,7 @@ def test_token_credentials_env_case_insensitive(): def test_token_credentials_env_invalid(): - """Invalid AZURE_TOKEN_CREDENTIALS value should use default behavior""" + """Invalid AZURE_TOKEN_CREDENTIALS value should raise an error""" with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): with pytest.raises(ValueError): From 1399b984466fda76baddca28be9819c6f20076d6 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 11:41:40 -0700 Subject: [PATCH 08/12] Update sdk/identity/azure-identity/tests/test_token_credentials_env.py Co-authored-by: Paul Van Eck --- sdk/identity/azure-identity/tests/test_token_credentials_env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e7dca8739fe8..a3429cb9e5a2 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -110,7 +110,7 @@ def test_token_credentials_env_case_insensitive(): def test_token_credentials_env_invalid(): - """Invalid AZURE_TOKEN_CREDENTIALS value should use default behavior""" + """Invalid AZURE_TOKEN_CREDENTIALS value should raise an error""" with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): with pytest.raises(ValueError): From b036859ddd37d476751cf2ff267933cc85edb3b4 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 11:41:55 -0700 Subject: [PATCH 09/12] Update sdk/identity/azure-identity/tests/test_token_credentials_env.py Co-authored-by: Paul Van Eck --- .../azure-identity/tests/test_token_credentials_env.py | 6 ------ 1 file changed, 6 deletions(-) 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 a3429cb9e5a2..8175cafc592a 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -23,12 +23,6 @@ def test_token_credentials_env_dev(): """With AZURE_TOKEN_CREDENTIALS=dev, DefaultAzureCredential should use only developer credentials""" - dev_credentials = { - SharedTokenCacheCredential, - AzureCliCredential, - AzureDeveloperCliCredential, - AzurePowerShellCredential, - } prod_credentials = {EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential} From 14b0d6b98aef9bcae3e957a862868f7224edc051 Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 11:42:05 -0700 Subject: [PATCH 10/12] Update sdk/identity/azure-identity/tests/test_token_credentials_env_async.py Co-authored-by: Paul Van Eck --- .../tests/test_token_credentials_env_async.py | 6 ------ 1 file changed, 6 deletions(-) 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 f8df522bfaa7..a91f82658050 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 @@ -23,12 +23,6 @@ def test_token_credentials_env_dev(): """With AZURE_TOKEN_CREDENTIALS=dev, DefaultAzureCredential should use only developer credentials""" - dev_credentials = { - SharedTokenCacheCredential, - AzureCliCredential, - AzureDeveloperCliCredential, - AzurePowerShellCredential, - } prod_credentials = {EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential} From eaf602bfdca2f6907dae645cc714c15975b5a86f Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 11:42:15 -0700 Subject: [PATCH 11/12] Update sdk/identity/azure-identity/CHANGELOG.md Co-authored-by: Paul Van Eck --- sdk/identity/azure-identity/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 73c31fce72a9..e45d50e31ece 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -6,7 +6,7 @@ - Added `AZURE_TOKEN_CREDENTIALS` environment variable to `DefaultAzureCredential` to allow for choosing groups of credentials. - `prod` for `EnvironmentCredential`, `WorkloadIdentityCredential`, and `ManagedIdentityCredential`. - - `dev` for `SharedTokenCredential`, `AzureCliCredential`, `AzurePowershellCredential`, and `AzureDeveloperCliCredential`. + - `dev` for `SharedTokenCacheCredential`, `AzureCliCredential`, `AzurePowershellCredential`, and `AzureDeveloperCliCredential`. ### Breaking Changes From e7b8245385674e93e84a883f814a423828dba28f Mon Sep 17 00:00:00 2001 From: Xiang Yan Date: Tue, 13 May 2025 12:28:49 -0700 Subject: [PATCH 12/12] add tests --- .../azure-identity/tests/test_token_credentials_env.py | 9 ++++++++- .../tests/test_token_credentials_env_async.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) 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 8175cafc592a..10ac3783a1df 100644 --- a/sdk/identity/azure-identity/tests/test_token_credentials_env.py +++ b/sdk/identity/azure-identity/tests/test_token_credentials_env.py @@ -23,7 +23,6 @@ def test_token_credentials_env_dev(): """With AZURE_TOKEN_CREDENTIALS=dev, DefaultAzureCredential should use only developer credentials""" - prod_credentials = {EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential} with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): @@ -109,3 +108,11 @@ def test_token_credentials_env_invalid(): with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): with pytest.raises(ValueError): credential = DefaultAzureCredential() + + +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} + + assert EnvironmentCredential 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 a91f82658050..9db1193bd745 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 @@ -23,7 +23,6 @@ def test_token_credentials_env_dev(): """With AZURE_TOKEN_CREDENTIALS=dev, DefaultAzureCredential should use only developer credentials""" - prod_credentials = {EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential} with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "dev"}, clear=False): @@ -109,3 +108,11 @@ def test_token_credentials_env_invalid(): with patch.dict("os.environ", {EnvironmentVariables.AZURE_TOKEN_CREDENTIALS: "invalid"}, clear=False): with pytest.raises(ValueError): credential = DefaultAzureCredential() + + +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} + + assert EnvironmentCredential not in actual_classes