pypomes-iam 0.3.5__py3-none-any.whl → 0.3.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pypomes-iam might be problematic. Click here for more details.

pypomes_iam/iam_common.py CHANGED
@@ -5,6 +5,7 @@ from enum import StrEnum
5
5
  from logging import Logger
6
6
  from pypomes_core import TZ_LOCAL, exc_format
7
7
  from pypomes_crypto import crypto_jwk_convert
8
+ from threading import Lock
8
9
  from typing import Any, Final
9
10
 
10
11
 
@@ -16,7 +17,8 @@ class IamServer(StrEnum):
16
17
  IAM_KEYCLOAK = "iam-keycloak"
17
18
 
18
19
 
19
- # the logger for IAM operations
20
+ # the logger for IAM service operations
21
+ # (used exclusively at the HTTP endpoint - all other functions receive the lgger as parameter)
20
22
  __IAM_LOGGER: Logger | None = None
21
23
 
22
24
  # registry structure:
@@ -51,11 +53,18 @@ __IAM_LOGGER: Logger | None = None
51
53
  # }
52
54
  _IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
53
55
 
56
+ # the lock protecting the data in '_IAM_SERVER'
57
+ # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
58
+ _iam_lock: Final[Lock] = Lock()
59
+
54
60
 
55
61
  def _get_logger() -> Logger | None:
56
62
  """
57
63
  Retrieve the registered logger for *IAM* operations.
58
64
 
65
+ This function is invoked exclusively from the HTTP endpoints.
66
+ All other functions receive the logger as parameter.
67
+
59
68
  :return: the registered logger for *IAM* operations.
60
69
  """
61
70
  return __IAM_LOGGER
pypomes_iam/iam_pomes.py CHANGED
@@ -3,14 +3,13 @@ import requests
3
3
  import secrets
4
4
  import string
5
5
  import sys
6
- from cachetools import Cache
7
6
  from datetime import datetime
8
7
  from logging import Logger
9
8
  from pypomes_core import TZ_LOCAL, exc_format
10
9
  from typing import Any
11
10
 
12
11
  from .iam_common import (
13
- IamServer,
12
+ IamServer, _iam_lock,
14
13
  _register_logger, _get_iam_users, _get_iam_registry,
15
14
  _get_login_timeout, _get_user_data, _get_public_key
16
15
  )
@@ -48,30 +47,34 @@ def user_login(iam_server: IamServer,
48
47
  # build the user data
49
48
  # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
50
49
  oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
51
- user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
52
- user_id=oauth_state,
53
- errors=errors,
54
- logger=logger)
55
- if user_data:
56
- user_data["login-id"] = user_id
57
- timeout: int = _get_login_timeout(iam_server=iam_server,
58
- errors=errors,
59
- logger=logger)
60
- if not errors:
61
- user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout if timeout else None
62
- redirect_uri: str = args.get("redirect-uri")
63
-
64
- # build the login url
65
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
66
- errors=errors,
67
- logger=logger)
68
- if registry:
69
- registry["redirect-uri"] = redirect_uri
70
- result = {"login-url": (f"{registry["base-url"]}/protocol/openid-connect/auth"
71
- f"?response_type=code&scope=openid"
72
- f"&client_id={registry["client-id"]}"
73
- f"&redirect_uri={redirect_uri}"
74
- f"&state={oauth_state}")}
50
+
51
+ with _iam_lock:
52
+ # retrieve the user data from the IAM server's registry
53
+ user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
54
+ user_id=oauth_state,
55
+ errors=errors,
56
+ logger=logger)
57
+ if user_data:
58
+ user_data["login-id"] = user_id
59
+ timeout: int = _get_login_timeout(iam_server=iam_server,
60
+ errors=errors,
61
+ logger=logger)
62
+ if not errors:
63
+ user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
64
+ if timeout else None
65
+ redirect_uri: str = args.get("redirect-uri")
66
+
67
+ # build the login url
68
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
69
+ errors=errors,
70
+ logger=logger)
71
+ if registry:
72
+ registry["redirect-uri"] = redirect_uri
73
+ result = {"login-url": (f"{registry["base-url"]}/protocol/openid-connect/auth"
74
+ f"?response_type=code&scope=openid"
75
+ f"&client_id={registry["client-id"]}"
76
+ f"&redirect_uri={redirect_uri}"
77
+ f"&state={oauth_state}")}
75
78
  return result
76
79
 
77
80
 
@@ -94,14 +97,15 @@ def user_logout(iam_server: IamServer,
94
97
  user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
95
98
 
96
99
  if user_id:
97
- # retrieve the IAM server's cache storage
98
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
99
- errors=errors,
100
- logger=logger) or {}
101
- if user_id in users:
102
- users.pop(user_id)
103
- if logger:
104
- logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
100
+ with _iam_lock:
101
+ # retrieve the data for all users in the IAM server's registry
102
+ users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
103
+ errors=errors,
104
+ logger=logger) or {}
105
+ if user_id in users:
106
+ users.pop(user_id)
107
+ if logger:
108
+ logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
105
109
 
106
110
 
107
111
  def user_token(iam_server: IamServer,
@@ -111,7 +115,7 @@ def user_token(iam_server: IamServer,
111
115
  """
112
116
  Retrieve the authentication token for the user, from *iam_server*.
113
117
 
114
- The user is identified by the attribute *user-id*, *user_id*, or "login", provided in *args*.
118
+ The user is identified by the attribute *user-id*, *user_id*, or *login*, provided in *args*.
115
119
 
116
120
  :param iam_server: the reference registered *IAM* server
117
121
  :param args: the arguments passed when requesting the service
@@ -127,56 +131,58 @@ def user_token(iam_server: IamServer,
127
131
 
128
132
  err_msg: str | None = None
129
133
  if user_id:
130
- user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
131
- user_id=user_id,
132
- errors=errors,
133
- logger=logger)
134
- token: str = user_data["access-token"] if user_data else None
135
- if token:
136
- access_expiration: int = user_data.get("access-expiration")
137
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
138
- if now < access_expiration:
139
- result = token
140
- else:
141
- # access token has expired
142
- refresh_token: str = user_data["refresh-token"]
143
- if refresh_token:
144
- refresh_expiration = user_data["refresh-expiration"]
145
- if now < refresh_expiration:
146
- body_data: dict[str, str] = {
147
- "grant_type": "refresh_token",
148
- "refresh_token": refresh_token
149
- }
150
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
151
- token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
152
- body_data=body_data,
153
- errors=errors,
154
- logger=logger)
155
- # validate and store the token data
156
- if token_data:
157
- token_info: tuple[str, str] = __validate_and_store(iam_server=iam_server,
158
- user_data=user_data,
159
- token_data=token_data,
160
- now=now,
161
- errors=errors,
162
- logger=logger)
163
- result = token_info[1]
134
+ with _iam_lock:
135
+ # retrieve the user data in the IAM server's registry
136
+ user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
137
+ user_id=user_id,
138
+ errors=errors,
139
+ logger=logger)
140
+ token: str = user_data["access-token"] if user_data else None
141
+ if token:
142
+ access_expiration: int = user_data.get("access-expiration")
143
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
144
+ if now < access_expiration:
145
+ result = token
146
+ else:
147
+ # access token has expired
148
+ refresh_token: str = user_data["refresh-token"]
149
+ if refresh_token:
150
+ refresh_expiration = user_data["refresh-expiration"]
151
+ if now < refresh_expiration:
152
+ body_data: dict[str, str] = {
153
+ "grant_type": "refresh_token",
154
+ "refresh_token": refresh_token
155
+ }
156
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
157
+ token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
158
+ body_data=body_data,
159
+ errors=errors,
160
+ logger=logger)
161
+ # validate and store the token data
162
+ if token_data:
163
+ token_info: tuple[str, str] = __validate_and_store(iam_server=iam_server,
164
+ user_data=user_data,
165
+ token_data=token_data,
166
+ now=now,
167
+ errors=errors,
168
+ logger=logger)
169
+ result = token_info[1]
170
+ else:
171
+ # refresh token is no longer valid
172
+ user_data["refresh-token"] = None
164
173
  else:
165
- # refresh token is no longer valid
166
- user_data["refresh-token"] = None
174
+ # refresh token has expired
175
+ err_msg = "Access and refresh tokens expired"
176
+ if logger:
177
+ logger.error(msg=err_msg)
167
178
  else:
168
- # refresh token has expired
169
- err_msg = "Access and refresh tokens expired"
179
+ err_msg = "Access token expired, no refresh token available"
170
180
  if logger:
171
181
  logger.error(msg=err_msg)
172
- else:
173
- err_msg = "Access token expired, no refresh token available"
174
- if logger:
175
- logger.error(msg=err_msg)
176
- else:
177
- err_msg = f"User '{user_id}' not authenticated"
178
- if logger:
179
- logger.error(msg=err_msg)
182
+ else:
183
+ err_msg = f"User '{user_id}' not authenticated"
184
+ if logger:
185
+ logger.error(msg=err_msg)
180
186
  else:
181
187
  err_msg = "User identification not provided"
182
188
  if logger:
@@ -193,7 +199,11 @@ def login_callback(iam_server: IamServer,
193
199
  errors: list[str] = None,
194
200
  logger: Logger = None) -> tuple[str, str] | None:
195
201
  """
196
- Entry point for the callback from *iam_server* via the front-end application, on authentication operation.
202
+ Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
203
+
204
+ The relevant arguments received are:
205
+ - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
206
+ - *code*: the temporary authorization code, to be exchanged for the token
197
207
 
198
208
  :param iam_server: the reference registered *IAM* server
199
209
  :param args: the arguments passed when requesting the service
@@ -204,15 +214,13 @@ def login_callback(iam_server: IamServer,
204
214
  # initialize the return variable
205
215
  result: tuple[str, str] | None = None
206
216
 
207
- # retrieve the users authentication data
208
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
209
- errors=errors,
210
- logger=logger)
211
- cache: Cache = registry["cache"] if registry else None
212
- if cache:
213
- users: dict[str, dict[str, Any]] = cache.get("users")
214
-
215
- # validate the OAuth2 state
217
+ with _iam_lock:
218
+ # retrieve the IAM server's registry and the data for all users therein
219
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
220
+ errors=errors,
221
+ logger=logger)
222
+ users: dict[str, dict[str, Any]] = (registry["cache"]["users"] or {}) if registry else {}
223
+ # retrieve the OAuth2 state
216
224
  oauth_state: str = args.get("state")
217
225
  user_data: dict[str, Any] | None = None
218
226
  if oauth_state:
@@ -221,7 +229,7 @@ def login_callback(iam_server: IamServer,
221
229
  user_data = data
222
230
  break
223
231
 
224
- # exchange 'code' for the token
232
+ # exchange 'code' received for the token
225
233
  if user_data:
226
234
  expiration: int = user_data["login-expiration"] or sys.maxsize
227
235
  if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
@@ -268,6 +276,15 @@ def token_exchange(iam_server: IamServer,
268
276
  - client-id: identification for the reference user (aliases: 'client_id', 'login')
269
277
  - token: the token to be exchanged
270
278
 
279
+ The typical data set returned contains the following attributes:
280
+ {
281
+ "token_type": "Bearer",
282
+ "access_token": <str>,
283
+ "expires_in": <number-of-seconds>,
284
+ "refresh_token": <str>,
285
+ "refesh_expires_in": <number-of-seconds>
286
+ }
287
+
271
288
  :param iam_server: the reference registered *IAM* server
272
289
  :param args: the arguments passed when requesting the service
273
290
  :param errors: incidental errors
@@ -280,40 +297,41 @@ def token_exchange(iam_server: IamServer,
280
297
  # obtain the user's identification
281
298
  user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
282
299
 
283
- # retrieve the token to be exchanges
300
+ # obtain the token to be exchanges
284
301
  token: str = args.get("token")
285
302
 
286
303
  if user_id and token:
287
304
  # HAZARD: only 'IAM_KEYCLOAK' is currently supported
288
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
289
- errors=errors,
290
- logger=logger)
291
- if registry:
292
- body_data: dict[str, str] = {
293
- "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
294
- "subject_token": token,
295
- "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
296
- "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
297
- "audience": registry["client-id"],
298
- "subject_issuer": "oidc"
299
- }
300
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
301
- token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
302
- body_data=body_data,
303
- errors=errors,
304
- logger=logger)
305
- # validate and store the token data
306
- if token_data:
307
- user_data: dict[str, Any] = {}
308
- result = __validate_and_store(iam_server=iam_server,
309
- user_data=user_data,
310
- token_data=token_data,
311
- now=now,
312
- errors=errors,
313
- logger=logger)
314
-
305
+ with _iam_lock:
306
+ # retrieve the IAM server's registry
307
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
308
+ errors=errors,
309
+ logger=logger)
310
+ if registry:
311
+ body_data: dict[str, str] = {
312
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
313
+ "subject_token": token,
314
+ "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
315
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
316
+ "audience": registry["client-id"],
317
+ "subject_issuer": "oidc"
318
+ }
319
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
320
+ token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
321
+ body_data=body_data,
322
+ errors=errors,
323
+ logger=logger)
324
+ # validate and store the token data
325
+ if token_data:
326
+ user_data: dict[str, Any] = {}
327
+ result = __validate_and_store(iam_server=iam_server,
328
+ user_data=user_data,
329
+ token_data=token_data,
330
+ now=now,
331
+ errors=errors,
332
+ logger=logger)
315
333
  else:
316
- msg: str = "User identification and token must be provided"
334
+ msg: str = "User identification or token not provided"
317
335
  if logger:
318
336
  logger.error(msg=msg)
319
337
  if isinstance(errors, list):
@@ -353,7 +371,7 @@ def __post_for_token(iam_server: IamServer,
353
371
  If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
354
372
  Otherwise, *errors* will contain the appropriate error message.
355
373
 
356
- The typical data returned contains the following attributes:
374
+ The typical data set returned contains the following attributes:
357
375
  {
358
376
  "token_type": "Bearer",
359
377
  "access_token": <str>,
@@ -371,52 +389,53 @@ def __post_for_token(iam_server: IamServer,
371
389
  # initialize the return variable
372
390
  result: dict[str, Any] | None = None
373
391
 
374
- # PBTAIN THE iam SERVER'S REGISTRY
375
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
376
- errors=errors,
377
- logger=logger)
378
392
  err_msg: str | None = None
379
- if registry:
380
- # complete the data to send in body of request
381
- body_data["client_id"] = registry["client-id"]
382
- client_secret: str = registry["client-secret"]
383
- if client_secret:
384
- body_data["client_secret"] = client_secret
385
-
386
- # obtain the token
387
- url: str = registry["base-url"] + "/protocol/openid-connect/token"
388
- if logger:
389
- logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
390
- ensure_ascii=False)}")
391
- try:
392
- # typical return on a token request:
393
- # {
394
- # "token_type": "Bearer",
395
- # "access_token": <str>,
396
- # "expires_in": <number-of-seconds>,
397
- # "refresh_token": <str>,
398
- # "refesh_expires_in": <number-of-seconds>
399
- # }
400
- response: requests.Response = requests.post(url=url,
401
- data=body_data)
402
- if response.status_code == 200:
403
- # request succeeded
404
- if logger:
405
- logger.debug(msg=f"POST success, status {response.status_code}")
406
- result = response.json()
407
- else:
408
- # request resulted in error
409
- err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
410
- if hasattr(response, "content") and response.content:
411
- err_msg += f", content '{response.content}'"
393
+ with _iam_lock:
394
+ # retrieve the IAM server's registry
395
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
396
+ errors=errors,
397
+ logger=logger)
398
+ if registry:
399
+ # complete the data to send in body of request
400
+ body_data["client_id"] = registry["client-id"]
401
+ client_secret: str = registry["client-secret"]
402
+ if client_secret:
403
+ body_data["client_secret"] = client_secret
404
+
405
+ # obtain the token
406
+ url: str = registry["base-url"] + "/protocol/openid-connect/token"
407
+ if logger:
408
+ logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
409
+ ensure_ascii=False)}")
410
+ try:
411
+ # typical return on a token request:
412
+ # {
413
+ # "token_type": "Bearer",
414
+ # "access_token": <str>,
415
+ # "expires_in": <number-of-seconds>,
416
+ # "refresh_token": <str>,
417
+ # "refesh_expires_in": <number-of-seconds>
418
+ # }
419
+ response: requests.Response = requests.post(url=url,
420
+ data=body_data)
421
+ if response.status_code == 200:
422
+ # request succeeded
423
+ if logger:
424
+ logger.debug(msg=f"POST success, status {response.status_code}")
425
+ result = response.json()
426
+ else:
427
+ # request resulted in error
428
+ err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
429
+ if hasattr(response, "content") and response.content:
430
+ err_msg += f", content '{response.content}'"
431
+ if logger:
432
+ logger.error(msg=err_msg)
433
+ except Exception as e:
434
+ # the operation raised an exception
435
+ err_msg = exc_format(exc=e,
436
+ exc_info=sys.exc_info())
412
437
  if logger:
413
438
  logger.error(msg=err_msg)
414
- except Exception as e:
415
- # the operation raised an exception
416
- err_msg = exc_format(exc=e,
417
- exc_info=sys.exc_info())
418
- if logger:
419
- logger.error(msg=err_msg)
420
439
 
421
440
  if err_msg and isinstance(errors, list):
422
441
  errors.append(err_msg)
@@ -452,38 +471,39 @@ def __validate_and_store(iam_server: IamServer,
452
471
  # initialize the return variable
453
472
  result: tuple[str, str] | None = None
454
473
 
455
- # retrieve the IAM server's registry
456
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
457
- errors=errors,
458
- logger=logger)
459
- if registry:
460
- token: str = token_data.get("access_token")
461
- user_data["access-token"] = token
462
- # keep current refresh token if a new one is not provided
463
- if token_data.get("refresh_token"):
464
- user_data["refresh-token"] = token_data.get("refresh_token")
465
- user_data["access-expiration"] = now + token_data.get("expires_in")
466
- refresh_exp: int = user_data.get("refresh_expires_in")
467
- user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
468
- public_key: str = _get_public_key(iam_server=iam_server,
469
- errors=errors,
470
- logger=logger)
471
- if public_key:
472
- recipient_attr = registry["recipient_attr"]
473
- login_id = user_data.pop("login-id", None)
474
- claims: dict[str, dict[str, Any]] = token_validate(token=token,
475
- issuer=registry["base-url"],
476
- recipient_id=login_id,
477
- recipient_attr=recipient_attr,
478
- public_key=public_key,
479
- errors=errors,
480
- logger=logger)
481
- if claims:
482
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
483
- errors=errors,
484
- logger=logger)
485
- if users:
486
- user_id: str = login_id if login_id else claims["payload"][recipient_attr]
487
- users[user_id] = user_data
488
- result = (user_id, token)
474
+ with _iam_lock:
475
+ # retrieve the IAM server's registry
476
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
477
+ errors=errors,
478
+ logger=logger)
479
+ if registry:
480
+ token: str = token_data.get("access_token")
481
+ user_data["access-token"] = token
482
+ # keep current refresh token if a new one is not provided
483
+ if token_data.get("refresh_token"):
484
+ user_data["refresh-token"] = token_data.get("refresh_token")
485
+ user_data["access-expiration"] = now + token_data.get("expires_in")
486
+ refresh_exp: int = user_data.get("refresh_expires_in")
487
+ user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
488
+ public_key: str = _get_public_key(iam_server=iam_server,
489
+ errors=errors,
490
+ logger=logger)
491
+ if public_key:
492
+ recipient_attr = registry["recipient_attr"]
493
+ login_id = user_data.pop("login-id", None)
494
+ claims: dict[str, dict[str, Any]] = token_validate(token=token,
495
+ issuer=registry["base-url"],
496
+ recipient_id=login_id,
497
+ recipient_attr=recipient_attr,
498
+ public_key=public_key,
499
+ errors=errors,
500
+ logger=logger)
501
+ if claims:
502
+ users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
503
+ errors=errors,
504
+ logger=logger)
505
+ if users:
506
+ user_id: str = login_id if login_id else claims["payload"][recipient_attr]
507
+ users[user_id] = user_data
508
+ result = (user_id, token)
489
509
  return result
@@ -3,8 +3,14 @@ from flask import Request, Response, request, jsonify
3
3
  from logging import Logger
4
4
  from typing import Any
5
5
 
6
- from .iam_common import IamServer, _get_logger, _get_iam_server
7
- from .iam_pomes import user_login, user_logout, user_token, token_exchange, login_callback
6
+ from .iam_common import (
7
+ IamServer, _iam_lock,
8
+ _get_logger, _get_iam_server
9
+ )
10
+ from .iam_pomes import (
11
+ user_login, user_logout,
12
+ user_token, token_exchange, login_callback
13
+ )
8
14
 
9
15
 
10
16
  # @flask_app.route(rule=<login_endpoint>, # JUSBR_ENDPOINT_LOGIN
@@ -28,19 +34,20 @@ def service_login() -> Response:
28
34
  # log the request
29
35
  logger.debug(msg=_log_init(request=request))
30
36
 
31
- # retrieve the IAM server
32
37
  errors: list[str] = []
33
- iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
34
- errors=errors,
35
- logger=logger)
36
- if iam_server:
37
- # obtain the login URL
38
- login_data: dict[str, str] = user_login(iam_server=iam_server,
39
- args=request.args,
40
- errors=errors,
41
- logger=logger)
42
- if login_data:
43
- result = jsonify(login_data)
38
+ with _iam_lock:
39
+ # retrieve the IAM server
40
+ iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
41
+ errors=errors,
42
+ logger=logger)
43
+ if iam_server:
44
+ # obtain the login URL
45
+ login_data: dict[str, str] = user_login(iam_server=iam_server,
46
+ args=request.args,
47
+ errors=errors,
48
+ logger=logger)
49
+ if login_data:
50
+ result = jsonify(login_data)
44
51
 
45
52
  if errors:
46
53
  result = Response("; ".join(errors))
@@ -74,17 +81,18 @@ def service_logout() -> Response:
74
81
  # log the request
75
82
  logger.debug(msg=_log_init(request=request))
76
83
 
77
- # retrieve the IAM server
78
84
  errors: list[str] = []
79
- iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
80
- errors=errors,
81
- logger=logger)
82
- if iam_server:
83
- # logout the user
84
- user_logout(iam_server=iam_server,
85
- args=request.args,
86
- errors=errors,
87
- logger=logger)
85
+ with _iam_lock:
86
+ # retrieve the IAM server
87
+ iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
88
+ errors=errors,
89
+ logger=logger)
90
+ if iam_server:
91
+ # logout the user
92
+ user_logout(iam_server=iam_server,
93
+ args=request.args,
94
+ errors=errors,
95
+ logger=logger)
88
96
  if errors:
89
97
  result = Response("; ".join(errors))
90
98
  result.status_code = 400
@@ -106,10 +114,11 @@ def service_callback() -> Response:
106
114
  """
107
115
  Entry point for the callback from JusBR on authentication operation.
108
116
 
109
- This callback is typically invoked from a front-end application after a successful login at the
110
- JusBR login page, forwarding the data received.
117
+ This callback is invoked from a front-end application after a successful login at the
118
+ *IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
119
+ this data is then used to effectively obtain the token from the *IAM* server.
111
120
 
112
- :return: the response containing the token, or *BAD REQUEST*
121
+ :return: the *Response* containing the reference user identification and the token, or *BAD REQUEST*
113
122
  """
114
123
  # retrieve the operations's logger
115
124
  logger: Logger = _get_logger()
@@ -117,18 +126,19 @@ def service_callback() -> Response:
117
126
  # log the request
118
127
  logger.debug(msg=_log_init(request=request))
119
128
 
120
- # retrieve the IAM server
121
129
  errors: list[str] = []
122
130
  token_data: tuple[str, str] | None = None
123
- iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
124
- errors=errors,
125
- logger=logger)
126
- if iam_server:
127
- # process the callback operation
128
- token_data = login_callback(iam_server=iam_server,
129
- args=request.args,
130
- errors=errors,
131
- logger=logger)
131
+ with _iam_lock:
132
+ # retrieve the IAM server
133
+ iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
134
+ errors=errors,
135
+ logger=logger)
136
+ if iam_server:
137
+ # process the callback operation
138
+ token_data = login_callback(iam_server=iam_server,
139
+ args=request.args,
140
+ errors=errors,
141
+ logger=logger)
132
142
  result: Response
133
143
  if errors:
134
144
  result = jsonify({"errors": "; ".join(errors)})
@@ -136,10 +146,8 @@ def service_callback() -> Response:
136
146
  if logger:
137
147
  logger.error(msg=json.dumps(obj=result))
138
148
  else:
139
- result = jsonify({
140
- "user-id": token_data[0],
141
- "access-token": token_data[1]})
142
-
149
+ result = jsonify({"user-id": token_data[0],
150
+ "access-token": token_data[1]})
143
151
  # log the response
144
152
  if logger:
145
153
  logger.debug(msg=f"Response {result}")
@@ -148,14 +156,14 @@ def service_callback() -> Response:
148
156
 
149
157
 
150
158
  # @flask_app.route(rule=<token_endpoint>, # JUSBR_ENDPOINT_TOKEN
151
- # @flask_app.route(rule=<token_endpoint>, # KEYCLOAK_ENDPOINT_TOKEN
152
159
  # methods=["GET"])
160
+ # @flask_app.route(rule=<token_endpoint>, # KEYCLOAK_ENDPOINT_TOKEN
153
161
  # methods=["GET"])
154
162
  def service_token() -> Response:
155
163
  """
156
- Entry point for retrieving token from the *IAM* server.
164
+ Entry point for retrieving a token from the *IAM* server.
157
165
 
158
- :return: the response containing the token, or *UNAUTHORIZED*
166
+ :return: the *Response* containing the user reference identification and the token, or *BAD REQUEST*
159
167
  """
160
168
  # retrieve the operations's logger
161
169
  logger: Logger = _get_logger()
@@ -163,26 +171,38 @@ def service_token() -> Response:
163
171
  # log the request
164
172
  logger.debug(msg=_log_init(request=request))
165
173
 
166
- # retrieve the IAM server
174
+ # obtain the user's identification
175
+ args: dict[str, Any] = request.args
176
+ user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
177
+
167
178
  errors: list[str] = []
168
- iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
169
- errors=errors,
170
- logger=logger)
171
- # retrieve the token
172
179
  token: str | None = None
173
- if iam_server:
174
- errors: list[str] = []
175
- token: str = user_token(iam_server=iam_server,
176
- args=request.args,
177
- errors=errors,
178
- logger=logger)
180
+ if user_id:
181
+ with _iam_lock:
182
+ # retrieve the IAM server
183
+ iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
184
+ errors=errors,
185
+ logger=logger)
186
+ if iam_server:
187
+ # retrieve the token
188
+ errors: list[str] = []
189
+ token: str = user_token(iam_server=iam_server,
190
+ args=args,
191
+ errors=errors,
192
+ logger=logger)
193
+ else:
194
+ msg: str = "User identification not provided"
195
+ errors.append(msg)
196
+ if logger:
197
+ logger.error(msg=msg)
198
+
179
199
  result: Response
180
200
  if errors:
181
201
  result = Response("; ".join(errors))
182
202
  result.status_code = 400
183
203
  else:
184
- result = jsonify({"token": token})
185
-
204
+ result = jsonify({"user-id": user_id,
205
+ "token": token})
186
206
  # log the response
187
207
  if logger:
188
208
  logger.debug(msg=f"Response {result}")
@@ -213,24 +233,28 @@ def service_exchange() -> Response:
213
233
  "refesh_expires_in": <number-of-seconds>
214
234
  }
215
235
 
216
- :return: the response containing the token data, or *UNAUTHORIZED*
236
+ :return: the *Response* containing the token data, or *UNAUTHORIZED*
217
237
  """
218
238
  # retrieve the operations's logger
219
239
  logger: Logger = _get_logger()
240
+ if logger:
241
+ # log the request
242
+ logger.debug(msg=_log_init(request=request))
220
243
 
221
- # retrieve the IAM server (currently, only 'Keycloak' is supported)
222
244
  errors: list[str] = []
223
- iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
224
- errors=errors,
225
- logger=logger)
226
- # exchange the token
227
- token_data: dict[str, Any] | None = None
228
- if iam_server:
229
- errors: list[str] = []
230
- token_data = token_exchange(iam_server=iam_server,
231
- args=request.args,
232
- errors=errors,
233
- logger=logger)
245
+ with _iam_lock:
246
+ # retrieve the IAM server (currently, only 'IAM_KEYCLOAK' is supported)
247
+ iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
248
+ errors=errors,
249
+ logger=logger)
250
+ # exchange the token
251
+ token_data: dict[str, Any] | None = None
252
+ if iam_server:
253
+ errors: list[str] = []
254
+ token_data = token_exchange(iam_server=iam_server,
255
+ args=request.args,
256
+ errors=errors,
257
+ logger=logger)
234
258
  result: Response
235
259
  if errors:
236
260
  result = Response("; ".join(errors))
@@ -7,7 +7,7 @@ from pypomes_core import (
7
7
  )
8
8
  from typing import Any, Final
9
9
 
10
- from .iam_common import _IAM_SERVERS, IamServer
10
+ from .iam_common import _IAM_SERVERS, IamServer, _iam_lock
11
11
  from .iam_pomes import user_token
12
12
 
13
13
  JUSBR_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_ID")
@@ -65,18 +65,19 @@ def jusbr_setup(flask_app: Flask,
65
65
  # configure the JusBR registry
66
66
  cache: Cache = FIFOCache(maxsize=1048576)
67
67
  cache["users"] = {}
68
- _IAM_SERVERS[IamServer.IAM_JUSRBR] = {
69
- "client-id": client_id,
70
- "client-secret": client_secret,
71
- "client-timeout": client_timeout,
72
- "recipient-attr": recipient_attribute,
73
- "base-url": base_url,
74
- "pk-expiration": sys.maxsize,
75
- "pk-lifetime": public_key_lifetime,
76
- "cache": cache,
77
- "logger": logger,
78
- "redirect-uri": None
79
- }
68
+ with _iam_lock:
69
+ _IAM_SERVERS[IamServer.IAM_JUSRBR] = {
70
+ "client-id": client_id,
71
+ "client-secret": client_secret,
72
+ "client-timeout": client_timeout,
73
+ "recipient-attr": recipient_attribute,
74
+ "base-url": base_url,
75
+ "pk-expiration": sys.maxsize,
76
+ "pk-lifetime": public_key_lifetime,
77
+ "cache": cache,
78
+ "logger": logger,
79
+ "redirect-uri": None
80
+ }
80
81
 
81
82
  # establish the endpoints
82
83
  if login_endpoint:
@@ -112,9 +113,14 @@ def jusbr_get_token(user_id: str,
112
113
  :param logger: optional logger
113
114
  :return: the uthentication tokem
114
115
  """
116
+ # declare the return variable
117
+ result: str
118
+
115
119
  # retrieve the token
116
120
  args: dict[str, Any] = {"user-id": user_id}
117
- return user_token(iam_server=IamServer.IAM_JUSRBR,
118
- args=args,
119
- errors=errors,
120
- logger=logger)
121
+ with _iam_lock:
122
+ result = user_token(iam_server=IamServer.IAM_JUSRBR,
123
+ args=args,
124
+ errors=errors,
125
+ logger=logger)
126
+ return result
@@ -7,7 +7,7 @@ from pypomes_core import (
7
7
  )
8
8
  from typing import Any, Final
9
9
 
10
- from .iam_common import _IAM_SERVERS, IamServer
10
+ from .iam_common import _IAM_SERVERS, IamServer, _iam_lock
11
11
  from .iam_pomes import user_token
12
12
 
13
13
  KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
@@ -74,18 +74,19 @@ def keycloak_setup(flask_app: Flask,
74
74
  # configure the Keycloak registry
75
75
  cache: Cache = FIFOCache(maxsize=1048576)
76
76
  cache["users"] = {}
77
- _IAM_SERVERS[IamServer.IAM_KEYCLOAK] = {
78
- "client-id": client_id,
79
- "client-secret": client_secret,
80
- "client-timeout": client_timeout,
81
- "recipient-attr": recipient_attribute,
82
- "base-url": f"{base_url}/realms/{realm}",
83
- "pk-expiration": sys.maxsize,
84
- "pk-lifetime": public_key_lifetime,
85
- "cache": cache,
86
- "logger": logger,
87
- "redirect-uri": None
88
- }
77
+ with _iam_lock:
78
+ _IAM_SERVERS[IamServer.IAM_KEYCLOAK] = {
79
+ "client-id": client_id,
80
+ "client-secret": client_secret,
81
+ "client-timeout": client_timeout,
82
+ "recipient-attr": recipient_attribute,
83
+ "base-url": f"{base_url}/realms/{realm}",
84
+ "pk-expiration": sys.maxsize,
85
+ "pk-lifetime": public_key_lifetime,
86
+ "cache": cache,
87
+ "logger": logger,
88
+ "redirect-uri": None
89
+ }
89
90
 
90
91
  # establish the endpoints
91
92
  if login_endpoint:
@@ -126,9 +127,14 @@ def keycloak_get_token(user_id: str,
126
127
  :param logger: optional logger
127
128
  :return: the uthentication tokem
128
129
  """
130
+ # declare the return variable
131
+ result: str
132
+
129
133
  # retrieve the token
130
134
  args: dict[str, Any] = {"user-id": user_id}
131
- return user_token(iam_server=IamServer.IAM_KEYCLOAK,
132
- args=args,
133
- errors=errors,
134
- logger=logger)
135
+ with _iam_lock:
136
+ result = user_token(iam_server=IamServer.IAM_KEYCLOAK,
137
+ args=args,
138
+ errors=errors,
139
+ logger=logger)
140
+ return result
@@ -4,7 +4,8 @@ from base64 import b64encode
4
4
  from datetime import datetime
5
5
  from logging import Logger
6
6
  from pypomes_core import TZ_LOCAL, exc_format
7
- from typing import Any
7
+ from threading import Lock
8
+ from typing import Any, Final
8
9
 
9
10
  # structure:
10
11
  # {
@@ -19,7 +20,11 @@ from typing import Any
19
20
  # "expiration": <timestamp>
20
21
  # }
21
22
  # }
22
- _provider_registry: dict[str, dict[str, Any]] = {}
23
+ _provider_registry: Final[dict[str, dict[str, Any]]] = {}
24
+
25
+ # the lock protecting the data in '_provider_registry'
26
+ # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
27
+ _provider_lock: Final[Lock] = Lock()
23
28
 
24
29
 
25
30
  def provider_register(provider_id: str,
@@ -48,18 +53,19 @@ def provider_register(provider_id: str,
48
53
  :param headers_data: optional key-value pairs to be added to the request headers
49
54
  :param body_data: optional key-value pairs to be added to the request body
50
55
  """
51
- global _provider_registry # noqa: PLW0602
56
+ global _provider_registry
52
57
 
53
- _provider_registry[provider_id] = {
54
- "url": auth_url,
55
- "user": auth_user,
56
- "pwd": auth_pwd,
57
- "custom-auth": custom_auth,
58
- "headers-data": headers_data,
59
- "body-data": body_data,
60
- "token": None,
61
- "expiration": datetime.now(tz=TZ_LOCAL).timestamp()
62
- }
58
+ with _provider_lock:
59
+ _provider_registry[provider_id] = {
60
+ "url": auth_url,
61
+ "user": auth_user,
62
+ "pwd": auth_pwd,
63
+ "custom-auth": custom_auth,
64
+ "headers-data": headers_data,
65
+ "body-data": body_data,
66
+ "token": None,
67
+ "expiration": datetime.now(tz=TZ_LOCAL).timestamp()
68
+ }
63
69
 
64
70
 
65
71
  def provider_get_token(provider_id: str,
@@ -78,53 +84,54 @@ def provider_get_token(provider_id: str,
78
84
  result: str | None = None
79
85
 
80
86
  err_msg: str | None = None
81
- provider: dict[str, Any] = _provider_registry.get(provider_id)
82
- if provider:
83
- now: float = datetime.now(tz=TZ_LOCAL).timestamp()
84
- if now > provider.get("expiration"):
85
- user: str = provider.get("user")
86
- pwd: str = provider.get("pwd")
87
- headers_data: dict[str, str] = provider.get("headers-data") or {}
88
- body_data: dict[str, str] = provider.get("body-data") or {}
89
- custom_auth: tuple[str, str] = provider.get("custom-auth")
90
- if custom_auth:
91
- body_data[custom_auth[0]] = user
92
- body_data[custom_auth[1]] = pwd
93
- else:
94
- enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
95
- headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
96
- url: str = provider.get("url")
97
- try:
98
- # typical return on a token request:
99
- # {
100
- # "token_type": "Bearer",
101
- # "access_token": <str>,
102
- # "expires_in": <number-of-seconds>,
103
- # optional data:
104
- # "refresh_token": <str>,
105
- # "refresh_expires_in": <number-of-seconds>
106
- # }
107
- response: requests.Response = requests.post(url=url,
108
- data=body_data,
109
- headers=headers_data,
110
- timeout=None)
111
- if response.status_code < 200 or response.status_code >= 300:
112
- # request resulted in error, report the problem
113
- err_msg = (f"POST '{url}': failed, "
114
- f"status {response.status_code}, reason '{response.reason}'")
87
+ with _provider_lock:
88
+ provider: dict[str, Any] = _provider_registry.get(provider_id)
89
+ if provider:
90
+ now: float = datetime.now(tz=TZ_LOCAL).timestamp()
91
+ if now > provider.get("expiration"):
92
+ user: str = provider.get("user")
93
+ pwd: str = provider.get("pwd")
94
+ headers_data: dict[str, str] = provider.get("headers-data") or {}
95
+ body_data: dict[str, str] = provider.get("body-data") or {}
96
+ custom_auth: tuple[str, str] = provider.get("custom-auth")
97
+ if custom_auth:
98
+ body_data[custom_auth[0]] = user
99
+ body_data[custom_auth[1]] = pwd
115
100
  else:
116
- reply: dict[str, Any] = response.json()
117
- provider["token"] = reply.get("access_token")
118
- provider["expiration"] = now + int(reply.get("expires_in"))
119
- if logger:
120
- logger.debug(msg=f"POST '{url}': status {response.status_code}")
121
- except Exception as e:
122
- # the operation raised an exception
123
- err_msg = exc_format(exc=e,
124
- exc_info=sys.exc_info())
125
- err_msg = f"POST '{url}': error, '{err_msg}'"
126
- else:
127
- err_msg: str = f"Provider '{provider_id}' not registered"
101
+ enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
102
+ headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
103
+ url: str = provider.get("url")
104
+ try:
105
+ # typical return on a token request:
106
+ # {
107
+ # "token_type": "Bearer",
108
+ # "access_token": <str>,
109
+ # "expires_in": <number-of-seconds>,
110
+ # optional data:
111
+ # "refresh_token": <str>,
112
+ # "refresh_expires_in": <number-of-seconds>
113
+ # }
114
+ response: requests.Response = requests.post(url=url,
115
+ data=body_data,
116
+ headers=headers_data,
117
+ timeout=None)
118
+ if response.status_code < 200 or response.status_code >= 300:
119
+ # request resulted in error, report the problem
120
+ err_msg = (f"POST '{url}': failed, "
121
+ f"status {response.status_code}, reason '{response.reason}'")
122
+ else:
123
+ reply: dict[str, Any] = response.json()
124
+ provider["token"] = reply.get("access_token")
125
+ provider["expiration"] = now + int(reply.get("expires_in"))
126
+ if logger:
127
+ logger.debug(msg=f"POST '{url}': status {response.status_code}")
128
+ except Exception as e:
129
+ # the operation raised an exception
130
+ err_msg = exc_format(exc=e,
131
+ exc_info=sys.exc_info())
132
+ err_msg = f"POST '{url}': error, '{err_msg}'"
133
+ else:
134
+ err_msg: str = f"Provider '{provider_id}' not registered"
128
135
 
129
136
  if err_msg:
130
137
  if isinstance(errors, list):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: A collection of Python pomes, penyeach (IAM modules)
5
5
  Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-IAM
6
6
  Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-IAM/issues
@@ -0,0 +1,12 @@
1
+ pypomes_iam/__init__.py,sha256=H7rUCaUEJBLNJv2rtdmBxwcAB28OItdEPenpv_UEOVw,965
2
+ pypomes_iam/iam_common.py,sha256=f74FUDcnMM2cgzJg-AF17GwCKwbfoezS8LppwxYwPys,10049
3
+ pypomes_iam/iam_pomes.py,sha256=gvDpgff6arB4_Y8AAf6QH2CEWRmdy-kcnQLyD0hx4Y4,23966
4
+ pypomes_iam/iam_services.py,sha256=Ae_hLz5luRjK-l_rhBcuuY03Ov7n7o67UYgBb5rbBys,10002
5
+ pypomes_iam/jusbr_pomes.py,sha256=0qbjJ6EGnlx17K-4Lqh5XkfH58y0joVZiD6HykbwpoE,5823
6
+ pypomes_iam/keycloak_pomes.py,sha256=5ZfpncofF20C1IB5ndO31vfrvfa8Ffy7FJxkGoKKoQQ,6836
7
+ pypomes_iam/provider_pomes.py,sha256=3Rui68hmj8zwY0tnw4aWurz-yQ-niacJFQpi6nWzh-M,6355
8
+ pypomes_iam/token_pomes.py,sha256=1g6PMNNMbmdwLrsvSXvpO8-zdRhso1IFnwAyndNmV4Q,5332
9
+ pypomes_iam-0.3.6.dist-info/METADATA,sha256=q53TFkBnU4mUAZgsJ_r_730kASXLiIyCwW_5mkFz8TU,694
10
+ pypomes_iam-0.3.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
+ pypomes_iam-0.3.6.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
12
+ pypomes_iam-0.3.6.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- pypomes_iam/__init__.py,sha256=H7rUCaUEJBLNJv2rtdmBxwcAB28OItdEPenpv_UEOVw,965
2
- pypomes_iam/iam_common.py,sha256=1NgXFTiD4qpbVqLfYsCfHrE0khEaczp-nR3AYXmzmvU,9608
3
- pypomes_iam/iam_pomes.py,sha256=qmnHX88iaiMGaGeZfbs4VT-G_XMTpRT6wqRZeCOOKbQ,22294
4
- pypomes_iam/iam_services.py,sha256=qdPPfwR9jIdGak-wr4t2NkdfhVaMqauBdvmVJvmFqyg,8914
5
- pypomes_iam/jusbr_pomes.py,sha256=M47h_PUUgbCmFQyKz2sN1H9T00BC5v_oPgwl5ATWMSA,5625
6
- pypomes_iam/keycloak_pomes.py,sha256=GtXJb4TZb-a_5b9ExYdJGetBcU1pEP96ONO6prA_vDo,6638
7
- pypomes_iam/provider_pomes.py,sha256=eP8XzjTUEpwejTkO0wmDiqKjqbIEOzRNCR2ju5E15og,5856
8
- pypomes_iam/token_pomes.py,sha256=1g6PMNNMbmdwLrsvSXvpO8-zdRhso1IFnwAyndNmV4Q,5332
9
- pypomes_iam-0.3.5.dist-info/METADATA,sha256=fGRhn3H98wOkQSrxMh9FFTiheHez3TMIpX6R91wysoU,694
10
- pypomes_iam-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- pypomes_iam-0.3.5.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
12
- pypomes_iam-0.3.5.dist-info/RECORD,,