1+ # TODO: Change the module name from imds to managed_identity
12# Copyright (c) Microsoft Corporation.
23# All rights reserved.
34#
1112 from urlparse import urlparse
1213except : # Python 3
1314 from urllib .parse import urlparse
15+ try : # Python 3
16+ from collections import UserDict
17+ except :
18+ UserDict = dict # The real UserDict is an old-style class which fails super()
19+
1420
1521logger = logging .getLogger (__name__ )
1622
23+ class ManagedIdentity (UserDict ):
24+ # The key names used in config dict
25+ ID_TYPE = "ManagedIdentityIdType"
26+ ID = "Id"
27+ def __init__ (self , identifier = None , id_type = None ):
28+ super (ManagedIdentity , self ).__init__ ({
29+ self .ID_TYPE : id_type ,
30+ self .ID : identifier ,
31+ })
32+
33+
34+ class UserAssignedManagedIdentity (ManagedIdentity ):
35+ """Feed an instance of this class to :class:`msal.ManagedIdentityClient`
36+ to acquire token for user-assigned managed identity.
37+
38+ By design, an instance of this class is equivalent to a dict in
39+ one of these shapes::
40+
41+ {"ManagedIdentityIdType": "ClientId", "Id": "foo"}
42+
43+ {"ManagedIdentityIdType": "ResourceId", "Id": "foo"}
44+
45+ {"ManagedIdentityIdType": "ObjectId", "Id": "foo"}
46+
47+ so that you may load it from a json configuration file or an env var,
48+ and feed it to :class:`Client`.
49+ """
50+ CLIENT_ID = "ClientId"
51+ RESOURCE_ID = "ResourceId"
52+ OBJECT_ID = "ObjectId"
53+ _types_mapping = { # Maps type name in configuration to type name on wire
54+ CLIENT_ID : "client_id" ,
55+ RESOURCE_ID : "mi_res_id" ,
56+ OBJECT_ID : "object_id" ,
57+ }
58+ def __init__ (self , identifier , id_type ):
59+ """Construct a UserAssignedManagedIdentity instance.
60+
61+ :param string identifier: The id.
62+ :param string id_type: It shall be one of these three::
63+
64+ UserAssignedManagedIdentity.CLIENT_ID
65+ UserAssignedManagedIdentity.RESOURCE_ID
66+ UserAssignedManagedIdentity.OBJECT_ID
67+ """
68+ if id_type not in self ._types_mapping :
69+ raise ValueError ("id_type only accepts one of: {}" .format (
70+ list (self ._types_mapping )))
71+ super (UserAssignedManagedIdentity , self ).__init__ (
72+ identifier = identifier ,
73+ id_type = id_type ,
74+ )
75+
76+
77+ class SystemAssignedManagedIdentity (ManagedIdentity ):
78+ """Feed an instance of this class to :class:`msal.ManagedIdentityClient`
79+ to acquire token for system-assigned managed identity.
80+
81+ By design, an instance of this class is equivalent to::
82+
83+ {"ManagedIdentityIdType": "SystemAssignedManagedIdentity", "Id": None}
84+
85+ so that you may load it from a json configuration file or an env var,
86+ and feed it to :class:`Client`.
87+ """
88+ def __init__ (self ):
89+ super (SystemAssignedManagedIdentity , self ).__init__ (
90+ id_type = "SystemAssignedManagedIdentity" , # As of this writing,
91+ # It can be any value other than
92+ # UserAssignedManagedIdentity._types_mapping's key names
93+ )
94+
95+
1796def _scope_to_resource (scope ): # This is an experimental reasonable-effort approach
1897 u = urlparse (scope )
1998 if u .scheme :
2099 return "{}://{}" .format (u .scheme , u .netloc )
21100 return scope # There is no much else we can do here
22101
23102
24- def _obtain_token (http_client , resource , client_id = None , object_id = None , mi_res_id = None ):
103+ def _obtain_token (http_client , managed_identity , resource ):
25104 if ("IDENTITY_ENDPOINT" in os .environ and "IDENTITY_HEADER" in os .environ
26105 and "IDENTITY_SERVER_THUMBPRINT" in os .environ
27106 ):
28- if client_id or object_id or mi_res_id :
107+ if managed_identity :
29108 logger .debug (
30- "Ignoring client_id/object_id/mi_res_id . "
109+ "Ignoring managed_identity parameter . "
31110 "Managed Identity in Service Fabric is configured in the cluster, "
32111 "not during runtime. See also "
33112 "https://learn.microsoft.com/en-us/azure/service-fabric/configure-existing-cluster-enable-managed-identity-token-service" )
34113 return _obtain_token_on_service_fabric (
35- http_client , os .environ ["IDENTITY_ENDPOINT" ], os .environ ["IDENTITY_HEADER" ],
36- os .environ ["IDENTITY_SERVER_THUMBPRINT" ], resource )
114+ http_client ,
115+ os .environ ["IDENTITY_ENDPOINT" ],
116+ os .environ ["IDENTITY_HEADER" ],
117+ os .environ ["IDENTITY_SERVER_THUMBPRINT" ],
118+ resource ,
119+ )
37120 if "IDENTITY_ENDPOINT" in os .environ and "IDENTITY_HEADER" in os .environ :
38121 return _obtain_token_on_app_service (
39- http_client , os .environ ["IDENTITY_ENDPOINT" ], os .environ ["IDENTITY_HEADER" ],
40- resource , client_id = client_id , object_id = object_id , mi_res_id = mi_res_id )
41- return _obtain_token_on_azure_vm (
42- http_client ,
43- resource , client_id = client_id , object_id = object_id , mi_res_id = mi_res_id )
122+ http_client ,
123+ os .environ ["IDENTITY_ENDPOINT" ],
124+ os .environ ["IDENTITY_HEADER" ],
125+ managed_identity ,
126+ resource ,
127+ )
128+ return _obtain_token_on_azure_vm (http_client , managed_identity , resource )
44129
45130
46- def _obtain_token_on_azure_vm (http_client , resource ,
47- client_id = None , object_id = None , mi_res_id = None ,
48- ):
131+ def _adjust_param (params , managed_identity ):
132+ id_name = UserAssignedManagedIdentity ._types_mapping .get (
133+ managed_identity .get (ManagedIdentity .ID_TYPE ))
134+ if id_name :
135+ params [id_name ] = managed_identity [ManagedIdentity .ID ]
136+
137+ def _obtain_token_on_azure_vm (http_client , managed_identity , resource ):
49138 # Based on https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
50139 logger .debug ("Obtaining token via managed identity on Azure VM" )
51140 params = {
52141 "api-version" : "2018-02-01" ,
53142 "resource" : resource ,
54143 }
55- if client_id :
56- params ["client_id" ] = client_id
57- if object_id :
58- params ["object_id" ] = object_id
59- if mi_res_id :
60- params ["mi_res_id" ] = mi_res_id
144+ _adjust_param (params , managed_identity )
61145 resp = http_client .get (
62146 "http://169.254.169.254/metadata/identity/oauth2/token" ,
63147 params = params ,
@@ -77,8 +161,8 @@ def _obtain_token_on_azure_vm(http_client, resource,
77161 logger .debug ("IMDS emits unexpected payload: %s" , resp .text )
78162 raise
79163
80- def _obtain_token_on_app_service (http_client , endpoint , identity_header , resource ,
81- client_id = None , object_id = None , mi_res_id = None ,
164+ def _obtain_token_on_app_service (
165+ http_client , endpoint , identity_header , managed_identity , resource ,
82166):
83167 """Obtains token for
84168 `App Service <https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity?tabs=portal%2Chttp#rest-endpoint-reference>`_,
@@ -92,12 +176,7 @@ def _obtain_token_on_app_service(http_client, endpoint, identity_header, resourc
92176 "api-version" : "2019-08-01" ,
93177 "resource" : resource ,
94178 }
95- if client_id :
96- params ["client_id" ] = client_id
97- if object_id :
98- params ["object_id" ] = object_id
99- if mi_res_id :
100- params ["mi_res_id" ] = mi_res_id
179+ _adjust_param (params , managed_identity )
101180 resp = http_client .get (
102181 endpoint ,
103182 params = params ,
@@ -167,32 +246,24 @@ def _obtain_token_on_service_fabric(
167246
168247
169248
170- class ManagedIdentity (object ):
249+ class ManagedIdentityClient (object ):
171250 _instance , _tenant = socket .getfqdn (), "managed_identity" # Placeholders
172251
173- def __init__ (self , http_client ,
174- client_id = None , object_id = None , mi_res_id = None ,
175- token_cache = None ,
176- ):
177- """Create a managed identity object.
252+ def __init__ (self , http_client , managed_identity , token_cache = None ):
253+ """Create a managed identity client.
178254
179255 :param http_client:
180256 An http client object. For example, you can use `requests.Session()`.
181257
182- :param str client_id:
183- Optional.
184- It accepts the Client ID (NOT the Object ID) of your user-assigned managed identity.
185- If it is None, it means to use a system-assigned managed identity.
258+ :param dict managed_identity:
259+ It accepts an instance of :class:`SystemAssignedManagedIdentity`
260+ or :class:`UserAssignedManagedIdentity`, or their equivalent dict.
186261
187262 :param token_cache:
188263 Optional. It accepts a :class:`msal.TokenCache` instance to store tokens.
189264 """
190- if len (list (filter (bool , [client_id , object_id , mi_res_id ]))) > 1 :
191- raise ValueError ("You can use up to one of these: client_id, object_id, mi_res_id" )
192265 self ._http_client = http_client
193- self ._client_id = client_id
194- self ._object_id = object_id
195- self ._mi_res_id = mi_res_id
266+ self ._managed_identity = managed_identity
196267 self ._token_cache = token_cache
197268
198269 def acquire_token (self , resource = None ):
@@ -202,9 +273,8 @@ def acquire_token(self, resource=None):
202273 "It is only declared as optional in method signature, "
203274 "in case we want to support scope parameter in the future." )
204275 access_token_from_cache = None
205- client_id_in_cache = (
206- self ._client_id or self ._object_id or self ._mi_res_id
207- or "SYSTEM_ASSIGNED_MANAGED_IDENTITY" )
276+ client_id_in_cache = self ._managed_identity .get (
277+ ManagedIdentity .ID , "SYSTEM_ASSIGNED_MANAGED_IDENTITY" )
208278 if self ._token_cache :
209279 matches = self ._token_cache .find (
210280 self ._token_cache .CredentialType .ACCESS_TOKEN ,
@@ -230,13 +300,7 @@ def acquire_token(self, resource=None):
230300 if "refresh_on" in entry and int (entry ["refresh_on" ]) < now : # aging
231301 break # With a fallback in hand, we break here to go refresh
232302 return access_token_from_cache # It is still good as new
233- result = _obtain_token (
234- self ._http_client ,
235- resource ,
236- client_id = self ._client_id ,
237- object_id = self ._object_id ,
238- mi_res_id = self ._mi_res_id ,
239- )
303+ result = _obtain_token (self ._http_client , self ._managed_identity , resource )
240304 if self ._token_cache and "access_token" in result :
241305 self ._token_cache .add (dict (
242306 client_id = client_id_in_cache ,
0 commit comments