pypomes-iam 0.5.5__py3-none-any.whl → 0.5.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/__init__.py CHANGED
@@ -1,7 +1,12 @@
1
+ from .iam_actions import (
2
+ action_callback, action_exchange,
3
+ action_login, action_logout, action_token
4
+ )
5
+ from .iam_common import (
6
+ IamServer
7
+ )
1
8
  from .iam_pomes import (
2
- IamServer,
3
- login_callback, token_exchange,
4
- user_login, user_logout, user_token
9
+ jwt_required
5
10
  )
6
11
  from .iam_services import (
7
12
  logger_register
@@ -20,10 +25,13 @@ from .token_pomes import (
20
25
  )
21
26
 
22
27
  __all__ = [
23
- # iam_pomes
28
+ # iam_actions
29
+ "action_callback", "action_exchange",
30
+ "action_login", "action_logout", "action_token",
31
+ # iam_commons
24
32
  "IamServer",
25
- "login_callback", "token_exchange",
26
- "user_login", "user_logout", "user_token",
33
+ # iam_pomes
34
+ "jwt_required",
27
35
  # iam_services
28
36
  "logger_register",
29
37
  # jusbr_pomes
@@ -0,0 +1,511 @@
1
+ import json
2
+ import requests
3
+ import secrets
4
+ import string
5
+ import sys
6
+ from datetime import datetime
7
+ from logging import Logger
8
+ from pypomes_core import TZ_LOCAL, exc_format
9
+ from typing import Any
10
+
11
+ from .iam_common import (
12
+ IamServer, _iam_lock,
13
+ _get_iam_users, _get_iam_registry, # _get_public_key,
14
+ _get_login_timeout, _get_user_data
15
+ )
16
+ from .token_pomes import token_validate
17
+
18
+
19
+ def action_login(iam_server: IamServer,
20
+ args: dict[str, Any],
21
+ errors: list[str] = None,
22
+ logger: Logger = None) -> str:
23
+ """
24
+ Build the URL for redirecting the request to *iam_server*'s authentication page.
25
+
26
+ These are the expected attributes in *args*:
27
+ - user-id: optional, identifies the reference user (alias: 'login')
28
+ - redirect-uri: a parameter to be added to the query part of the returned URL
29
+
30
+ If provided, the user identification will be validated against the authorization data
31
+ returned by *iam_server* upon login. On success, the appropriate URL for invoking
32
+ the IAM server's authentication page is returned.
33
+
34
+ :param iam_server: the reference registered *IAM* server
35
+ :param args: the arguments passed when requesting the service
36
+ :param errors: incidental error messages
37
+ :param logger: optional logger
38
+ :return: the callback URL, with the appropriate parameters, of *None* if error
39
+ """
40
+ # initialize the return variable
41
+ result: str | None = None
42
+
43
+ # obtain the optional user's identification
44
+ user_id: str = args.get("user-id") or args.get("login")
45
+
46
+ # build the user data
47
+ # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
48
+ oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
49
+
50
+ with _iam_lock:
51
+ # retrieve the user data from the IAM server's registry
52
+ user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
53
+ user_id=oauth_state,
54
+ errors=errors,
55
+ logger=logger)
56
+ if user_data:
57
+ user_data["login-id"] = user_id
58
+ timeout: int = _get_login_timeout(iam_server=iam_server,
59
+ errors=errors,
60
+ logger=logger)
61
+ if not errors:
62
+ user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
63
+ if timeout else None
64
+ redirect_uri: str = args.get("redirect-uri")
65
+ user_data["redirect-uri"] = 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
+ result = (f"{registry["base-url"]}/protocol/openid-connect/auth"
73
+ f"?response_type=code&scope=openid"
74
+ f"&client_id={registry["client-id"]}"
75
+ f"&redirect_uri={redirect_uri}"
76
+ f"&state={oauth_state}")
77
+ return result
78
+
79
+
80
+ def action_logout(iam_server: IamServer,
81
+ args: dict[str, Any],
82
+ errors: list[str] = None,
83
+ logger: Logger = None) -> None:
84
+ """
85
+ Logout the user, by removing all data associating it from *iam_server*'s registry.
86
+
87
+ The user is identified by the attribute *user-id* or "login", provided in *args*.
88
+ If successful, remove all data relating to the user from the *IAM* server's registry.
89
+ Otherwise, this operation fails silently, unless an error has ocurred.
90
+
91
+ :param iam_server: the reference registered *IAM* server
92
+ :param args: the arguments passed when requesting the service
93
+ :param errors: incidental error messages
94
+ :param logger: optional logger
95
+ """
96
+ # obtain the user's identification
97
+ user_id: str = args.get("user-id") or args.get("login")
98
+
99
+ if user_id:
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")
109
+
110
+
111
+ def action_token(iam_server: IamServer,
112
+ args: dict[str, Any],
113
+ errors: list[str] = None,
114
+ logger: Logger = None) -> str:
115
+ """
116
+ Retrieve the authentication token for the user, from *iam_server*.
117
+
118
+ The user is identified by the attribute *user-id* or *login*, provided in *args*.
119
+
120
+ :param iam_server: the reference registered *IAM* server
121
+ :param args: the arguments passed when requesting the service
122
+ :param errors: incidental error messages
123
+ :param logger: optional logger
124
+ :return: the token for user indicated, or *None* if error
125
+ """
126
+ # initialize the return variable
127
+ result: str | None = None
128
+
129
+ # obtain the user's identification
130
+ user_id: str = args.get("user-id") or args.get("login")
131
+
132
+ err_msg: str | None = None
133
+ if user_id:
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
173
+ else:
174
+ # refresh token has expired
175
+ err_msg = "Access and refresh tokens expired"
176
+ if logger:
177
+ logger.error(msg=err_msg)
178
+ else:
179
+ err_msg = "Access token expired, no refresh token available"
180
+ if logger:
181
+ 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)
186
+ else:
187
+ err_msg = "User identification not provided"
188
+ if logger:
189
+ logger.error(msg=err_msg)
190
+
191
+ if err_msg and isinstance(errors, list):
192
+ errors.append(err_msg)
193
+
194
+ return result
195
+
196
+
197
+ def action_callback(iam_server: IamServer,
198
+ args: dict[str, Any],
199
+ errors: list[str] = None,
200
+ logger: Logger = None) -> tuple[str, str] | None:
201
+ """
202
+ Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
203
+
204
+ The relevant expected arguments in *args* are:
205
+ - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
206
+ - *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
207
+
208
+ :param iam_server: the reference registered *IAM* server
209
+ :param args: the arguments passed when requesting the service
210
+ :param errors: incidental errors
211
+ :param logger: optional logger
212
+ :return: a tuple containing the reference user identification and the token obtained, or *None* if error
213
+ """
214
+ # initialize the return variable
215
+ result: tuple[str, str] | None = None
216
+
217
+ with _iam_lock:
218
+ # retrieve the IAM server's data for all users
219
+ users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
220
+ errors=errors,
221
+ logger=logger) or {}
222
+ # retrieve the OAuth2 state
223
+ oauth_state: str = args.get("state")
224
+ user_data: dict[str, Any] | None = None
225
+ if oauth_state:
226
+ for user, data in users.items():
227
+ if user == oauth_state:
228
+ user_data = data
229
+ break
230
+
231
+ # exchange 'code' received for the token
232
+ if user_data:
233
+ expiration: int = user_data["login-expiration"] or sys.maxsize
234
+ if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
235
+ errors.append("Operation timeout")
236
+ else:
237
+ users.pop(oauth_state)
238
+ code: str = args.get("code")
239
+ body_data: dict[str, Any] = {
240
+ "grant_type": "authorization_code",
241
+ "code": code,
242
+ "redirect_uri": user_data.pop("redirect-uri")
243
+ }
244
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
245
+ token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
246
+ body_data=body_data,
247
+ errors=errors,
248
+ logger=logger)
249
+ # validate and store the token data
250
+ if token_data:
251
+ result = __validate_and_store(iam_server=iam_server,
252
+ user_data=user_data,
253
+ token_data=token_data,
254
+ now=now,
255
+ errors=errors,
256
+ logger=logger)
257
+ else:
258
+ msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
259
+ if logger:
260
+ logger.error(msg=msg)
261
+ if isinstance(errors, list):
262
+ errors.append(msg)
263
+
264
+ return result
265
+
266
+
267
+ def action_exchange(iam_server: IamServer,
268
+ args: dict[str, Any],
269
+ errors: list[str] = None,
270
+ logger: Logger = None) -> dict[str, Any]:
271
+ """
272
+ Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
273
+
274
+ The expected parameters in *args* are:
275
+ - user-id: identification for the reference user (alias: 'login')
276
+ - token: the token to be exchanged
277
+
278
+ The typical data set returned contains the following attributes:
279
+ {
280
+ "token_type": "Bearer",
281
+ "access_token": <str>,
282
+ "expires_in": <number-of-seconds>,
283
+ "refresh_token": <str>,
284
+ "refesh_expires_in": <number-of-seconds>
285
+ }
286
+
287
+ :param iam_server: the reference registered *IAM* server
288
+ :param args: the arguments passed when requesting the service
289
+ :param errors: incidental errors
290
+ :param logger: optional logger
291
+ :return: the data for the new token, or *None* if error
292
+ """
293
+ # initialize the return variable
294
+ result: dict[str, Any] | None = None
295
+
296
+ # obtain the user's identification
297
+ user_id: str = args.get("user-id") or args.get("login")
298
+
299
+ # obtain the token to be exchanged
300
+ token: str = args.get("access-token")
301
+
302
+ if user_id and token:
303
+ # HAZARD: only 'IAM_KEYCLOAK' is currently supported
304
+ with _iam_lock:
305
+ # retrieve the IAM server's registry
306
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
307
+ errors=errors,
308
+ logger=logger)
309
+ if registry:
310
+ body_data: dict[str, str] = {
311
+ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
312
+ "subject_token": token,
313
+ "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
314
+ "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
315
+ "audience": registry["client-id"],
316
+ "subject_issuer": "oidc"
317
+ }
318
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
319
+ token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
320
+ body_data=body_data,
321
+ errors=errors,
322
+ logger=logger)
323
+ # validate and store the token data
324
+ if token_data:
325
+ user_data: dict[str, Any] = {}
326
+ result = __validate_and_store(iam_server=iam_server,
327
+ user_data=user_data,
328
+ token_data=token_data,
329
+ now=now,
330
+ errors=errors,
331
+ logger=logger)
332
+ else:
333
+ msg: str = "User identification or token not provided"
334
+ if logger:
335
+ logger.error(msg=msg)
336
+ if isinstance(errors, list):
337
+ errors.append(msg)
338
+
339
+ return result
340
+
341
+
342
+ def __post_for_token(iam_server: IamServer,
343
+ body_data: dict[str, Any],
344
+ errors: list[str] | None,
345
+ logger: Logger | None) -> dict[str, Any] | None:
346
+ """
347
+ Send a POST request to obtain the authentication token data, and return the data received.
348
+
349
+ For token acquisition, *body_data* will have the attributes:
350
+ - "grant_type": "authorization_code"
351
+ - "code": <16-character-random-code>
352
+ - "redirect_uri": <redirect-uri>
353
+
354
+ For token refresh, *body_data* will have the attributes:
355
+ - "grant_type": "refresh_token"
356
+ - "refresh_token": <current-refresh-token>
357
+
358
+ For token exchange, *body_data* will have the attributes:
359
+ - "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
360
+ - "subject_token": <token-to-be-exchanged>,
361
+ - "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
362
+ - "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
363
+ - "audience": <client-id>,
364
+ - "subject_issuer": "oidc"
365
+
366
+ These attributes are then added to *body_data*:
367
+ - "client_id": <client-id>,
368
+ - "client_secret": <client-secret>,
369
+
370
+ If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
371
+ Otherwise, *errors* will contain the appropriate error message.
372
+
373
+ The typical data set returned contains the following attributes:
374
+ {
375
+ "token_type": "Bearer",
376
+ "access_token": <str>,
377
+ "expires_in": <number-of-seconds>,
378
+ "refresh_token": <str>,
379
+ "refesh_expires_in": <number-of-seconds>
380
+ }
381
+
382
+ :param iam_server: the reference registered *IAM* server
383
+ :param body_data: the data to send in the body of the request
384
+ :param errors: incidental errors
385
+ :param logger: optional logger
386
+ :return: the token data, or *None* if error
387
+ """
388
+ # initialize the return variable
389
+ result: dict[str, Any] | None = None
390
+
391
+ err_msg: str | None = None
392
+ with _iam_lock:
393
+ # retrieve the IAM server's registry
394
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
395
+ errors=errors,
396
+ logger=logger)
397
+ if registry:
398
+ # complete the data to send in body of request
399
+ body_data["client_id"] = registry["client-id"]
400
+ client_secret: str = registry["client-secret"]
401
+
402
+ # obtain the token
403
+ url: str = registry["base-url"] + "/protocol/openid-connect/token"
404
+
405
+ # log the POST ('client_secret' data must not be shown in log)
406
+ if logger:
407
+ logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
408
+ ensure_ascii=False)}")
409
+ if client_secret:
410
+ body_data["client_secret"] = client_secret
411
+ try:
412
+ # typical return on a token request:
413
+ # {
414
+ # "token_type": "Bearer",
415
+ # "access_token": <str>,
416
+ # "expires_in": <number-of-seconds>,
417
+ # "refresh_token": <str>,
418
+ # "refesh_expires_in": <number-of-seconds>
419
+ # }
420
+ response: requests.Response = requests.post(url=url,
421
+ data=body_data)
422
+ if response.status_code == 200:
423
+ # request succeeded
424
+ result = response.json()
425
+ if logger:
426
+ logger.debug(msg=f"POST success, {json.dumps(obj=result,
427
+ ensure_ascii=False)}")
428
+ else:
429
+ # request resulted in error
430
+ err_msg = f"POST failure, status {response.status_code}, reason {response.reason}"
431
+ if hasattr(response, "content") and response.content:
432
+ err_msg += f", content '{response.content}'"
433
+ if logger:
434
+ logger.error(msg=err_msg)
435
+ except Exception as e:
436
+ # the operation raised an exception
437
+ err_msg = exc_format(exc=e,
438
+ exc_info=sys.exc_info())
439
+ if logger:
440
+ logger.error(msg=err_msg)
441
+
442
+ if err_msg and isinstance(errors, list):
443
+ errors.append(err_msg)
444
+
445
+ return result
446
+
447
+
448
+ def __validate_and_store(iam_server: IamServer,
449
+ user_data: dict[str, Any],
450
+ token_data: dict[str, Any],
451
+ now: int,
452
+ errors: list[str] | None,
453
+ logger: Logger) -> tuple[str, str] | None:
454
+ """
455
+ Validate and store the token data.
456
+
457
+ The typical *token_data* contains the following attributes:
458
+ {
459
+ "token_type": "Bearer",
460
+ "access_token": <str>,
461
+ "expires_in": <number-of-seconds>,
462
+ "refresh_token": <str>,
463
+ "refesh_expires_in": <number-of-seconds>
464
+ }
465
+
466
+ :param iam_server: the reference registered *IAM* server
467
+ :param user_data: the aurthentication data kepth in *iam_server*'s registry
468
+ :param token_data: the token data
469
+ :param errors: incidental errors
470
+ :param logger: optional logger
471
+ :return: tuple containing the user identification and the validated and stored token, or *None* if error
472
+ """
473
+ # initialize the return variable
474
+ result: tuple[str, str] | None = None
475
+
476
+ with _iam_lock:
477
+ # retrieve the IAM server's registry
478
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
479
+ errors=errors,
480
+ logger=logger)
481
+ if registry:
482
+ token: str = token_data.get("access_token")
483
+ user_data["access-token"] = token
484
+ # keep current refresh token if a new one is not provided
485
+ if token_data.get("refresh_token"):
486
+ user_data["refresh-token"] = token_data.get("refresh_token")
487
+ user_data["access-expiration"] = now + token_data.get("expires_in")
488
+ refresh_exp: int = user_data.get("refresh_expires_in")
489
+ user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
490
+ # public_key: str = _get_public_key(iam_server=iam_server,
491
+ # errors=errors,
492
+ # logger=logger)
493
+ recipient_attr = registry["recipient-attr"]
494
+ login_id = user_data.pop("login-id", None)
495
+ claims: dict[str, dict[str, Any]] = token_validate(token=token,
496
+ issuer=registry["base-url"],
497
+ recipient_id=login_id,
498
+ recipient_attr=recipient_attr,
499
+ # public_key=public_key,
500
+ errors=errors,
501
+ logger=logger)
502
+ if claims:
503
+ users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
504
+ errors=errors,
505
+ logger=logger)
506
+ # must test with 'not errors'
507
+ if not errors:
508
+ user_id: str = login_id if login_id else claims["payload"][recipient_attr]
509
+ users[user_id] = user_data
510
+ result = (user_id, token)
511
+ return result
pypomes_iam/iam_common.py CHANGED
@@ -50,7 +50,7 @@ class IamServer(StrEnum):
50
50
  # }
51
51
  _IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
52
52
 
53
- # the lock protecting the data in '_IAM_SERVER'
53
+ # the lock protecting the data in '_IAM_SERVERS'
54
54
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
55
55
  _iam_lock: Final[RLock] = RLock()
56
56
 
pypomes_iam/iam_pomes.py CHANGED
@@ -1,18 +1,9 @@
1
- import json
2
- import requests
3
- import secrets
4
- import string
5
- import sys
6
- from datetime import datetime
7
1
  from flask import Request, Response, request
8
- from logging import Logger
9
- from pypomes_core import TZ_LOCAL, exc_format
10
2
  from typing import Any
11
3
 
12
4
  from .iam_common import (
13
- IamServer, _iam_lock,
14
- _get_iam_users, _get_iam_registry, # _get_public_key,
15
- _get_login_timeout, _get_user_data, _iam_server_from_issuer
5
+ IamServer, _iam_lock, _get_iam_registry,
6
+ _iam_server_from_issuer # _get_public_key
16
7
  )
17
8
  from .token_pomes import token_get_claims, token_validate
18
9
 
@@ -34,329 +25,6 @@ def jwt_required(func: callable) -> callable:
34
25
  return wrapper
35
26
 
36
27
 
37
- def user_login(iam_server: IamServer,
38
- args: dict[str, Any],
39
- errors: list[str] = None,
40
- logger: Logger = None) -> str:
41
- """
42
- Build the URL for redirecting the request to *iam_server*'s authentication page.
43
-
44
- These are the expected attributes in *args*:
45
- - user-id: optional, identifies the reference user (alias: 'login')
46
- - redirect-uri: a parameter to be added to the query part of the returned URL
47
-
48
- If provided, the user identification will be validated against the authorization data
49
- returned by *iam_server* upon login. On success, the appropriate URL for invoking
50
- the IAM server's authentication page is returned.
51
-
52
- :param iam_server: the reference registered *IAM* server
53
- :param args: the arguments passed when requesting the service
54
- :param errors: incidental error messages
55
- :param logger: optional logger
56
- :return: the callback URL, with the appropriate parameters, of *None* if error
57
- """
58
- # initialize the return variable
59
- result: str | None = None
60
-
61
- # obtain the optional user's identification
62
- user_id: str = args.get("user-id") or args.get("login")
63
-
64
- # build the user data
65
- # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
66
- oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
67
-
68
- with _iam_lock:
69
- # retrieve the user data from the IAM server's registry
70
- user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
71
- user_id=oauth_state,
72
- errors=errors,
73
- logger=logger)
74
- if user_data:
75
- user_data["login-id"] = user_id
76
- timeout: int = _get_login_timeout(iam_server=iam_server,
77
- errors=errors,
78
- logger=logger)
79
- if not errors:
80
- user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
81
- if timeout else None
82
- redirect_uri: str = args.get("redirect-uri")
83
- user_data["redirect-uri"] = redirect_uri
84
-
85
- # build the login url
86
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
87
- errors=errors,
88
- logger=logger)
89
- if registry:
90
- result = (f"{registry["base-url"]}/protocol/openid-connect/auth"
91
- f"?response_type=code&scope=openid"
92
- f"&client_id={registry["client-id"]}"
93
- f"&redirect_uri={redirect_uri}"
94
- f"&state={oauth_state}")
95
- return result
96
-
97
-
98
- def user_logout(iam_server: IamServer,
99
- args: dict[str, Any],
100
- errors: list[str] = None,
101
- logger: Logger = None) -> None:
102
- """
103
- Logout the user, by removing all data associating it from *iam_server*'s registry.
104
-
105
- The user is identified by the attribute *user-id* or "login", provided in *args*.
106
- If successful, remove all data relating to the user from the *IAM* server's registry.
107
- Otherwise, this operation fails silently, unless an error has ocurred.
108
-
109
- :param iam_server: the reference registered *IAM* server
110
- :param args: the arguments passed when requesting the service
111
- :param errors: incidental error messages
112
- :param logger: optional logger
113
- """
114
- # obtain the user's identification
115
- user_id: str = args.get("user-id") or args.get("login")
116
-
117
- if user_id:
118
- with _iam_lock:
119
- # retrieve the data for all users in the IAM server's registry
120
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
121
- errors=errors,
122
- logger=logger) or {}
123
- if user_id in users:
124
- users.pop(user_id)
125
- if logger:
126
- logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
127
-
128
-
129
- def user_token(iam_server: IamServer,
130
- args: dict[str, Any],
131
- errors: list[str] = None,
132
- logger: Logger = None) -> str:
133
- """
134
- Retrieve the authentication token for the user, from *iam_server*.
135
-
136
- The user is identified by the attribute *user-id* or *login*, provided in *args*.
137
-
138
- :param iam_server: the reference registered *IAM* server
139
- :param args: the arguments passed when requesting the service
140
- :param errors: incidental error messages
141
- :param logger: optional logger
142
- :return: the token for user indicated, or *None* if error
143
- """
144
- # initialize the return variable
145
- result: str | None = None
146
-
147
- # obtain the user's identification
148
- user_id: str = args.get("user-id") or args.get("login")
149
-
150
- err_msg: str | None = None
151
- if user_id:
152
- with _iam_lock:
153
- # retrieve the user data in the IAM server's registry
154
- user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
155
- user_id=user_id,
156
- errors=errors,
157
- logger=logger)
158
- token: str = user_data["access-token"] if user_data else None
159
- if token:
160
- access_expiration: int = user_data.get("access-expiration")
161
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
162
- if now < access_expiration:
163
- result = token
164
- else:
165
- # access token has expired
166
- refresh_token: str = user_data["refresh-token"]
167
- if refresh_token:
168
- refresh_expiration = user_data["refresh-expiration"]
169
- if now < refresh_expiration:
170
- body_data: dict[str, str] = {
171
- "grant_type": "refresh_token",
172
- "refresh_token": refresh_token
173
- }
174
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
175
- token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
176
- body_data=body_data,
177
- errors=errors,
178
- logger=logger)
179
- # validate and store the token data
180
- if token_data:
181
- token_info: tuple[str, str] = __validate_and_store(iam_server=iam_server,
182
- user_data=user_data,
183
- token_data=token_data,
184
- now=now,
185
- errors=errors,
186
- logger=logger)
187
- result = token_info[1]
188
- else:
189
- # refresh token is no longer valid
190
- user_data["refresh-token"] = None
191
- else:
192
- # refresh token has expired
193
- err_msg = "Access and refresh tokens expired"
194
- if logger:
195
- logger.error(msg=err_msg)
196
- else:
197
- err_msg = "Access token expired, no refresh token available"
198
- if logger:
199
- logger.error(msg=err_msg)
200
- else:
201
- err_msg = f"User '{user_id}' not authenticated"
202
- if logger:
203
- logger.error(msg=err_msg)
204
- else:
205
- err_msg = "User identification not provided"
206
- if logger:
207
- logger.error(msg=err_msg)
208
-
209
- if err_msg and isinstance(errors, list):
210
- errors.append(err_msg)
211
-
212
- return result
213
-
214
-
215
- def login_callback(iam_server: IamServer,
216
- args: dict[str, Any],
217
- errors: list[str] = None,
218
- logger: Logger = None) -> tuple[str, str] | None:
219
- """
220
- Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
221
-
222
- The relevant expected arguments in *args* are:
223
- - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
224
- - *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
225
-
226
- :param iam_server: the reference registered *IAM* server
227
- :param args: the arguments passed when requesting the service
228
- :param errors: incidental errors
229
- :param logger: optional logger
230
- :return: a tuple containing the reference user identification and the token obtained, or *None* if error
231
- """
232
- # initialize the return variable
233
- result: tuple[str, str] | None = None
234
-
235
- with _iam_lock:
236
- # retrieve the IAM server's data for all users
237
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
238
- errors=errors,
239
- logger=logger) or {}
240
- # retrieve the OAuth2 state
241
- oauth_state: str = args.get("state")
242
- user_data: dict[str, Any] | None = None
243
- if oauth_state:
244
- for user, data in users.items():
245
- if user == oauth_state:
246
- user_data = data
247
- break
248
-
249
- # exchange 'code' received for the token
250
- if user_data:
251
- expiration: int = user_data["login-expiration"] or sys.maxsize
252
- if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
253
- errors.append("Operation timeout")
254
- else:
255
- users.pop(oauth_state)
256
- code: str = args.get("code")
257
- body_data: dict[str, Any] = {
258
- "grant_type": "authorization_code",
259
- "code": code,
260
- "redirect_uri": user_data.pop("redirect-uri")
261
- }
262
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
263
- token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
264
- body_data=body_data,
265
- errors=errors,
266
- logger=logger)
267
- # validate and store the token data
268
- if token_data:
269
- result = __validate_and_store(iam_server=iam_server,
270
- user_data=user_data,
271
- token_data=token_data,
272
- now=now,
273
- errors=errors,
274
- logger=logger)
275
- else:
276
- msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
277
- if logger:
278
- logger.error(msg=msg)
279
- if isinstance(errors, list):
280
- errors.append(msg)
281
-
282
- return result
283
-
284
-
285
- def token_exchange(iam_server: IamServer,
286
- args: dict[str, Any],
287
- errors: list[str] = None,
288
- logger: Logger = None) -> dict[str, Any]:
289
- """
290
- Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
291
-
292
- The expected parameters in *args* are:
293
- - user-id: identification for the reference user (alias: 'login')
294
- - token: the token to be exchanged
295
-
296
- The typical data set returned contains the following attributes:
297
- {
298
- "token_type": "Bearer",
299
- "access_token": <str>,
300
- "expires_in": <number-of-seconds>,
301
- "refresh_token": <str>,
302
- "refesh_expires_in": <number-of-seconds>
303
- }
304
-
305
- :param iam_server: the reference registered *IAM* server
306
- :param args: the arguments passed when requesting the service
307
- :param errors: incidental errors
308
- :param logger: optional logger
309
- :return: the data for the new token, or *None* if error
310
- """
311
- # initialize the return variable
312
- result: dict[str, Any] | None = None
313
-
314
- # obtain the user's identification
315
- user_id: str = args.get("user-id") or args.get("login")
316
-
317
- # obtain the token to be exchanged
318
- token: str = args.get("access-token")
319
-
320
- if user_id and token:
321
- # HAZARD: only 'IAM_KEYCLOAK' is currently supported
322
- with _iam_lock:
323
- # retrieve the IAM server's registry
324
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
325
- errors=errors,
326
- logger=logger)
327
- if registry:
328
- body_data: dict[str, str] = {
329
- "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
330
- "subject_token": token,
331
- "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
332
- "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
333
- "audience": registry["client-id"],
334
- "subject_issuer": "oidc"
335
- }
336
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
337
- token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
338
- body_data=body_data,
339
- errors=errors,
340
- logger=logger)
341
- # validate and store the token data
342
- if token_data:
343
- user_data: dict[str, Any] = {}
344
- result = __validate_and_store(iam_server=iam_server,
345
- user_data=user_data,
346
- token_data=token_data,
347
- now=now,
348
- errors=errors,
349
- logger=logger)
350
- else:
351
- msg: str = "User identification or token not provided"
352
- if logger:
353
- logger.error(msg=msg)
354
- if isinstance(errors, list):
355
- errors.append(msg)
356
-
357
- return result
358
-
359
-
360
28
  def __request_validate(request: Request) -> Response:
361
29
  """
362
30
  Verify whether the HTTP *request* has the proper authorization, as per the JWT standard.
@@ -412,175 +80,3 @@ def __request_validate(request: Request) -> Response:
412
80
  result = Response(response="Authorization failed",
413
81
  status=401)
414
82
  return result
415
-
416
-
417
- def __post_for_token(iam_server: IamServer,
418
- body_data: dict[str, Any],
419
- errors: list[str] | None,
420
- logger: Logger | None) -> dict[str, Any] | None:
421
- """
422
- Send a POST request to obtain the authentication token data, and return the data received.
423
-
424
- For token acquisition, *body_data* will have the attributes:
425
- - "grant_type": "authorization_code"
426
- - "code": <16-character-random-code>
427
- - "redirect_uri": <redirect-uri>
428
-
429
- For token refresh, *body_data* will have the attributes:
430
- - "grant_type": "refresh_token"
431
- - "refresh_token": <current-refresh-token>
432
-
433
- For token exchange, *body_data* will have the attributes:
434
- - "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
435
- - "subject_token": <token-to-be-exchanged>,
436
- - "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
437
- - "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
438
- - "audience": <client-id>,
439
- - "subject_issuer": "oidc"
440
-
441
- These attributes are then added to *body_data*:
442
- - "client_id": <client-id>,
443
- - "client_secret": <client-secret>,
444
-
445
- If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
446
- Otherwise, *errors* will contain the appropriate error message.
447
-
448
- The typical data set returned contains the following attributes:
449
- {
450
- "token_type": "Bearer",
451
- "access_token": <str>,
452
- "expires_in": <number-of-seconds>,
453
- "refresh_token": <str>,
454
- "refesh_expires_in": <number-of-seconds>
455
- }
456
-
457
- :param iam_server: the reference registered *IAM* server
458
- :param body_data: the data to send in the body of the request
459
- :param errors: incidental errors
460
- :param logger: optional logger
461
- :return: the token data, or *None* if error
462
- """
463
- # initialize the return variable
464
- result: dict[str, Any] | None = None
465
-
466
- err_msg: str | None = None
467
- with _iam_lock:
468
- # retrieve the IAM server's registry
469
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
470
- errors=errors,
471
- logger=logger)
472
- if registry:
473
- # complete the data to send in body of request
474
- body_data["client_id"] = registry["client-id"]
475
- client_secret: str = registry["client-secret"]
476
-
477
- # obtain the token
478
- url: str = registry["base-url"] + "/protocol/openid-connect/token"
479
-
480
- # log the POST ('client_secret' data must not be shown in log)
481
- if logger:
482
- logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
483
- ensure_ascii=False)}")
484
- if client_secret:
485
- body_data["client_secret"] = client_secret
486
- try:
487
- # typical return on a token request:
488
- # {
489
- # "token_type": "Bearer",
490
- # "access_token": <str>,
491
- # "expires_in": <number-of-seconds>,
492
- # "refresh_token": <str>,
493
- # "refesh_expires_in": <number-of-seconds>
494
- # }
495
- response: requests.Response = requests.post(url=url,
496
- data=body_data)
497
- if response.status_code == 200:
498
- # request succeeded
499
- result = response.json()
500
- if logger:
501
- logger.debug(msg=f"POST success, {json.dumps(obj=result,
502
- ensure_ascii=False)}")
503
- else:
504
- # request resulted in error
505
- err_msg = f"POST failure, status {response.status_code}, reason {response.reason}"
506
- if hasattr(response, "content") and response.content:
507
- err_msg += f", content '{response.content}'"
508
- if logger:
509
- logger.error(msg=err_msg)
510
- except Exception as e:
511
- # the operation raised an exception
512
- err_msg = exc_format(exc=e,
513
- exc_info=sys.exc_info())
514
- if logger:
515
- logger.error(msg=err_msg)
516
-
517
- if err_msg and isinstance(errors, list):
518
- errors.append(err_msg)
519
-
520
- return result
521
-
522
-
523
- def __validate_and_store(iam_server: IamServer,
524
- user_data: dict[str, Any],
525
- token_data: dict[str, Any],
526
- now: int,
527
- errors: list[str] | None,
528
- logger: Logger) -> tuple[str, str] | None:
529
- """
530
- Validate and store the token data.
531
-
532
- The typical *token_data* contains the following attributes:
533
- {
534
- "token_type": "Bearer",
535
- "access_token": <str>,
536
- "expires_in": <number-of-seconds>,
537
- "refresh_token": <str>,
538
- "refesh_expires_in": <number-of-seconds>
539
- }
540
-
541
- :param iam_server: the reference registered *IAM* server
542
- :param user_data: the aurthentication data kepth in *iam_server*'s registry
543
- :param token_data: the token data
544
- :param errors: incidental errors
545
- :param logger: optional logger
546
- :return: tuple containing the user identification and the validated and stored token, or *None* if error
547
- """
548
- # initialize the return variable
549
- result: tuple[str, str] | None = None
550
-
551
- with _iam_lock:
552
- # retrieve the IAM server's registry
553
- registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
554
- errors=errors,
555
- logger=logger)
556
- if registry:
557
- token: str = token_data.get("access_token")
558
- user_data["access-token"] = token
559
- # keep current refresh token if a new one is not provided
560
- if token_data.get("refresh_token"):
561
- user_data["refresh-token"] = token_data.get("refresh_token")
562
- user_data["access-expiration"] = now + token_data.get("expires_in")
563
- refresh_exp: int = user_data.get("refresh_expires_in")
564
- user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
565
- # public_key: str = _get_public_key(iam_server=iam_server,
566
- # errors=errors,
567
- # logger=logger)
568
- recipient_attr = registry["recipient-attr"]
569
- login_id = user_data.pop("login-id", None)
570
- claims: dict[str, dict[str, Any]] = token_validate(token=token,
571
- issuer=registry["base-url"],
572
- recipient_id=login_id,
573
- recipient_attr=recipient_attr,
574
- # public_key=public_key,
575
- errors=errors,
576
- logger=logger)
577
- if claims:
578
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
579
- errors=errors,
580
- logger=logger)
581
- # must test with 'not errors'
582
- if not errors:
583
- user_id: str = login_id if login_id else claims["payload"][recipient_attr]
584
- users[user_id] = user_data
585
- result = (user_id, token)
586
- return result
@@ -4,9 +4,9 @@ from logging import Logger
4
4
  from typing import Any
5
5
 
6
6
  from .iam_common import IamServer, _iam_lock, _iam_server_from_endpoint
7
- from .iam_pomes import (
8
- user_login, user_logout,
9
- user_token, token_exchange, login_callback
7
+ from .iam_actions import (
8
+ action_login, action_logout,
9
+ action_token, action_exchange, action_callback
10
10
  )
11
11
 
12
12
  # the logger for IAM service operations
@@ -60,10 +60,10 @@ def service_login() -> Response:
60
60
  logger=__IAM_LOGGER)
61
61
  if iam_server:
62
62
  # obtain the login URL
63
- login_url: str = user_login(iam_server=iam_server,
64
- args=request.args,
65
- errors=errors,
66
- logger=__IAM_LOGGER)
63
+ login_url: str = action_login(iam_server=iam_server,
64
+ args=request.args,
65
+ errors=errors,
66
+ logger=__IAM_LOGGER)
67
67
  if login_url:
68
68
  result = jsonify({"login-url": login_url})
69
69
  if errors:
@@ -106,10 +106,10 @@ def service_logout() -> Response:
106
106
  logger=__IAM_LOGGER)
107
107
  if iam_server:
108
108
  # logout the user
109
- user_logout(iam_server=iam_server,
110
- args=request.args,
111
- errors=errors,
112
- logger=__IAM_LOGGER)
109
+ action_logout(iam_server=iam_server,
110
+ args=request.args,
111
+ errors=errors,
112
+ logger=__IAM_LOGGER)
113
113
  if errors:
114
114
  result = Response(response="; ".join(errors),
115
115
  status=400)
@@ -160,10 +160,10 @@ def service_callback() -> Response:
160
160
  logger=__IAM_LOGGER)
161
161
  if iam_server:
162
162
  # process the callback operation
163
- token_data = login_callback(iam_server=iam_server,
164
- args=request.args,
165
- errors=errors,
166
- logger=__IAM_LOGGER)
163
+ token_data = action_callback(iam_server=iam_server,
164
+ args=request.args,
165
+ errors=errors,
166
+ logger=__IAM_LOGGER)
167
167
  result: Response
168
168
  if errors:
169
169
  result = jsonify({"errors": "; ".join(errors)})
@@ -215,10 +215,10 @@ def service_token() -> Response:
215
215
  if iam_server:
216
216
  # retrieve the token
217
217
  errors: list[str] = []
218
- token: str = user_token(iam_server=iam_server,
219
- args=args,
220
- errors=errors,
221
- logger=__IAM_LOGGER)
218
+ token: str = action_token(iam_server=iam_server,
219
+ args=args,
220
+ errors=errors,
221
+ logger=__IAM_LOGGER)
222
222
  else:
223
223
  msg: str = "User identification not provided"
224
224
  errors.append(msg)
@@ -278,10 +278,10 @@ def service_exchange() -> Response:
278
278
  token_data: dict[str, Any] | None = None
279
279
  if iam_server:
280
280
  errors: list[str] = []
281
- token_data = token_exchange(iam_server=iam_server,
282
- args=request.args,
283
- errors=errors,
284
- logger=__IAM_LOGGER)
281
+ token_data = action_exchange(iam_server=iam_server,
282
+ args=request.args,
283
+ errors=errors,
284
+ logger=__IAM_LOGGER)
285
285
  result: Response
286
286
  if errors:
287
287
  result = Response(response="; ".join(errors),
@@ -7,7 +7,7 @@ from pypomes_core import (
7
7
  from typing import Any, Final
8
8
 
9
9
  from .iam_common import _IAM_SERVERS, IamServer, _iam_lock
10
- from .iam_pomes import user_token
10
+ from .iam_actions import action_token
11
11
 
12
12
  JUSBR_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_ID")
13
13
  JUSBR_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_SECRET")
@@ -118,8 +118,8 @@ def jusbr_get_token(user_id: str,
118
118
  # retrieve the token
119
119
  args: dict[str, Any] = {"user-id": user_id}
120
120
  with _iam_lock:
121
- result = user_token(iam_server=IamServer.IAM_JUSRBR,
122
- args=args,
123
- errors=errors,
124
- logger=logger)
121
+ result = action_token(iam_server=IamServer.IAM_JUSRBR,
122
+ args=args,
123
+ errors=errors,
124
+ logger=logger)
125
125
  return result
@@ -7,7 +7,7 @@ from pypomes_core import (
7
7
  from typing import Any, Final
8
8
 
9
9
  from .iam_common import _IAM_SERVERS, IamServer, _iam_lock
10
- from .iam_pomes import user_token
10
+ from .iam_actions import action_token
11
11
 
12
12
  KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
13
13
  KEYCLOAK_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_SECRET")
@@ -129,8 +129,8 @@ def keycloak_get_token(user_id: str,
129
129
  # retrieve the token
130
130
  args: dict[str, Any] = {"user-id": user_id}
131
131
  with _iam_lock:
132
- result = user_token(iam_server=IamServer.IAM_KEYCLOAK,
133
- args=args,
134
- errors=errors,
135
- logger=logger)
132
+ result = action_token(iam_server=IamServer.IAM_KEYCLOAK,
133
+ args=args,
134
+ errors=errors,
135
+ logger=logger)
136
136
  return result
@@ -5,7 +5,7 @@ from base64 import b64encode
5
5
  from datetime import datetime
6
6
  from logging import Logger
7
7
  from pypomes_core import TZ_LOCAL, exc_format
8
- from threading import RLock
8
+ from threading import Lock
9
9
  from typing import Any, Final
10
10
 
11
11
  # structure:
@@ -25,7 +25,7 @@ _provider_registry: Final[dict[str, dict[str, Any]]] = {}
25
25
 
26
26
  # the lock protecting the data in '_provider_registry'
27
27
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
28
- _provider_lock: Final[RLock] = RLock()
28
+ _provider_lock: Final[Lock] = Lock()
29
29
 
30
30
 
31
31
  def provider_register(provider_id: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.5.5
3
+ Version: 0.5.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,13 @@
1
+ pypomes_iam/__init__.py,sha256=ip9p9-0qCaRPuMVae2JTLZHq6-OPgNKBIL6t6PaSHWg,1180
2
+ pypomes_iam/iam_actions.py,sha256=0x5kPaDor2rHiOznyF9DLzsNRGLleB66K6RJBPaJkBc,24178
3
+ pypomes_iam/iam_common.py,sha256=Xu3Jz-wXzYtEk1hi06lFJ997e9n77I_eeRbpRQ2qCy4,10365
4
+ pypomes_iam/iam_pomes.py,sha256=yA0ZRaD-fp7aZZ-yDnFlh6CvCsEWd-Tf123twQoTPGg,3456
5
+ pypomes_iam/iam_services.py,sha256=ZwSwCiA3XssjG_HgTdkkKtdnQg9UjuqlvFhWVPQfSH8,11871
6
+ pypomes_iam/jusbr_pomes.py,sha256=X_YgY45122tflAzQdAMEcEyVbPvzFigjHLal0qL1v_M,5916
7
+ pypomes_iam/keycloak_pomes.py,sha256=FGdkPjVGEDp5Pwfav4EIc9uSbT4_pG7oPqaiHeJBSLU,6763
8
+ pypomes_iam/provider_pomes.py,sha256=CdEjYjepGXsehn_ujljUQKs0Ws7xNOzBYG6wKp9C7-E,7233
9
+ pypomes_iam/token_pomes.py,sha256=Bz9pT2oU6jTEr_ZEZEJ3kUjH3TfxRyY1_vR319v6CEo,6692
10
+ pypomes_iam-0.5.6.dist-info/METADATA,sha256=ZowLxR_xl3hWHutcz_hGAhvW0B1vsIyIIrrCnwF5uXg,694
11
+ pypomes_iam-0.5.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ pypomes_iam-0.5.6.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
13
+ pypomes_iam-0.5.6.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- pypomes_iam/__init__.py,sha256=KX_QLdqAD-dNUl3G1mDeutxL9e58S9OsMoJlrgM9R28,1027
2
- pypomes_iam/iam_common.py,sha256=xRMVPIDxbVPoudPDKFCqGCA7Klt9HsZM61dSGfEy7Tw,10364
3
- pypomes_iam/iam_pomes.py,sha256=sB2DDCaN5nN6ehZH4HCsWxPM9IbP5IRt42UhIRtn07Q,27436
4
- pypomes_iam/iam_services.py,sha256=fdpOc6EmCLcMtkhZU5OX9gYJIA1RNyM3JcoMX0RqwXA,11829
5
- pypomes_iam/jusbr_pomes.py,sha256=cuQWB5OTeAHarmUqAGIU4udSEJA1C6W6lOtauWA7gqw,5904
6
- pypomes_iam/keycloak_pomes.py,sha256=OvPhfUXpqxll-p6CdRq2j5jp5ST9Z0feXg6TVbqC2cY,6751
7
- pypomes_iam/provider_pomes.py,sha256=EFgWZO7kdUVJ5_z8egdzG9nmmpa7wTXOyo9Ops5NAEE,7236
8
- pypomes_iam/token_pomes.py,sha256=Bz9pT2oU6jTEr_ZEZEJ3kUjH3TfxRyY1_vR319v6CEo,6692
9
- pypomes_iam-0.5.5.dist-info/METADATA,sha256=FvKoV08gCZ-WvswVzMMlEOFi6jPlNZba7zUPeLtIVLU,694
10
- pypomes_iam-0.5.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- pypomes_iam-0.5.5.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
12
- pypomes_iam-0.5.5.dist-info/RECORD,,