Skip to content

Commit 896380d

Browse files
authored
Refactor ImdsCredential to use ManagedIdentityClient (Azure#18120)
1 parent e0de5e5 commit 896380d

File tree

15 files changed

+208
-867
lines changed

15 files changed

+208
-867
lines changed

sdk/identity/azure-identity/azure/identity/_authn_client.py

Lines changed: 0 additions & 237 deletions
This file was deleted.

sdk/identity/azure-identity/azure/identity/_constants.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,3 @@ class EnvironmentVariables:
4343
MSI_SECRET = "MSI_SECRET"
4444

4545
AZURE_AUTHORITY_HOST = "AZURE_AUTHORITY_HOST"
46-
47-
48-
class Endpoints:
49-
# https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
50-
IMDS = "http://169.254.169.254/metadata/identity/oauth2/token"
51-
52-
AAD_OAUTH2_V2_FORMAT = "https://" + KnownAuthorities.AZURE_PUBLIC_CLOUD + "/{}/oauth2/v2.0/token"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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

Comments
 (0)