Skip to content

Commit 1f32b91

Browse files
authored
Default credentials are configurable by kwargs (#8514)
1 parent c4c060f commit 1f32b91

5 files changed

Lines changed: 142 additions & 27 deletions

File tree

sdk/identity/azure-identity/HISTORY.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
### 1.1.0b1 Unreleased
44
- Constructing `DefaultAzureCredential` no longer raises `ImportError` on Python
55
3.8 on Windows ([8294](https://github.com/Azure/azure-sdk-for-python/pull/8294))
6+
- `InteractiveBrowserCredential` raises when unable to open a web browser
7+
([8465](https://github.com/Azure/azure-sdk-for-python/pull/8465))
8+
- `InteractiveBrowserCredential` prompts for account selection
9+
([8470](https://github.com/Azure/azure-sdk-for-python/pull/8470))
10+
- The credentials composing `DefaultAzureCredential` are configurable by keyword
11+
arguments ([8514](https://github.com/Azure/azure-sdk-for-python/pull/8514))
612

713

814
### 2019-11-05 1.0.1

sdk/identity/azure-identity/azure/identity/_credentials/default.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from azure.core.exceptions import ClientAuthenticationError
99

10-
from .._constants import EnvironmentVariables
10+
from .._constants import EnvironmentVariables, KnownAuthorities
11+
from .browser import InteractiveBrowserCredential
1112
from .chained import ChainedTokenCredential
1213
from .environment import EnvironmentCredential
1314
from .managed_identity import ManagedIdentityCredential
@@ -29,25 +30,46 @@ class DefaultAzureCredential(ChainedTokenCredential):
2930
identities are in the cache, then the value of the environment variable ``AZURE_USERNAME`` is used to select
3031
which identity to use. See :class:`~azure.identity.SharedTokenCacheCredential` for more details.
3132
33+
This default behavior is configurable with keyword arguments.
34+
3235
:keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com',
3336
the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities`
3437
defines authorities for other clouds. Managed identities ignore this because they reside in a single cloud.
38+
:keyword bool exclude_environment_credential: Whether to exclude a service principal configured by environment
39+
variables from the credential. Defaults to **False**.
40+
:keyword bool exclude_managed_identity_credential: Whether to exclude managed identity from the credential.
41+
Defaults to **False**.
42+
:keyword bool exclude_shared_token_cache_credential: Whether to exclude the shared token cache. Defaults to
43+
**False**.
44+
:keyword bool exclude_interactive_browser_credential: Whether to exclude interactive browser authentication (see
45+
:class:`~azure.identity.InteractiveBrowserCredential`). Defaults to **True**.
3546
"""
3647

3748
def __init__(self, **kwargs):
38-
authority = kwargs.pop("authority", None)
39-
credentials = [EnvironmentCredential(authority=authority, **kwargs), ManagedIdentityCredential(**kwargs)]
49+
authority = kwargs.pop("authority", KnownAuthorities.AZURE_PUBLIC_CLOUD)
50+
51+
username = kwargs.pop("username", os.environ.get(EnvironmentVariables.AZURE_USERNAME))
52+
53+
exclude_environment_credential = kwargs.pop("exclude_environment_credential", False)
54+
exclude_managed_identity_credential = kwargs.pop("exclude_managed_identity_credential", False)
55+
exclude_shared_token_cache_credential = kwargs.pop("exclude_shared_token_cache_credential", False)
56+
exclude_interactive_browser_credential = kwargs.pop("exclude_interactive_browser_credential", True)
4057

41-
# SharedTokenCacheCredential is part of the default only on supported platforms.
42-
if SharedTokenCacheCredential.supported():
58+
credentials = []
59+
if not exclude_environment_credential:
60+
credentials.append(EnvironmentCredential(authority=authority, **kwargs))
61+
if not exclude_managed_identity_credential:
62+
credentials.append(ManagedIdentityCredential(**kwargs))
63+
if not exclude_shared_token_cache_credential and SharedTokenCacheCredential.supported():
4364
try:
4465
# username is only required to disambiguate, when the cache contains tokens for multiple identities
45-
username = os.environ.get(EnvironmentVariables.AZURE_USERNAME)
4666
shared_cache = SharedTokenCacheCredential(username=username, authority=authority, **kwargs)
4767
credentials.append(shared_cache)
4868
except Exception as ex: # pylint:disable=broad-except
4969
# transitive dependency pywin32 doesn't support 3.8 (https://github.com/mhammond/pywin32/issues/1431)
5070
_LOGGER.info("Shared token cache is unavailable: '%s'", ex)
71+
if not exclude_interactive_browser_credential:
72+
credentials.append(InteractiveBrowserCredential())
5173

5274
super(DefaultAzureCredential, self).__init__(*credentials)
5375

@@ -56,7 +78,7 @@ def get_token(self, *scopes, **kwargs):
5678
return super(DefaultAzureCredential, self).get_token(*scopes, **kwargs)
5779
except ClientAuthenticationError as e:
5880
raise ClientAuthenticationError(message="""
59-
{}\n\nPlease visit the Azure identity Python SDK docs at
60-
https://aka.ms/python-sdk-identity#defaultazurecredential
81+
{}\n\nPlease visit the Azure identity Python SDK docs at
82+
https://aka.ms/python-sdk-identity#defaultazurecredential
6183
to learn what options DefaultAzureCredential supports"""
6284
.format(e.message))

sdk/identity/azure-identity/azure/identity/aio/_credentials/default.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
import os
77

8-
from ..._constants import EnvironmentVariables
8+
from ..._constants import EnvironmentVariables, KnownAuthorities
99
from .chained import ChainedTokenCredential
1010
from .environment import EnvironmentCredential
1111
from .managed_identity import ManagedIdentityCredential
@@ -27,22 +27,36 @@ class DefaultAzureCredential(ChainedTokenCredential):
2727
identities are in the cache, then the value of the environment variable ``AZURE_USERNAME`` is used to select
2828
which identity to use. See :class:`~azure.identity.aio.SharedTokenCacheCredential` for more details.
2929
30+
This default behavior is configurable with keyword arguments.
31+
3032
:keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com',
3133
the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities`
3234
defines authorities for other clouds. Managed identities ignore this because they reside in a single cloud.
35+
:keyword bool exclude_environment_credential: Whether to exclude a service principal configured by environment
36+
variables from the credential. Defaults to **False**.
37+
:keyword bool exclude_managed_identity_credential: Whether to exclude managed identity from the credential.
38+
Defaults to **False**.
39+
:keyword bool exclude_shared_token_cache_credential: Whether to exclude the shared token cache. Defaults to
40+
**False**.
3341
"""
3442

3543
def __init__(self, **kwargs):
36-
authority = kwargs.pop("authority", None)
37-
credentials = [EnvironmentCredential(authority=authority, **kwargs), ManagedIdentityCredential(**kwargs)]
44+
authority = kwargs.pop("authority", KnownAuthorities.AZURE_PUBLIC_CLOUD)
45+
46+
username = kwargs.pop("username", os.environ.get(EnvironmentVariables.AZURE_USERNAME))
47+
48+
exclude_environment_credential = kwargs.pop("exclude_environment_credential", False)
49+
exclude_managed_identity_credential = kwargs.pop("exclude_managed_identity_credential", False)
50+
exclude_shared_token_cache_credential = kwargs.pop("exclude_shared_token_cache_credential", False)
3851

39-
# SharedTokenCacheCredential is part of the default only on supported platforms.
40-
if SharedTokenCacheCredential.supported():
52+
credentials = []
53+
if not exclude_environment_credential:
54+
credentials.append(EnvironmentCredential(authority=authority, **kwargs))
55+
if not exclude_managed_identity_credential:
56+
credentials.append(ManagedIdentityCredential(**kwargs))
57+
if not exclude_shared_token_cache_credential and SharedTokenCacheCredential.supported():
4158
try:
42-
# username is only required to disambiguate, when the cache contains tokens for multiple identities
43-
username = os.environ.get(EnvironmentVariables.AZURE_USERNAME)
44-
shared_cache = SharedTokenCacheCredential(username=username, authority=authority, **kwargs)
45-
credentials.append(shared_cache)
59+
credentials.append(SharedTokenCacheCredential(username=username, authority=authority, **kwargs))
4660
except Exception as ex: # pylint:disable=broad-except
4761
# transitive dependency pywin32 doesn't support 3.8 (https://github.com/mhammond/pywin32/issues/1431)
4862
_LOGGER.info("Shared token cache is unavailable: '%s'", ex)

sdk/identity/azure-identity/tests/test_default.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
# Licensed under the MIT License.
44
# ------------------------------------
55
from azure.core.credentials import AccessToken
6-
from azure.identity import DefaultAzureCredential, KnownAuthorities, SharedTokenCacheCredential
6+
from azure.identity import (
7+
DefaultAzureCredential,
8+
InteractiveBrowserCredential,
9+
KnownAuthorities,
10+
SharedTokenCacheCredential,
11+
)
712
from azure.identity._constants import EnvironmentVariables
13+
from azure.identity._credentials.managed_identity import ImdsCredential, MsiCredential
814
from six.moves.urllib_parse import urlparse
915

1016
from helpers import mock_response
@@ -32,11 +38,12 @@ def test_default_credential_authority():
3238

3339
def exercise_credentials(authority_kwarg, expected_authority=None):
3440
expected_authority = expected_authority or authority_kwarg
41+
3542
def send(request, **_):
36-
scheme, netloc, path, _, _, _ = urlparse(request.url)
37-
assert scheme == "https"
38-
assert netloc == expected_authority
39-
assert path.startswith("/" + tenant_id)
43+
url = urlparse(request.url)
44+
assert url.scheme == "https"
45+
assert url.netloc == expected_authority
46+
assert url.path.startswith("/" + tenant_id)
4047
return response
4148

4249
# environment credential configured with client secret should respect authority
@@ -46,7 +53,7 @@ def send(request, **_):
4653
EnvironmentVariables.AZURE_TENANT_ID: tenant_id,
4754
}
4855
with patch("os.environ", environment):
49-
transport=Mock(send=send)
56+
transport = Mock(send=send)
5057
access_token, _ = DefaultAzureCredential(authority=authority_kwarg, transport=transport).get_token("scope")
5158
assert access_token == expected_access_token
5259

@@ -59,3 +66,38 @@ def send(request, **_):
5966
# all credentials not representing managed identities should use a specified authority or default to public cloud
6067
exercise_credentials("authority.com")
6168
exercise_credentials(None, KnownAuthorities.AZURE_PUBLIC_CLOUD)
69+
70+
71+
def test_exclude_options():
72+
def assert_credentials_not_present(chain, *excluded_credential_classes):
73+
actual = {c.__class__ for c in chain.credentials}
74+
assert len(actual)
75+
76+
# no unexpected credential is in the chain
77+
excluded = set(excluded_credential_classes)
78+
assert len(actual & excluded) == 0
79+
80+
# only excluded credentials have been excluded from the default
81+
default = {c.__class__ for c in DefaultAzureCredential().credentials}
82+
assert actual <= default # n.b. we know actual is non-empty
83+
assert default - actual <= excluded
84+
85+
# with no environment variables set, ManagedIdentityCredential = ImdsCredential
86+
with patch("os.environ", {}):
87+
credential = DefaultAzureCredential(exclude_managed_identity_credential=True)
88+
assert_credentials_not_present(credential, ImdsCredential, MsiCredential)
89+
90+
# with $MSI_ENDPOINT set, ManagedIdentityCredential = MsiCredential
91+
with patch("os.environ", {"MSI_ENDPOINT": "spam"}):
92+
credential = DefaultAzureCredential(exclude_managed_identity_credential=True)
93+
assert_credentials_not_present(credential, ImdsCredential, MsiCredential)
94+
95+
if SharedTokenCacheCredential.supported():
96+
credential = DefaultAzureCredential(exclude_shared_token_cache_credential=True)
97+
assert_credentials_not_present(credential, SharedTokenCacheCredential)
98+
99+
# interactive auth is excluded by default
100+
credential = DefaultAzureCredential(exclude_interactive_browser_credential=False)
101+
actual = {c.__class__ for c in credential.credentials}
102+
default = {c.__class__ for c in DefaultAzureCredential().credentials}
103+
assert actual - default == {InteractiveBrowserCredential}

sdk/identity/azure-identity/tests/test_default_async.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from azure.core.credentials import AccessToken
1010
from azure.identity import KnownAuthorities
1111
from azure.identity.aio import DefaultAzureCredential, SharedTokenCacheCredential
12+
from azure.identity.aio._credentials.managed_identity import ImdsCredential, MsiCredential
1213
from azure.identity._constants import EnvironmentVariables
1314
import pytest
1415

@@ -35,11 +36,12 @@ async def test_default_credential_authority():
3536

3637
async def exercise_credentials(authority_kwarg, expected_authority=None):
3738
expected_authority = expected_authority or authority_kwarg
39+
3840
async def send(request, **_):
39-
scheme, netloc, path, _, _, _ = urlparse(request.url)
40-
assert scheme == "https"
41-
assert netloc == expected_authority
42-
assert path.startswith("/" + tenant_id)
41+
url = urlparse(request.url)
42+
assert url.scheme == "https"
43+
assert url.netloc == expected_authority
44+
assert url.path.startswith("/" + tenant_id)
4345
return response
4446

4547
# environment credential configured with client secret should respect authority
@@ -70,3 +72,32 @@ async def send(request, **_):
7072
# all credentials not representing managed identities should use a specified authority or default to public cloud
7173
await exercise_credentials("authority.com")
7274
await exercise_credentials(None, KnownAuthorities.AZURE_PUBLIC_CLOUD)
75+
76+
77+
def test_exclude_options():
78+
def assert_credentials_not_present(chain, *credential_classes):
79+
actual = {c.__class__ for c in chain.credentials}
80+
assert len(actual)
81+
82+
# no unexpected credential is in the chain
83+
excluded = set(credential_classes)
84+
assert len(actual & excluded) == 0
85+
86+
# only excluded credentials have been excluded from the default
87+
default = {c.__class__ for c in DefaultAzureCredential().credentials}
88+
assert actual <= default # n.b. we know actual is non-empty
89+
assert default - actual <= excluded
90+
91+
# with no environment variables set, ManagedIdentityCredential = ImdsCredential
92+
with patch("os.environ", {}):
93+
credential = DefaultAzureCredential(exclude_managed_identity_credential=True)
94+
assert_credentials_not_present(credential, ImdsCredential, MsiCredential)
95+
96+
# with $MSI_ENDPOINT set, ManagedIdentityCredential = MsiCredential
97+
with patch("os.environ", {"MSI_ENDPOINT": "spam"}):
98+
credential = DefaultAzureCredential(exclude_managed_identity_credential=True)
99+
assert_credentials_not_present(credential, ImdsCredential, MsiCredential)
100+
101+
if SharedTokenCacheCredential.supported():
102+
credential = DefaultAzureCredential(exclude_shared_token_cache_credential=True)
103+
assert_credentials_not_present(credential, SharedTokenCacheCredential)

0 commit comments

Comments
 (0)