|
| 1 | +# ------------------------------------ |
| 2 | +# Copyright (c) Microsoft Corporation. |
| 3 | +# Licensed under the MIT License. |
| 4 | +# ------------------------------------ |
| 5 | +import logging |
| 6 | +from typing import TYPE_CHECKING |
| 7 | + |
| 8 | +import six |
| 9 | + |
| 10 | +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError |
| 11 | +from azure.core.pipeline.transport import HttpRequest |
| 12 | + |
| 13 | +from .. import CredentialUnavailableError |
| 14 | +from .._internal.get_token_mixin import GetTokenMixin |
| 15 | +from .._internal.managed_identity_client import ManagedIdentityClient |
| 16 | + |
| 17 | +if TYPE_CHECKING: |
| 18 | + # pylint:disable=ungrouped-imports |
| 19 | + from typing import Any, Optional |
| 20 | + from azure.core.credentials import AccessToken |
| 21 | + |
| 22 | +_LOGGER = logging.getLogger(__name__) |
| 23 | + |
| 24 | +IMDS_URL = "http://169.254.169.254/metadata/identity/oauth2/token" |
| 25 | + |
| 26 | +PIPELINE_SETTINGS = { |
| 27 | + "connection_timeout": 2, |
| 28 | + "retry_backoff_factor": 4, |
| 29 | + "retry_backoff_max": 60, |
| 30 | + "retry_on_status_codes": [404, 429] + list(range(500, 600)), |
| 31 | + "retry_status": 5, |
| 32 | + "retry_total": 5, |
| 33 | +} |
| 34 | + |
| 35 | + |
| 36 | +def get_request(scope, identity_config): |
| 37 | + request = HttpRequest("GET", IMDS_URL) |
| 38 | + request.format_parameters(dict({"api-version": "2018-02-01", "resource": scope}, **identity_config)) |
| 39 | + return request |
| 40 | + |
| 41 | + |
| 42 | +class ImdsCredential(GetTokenMixin): |
| 43 | + def __init__(self, **kwargs): |
| 44 | + # type: (**Any) -> None |
| 45 | + super(ImdsCredential, self).__init__() |
| 46 | + |
| 47 | + self._client = ManagedIdentityClient( |
| 48 | + get_request, _identity_config=kwargs.pop("identity_config", None) or {}, **dict(PIPELINE_SETTINGS, **kwargs) |
| 49 | + ) |
| 50 | + self._endpoint_available = None # type: Optional[bool] |
| 51 | + self._user_assigned_identity = "client_id" in kwargs or "identity_config" in kwargs |
| 52 | + |
| 53 | + def _acquire_token_silently(self, *scopes): |
| 54 | + # type: (*str) -> Optional[AccessToken] |
| 55 | + return self._client.get_cached_token(*scopes) |
| 56 | + |
| 57 | + def _request_token(self, *scopes, **kwargs): # pylint:disable=unused-argument |
| 58 | + # type: (*str, **Any) -> AccessToken |
| 59 | + if self._endpoint_available is None: |
| 60 | + # Lacking another way to determine whether the IMDS endpoint is listening, |
| 61 | + # we send a request it would immediately reject (because it lacks the Metadata header), |
| 62 | + # setting a short timeout. |
| 63 | + try: |
| 64 | + self._client.request_token(*scopes, connection_timeout=0.3, retry_total=0) |
| 65 | + self._endpoint_available = True |
| 66 | + except HttpResponseError: |
| 67 | + # received a response, choked on it |
| 68 | + self._endpoint_available = True |
| 69 | + except Exception: # pylint:disable=broad-except |
| 70 | + # if anything else was raised, assume the endpoint is unavailable |
| 71 | + self._endpoint_available = False |
| 72 | + _LOGGER.info("No response from the IMDS endpoint.") |
| 73 | + |
| 74 | + if not self._endpoint_available: |
| 75 | + message = "ManagedIdentityCredential authentication unavailable, no managed identity endpoint found." |
| 76 | + raise CredentialUnavailableError(message=message) |
| 77 | + |
| 78 | + try: |
| 79 | + token = self._client.request_token(*scopes, headers={"Metadata": "true"}) |
| 80 | + except HttpResponseError as ex: |
| 81 | + # 400 in response to a token request indicates managed identity is disabled, |
| 82 | + # or the identity with the specified client_id is not available |
| 83 | + if ex.status_code == 400: |
| 84 | + self._endpoint_available = False |
| 85 | + message = "ManagedIdentityCredential authentication unavailable. " |
| 86 | + if self._user_assigned_identity: |
| 87 | + message += "The requested identity has not been assigned to this resource." |
| 88 | + else: |
| 89 | + message += "No identity has been assigned to this resource." |
| 90 | + six.raise_from(CredentialUnavailableError(message=message), ex) |
| 91 | + |
| 92 | + # any other error is unexpected |
| 93 | + six.raise_from(ClientAuthenticationError(message=ex.message, response=ex.response), None) |
| 94 | + return token |
0 commit comments