diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/authorization_code.py b/sdk/identity/azure-identity/azure/identity/_credentials/authorization_code.py index 59dacfcaa9c6..c535d0371f13 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/authorization_code.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/authorization_code.py @@ -6,6 +6,7 @@ from azure.core.exceptions import ClientAuthenticationError from .._internal.aad_client import AadClient +from .._internal.decorators import log_get_token if TYPE_CHECKING: # pylint:disable=unused-import,ungrouped-imports @@ -38,6 +39,7 @@ def __init__(self, tenant_id, client_id, authorization_code, redirect_uri, **kwa self._client = kwargs.pop("client", None) or AadClient(tenant_id, client_id, **kwargs) self._redirect_uri = redirect_uri + @log_get_token("AuthorizationCodeCredential") def get_token(self, *scopes, **kwargs): # type: (*str, **Any) -> AccessToken """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py index c5fe99a1119c..8e609d83ff05 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.py @@ -17,11 +17,13 @@ from .. import CredentialUnavailableError from .._internal import _scopes_to_resource +from .._internal.decorators import log_get_token if TYPE_CHECKING: # pylint:disable=ungrouped-imports from typing import Any + CLI_NOT_FOUND = "Azure CLI not found on path" COMMAND_LINE = "az account get-access-token --output json --resource {}" NOT_LOGGED_IN = "Please run 'az login' to set up an account" @@ -33,6 +35,7 @@ class AzureCliCredential(object): This requires previously logging in to Azure via "az login", and will use the CLI's currently logged in identity. """ + @log_get_token("AzureCliCredential") def get_token(self, *scopes, **kwargs): # pylint:disable=no-self-use,unused-argument # type: (*str, **Any) -> AccessToken """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py index d88972c75265..f0f032d49c9a 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING from .._internal import AadClient, CertificateCredentialBase +from .._internal.decorators import log_get_token if TYPE_CHECKING: from azure.core.credentials import AccessToken @@ -30,6 +31,7 @@ class CertificateCredential(CertificateCredentialBase): is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. """ + @log_get_token("CertificateCredential") def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument # type: (*str, **Any) -> AccessToken """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/chained.py b/sdk/identity/azure-identity/azure/identity/_credentials/chained.py index b40c373afa1d..752e16cf1f7d 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/chained.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/chained.py @@ -2,6 +2,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +import logging + from azure.core.exceptions import ClientAuthenticationError from .. import CredentialUnavailableError @@ -16,6 +18,8 @@ from typing import Any, Optional from azure.core.credentials import AccessToken, TokenCredential +_LOGGER = logging.getLogger(__name__) + def _get_error_message(history): attempts = [] @@ -61,16 +65,26 @@ def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument for credential in self.credentials: try: token = credential.get_token(*scopes, **kwargs) + _LOGGER.info("%s acquired a token from %s", self.__class__.__name__, credential.__class__.__name__) self._successful_credential = credential return token except CredentialUnavailableError as ex: # credential didn't attempt authentication because it lacks required data or state -> continue history.append((credential, ex.message)) + _LOGGER.info("%s - %s is unavailable", self.__class__.__name__, credential.__class__.__name__) except Exception as ex: # pylint: disable=broad-except # credential failed to authenticate, or something unexpectedly raised -> break history.append((credential, str(ex))) + _LOGGER.warning( + '%s.get_token failed: %s raised unexpected error "%s"', + self.__class__.__name__, + credential.__class__.__name__, + ex, + exc_info=_LOGGER.isEnabledFor(logging.DEBUG), + ) break attempts = _get_error_message(history) message = self.__class__.__name__ + " failed to retrieve a token from the included credentials." + attempts + _LOGGER.warning(message) raise ClientAuthenticationError(message=message) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py index 9e5e504ed785..94da7b0655a9 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ from .._internal import AadClient, ClientSecretCredentialBase +from .._internal.decorators import log_get_token try: from typing import TYPE_CHECKING @@ -31,6 +32,7 @@ class ClientSecretCredential(ClientSecretCredentialBase): is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. """ + @log_get_token("ClientSecretCredential") def get_token(self, *scopes, **kwargs): # type: (*str, **Any) -> AccessToken """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/default.py b/sdk/identity/azure-identity/azure/identity/_credentials/default.py index 44a802ee27c9..7282630aa80f 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/default.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/default.py @@ -102,7 +102,6 @@ def __init__(self, **kwargs): ) credentials.append(shared_cache) except Exception as ex: # pylint:disable=broad-except - # transitive dependency pywin32 doesn't support 3.8 (https://github.com/mhammond/pywin32/issues/1431) _LOGGER.info("Shared token cache is unavailable: '%s'", ex) if not exclude_visual_studio_code_credential: credentials.append(VSCodeCredential()) @@ -124,6 +123,10 @@ def get_token(self, *scopes, **kwargs): `message` attribute listing each authentication attempt and its error message. """ if self._successful_credential: - return self._successful_credential.get_token(*scopes, **kwargs) + token = self._successful_credential.get_token(*scopes, **kwargs) + _LOGGER.info( + "%s acquired a token from %s", self.__class__.__name__, self._successful_credential.__class__.__name__ + ) + return token return super(DefaultAzureCredential, self).get_token(*scopes, **kwargs) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/environment.py b/sdk/identity/azure-identity/azure/identity/_credentials/environment.py index dc37abca83c1..565a8006b324 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/environment.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/environment.py @@ -2,10 +2,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +import logging import os + from .. import CredentialUnavailableError from .._constants import EnvironmentVariables +from .._internal.decorators import log_get_token from .certificate import CertificateCredential from .client_secret import ClientSecretCredential from .user_password import UsernamePasswordCredential @@ -22,6 +25,8 @@ EnvironmentCredentialTypes = Union["CertificateCredential", "ClientSecretCredential", "UsernamePasswordCredential"] +_LOGGER = logging.getLogger(__name__) + class EnvironmentCredential(object): """A credential configured by environment variables. @@ -76,6 +81,21 @@ def __init__(self, **kwargs): **kwargs ) + if self._credential: + _LOGGER.info("Environment is configured for %s", self._credential.__class__.__name__) + else: + expected_variables = set( + EnvironmentVariables.CERT_VARS + + EnvironmentVariables.CLIENT_SECRET_VARS + + EnvironmentVariables.USERNAME_PASSWORD_VARS + ) + set_variables = [v for v in expected_variables if v in os.environ] + if set_variables: + _LOGGER.warning("Incomplete environment configuration. Set variables: %s", ", ".join(set_variables)) + else: + _LOGGER.info("No environment configuration found.") + + @log_get_token("EnvironmentCredential") def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument # type: (*str, **Any) -> AccessToken """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/managed_identity.py b/sdk/identity/azure-identity/azure/identity/_credentials/managed_identity.py index 3cf055f816c1..8bdd46f5cba9 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/managed_identity.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/managed_identity.py @@ -2,6 +2,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +import logging import os import six @@ -21,6 +22,7 @@ from .. import CredentialUnavailableError from .._authn_client import AuthnClient from .._constants import Endpoints, EnvironmentVariables +from .._internal.decorators import log_get_token from .._internal.user_agent import USER_AGENT try: @@ -32,6 +34,8 @@ # pylint:disable=unused-import from typing import Any, Optional, Type +_LOGGER = logging.getLogger(__name__) + class ManagedIdentityCredential(object): """Authenticates with an Azure managed identity in any hosting environment which supports managed identities. @@ -50,10 +54,13 @@ def __init__(self, **kwargs): # type: (**Any) -> None self._credential = None if os.environ.get(EnvironmentVariables.MSI_ENDPOINT): + _LOGGER.info("%s will use MSI", self.__class__.__name__) self._credential = MsiCredential(**kwargs) else: + _LOGGER.info("%s will use IMDS", self.__class__.__name__) self._credential = ImdsCredential(**kwargs) + @log_get_token("ManagedIdentityCredential") def get_token(self, *scopes, **kwargs): # type: (*str, **Any) -> AccessToken """Request an access token for `scopes`. @@ -160,6 +167,7 @@ def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument except Exception: # pylint:disable=broad-except # if anything else was raised, assume the endpoint is unavailable self._endpoint_available = False + _LOGGER.info("No response from the IMDS endpoint.") if not self._endpoint_available: message = "ManagedIdentityCredential authentication unavailable, no managed identity endpoint found." diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py index 2c6e84be8782..27b7ef30d074 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py @@ -5,6 +5,7 @@ from .. import CredentialUnavailableError from .._constants import AZURE_CLI_CLIENT_ID from .._internal import AadClient +from .._internal.decorators import log_get_token from .._internal.shared_token_cache import NO_TOKEN, SharedTokenCacheBase try: @@ -36,6 +37,7 @@ class SharedTokenCacheCredential(SharedTokenCacheBase): is unavailable. Defaults to False. """ + @log_get_token("SharedTokenCacheCredential") def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument # type (*str, **Any) -> AccessToken """Get an access token for `scopes` from the shared cache. diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py b/sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py index a5bcf3fc66c8..76273368a5ab 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/vscode_credential.py @@ -4,9 +4,11 @@ # ------------------------------------ import sys from typing import TYPE_CHECKING + from .._exceptions import CredentialUnavailableError from .._constants import AZURE_VSCODE_CLIENT_ID from .._internal.aad_client import AadClient +from .._internal.decorators import log_get_token if sys.platform.startswith("win"): from .._internal.win_vscode_adapter import get_credentials @@ -29,6 +31,7 @@ def __init__(self, **kwargs): self._client = kwargs.pop("_client", None) or AadClient("organizations", AZURE_VSCODE_CLIENT_ID, **kwargs) self._refresh_token = None + @log_get_token("VSCodeCredential") def get_token(self, *scopes, **kwargs): # type: (*str, **Any) -> AccessToken """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py index 85851d26a165..60d776175e4f 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py @@ -36,7 +36,7 @@ def get_default_authority(): from .aadclient_certificate import AadClientCertificate from .certificate_credential_base import CertificateCredentialBase from .client_secret_credential_base import ClientSecretCredentialBase -from .exception_wrapper import wrap_exceptions +from .decorators import wrap_exceptions from .msal_credentials import InteractiveCredential diff --git a/sdk/identity/azure-identity/azure/identity/_internal/decorators.py b/sdk/identity/azure-identity/azure/identity/_internal/decorators.py new file mode 100644 index 000000000000..6c00f74bff67 --- /dev/null +++ b/sdk/identity/azure-identity/azure/identity/_internal/decorators.py @@ -0,0 +1,52 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import functools +import logging + +from six import raise_from +from azure.core.exceptions import ClientAuthenticationError + +_LOGGER = logging.getLogger(__name__) + + +def log_get_token(class_name): + """Adds logging around get_token calls. + + :param str class_name: required for the sake of Python 2.7, which lacks an easy way to get the credential's class + name from the decorated function + """ + + def decorator(fn): + qualified_name = class_name + ".get_token" + + @functools.wraps(fn) + def wrapper(*args, **kwargs): + try: + token = fn(*args, **kwargs) + _LOGGER.info("%s succeeded", qualified_name) + return token + except Exception as ex: + _LOGGER.warning("%s failed: %s", qualified_name, ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + raise + + return wrapper + + return decorator + + +def wrap_exceptions(fn): + """Prevents leaking exceptions defined outside azure-core by raising ClientAuthenticationError from them.""" + + @functools.wraps(fn) + def wrapper(*args, **kwargs): + try: + return fn(*args, **kwargs) + except ClientAuthenticationError: + raise + except Exception as ex: # pylint:disable=broad-except + auth_error = ClientAuthenticationError(message="Authentication failed: {}".format(ex)) + raise_from(auth_error, ex) + + return wrapper diff --git a/sdk/identity/azure-identity/azure/identity/_internal/exception_wrapper.py b/sdk/identity/azure-identity/azure/identity/_internal/exception_wrapper.py deleted file mode 100644 index 4d07f54c85a7..000000000000 --- a/sdk/identity/azure-identity/azure/identity/_internal/exception_wrapper.py +++ /dev/null @@ -1,25 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import functools - -from six import raise_from - -from azure.core.exceptions import ClientAuthenticationError - - -def wrap_exceptions(fn): - """Prevents leaking exceptions defined outside azure-core by raising ClientAuthenticationError from them.""" - - @functools.wraps(fn) - def wrapper(*args, **kwargs): - try: - return fn(*args, **kwargs) - except ClientAuthenticationError: - raise - except Exception as ex: # pylint:disable=broad-except - auth_error = ClientAuthenticationError(message="Authentication failed: {}".format(ex)) - raise_from(auth_error, ex) - - return wrapper diff --git a/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py b/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py index fe6b3e1faedb..ac2966c93e1d 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py @@ -13,12 +13,11 @@ from azure.core.credentials import AccessToken from azure.core.exceptions import ClientAuthenticationError -from .exception_wrapper import wrap_exceptions from .msal_client import MsalClient from .persistent_cache import load_user_cache from .._constants import KnownAuthorities from .._exceptions import AuthenticationRequiredError, CredentialUnavailableError -from .._internal import get_default_authority, normalize_authority +from .._internal import get_default_authority, normalize_authority, wrap_exceptions from .._auth_record import AuthenticationRecord try: @@ -35,7 +34,6 @@ # pylint:disable=ungrouped-imports,unused-import from typing import Any, Mapping, Optional, Type, Union - _LOGGER = logging.getLogger(__name__) _DEFAULT_AUTHENTICATE_SCOPES = { @@ -159,26 +157,43 @@ def get_token(self, *scopes, **kwargs): configured not to begin this automatically. Call :func:`authenticate` to begin interactive authentication. """ if not scopes: - raise ValueError("'get_token' requires at least one scope") + message = "'get_token' requires at least one scope" + _LOGGER.warning("%s.get_token failed: %s", self.__class__.__name__, message) + raise ValueError(message) allow_prompt = kwargs.pop("_allow_prompt", not self._disable_automatic_authentication) try: - return self._acquire_token_silent(*scopes, **kwargs) - except AuthenticationRequiredError: - if not allow_prompt: + token = self._acquire_token_silent(*scopes, **kwargs) + _LOGGER.info("%s.get_token succeeded", self.__class__.__name__) + return token + except Exception as ex: # pylint:disable=broad-except + if not (isinstance(ex, AuthenticationRequiredError) and allow_prompt): + _LOGGER.warning( + "%s.get_token failed: %s", + self.__class__.__name__, + ex, + exc_info=_LOGGER.isEnabledFor(logging.DEBUG), + ) raise # silent authentication failed -> authenticate interactively now = int(time.time()) - result = self._request_token(*scopes, **kwargs) - if "access_token" not in result: - message = "Authentication failed: {}".format(result.get("error_description") or result.get("error")) - raise ClientAuthenticationError(message=message) - - # this may be the first authentication, or the user may have authenticated a different identity - self._auth_record = _build_auth_record(result) + try: + result = self._request_token(*scopes, **kwargs) + if "access_token" not in result: + message = "Authentication failed: {}".format(result.get("error_description") or result.get("error")) + raise ClientAuthenticationError(message=message) + + # this may be the first authentication, or the user may have authenticated a different identity + self._auth_record = _build_auth_record(result) + except Exception as ex: # pylint:disable=broad-except + _LOGGER.warning( + "%s.get_token failed: %s", self.__class__.__name__, ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG), + ) + raise + _LOGGER.info("%s.get_token succeeded", self.__class__.__name__) return AccessToken(result["access_token"], now + int(result["expires_in"])) def authenticate(self, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/authorization_code.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/authorization_code.py index b996ecca8860..7528fbb05516 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/authorization_code.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/authorization_code.py @@ -7,6 +7,7 @@ from azure.core.exceptions import ClientAuthenticationError from .base import AsyncCredentialBase from .._internal import AadClient +from .._internal.decorators import log_get_token_async if TYPE_CHECKING: # pylint:disable=unused-import,ungrouped-imports @@ -51,6 +52,7 @@ def __init__( self._client = kwargs.pop("client", None) or AadClient(tenant_id, client_id, **kwargs) self._redirect_uri = redirect_uri + @log_get_token_async async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py index 56c025680db3..00d348db648e 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.py @@ -8,6 +8,7 @@ from azure.core.exceptions import ClientAuthenticationError from .._credentials.base import AsyncCredentialBase +from .._internal.decorators import log_get_token_async from ... import CredentialUnavailableError from ..._credentials.azure_cli import ( AzureCliCredential as _SyncAzureCliCredential, @@ -27,6 +28,7 @@ class AzureCliCredential(AsyncCredentialBase): This requires previously logging in to Azure via "az login", and will use the CLI's currently logged in identity. """ + @log_get_token_async async def get_token(self, *scopes, **kwargs): """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py index 922d1ccbaa11..180d9cba11e5 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py @@ -6,6 +6,7 @@ from .base import AsyncCredentialBase from .._internal import AadClient +from .._internal.decorators import log_get_token_async from ..._internal import CertificateCredentialBase if TYPE_CHECKING: @@ -37,7 +38,8 @@ async def close(self): await self._client.__aexit__() - async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": + @log_get_token_async + async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": # pylint:disable=unused-argument """Asynchronously request an access token for `scopes`. .. note:: This method is called by Azure SDK clients. It isn't intended for use in application code. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/chained.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/chained.py index aab679966e15..b20538d53e49 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/chained.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/chained.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ import asyncio +import logging from typing import TYPE_CHECKING from azure.core.exceptions import ClientAuthenticationError @@ -15,6 +16,8 @@ from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential +_LOGGER = logging.getLogger(__name__) + class ChainedTokenCredential(AsyncCredentialBase): """A sequence of credentials that is itself a credential. @@ -53,14 +56,23 @@ async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": for credential in self.credentials: try: token = await credential.get_token(*scopes, **kwargs) + _LOGGER.info("%s acquired a token from %s", self.__class__.__name__, credential.__class__.__name__) self._successful_credential = credential return token except CredentialUnavailableError as ex: # credential didn't attempt authentication because it lacks required data or state -> continue history.append((credential, ex.message)) + _LOGGER.info("%s - %s is unavailable", self.__class__.__name__, credential.__class__.__name__) except Exception as ex: # pylint: disable=broad-except # credential failed to authenticate, or something unexpectedly raised -> break history.append((credential, str(ex))) + _LOGGER.warning( + '%s.get_token failed: %s raised unexpected error "%s"', + self.__class__.__name__, + credential.__class__.__name__, + ex, + exc_info=_LOGGER.isEnabledFor(logging.DEBUG), + ) break attempts = _get_error_message(history) diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py index 767a80b3cf84..f6249dc61ec0 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py @@ -6,6 +6,7 @@ from .base import AsyncCredentialBase from .._internal import AadClient +from .._internal.decorators import log_get_token_async from ..._internal import ClientSecretCredentialBase if TYPE_CHECKING: @@ -38,6 +39,7 @@ async def close(self): await self._client.__aexit__() + @log_get_token_async async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": """Asynchronously request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/environment.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/environment.py index 7e1197d702c2..2599e54ee69a 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/environment.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/environment.py @@ -2,9 +2,12 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ +import logging import os from typing import TYPE_CHECKING +from .._internal.decorators import log_get_token_async + from ... import CredentialUnavailableError from ..._constants import EnvironmentVariables from .certificate import CertificateCredential @@ -15,6 +18,8 @@ from typing import Any, Optional, Union from azure.core.credentials import AccessToken +_LOGGER = logging.getLogger(__name__) + class EnvironmentCredential(AsyncCredentialBase): """A credential configured by environment variables. @@ -30,7 +35,7 @@ class EnvironmentCredential(AsyncCredentialBase): Service principal with certificate: - **AZURE_TENANT_ID**: ID of the service principal's tenant. Also called its 'directory' ID. - **AZURE_CLIENT_ID**: the service principal's client ID - - **AZURE_CLIENT_CERTIFICATE_PATH**: path to a PEM-encoded certificate file including the private key The + - **AZURE_CLIENT_CERTIFICATE_PATH**: path to a PEM-encoded certificate file including the private key. The certificate must not be password-protected. """ @@ -52,6 +57,16 @@ def __init__(self, **kwargs: "Any") -> None: **kwargs ) + if self._credential: + _LOGGER.info("Environment is configured for %s", self._credential.__class__.__name__) + else: + expected_variables = set(EnvironmentVariables.CERT_VARS + EnvironmentVariables.CLIENT_SECRET_VARS) + set_variables = [v for v in expected_variables if v in os.environ] + if set_variables: + _LOGGER.warning("Incomplete environment configuration. Set variables: %s", ", ".join(set_variables)) + else: + _LOGGER.info("No environment configuration found.") + async def __aenter__(self): if self._credential: await self._credential.__aenter__() @@ -63,6 +78,7 @@ async def close(self): if self._credential: await self._credential.__aexit__() + @log_get_token_async async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": """Asynchronously request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py index 6b17a55ada91..7def20143526 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ import abc +import logging import os from typing import TYPE_CHECKING @@ -10,16 +11,19 @@ from azure.core.exceptions import ClientAuthenticationError, HttpResponseError from azure.core.pipeline.policies import AsyncRetryPolicy -from azure.identity._credentials.managed_identity import _ManagedIdentityBase from .base import AsyncCredentialBase from .._authn_client import AsyncAuthnClient +from .._internal.decorators import log_get_token_async from ... import CredentialUnavailableError from ..._constants import Endpoints, EnvironmentVariables +from ..._credentials.managed_identity import _ManagedIdentityBase if TYPE_CHECKING: from typing import Any, Optional from azure.core.configuration import Configuration +_LOGGER = logging.getLogger(__name__) + class ManagedIdentityCredential(AsyncCredentialBase): """Authenticates with an Azure managed identity in any hosting environment which supports managed identities. @@ -37,8 +41,10 @@ class ManagedIdentityCredential(AsyncCredentialBase): def __init__(self, **kwargs: "Any") -> None: self._credential = None if os.environ.get(EnvironmentVariables.MSI_ENDPOINT): + _LOGGER.info("%s will use MSI", self.__class__.__name__) self._credential = MsiCredential(**kwargs) else: + _LOGGER.info("%s will use IMDS", self.__class__.__name__) self._credential = ImdsCredential(**kwargs) async def __aenter__(self): @@ -51,6 +57,7 @@ async def close(self): if self._credential: await self._credential.__aexit__() + @log_get_token_async async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": """Asynchronously request an access token for `scopes`. @@ -120,6 +127,7 @@ async def get_token(self, *scopes: str, **kwargs: "Any") -> AccessToken: # pyli except Exception: # pylint:disable=broad-except # if anything else was raised, assume the endpoint is unavailable self._endpoint_available = False + _LOGGER.info("No response from the IMDS endpoint.") if not self._endpoint_available: message = "ManagedIdentityCredential authentication unavailable, no managed identity endpoint found." diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py index 66f25038aa6b..7b7ef7c28590 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py @@ -8,6 +8,7 @@ from ..._constants import AZURE_CLI_CLIENT_ID from ..._internal.shared_token_cache import NO_TOKEN, SharedTokenCacheBase from .._internal.aad_client import AadClient +from .._internal.decorators import log_get_token_async from .base import AsyncCredentialBase if TYPE_CHECKING: @@ -45,6 +46,7 @@ async def close(self): if self._client: await self._client.__aexit__() + @log_get_token_async async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": # pylint:disable=unused-argument """Get an access token for `scopes` from the shared cache. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py index 6b1da3a6f8ae..eb235bb19776 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/vscode_credential.py @@ -3,10 +3,12 @@ # Licensed under the MIT License. # ------------------------------------ from typing import TYPE_CHECKING + from ..._exceptions import CredentialUnavailableError from .._credentials.base import AsyncCredentialBase from ..._constants import AZURE_VSCODE_CLIENT_ID from .._internal.aad_client import AadClient +from .._internal.decorators import log_get_token_async from ..._credentials.vscode_credential import get_credentials if TYPE_CHECKING: @@ -33,6 +35,7 @@ async def close(self): if self._client: await self._client.__aexit__() + @log_get_token_async async def get_token(self, *scopes, **kwargs): # type: (*str, **Any) -> AccessToken """Request an access token for `scopes`. diff --git a/sdk/identity/azure-identity/azure/identity/aio/_internal/__init__.py b/sdk/identity/azure-identity/azure/identity/aio/_internal/__init__.py index 82f17fb260f8..9653b45acab7 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_internal/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_internal/__init__.py @@ -3,6 +3,6 @@ # Licensed under the MIT License. # ------------------------------------ from .aad_client import AadClient -from .exception_wrapper import wrap_exceptions +from .decorators import wrap_exceptions __all__ = ["AadClient", "wrap_exceptions"] diff --git a/sdk/identity/azure-identity/azure/identity/aio/_internal/exception_wrapper.py b/sdk/identity/azure-identity/azure/identity/aio/_internal/decorators.py similarity index 62% rename from sdk/identity/azure-identity/azure/identity/aio/_internal/exception_wrapper.py rename to sdk/identity/azure-identity/azure/identity/aio/_internal/decorators.py index 4d6f40f954cb..10bbd33269d2 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_internal/exception_wrapper.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_internal/decorators.py @@ -3,9 +3,25 @@ # Licensed under the MIT License. # ------------------------------------ import functools +import logging from azure.core.exceptions import ClientAuthenticationError +_LOGGER = logging.getLogger(__name__) + + +def log_get_token_async(fn): + @functools.wraps(fn) + async def wrapper(*args, **kwargs): + try: + token = await fn(*args, **kwargs) + _LOGGER.info("%s succeeded", fn.__qualname__) + return token + except Exception as ex: + _LOGGER.warning("%s failed: %s", fn.__qualname__, ex, exc_info=_LOGGER.isEnabledFor(logging.DEBUG)) + raise + return wrapper + def wrap_exceptions(fn): """Prevents leaking exceptions defined outside azure-core by raising ClientAuthenticationError from them."""