pypomes-iam 0.5.8__py3-none-any.whl → 0.7.1__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.
pypomes_iam/__init__.py CHANGED
@@ -6,7 +6,7 @@ from .iam_common import (
6
6
  IamServer, IamParam
7
7
  )
8
8
  from .iam_pomes import (
9
- iam_setup, iam_get_token
9
+ iam_setup, iam_get_env_parameters, iam_get_token
10
10
  )
11
11
  from .iam_services import (
12
12
  jwt_required, logger_register
@@ -25,7 +25,7 @@ __all__ = [
25
25
  # iam_commons
26
26
  "IamServer", "IamParam",
27
27
  # iam_pomes
28
- "iam_setup", "iam_get_token",
28
+ "iam_setup", "iam_get_env_parameters", "iam_get_token",
29
29
  # iam_services
30
30
  "jwt_required", "logger_register",
31
31
  # provider_pomes
@@ -10,8 +10,8 @@ from typing import Any
10
10
 
11
11
  from .iam_common import (
12
12
  IamServer, IamParam, UserParam, _iam_lock,
13
- _get_iam_users, _get_iam_registry, # _get_public_key,
14
- _get_login_timeout, _get_user_data
13
+ _get_iam_users, _get_iam_registry, _get_public_key,
14
+ _get_login_timeout, _get_user_data, _iam_server_from_issuer
15
15
  )
16
16
  from .token_pomes import token_get_claims, token_validate
17
17
 
@@ -59,7 +59,7 @@ def action_login(iam_server: IamServer,
59
59
  errors=errors,
60
60
  logger=logger)
61
61
  if not errors:
62
- user_data[UserParam.LOGIN_EXPIRATION] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
62
+ user_data[UserParam.LOGIN_EXPIRATION] = (int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout) \
63
63
  if timeout else None
64
64
  redirect_uri: str = args.get(UserParam.REDIRECT_URI)
65
65
  user_data[UserParam.REDIRECT_URI] = redirect_uri
@@ -138,6 +138,7 @@ def action_token(iam_server: IamServer,
138
138
  user_id=user_id,
139
139
  errors=errors,
140
140
  logger=logger)
141
+ # retrieve the stored access token
141
142
  token: str = user_data[UserParam.ACCESS_TOKEN] if user_data else None
142
143
  if token:
143
144
  access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
@@ -148,7 +149,7 @@ def action_token(iam_server: IamServer,
148
149
  # access token has expired
149
150
  refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
150
151
  if refresh_token:
151
- refresh_expiration = user_data[UserParam.REFRESH_EXPIRATION]
152
+ refresh_expiration: int = user_data[UserParam.REFRESH_EXPIRATION]
152
153
  if now < refresh_expiration:
153
154
  header_data: dict[str, str] = {
154
155
  "Content-Type": "application/json"
@@ -242,7 +243,7 @@ def action_callback(iam_server: IamServer,
242
243
  users.pop(oauth_state)
243
244
  code: str = args.get("code")
244
245
  header_data: dict[str, str] = {
245
- "Content-Type": "application/json"
246
+ "Content-Type": "application/x-www-form-urlencoded"
246
247
  }
247
248
  body_data: dict[str, Any] = {
248
249
  "grant_type": "authorization_code",
@@ -276,7 +277,7 @@ def action_callback(iam_server: IamServer,
276
277
  def action_exchange(iam_server: IamServer,
277
278
  args: dict[str, Any],
278
279
  errors: list[str] = None,
279
- logger: Logger = None) -> dict[str, Any]:
280
+ logger: Logger = None) -> tuple[str, str]:
280
281
  """
281
282
  Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
282
283
 
@@ -297,17 +298,23 @@ def action_exchange(iam_server: IamServer,
297
298
  :param args: the arguments passed when requesting the service
298
299
  :param errors: incidental errors
299
300
  :param logger: optional logger
300
- :return: the data for the new token, or *None* if error
301
+ :return: a tuple containing the reference user identification and the token obtained, or *None* if error
301
302
  """
302
303
  # initialize the return variable
303
- result: dict[str, Any] | None = None
304
+ result: tuple[str, str] | None = None
304
305
 
305
306
  # obtain the user's identification
306
307
  user_id: str = args.get("user-id") or args.get("login")
307
308
 
308
309
  # obtain the token to be exchanged
309
310
  token: str = args.get("access-token") if user_id else None
310
- if token:
311
+ token_claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
312
+ errors=errors,
313
+ logger=logger) if token else None
314
+ token_issuer: str = _iam_server_from_issuer(issuer=token_claims["payload"]["iss"],
315
+ errors=errors,
316
+ logger=logger) if token_claims else None
317
+ if not errors:
311
318
  # HAZARD: only 'IAM_KEYCLOAK' is currently supported
312
319
  with _iam_lock:
313
320
  # retrieve the IAM server's registry
@@ -323,6 +330,8 @@ def action_exchange(iam_server: IamServer,
323
330
  logger=logger)
324
331
  if not errors:
325
332
  # exchange the token
333
+ if logger:
334
+ logger.debug(msg=f"Requesting the token exchange to IAM server '{iam_server}'")
326
335
  header_data: dict[str, Any] = {
327
336
  "Content-Type": "application/x-www-form-urlencoded"
328
337
  }
@@ -332,7 +341,7 @@ def action_exchange(iam_server: IamServer,
332
341
  "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
333
342
  "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
334
343
  "audience": registry[IamParam.CLIENT_ID],
335
- "subject_issuer": "oidc"
344
+ "subject_issuer": token_issuer
336
345
  }
337
346
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
338
347
  token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
@@ -367,7 +376,7 @@ def __assert_link(iam_server: IamServer,
367
376
  """
368
377
  Make sure *iam_server* has a link associating *user_id* to an internal user identification.
369
378
  This is a requirement for exchanging a token issued by a federated *IAM* server for an equivalent
370
- one from *iam_server.
379
+ one from *iam_server*.
371
380
 
372
381
  :param iam_server: the reference *IAM* server
373
382
  :param user_id: the reference user identification
@@ -375,6 +384,9 @@ def __assert_link(iam_server: IamServer,
375
384
  :param errors: incidental errors
376
385
  :param logger: optional logger
377
386
  """
387
+ if logger:
388
+ logger.debug(msg="Verifying associations for user "
389
+ f"'{user_id}' in IAM server '{iam_server}'")
378
390
  # obtain a token with administrative rights
379
391
  admin_token: str = __get_administrative_token(iam_server=iam_server,
380
392
  errors=errors,
@@ -383,7 +395,10 @@ def __assert_link(iam_server: IamServer,
383
395
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
384
396
  errors=errors,
385
397
  logger=logger)
386
- # obtain the internal user identification for 'from_id'
398
+ # obtain the internal user identification for 'user_id'
399
+ if logger:
400
+ logger.debug(msg="Obtaining internal identification "
401
+ f"for user {user_id} in IAM server {iam_server}")
387
402
  url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
388
403
  header_data: dict[str, str] = {
389
404
  "Authorization": f"Bearer {admin_token}",
@@ -393,15 +408,18 @@ def __assert_link(iam_server: IamServer,
393
408
  "username": user_id,
394
409
  "exact": "true"
395
410
  }
396
- users: dict[str, Any] = __get_for_data(url=url,
397
- header_data=header_data,
398
- params=params,
399
- errors=errors,
400
- logger=logger)
411
+ users: list[dict[str, Any]] = __get_for_data(url=url,
412
+ header_data=header_data,
413
+ params=params,
414
+ errors=errors,
415
+ logger=logger)
401
416
  if users:
402
417
  # verify whether the 'oidc' protocol is referred to in an
403
- # association between 'from_id' and the internal user identification
404
- internal_id: str = users.get("id")
418
+ # association between 'user_id' and the internal user identification
419
+ internal_id: str = users[0].get("id")
420
+ if logger:
421
+ logger.debug(msg="Obtaining the providers federated with "
422
+ f"IAM server '{iam_server}' for internal identification '{internal_id}'")
405
423
  url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
406
424
  f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
407
425
  providers: list[dict[str, Any]] = __get_for_data(url=url,
@@ -410,27 +428,38 @@ def __assert_link(iam_server: IamServer,
410
428
  errors=errors,
411
429
  logger=logger)
412
430
  no_link: bool = True
413
- for provider in providers:
414
- if provider.get("identityProvider") == "oidc":
415
- no_link = False
416
- break
417
- if no_link:
418
- # link the identities
419
- claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
420
- errors=errors,
421
- logger=logger)
422
- if claims:
423
- token_sub: str = claims["paylod"]["sub"]
424
- url += "/oidc"
425
- body_data: dict[str, Any] = {
426
- "userId": token_sub,
427
- "userName": user_id
428
- }
429
- __post_data(url=url,
430
- header_data=header_data,
431
- body_data=body_data,
432
- errors=errors,
433
- logger=logger)
431
+ claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
432
+ errors=errors,
433
+ logger=logger)
434
+ issuer: str = claims["payload"]["iss"] if claims else None
435
+ provider_name: str = _iam_server_from_issuer(issuer=issuer,
436
+ errors=errors,
437
+ logger=logger) if issuer else None
438
+ if provider_name:
439
+ for provider in providers:
440
+ if provider.get("identityProvider") == provider_name:
441
+ no_link = False
442
+ break
443
+ if no_link:
444
+ # link the identities
445
+ claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
446
+ errors=errors,
447
+ logger=logger)
448
+ if claims:
449
+ token_sub: str = claims["payload"]["sub"]
450
+ if logger:
451
+ logger.debug(msg="Creating an association between identifications "
452
+ f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
453
+ url += f"/{provider_name}"
454
+ body_data: dict[str, Any] = {
455
+ "userId": token_sub,
456
+ "userName": user_id
457
+ }
458
+ __post_data(url=url,
459
+ header_data=header_data,
460
+ body_data=body_data,
461
+ errors=errors,
462
+ logger=logger)
434
463
 
435
464
 
436
465
  def __get_administrative_token(iam_server: IamServer,
@@ -450,31 +479,47 @@ def __get_administrative_token(iam_server: IamServer,
450
479
  # initialize the return variable
451
480
  result: str | None = None
452
481
 
482
+ if logger:
483
+ logger.debug(msg="Requesting a token with "
484
+ f"administrative rights to IAM Server '{iam_server}'")
485
+
453
486
  # obtain the IAM server's registry
454
487
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
455
488
  errors=errors,
456
489
  logger=logger)
457
- if registry and registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
458
- header_data: dict[str, str] = {
459
- "Content-Type": "application/json"
460
- }
461
- body_data: dict[str, str] = {
462
- "grant-type": "password",
463
- "username": registry[IamParam.ADMIN_ID],
464
- "password": registry[IamParam.ADMIN_SECRET],
465
- "client_id": registry[IamParam.CLIENT_ID]
466
- }
467
- token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
468
- header_data=header_data,
469
- body_data=body_data,
470
- errors=errors,
471
- logger=logger)
472
- if token_data:
473
- # obtain the token
474
- result = token_data["access_token"]
475
- else:
476
- msg: str = (f"To obtain token with administrative rights from '{iam_server}', "
477
- f"the credentials for the realm administrator must be provided at setup time")
490
+ if registry:
491
+ if registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
492
+ header_data: dict[str, str] = {
493
+ "Content-Type": "application/x-www-form-urlencoded"
494
+ }
495
+ body_data: dict[str, str] = {
496
+ "grant_type": "password",
497
+ "username": registry[IamParam.ADMIN_ID],
498
+ "password": registry[IamParam.ADMIN_SECRET],
499
+ "client_id": "admin-cli"
500
+ }
501
+ token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
502
+ header_data=header_data,
503
+ body_data=body_data,
504
+ errors=errors,
505
+ logger=logger)
506
+ if token_data:
507
+ # obtain the token
508
+ result = token_data["access_token"]
509
+ if logger:
510
+ logger.debug(msg="Administrative token obtained")
511
+
512
+ elif logger or isinstance(errors, list):
513
+ msg: str = ("Credentials for administrator of realm "
514
+ f"'{registry[IamParam.CLIENT_REALM]}' "
515
+ f"at IAM server '{iam_server}' not provided")
516
+ if logger:
517
+ logger.error(msg=msg)
518
+ if isinstance(errors, list):
519
+ errors.append(msg)
520
+
521
+ elif logger or isinstance(errors, list):
522
+ msg: str = f"Unknown IAM server {iam_server}"
478
523
  if logger:
479
524
  logger.error(msg=msg)
480
525
  if isinstance(errors, list):
@@ -510,14 +555,19 @@ def __get_client_secret(iam_server: IamServer,
510
555
  errors=errors,
511
556
  logger=logger)
512
557
  if token:
558
+ realm: str = registry[IamParam.CLIENT_REALM]
559
+ client_id: str = registry[IamParam.CLIENT_ID]
560
+ if logger:
561
+ logger.debug(msg=f"Obtaining the UUID for client '{client_id}', "
562
+ f"in realm '{realm}' at IAM server '{iam_server}'")
513
563
  # obtain the client UUID
514
- url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}/clients"
564
+ url: str = f"{registry[IamParam.URL_BASE]}/realms/{realm}/clients"
515
565
  header_data: dict[str, str] = {
516
566
  "Authorization": f"Bearer {token}",
517
567
  "Content-Type": "application/json"
518
568
  }
519
569
  params: dict[str, str] = {
520
- "clientId": registry[IamParam.CLIENT_ID]
570
+ "clientId": client_id
521
571
  }
522
572
  clients: list[dict[str, Any]] = __get_for_data(url=url,
523
573
  header_data=header_data,
@@ -527,6 +577,9 @@ def __get_client_secret(iam_server: IamServer,
527
577
  if clients:
528
578
  # obtain the client's secret password
529
579
  client_uuid: str = clients[0]["id"]
580
+ if logger:
581
+ logger.debug(msg=f"Obtaining the secret for client UUID '{client_uuid}', "
582
+ f"in realm '{realm}' at IAM server '{iam_server}'")
530
583
  url += f"/{client_uuid}/client-secret"
531
584
  reply: dict[str, Any] = __get_for_data(url=url,
532
585
  header_data=header_data,
@@ -573,7 +626,7 @@ def __get_for_data(url: str,
573
626
  logger.debug(msg=f"GET success, {json.dumps(obj=result,
574
627
  ensure_ascii=False)}")
575
628
  else:
576
- # request resulted in error
629
+ # request failed, report the problem
577
630
  msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
578
631
  if hasattr(response, "content") and response.content:
579
632
  msg += f", content '{response.content}'"
@@ -615,7 +668,7 @@ def __post_data(url: str,
615
668
  headers=header_data,
616
669
  data=body_data)
617
670
  if response.status_code >= 400:
618
- # request resulted in error
671
+ # request failed, report the problem
619
672
  msg = f"POST failure, status {response.status_code}, reason {response.reason}"
620
673
  if hasattr(response, "content") and response.content:
621
674
  msg += f", content '{response.content}'"
@@ -665,9 +718,9 @@ def __post_for_token(iam_server: IamServer,
665
718
  - "username": <realm-administrator-identification>
666
719
  - "password": <realm-administrator-secret>
667
720
 
668
- These attributes are then added to *body_data*:
721
+ These attributes are then added to *body_data*, except for acquiring administrative tokens:
669
722
  - "client_id": <client-id>
670
- - "client_secret": <client-secret> <- except for acquiring administrative tokens
723
+ - "client_secret": <client-secret>
671
724
 
672
725
  If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
673
726
  Otherwise, *errors* will contain the appropriate error message.
@@ -699,21 +752,25 @@ def __post_for_token(iam_server: IamServer,
699
752
  logger=logger)
700
753
  if registry:
701
754
  # complete the data to send in body of request
702
- body_data["client_id"] = registry[IamParam.CLIENT_ID]
755
+ if body_data["grant_type"] != "password":
756
+ body_data["client_id"] = registry[IamParam.CLIENT_ID]
703
757
 
704
758
  # build the URL
705
759
  base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
706
760
  url: str = f"{base_url}/protocol/openid-connect/token"
707
-
708
- # log the POST ('client_secret' data must not be shown in log)
761
+ # 'client_secret' data must not be shown in log
762
+ msg: str = f"POST {url}, {json.dumps(obj=body_data,
763
+ ensure_ascii=False)}"
764
+ if body_data["grant_type"] != "password":
765
+ # 'client_secret' not required for requesting tokens from staging environments
766
+ client_secret: str = __get_client_secret(iam_server=iam_server,
767
+ errors=None,
768
+ logger=logger)
769
+ if client_secret:
770
+ body_data["client_secret"] = client_secret
771
+ # log the POST
709
772
  if logger:
710
- logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
711
- ensure_ascii=False)}")
712
- client_secret: str = __get_client_secret(iam_server=iam_server,
713
- errors=errors,
714
- logger=logger)
715
- if body_data["grant_type"] != "password" and client_secret:
716
- body_data["client_secret"] = client_secret
773
+ logger.debug(msg=msg)
717
774
 
718
775
  # obtain the token
719
776
  try:
@@ -735,7 +792,7 @@ def __post_for_token(iam_server: IamServer,
735
792
  logger.debug(msg=f"POST success, {json.dumps(obj=result,
736
793
  ensure_ascii=False)}")
737
794
  else:
738
- # request resulted in error
795
+ # request failed, report the problem
739
796
  err_msg = f"POST failure, status {response.status_code}, reason {response.reason}"
740
797
  if hasattr(response, "content") and response.content:
741
798
  err_msg += f", content '{response.content}'"
@@ -796,9 +853,9 @@ def __validate_and_store(iam_server: IamServer,
796
853
  user_data["access-expiration"] = now + token_data.get("expires_in")
797
854
  refresh_exp: int = user_data.get("refresh_expires_in")
798
855
  user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
799
- # public_key: str = _get_public_key(iam_server=iam_server,
800
- # errors=errors,
801
- # logger=logger)
856
+ public_key: str = _get_public_key(iam_server=iam_server,
857
+ errors=errors,
858
+ logger=logger)
802
859
  recipient_attr = registry[IamParam.RECIPIENT_ATTR]
803
860
  login_id = user_data.pop("login-id", None)
804
861
  base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
@@ -806,7 +863,7 @@ def __validate_and_store(iam_server: IamServer,
806
863
  issuer=base_url,
807
864
  recipient_id=login_id,
808
865
  recipient_attr=recipient_attr,
809
- # public_key=public_key,
866
+ public_key=public_key,
810
867
  errors=errors,
811
868
  logger=logger)
812
869
  if claims:
pypomes_iam/iam_common.py CHANGED
@@ -3,10 +3,7 @@ import sys
3
3
  from datetime import datetime
4
4
  from enum import StrEnum, auto
5
5
  from logging import Logger
6
- from pypomes_core import (
7
- APP_PREFIX, TZ_LOCAL,
8
- env_get_int, env_get_str, env_get_enum, env_get_enums, exc_format
9
- )
6
+ from pypomes_core import TZ_LOCAL, exc_format
10
7
  from pypomes_crypto import crypto_jwk_convert
11
8
  from threading import RLock
12
9
  from typing import Any, Final
@@ -16,7 +13,7 @@ class IamServer(StrEnum):
16
13
  """
17
14
  Supported IAM servers.
18
15
  """
19
- JUSRBR = auto()
16
+ JUSBR = auto()
20
17
  KEYCLOAK = auto()
21
18
 
22
19
 
@@ -57,85 +54,38 @@ class UserParam(StrEnum):
57
54
  REDIRECT_URI = "redirect-uri"
58
55
 
59
56
 
60
- def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
61
- """
62
- Establish the configuration data for select *IAM* servers, from environment variables.
63
-
64
- The preferred way to specify configuration parameters is dynamically with *iam_setup()*;.
65
- Specifying configuration parameters with environment variables can be done in two ways:
66
-
67
- 1. for a single *IAM* server, specify the data set
68
- - *<APP_PREFIX>_IAM_SERVER* (required, one of *jusbr*, *keycloak*)
69
- - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed only if administrative duties are performed)
70
- - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed only if administrative duties are performed)
71
- - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
72
- - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
73
- - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
74
- - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (optional)
75
- - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (optional)
76
- - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (optional)
77
- - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (optional)
78
- - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (optional)
79
- - *<APP_PREFIX>_IAM_LOGIN_TIMEOUT* (optional, defaults to no timeout)
80
- - *<APP_PREFIX>_IAM_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
81
- - *<APP_PREFIX>_IAM_RECIPIENT_ATTR* (required)
82
- - *<APP_PREFIX>_IAM_URL_BASE* (required)
83
-
84
- 2. the parameters *PUBLIC_KEY*, *PK_EXPIRATION*, and *USERS* cannot be assigned values,
85
- as they are reserved for internal use
86
-
87
- 3. for multiple *IAM* servers, specify a comma-separated list of servers in
88
- *<APP_PREFIX>_IAM_SERVERS*, and for each server, specify the data set above,
89
- respectively replacing *_IAM_* with *_JUSBR_* or *_KEYCLOAK_*, for the servers listed above
90
-
91
- :return: the configuration data for the selected *IAM* servers
92
- """
93
- # initialize the return valiable
94
- result: dict[IamServer, dict[IamParam, Any]] = {}
95
-
96
- servers: list[IamServer] = []
97
- single_server: IamServer = env_get_enum(key=f"{APP_PREFIX}_IAM_SERVER",
98
- enum_class=IamServer)
99
- if single_server:
100
- default_setup: bool = True
101
- servers.append(single_server)
102
- else:
103
- default_setup: bool = False
104
- multi_servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_IAM_SERVERS",
105
- enum_class=IamServer)
106
- if multi_servers:
107
- servers.extend(multi_servers)
108
-
109
- for server in servers:
110
- if default_setup:
111
- prefix: str = "IAM"
112
- default_setup = False
113
- else:
114
- prefix: str = server
115
- result[server] = {
116
- IamParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
117
- IamParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_PWD"),
118
- IamParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
119
- IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
120
- IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
121
- IamParam.LOGIN_TIMEOUT: env_get_int(key=f"{APP_PREFIX}_{prefix}_CLIENT_TIMEOUT"),
122
- IamParam.ENDPOINT_CALLBACK: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_CALLBACK"),
123
- IamParam.ENDPOINT_LOGIN: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGIN"),
124
- IamParam.ENDPOINT_LOGOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGOUT"),
125
- IamParam.ENDPOINT_TOKEN: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_TOKEN"),
126
- IamParam.ENDPOINT_EXCHANGE: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE"),
127
- IamParam.PK_LIFETIME: env_get_str(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME"),
128
- IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
129
- IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_BASE")
130
- }
131
-
132
- return result
133
-
57
+ # The configuration parameters for the IAM servers are specified dynamically dynamically with *iam_setup()*
58
+ # Specifying configuration parameters with environment variables can be done in two ways:
59
+ #
60
+ # 1. for a single *IAM* server, specify the data set
61
+ # - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed if administrative duties are performed)
62
+ # - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed if administrative duties are performed)
63
+ # - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
64
+ # - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
65
+ # - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
66
+ # - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (required)
67
+ # - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (required)
68
+ # - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (required)
69
+ # - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (required)
70
+ # - *<APP_PREFIX>_IAM_ENDPOINT_PROVIDER* (optional, needed if requesting tokens to providers)
71
+ # - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (required)
72
+ # - *<APP_PREFIX>_IAM_LOGIN_TIMEOUT* (optional, defaults to no timeout)
73
+ # - *<APP_PREFIX>_IAM_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
74
+ # - *<APP_PREFIX>_IAM_RECIPIENT_ATTR* (required)
75
+ # - *<APP_PREFIX>_IAM_URL_BASE* (required)
76
+ #
77
+ # 2. for multiple *IAM* servers, specify the data set above for each server,
78
+ # respectively replacing *IAM* with a name in *IamServer* (currently, *JUSBR* and *KEYCLOAK* are supported).
79
+ #
80
+ # 3. the parameters *PUBLIC_KEY*, *PK_EXPIRATION*, and *USERS* cannot be assigned values,
81
+ # as they are reserved for internal use
134
82
 
135
83
  # registry structure:
136
84
  # { <IamServer>:
137
85
  # {
138
86
  # "base-url": <str>,
87
+ # "admin-id": <str>,
88
+ # "admin-secret": <str>,
139
89
  # "client-id": <str>,
140
90
  # "client-secret": <str>,
141
91
  # "client-realm": <str,
@@ -162,7 +112,7 @@ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
162
112
  # },
163
113
  # ...
164
114
  # }
165
- _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
115
+ _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = {}
166
116
 
167
117
 
168
118
  # the lock protecting the data in '_IAM_SERVERS'
@@ -186,7 +136,7 @@ def _iam_server_from_endpoint(endpoint: str,
186
136
 
187
137
  for iam_server in _IAM_SERVERS:
188
138
  if endpoint.startswith(iam_server):
189
- result = IamServer.JUSRBR
139
+ result = iam_server
190
140
  break
191
141
 
192
142
  if not result:
@@ -408,21 +358,15 @@ def _get_iam_registry(iam_server: IamServer,
408
358
  :param logger: optional logger
409
359
  :return: the registry associated with *iam_server*, or *None* if the server is unknown
410
360
  """
411
- # declare the return variable
412
- result: dict[str, Any] | None
413
-
414
- match iam_server:
415
- case IamServer.JUSRBR:
416
- result = _IAM_SERVERS[IamServer.JUSRBR]
417
- case IamServer.KEYCLOAK:
418
- result = _IAM_SERVERS[IamServer.KEYCLOAK]
419
- case _:
420
- result = None
421
- msg = f"Unknown IAM server '{iam_server}'"
422
- if logger:
423
- logger.error(msg=msg)
424
- if isinstance(errors, list):
425
- errors.append(msg)
361
+ # assign the return variable
362
+ result: dict[str, Any] = _IAM_SERVERS.get(iam_server)
363
+
364
+ if not result:
365
+ msg = f"Unknown IAM server '{iam_server}'"
366
+ if logger:
367
+ logger.error(msg=msg)
368
+ if isinstance(errors, list):
369
+ errors.append(msg)
426
370
 
427
371
  return result
428
372
 
pypomes_iam/iam_pomes.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from flask import Flask
2
2
  from logging import Logger
3
+ from pypomes_core import APP_PREFIX, env_get_int, env_get_str
3
4
  from typing import Any
4
5
 
5
6
  from .iam_common import (
@@ -16,17 +17,17 @@ def iam_setup(flask_app: Flask,
16
17
  base_url: str,
17
18
  client_id: str,
18
19
  client_realm: str,
20
+ client_secret: str | None,
19
21
  recipient_attribute: str,
20
- client_secret: str = None,
21
- login_timeout: int = None,
22
22
  admin_id: str = None,
23
23
  admin_secret: str = None,
24
+ login_timeout: int = None,
24
25
  public_key_lifetime: int = None,
25
26
  callback_endpoint: str = None,
27
+ exchange_endpoint: str = None,
26
28
  login_endpoint: str = None,
27
29
  logout_endpoint: str = None,
28
- token_endpoint: str = None,
29
- exchange_endpoint: str = None) -> None:
30
+ token_endpoint: str = None) -> None:
30
31
  """
31
32
  Establish the provided parameters for configuring the *IAM* server *iam_server*.
32
33
 
@@ -39,21 +40,21 @@ def iam_setup(flask_app: Flask,
39
40
  the first time it is needed.
40
41
 
41
42
  :param flask_app: the Flask application
42
- :param iam_server: identifies the supported *IAM* server (*jusbr* or *keycloak*)
43
+ :param iam_server: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
43
44
  :param base_url: base URL to request services
44
45
  :param client_id: the client's identification with the *IAM* server
45
46
  :param client_realm: the client realm
46
- :param recipient_attribute: attribute in the token's payload holding the token's subject
47
47
  :param client_secret: the client's password with the *IAM* server
48
- :param login_timeout: timeout for login authentication (in seconds,defaults to no timeout)
48
+ :param recipient_attribute: attribute in the token's payload holding the token's subject
49
49
  :param admin_id: identifies the realm administrator
50
50
  :param admin_secret: password for the realm administrator
51
+ :param login_timeout: timeout for login authentication (in seconds,defaults to no timeout)
51
52
  :param public_key_lifetime: how long to use *IAM* server's public key, before refreshing it (in seconds)
52
53
  :param callback_endpoint: endpoint for the callback from the front end
54
+ :param exchange_endpoint: endpoint for requesting token exchange
53
55
  :param login_endpoint: endpoint for redirecting user to the *IAM* server's login page
54
56
  :param logout_endpoint: endpoint for terminating user access
55
57
  :param token_endpoint: endpoint for retrieving authentication token
56
- :param exchange_endpoint: endpoint for requesting token exchange
57
58
  """
58
59
 
59
60
  # configure the Keycloak registry
@@ -63,18 +64,15 @@ def iam_setup(flask_app: Flask,
63
64
  IamParam.CLIENT_ID: client_id,
64
65
  IamParam.CLIENT_REALM: client_realm,
65
66
  IamParam.CLIENT_SECRET: client_secret,
66
- IamParam.LOGIN_TIMEOUT: login_timeout,
67
67
  IamParam.RECIPIENT_ATTR: recipient_attribute,
68
+ IamParam.ADMIN_ID: admin_id,
69
+ IamParam.ADMIN_SECRET: admin_secret,
70
+ IamParam.LOGIN_TIMEOUT: login_timeout,
71
+ IamParam.PK_LIFETIME: public_key_lifetime,
68
72
  IamParam.PK_EXPIRATION: 0,
69
73
  IamParam.PUBLIC_KEY: None,
70
74
  IamParam.USERS: {}
71
75
  }
72
- if admin_id and admin_secret:
73
- IamParam.ADMIN_ID = admin_id
74
- IamParam.ADMIN_SECRET = admin_secret
75
-
76
- if public_key_lifetime:
77
- IamParam.PK_LIFETIME = public_key_lifetime
78
76
 
79
77
  # establish the endpoints
80
78
  if callback_endpoint:
@@ -104,6 +102,34 @@ def iam_setup(flask_app: Flask,
104
102
  methods=["POST"])
105
103
 
106
104
 
105
+ def iam_get_env_parameters(iam_prefix: str = None) -> dict[str, Any]:
106
+ """
107
+ Retrieve the set parameters for a *IAM* server from the environment.
108
+
109
+ the parameters are returned ready to be used as a '**kwargs' parameter set in a call to *iam_setup()*,
110
+ and sorted in the order appropriate to use them instead with a '*args' parameter set.
111
+
112
+ :param iam_prefix: the prefix classifying the parameters
113
+ :return: the sorted parameters classified by *prefix*
114
+ """
115
+ return {
116
+ "base_url": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_URL_AUTH_BASE"),
117
+ "client_id": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_ID"),
118
+ "client_realm": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_REALM"),
119
+ "client_secret": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_SECRET"),
120
+ "recipient_attribute": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_RECIPIENT_ATTR"),
121
+ "admin_id": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ADMIN_ID"),
122
+ "admin_secret": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ADMIN_SECRET"),
123
+ "login_timeout": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_LOGIN_TIMEOUT"),
124
+ "public_key_lifetime": env_get_int(key=f"{APP_PREFIX}_{iam_prefix}_PUBLIC_KEY_LIFETIME"),
125
+ "callback_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_CALLBACK"),
126
+ "exchange_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_EXCHANGE"),
127
+ "login_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_LOGIN"),
128
+ "logout_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_LOGOUT"),
129
+ "token_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_TOKEN")
130
+ }
131
+
132
+
107
133
  def iam_get_token(iam_server: IamServer,
108
134
  user_id: str,
109
135
  errors: list[str] = None,
@@ -168,7 +168,7 @@ def service_login() -> Response:
168
168
  # methods=["GET"])
169
169
  def service_logout() -> Response:
170
170
  """
171
- Entry point for the JusBR logout service.
171
+ Entry point for the IAM server's logout service.
172
172
 
173
173
  The user is identified by the attribute *user-id* or "login", provided as a request parameter.
174
174
  If successful, remove all data relating to the user from the *IAM* server's registry.
@@ -214,7 +214,7 @@ def service_logout() -> Response:
214
214
  # methods=["POST"])
215
215
  def service_callback() -> Response:
216
216
  """
217
- Entry point for the callback from JusBR on authentication operation.
217
+ Entry point for the callback from the IAM server on authentication operation.
218
218
 
219
219
  This callback is invoked from a front-end application after a successful login at the
220
220
  *IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
@@ -338,13 +338,10 @@ def service_exchange() -> Response:
338
338
  If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
339
339
  Otherwise, *errors* will contain the appropriate error message.
340
340
 
341
- On success, the typical *Response* returned will contain the following attributes:
341
+ On success, the returned *Response* will contain the following JSON:
342
342
  {
343
- "token_type": "Bearer",
344
- "access_token": <str>,
345
- "expires_in": <number-of-seconds>,
346
- "refresh_token": <str>,
347
- "refesh_expires_in": <number-of-seconds>
343
+ "user-id": <reference-user-identification>,
344
+ "access-token": <token>
348
345
  }
349
346
 
350
347
  :return: *Response* containing the token data, or *BAD REQUEST*
@@ -360,10 +357,10 @@ def service_exchange() -> Response:
360
357
  errors=errors,
361
358
  logger=__IAM_LOGGER)
362
359
  # exchange the token
363
- token_data: dict[str, Any] | None = None
360
+ token_info: tuple[str, str] | None = None
364
361
  if iam_server:
365
362
  errors: list[str] = []
366
- token_data = action_exchange(iam_server=iam_server,
363
+ token_info = action_exchange(iam_server=iam_server,
367
364
  args=request.args,
368
365
  errors=errors,
369
366
  logger=__IAM_LOGGER)
@@ -372,7 +369,8 @@ def service_exchange() -> Response:
372
369
  result = Response(response="; ".join(errors),
373
370
  status=400)
374
371
  else:
375
- result = jsonify(token_data)
372
+ result = jsonify({"user-id": token_info[0],
373
+ "access-token": token_info[1]})
376
374
 
377
375
  # log the response
378
376
  if __IAM_LOGGER:
@@ -106,76 +106,137 @@ def provider_get_token(provider_id: str,
106
106
  # initialize the return variable
107
107
  result: str | None = None
108
108
 
109
- err_msg: str | None = None
110
109
  with _provider_lock:
111
110
  provider: dict[str, Any] = _provider_registry.get(provider_id)
112
111
  if provider:
113
- now: float = datetime.now(tz=TZ_LOCAL).timestamp()
114
- if now > provider.get(ProviderParam.ACCESS_EXPIRATION):
115
- user: str = provider.get(ProviderParam.USER)
116
- pwd: str = provider.get(ProviderParam.PWD)
117
- headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
118
- body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
119
- custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
120
- if custom_auth:
121
- body_data[custom_auth[0]] = user
122
- body_data[custom_auth[1]] = pwd
123
- else:
124
- enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
125
- headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
112
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
113
+ if now < provider.get(ProviderParam.ACCESS_EXPIRATION):
114
+ # retrieve the stored access token
115
+ result = provider.get(ProviderParam.ACCESS_TOKEN)
116
+ else:
117
+ # access token has expired
118
+ header_data: dict[str, str] | None = None
119
+ body_data: dict[str, str] | None = None
126
120
  url: str = provider.get(ProviderParam.URL)
127
- if logger:
128
- logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
129
- ensure_ascii=False)}")
130
- try:
131
- # typical return on a token request:
132
- # {
133
- # "token_type": "Bearer",
134
- # "access_token": <str>,
135
- # "expires_in": <number-of-seconds>,
136
- # optional data:
137
- # "refresh_token": <str>,
138
- # "refresh_expires_in": <number-of-seconds>
139
- # }
140
- response: requests.Response = requests.post(url=url,
141
- data=body_data,
142
- headers=headers_data,
143
- timeout=None)
144
- if response.status_code < 200 or response.status_code >= 300:
145
- # request resulted in error, report the problem
146
- err_msg = (f"POST failure, "
147
- f"status {response.status_code}, reason {response.reason}")
121
+ refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
122
+ if refresh_token:
123
+ # refresh token exists
124
+ refresh_expiration: int = provider.get(ProviderParam.REFRESH_EXPIRATION)
125
+ if now < refresh_expiration:
126
+ # refresh token has not expired
127
+ header_data: dict[str, str] = {
128
+ "Content-Type": "application/json"
129
+ }
130
+ body_data: dict[str, str] = {
131
+ "grant_type": "refresh_token",
132
+ "refresh_token": refresh_token
133
+ }
134
+ if not body_data:
135
+ # refresh token does not exist or has expired
136
+ user: str = provider.get(ProviderParam.USER)
137
+ pwd: str = provider.get(ProviderParam.PWD)
138
+ headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
139
+ body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
140
+ custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
141
+ if custom_auth:
142
+ body_data[custom_auth[0]] = user
143
+ body_data[custom_auth[1]] = pwd
148
144
  else:
149
- # request succeeded
150
- if logger:
151
- logger.debug(msg=f"POST success, status {response.status_code}")
152
- reply: dict[str, Any] = response.json()
153
- provider[ProviderParam.ACCESS_TOKEN] = reply.get("access_token")
154
- provider[ProviderParam.ACCESS_EXPIRATION] = now + int(reply.get("expires_in"))
155
- if reply.get(ProviderParam.REFRESH_TOKEN):
156
- provider[ProviderParam.REFRESH_TOKEN] = reply["refresh_token"]
157
- if reply.get("refresh_expires_in"):
158
- provider[ProviderParam.REFRESH_EXPIRATION] = now + int(reply.get("refresh_expires_in"))
159
- else:
160
- provider[ProviderParam.REFRESH_EXPIRATION] = sys.maxsize
161
- if logger:
162
- logger.debug(msg=f"POST {url}: status {response.status_code}")
163
- except Exception as e:
164
- # the operation raised an exception
165
- err_msg = exc_format(exc=e,
166
- exc_info=sys.exc_info())
167
- err_msg = f"POST error, '{err_msg}'"
168
- else:
169
- err_msg: str = f"Provider '{provider_id}' not registered"
170
-
171
- if err_msg:
172
- if isinstance(errors, list):
173
- errors.append(err_msg)
174
- if logger:
175
- logger.error(msg=err_msg)
176
- else:
177
- result = provider.get(ProviderParam.ACCESS_TOKEN)
145
+ enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
146
+ headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
147
+
148
+ # obtain the token
149
+ token_data: dict[str, Any] = __post_for_token(url=url,
150
+ header_data=header_data,
151
+ body_data=body_data,
152
+ errors=errors,
153
+ logger=logger)
154
+ if token_data:
155
+ result = token_data.get("access_token")
156
+ provider[ProviderParam.ACCESS_TOKEN] = result
157
+ provider[ProviderParam.ACCESS_EXPIRATION] = now + token_data.get("expires_in")
158
+ refresh_token = token_data.get("refresh_token")
159
+ if refresh_token:
160
+ provider[ProviderParam.REFRESH_TOKEN] = refresh_token
161
+ refresh_exp: int = token_data.get("refresh_expires_in")
162
+ provider[ProviderParam.REFRESH_EXPIRATION] = (now + refresh_exp) \
163
+ if refresh_exp else sys.maxsize
164
+
165
+ elif logger or isinstance(errors, list):
166
+ msg: str = f"Unknown provider '{provider_id}'"
167
+ if logger:
168
+ logger.error(msg=msg)
169
+ if isinstance(errors, list):
170
+ errors.append(msg)
178
171
 
179
172
  return result
180
173
 
181
174
 
175
+ def __post_for_token(url: str,
176
+ header_data: dict[str, str],
177
+ body_data: dict[str, Any],
178
+ errors: list[str] | None,
179
+ logger: Logger | None) -> dict[str, Any] | None:
180
+ """
181
+ Send a *POST* request to *url* and return the token data obtained.
182
+
183
+ Token acquisition and token refresh are the two types of requests contemplated herein.
184
+ For the former, *header_data* and *body_data* will have contents customized to the specific provider,
185
+ whereas the latter's *body_data* will contain these two attributes:
186
+ - "grant_type": "refresh_token"
187
+ - "refresh_token": <current-refresh-token>
188
+
189
+ The typical data set returned contains the following attributes:
190
+ {
191
+ "token_type": "Bearer",
192
+ "access_token": <str>,
193
+ "expires_in": <number-of-seconds>,
194
+ "refresh_token": <str>,
195
+ "refesh_expires_in": <number-of-seconds>
196
+ }
197
+
198
+ :param url: the target URL
199
+ :param header_data: the data to send in the header of the request
200
+ :param body_data: the data to send in the body of the request
201
+ :param errors: incidental errors
202
+ :param logger: optional logger
203
+ :return: the token data, or *None* if error
204
+ """
205
+ # initialize the return variable
206
+ result: dict[str, Any] | None = None
207
+
208
+ # log the POST
209
+ if logger:
210
+ logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
211
+ ensure_ascii=False)}")
212
+ try:
213
+ response: requests.Response = requests.post(url=url,
214
+ data=body_data,
215
+ headers=header_data,
216
+ timeout=None)
217
+ if response.status_code == 200:
218
+ # request succeeded
219
+ result = response.json()
220
+ if logger:
221
+ logger.debug(msg=f"POST success, status {response.status_code}")
222
+ else:
223
+ # request failed, report the problem
224
+ msg: str = (f"POST failure, "
225
+ f"status {response.status_code}, reason {response.reason}")
226
+ if hasattr(response, "content") and response.content:
227
+ msg += f", content '{response.content}'"
228
+ if logger:
229
+ logger.error(msg=msg)
230
+ if isinstance(errors, list):
231
+ errors.append(msg)
232
+ except Exception as e:
233
+ # the operation raised an exception
234
+ err_msg = exc_format(exc=e,
235
+ exc_info=sys.exc_info())
236
+ msg: str = f"POST error, {err_msg}"
237
+ if logger:
238
+ logger.debug(msg=msg)
239
+ if isinstance(errors, list):
240
+ errors.append(msg)
241
+
242
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.5.8
3
+ Version: 0.7.1
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
@@ -10,7 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
- Requires-Dist: cachetools>=6.2.1
14
13
  Requires-Dist: flask>=3.1.2
15
14
  Requires-Dist: pyjwt>=2.10.1
16
15
  Requires-Dist: pypomes-core>=2.8.1
@@ -0,0 +1,11 @@
1
+ pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
2
+ pypomes_iam/iam_actions.py,sha256=HDCQ91AphDt9__K9Pu2AqCIwN0fh1K9Oovgpjbp9ZMQ,42778
3
+ pypomes_iam/iam_common.py,sha256=ki_-m6fqJqUbGjgTD41r9zaE-FOXgA_c_tLisIYYTfU,15457
4
+ pypomes_iam/iam_pomes.py,sha256=_kLnrZG25XhJsIv3wqDl_2sIJ2ho_2TIMKrPCyPmA7Q,7362
5
+ pypomes_iam/iam_services.py,sha256=AzrZux2Pt_FoCNcTcXfWphHb587vB3WIbKYG7RFf5zE,15821
6
+ pypomes_iam/provider_pomes.py,sha256=3mMj5LQs53YEINUEOfFBAxOwOP3aOR_szlE4daEBLK0,10523
7
+ pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
8
+ pypomes_iam-0.7.1.dist-info/METADATA,sha256=--KMHaPTqpjvClqPS8B2xaRUsn98DJzVLpzosXk2yHk,661
9
+ pypomes_iam-0.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ pypomes_iam-0.7.1.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
+ pypomes_iam-0.7.1.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- pypomes_iam/__init__.py,sha256=f-2W_zrCmXExubJPExQrhAwGpiQCmybEC_wguRYFHsw,994
2
- pypomes_iam/iam_actions.py,sha256=Bmd8rBg3948Afsg10B6B1ZrFY4wYtbxi55rX4Rlqiyk,39167
3
- pypomes_iam/iam_common.py,sha256=I-HtwpvrhByTbOoSQrMktjpbYgeIPlYM1YC6wkFUhI4,18251
4
- pypomes_iam/iam_pomes.py,sha256=BetEVGv41wkcP9E1wRvYiQgmJElDXH4Iz8qgf7iH6X0,5617
5
- pypomes_iam/iam_services.py,sha256=IkCjrKDX1Ix7BiHh-BL3VKz5xogcNC8prXkHyJzQoZ8,15862
6
- pypomes_iam/provider_pomes.py,sha256=N0nL9_hgHmAjG9JKFoXC33zk8b1ckPG1veu1jTp-2JE,8045
7
- pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
8
- pypomes_iam-0.5.8.dist-info/METADATA,sha256=Q60cQU69Gbay_IjFewESe9P4O4Z6mQ5tz_tYvw7yIMM,694
9
- pypomes_iam-0.5.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- pypomes_iam-0.5.8.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
- pypomes_iam-0.5.8.dist-info/RECORD,,