10
10
from urllib .parse import urlparse # Python 3+
11
11
from collections import UserDict # Python 3+
12
12
from .token_cache import TokenCache
13
- from .throttled_http_client import ThrottledHttpClient
13
+ from .individual_cache import _IndividualCache as IndividualCache
14
+ from .throttled_http_client import ThrottledHttpClientBase , _parse_http_429_5xx_retry_after
14
15
15
16
16
17
logger = logging .getLogger (__name__ )
@@ -106,6 +107,22 @@ def __init__(self, *, client_id=None, resource_id=None, object_id=None):
106
107
"client_id, resource_id, object_id" )
107
108
108
109
110
+ class _ThrottledHttpClient (ThrottledHttpClientBase ):
111
+ def __init__ (self , http_client , http_cache ):
112
+ super (_ThrottledHttpClient , self ).__init__ (http_client , http_cache )
113
+ self .get = IndividualCache ( # All MIs (except Cloud Shell) use GETs
114
+ mapping = self ._expiring_mapping ,
115
+ key_maker = lambda func , args , kwargs : "POST {} hash={} 429/5xx/Retry-After" .format (
116
+ args [0 ], # It is the endpoint, typically a constant per MI type
117
+ _hash (
118
+ # Managed Identity flavors have inconsistent parameters.
119
+ # We simply choose to hash them all.
120
+ str (kwargs .get ("params" )) + str (kwargs .get ("data" ))),
121
+ ),
122
+ expires_in = _parse_http_429_5xx_retry_after ,
123
+ )(http_client .get )
124
+
125
+
109
126
class ManagedIdentityClient (object ):
110
127
"""This API encapulates multiple managed identity backends:
111
128
VM, App Service, Azure Automation (Runbooks), Azure Function, Service Fabric,
@@ -115,7 +132,8 @@ class ManagedIdentityClient(object):
115
132
"""
116
133
_instance , _tenant = socket .getfqdn (), "managed_identity" # Placeholders
117
134
118
- def __init__ (self , managed_identity , * , http_client , token_cache = None ):
135
+ def __init__ (
136
+ self , managed_identity , * , http_client , token_cache = None , http_cache = None ):
119
137
"""Create a managed identity client.
120
138
121
139
:param dict managed_identity:
@@ -141,6 +159,10 @@ def __init__(self, managed_identity, *, http_client, token_cache=None):
141
159
Optional. It accepts a :class:`msal.TokenCache` instance to store tokens.
142
160
It will use an in-memory token cache by default.
143
161
162
+ :param http_cache:
163
+ Optional. It has the same characteristics as the
164
+ :paramref:`msal.ClientApplication.http_cache`.
165
+
144
166
Recipe 1: Hard code a managed identity for your app::
145
167
146
168
import msal, requests
@@ -168,12 +190,21 @@ def __init__(self, managed_identity, *, http_client, token_cache=None):
168
190
token = client.acquire_token_for_client("resource")
169
191
"""
170
192
self ._managed_identity = managed_identity
171
- if isinstance (http_client , ThrottledHttpClient ):
172
- raise ValueError (
173
- # It is a precaution to reject application.py's throttled http_client,
174
- # whose cache life on HTTP GET 200 is too long for Managed Identity.
175
- "This class does not currently accept a ThrottledHttpClient." )
176
- self ._http_client = http_client
193
+ self ._http_client = _ThrottledHttpClient (
194
+ # This class only throttles excess token acquisition requests.
195
+ # It does not provide retry.
196
+ # Retry is the http_client or caller's responsibility, not MSAL's.
197
+ #
198
+ # FWIW, here is the inconsistent retry recommendation.
199
+ # 1. Only MI on VM defines exotic 404 and 410 retry recommendations
200
+ # ( https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token#error-handling )
201
+ # (especially for 410 which was supposed to be a permanent failure).
202
+ # 2. MI on Service Fabric specifically suggests to not retry on 404.
203
+ # ( https://learn.microsoft.com/en-us/azure/service-fabric/how-to-managed-cluster-managed-identity-service-fabric-app-code#error-handling )
204
+ http_client .http_client # Patch the raw (unpatched) http client
205
+ if isinstance (http_client , ThrottledHttpClientBase ) else http_client ,
206
+ {} if http_cache is None else http_cache , # Default to an in-memory dict
207
+ )
177
208
self ._token_cache = token_cache or TokenCache ()
178
209
179
210
def acquire_token_for_client (self , * , resource ): # We may support scope in the future
0 commit comments