pypomes-iam 0.7.0__tar.gz → 0.7.9__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.7.0
3
+ Version: 0.7.9
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
@@ -12,6 +12,6 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
13
  Requires-Dist: flask>=3.1.2
14
14
  Requires-Dist: pyjwt>=2.10.1
15
- Requires-Dist: pypomes-core>=2.8.1
15
+ Requires-Dist: pypomes-core>=2.8.4
16
16
  Requires-Dist: pypomes-crypto>=0.4.8
17
17
  Requires-Dist: requests>=2.32.5
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_iam"
9
- version = "0.7.0"
9
+ version = "0.7.9"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -21,7 +21,7 @@ classifiers = [
21
21
  dependencies = [
22
22
  "Flask>=3.1.2",
23
23
  "PyJWT>=2.10.1",
24
- "pypomes-core>=2.8.1",
24
+ "pypomes-core>=2.8.4",
25
25
  "pypomes-crypto>=0.4.8",
26
26
  "requests>=2.32.5"
27
27
  ]
@@ -0,0 +1,49 @@
1
+ from .iam_actions import (
2
+ iam_callback, iam_exchange,
3
+ iam_login, iam_logout, iam_get_token
4
+ )
5
+ from .iam_common import (
6
+ IamServer, IamParam
7
+ )
8
+ from .iam_pomes import (
9
+ iam_setup_server, iam_setup_endpoints
10
+ )
11
+ from .iam_services import (
12
+ jwt_required, iam_setup_logger,
13
+ service_setup_server, service_get_token,
14
+ service_login, service_logout,
15
+ service_callback, service_exchange,
16
+ service_callback_exchange
17
+ )
18
+ from .provider_pomes import (
19
+ service_get_token, provider_get_token,
20
+ provider_setup_endpoint, provider_setup_logger, provider_setup_server
21
+ )
22
+ from .token_pomes import (
23
+ token_get_claims, token_get_values, token_validate
24
+ )
25
+
26
+ __all__ = [
27
+ # iam_actions
28
+ "iam_callback", "iam_exchange",
29
+ "iam_login", "iam_logout", "iam_get_token",
30
+ # iam_commons
31
+ "IamServer", "IamParam",
32
+ # iam_pomes
33
+ "iam_setup_server", "iam_setup_endpoints",
34
+ # iam_services
35
+ "jwt_required", "iam_setup_logger",
36
+ "service_setup_server", "service_get_token",
37
+ "service_login", "service_logout",
38
+ "service_callback", "service_exchange",
39
+ "service_callback_exchange",
40
+ # provider_pomes
41
+ "provider_setup_server", "provider_get_token",
42
+ "provider_setup_endpoint", "provider_setup_logger", "provider_setup_server",
43
+ # token_pomes
44
+ "token_get_claims", "token_get_values", "token_validate"
45
+ ]
46
+
47
+ from importlib.metadata import version
48
+ __version__ = version("pypomes_iam")
49
+ __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit())
@@ -13,24 +13,30 @@ from .iam_common import (
13
13
  _get_iam_users, _get_iam_registry, _get_public_key,
14
14
  _get_login_timeout, _get_user_data, _iam_server_from_issuer
15
15
  )
16
- from .token_pomes import token_get_claims, token_validate
16
+ from .token_pomes import token_get_values, token_validate
17
17
 
18
18
 
19
- def action_login(iam_server: IamServer,
20
- args: dict[str, Any],
21
- errors: list[str] = None,
22
- logger: Logger = None) -> str:
19
+ def iam_login(iam_server: IamServer,
20
+ args: dict[str, Any],
21
+ errors: list[str] = None,
22
+ logger: Logger = None) -> str:
23
23
  """
24
24
  Build the URL for redirecting the request to *iam_server*'s authentication page.
25
25
 
26
26
  These are the expected attributes in *args*:
27
27
  - user-id: optional, identifies the reference user (alias: 'login')
28
28
  - redirect-uri: a parameter to be added to the query part of the returned URL
29
+ -target-idp: optionally, identify a target identity provider for the login operation
29
30
 
30
31
  If provided, the user identification will be validated against the authorization data
31
32
  returned by *iam_server* upon login. On success, the appropriate URL for invoking
32
33
  the IAM server's authentication page is returned.
33
34
 
35
+ if 'target_idp' is provided as an attribute in *args*, the OAuth2 state variable included in the
36
+ returned URL will be postfixed with the string *#idp=<target-idp>*. At the callback endpoint,
37
+ this instructs *iam_server* to act as a broker, forwading the authentication process to the
38
+ *IAM* server *target-idp*.
39
+
34
40
  :param iam_server: the reference registered *IAM* server
35
41
  :param args: the arguments passed when requesting the service
36
42
  :param errors: incidental error messages
@@ -43,9 +49,14 @@ def action_login(iam_server: IamServer,
43
49
  # obtain the optional user's identification
44
50
  user_id: str = args.get("user-id") or args.get("login")
45
51
 
52
+ # obtain the optional target identity provider
53
+ target_idp: str = args.get("target-idp")
54
+
46
55
  # build the user data
47
56
  # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
48
57
  oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
58
+ if target_idp:
59
+ oauth_state += f"#idp={target_idp}"
49
60
 
50
61
  with _iam_lock:
51
62
  # retrieve the user data from the IAM server's registry
@@ -75,13 +86,17 @@ def action_login(iam_server: IamServer,
75
86
  f"&client_id={registry[IamParam.CLIENT_ID]}"
76
87
  f"&redirect_uri={redirect_uri}"
77
88
  f"&state={oauth_state}")
89
+ if target_idp:
90
+ # HAZARD: the name 'kc_idp_hint' is Keycloak-specific
91
+ result += f"&kc_idp_hint={target_idp}"
92
+
78
93
  return result
79
94
 
80
95
 
81
- def action_logout(iam_server: IamServer,
82
- args: dict[str, Any],
83
- errors: list[str] = None,
84
- logger: Logger = None) -> None:
96
+ def iam_logout(iam_server: IamServer,
97
+ args: dict[str, Any],
98
+ errors: list[str] = None,
99
+ logger: Logger = None) -> None:
85
100
  """
86
101
  Logout the user, by removing all data associating it from *iam_server*'s registry.
87
102
 
@@ -109,23 +124,29 @@ def action_logout(iam_server: IamServer,
109
124
  logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
110
125
 
111
126
 
112
- def action_token(iam_server: IamServer,
113
- args: dict[str, Any],
114
- errors: list[str] = None,
115
- logger: Logger = None) -> str:
127
+ def iam_get_token(iam_server: IamServer,
128
+ args: dict[str, Any],
129
+ errors: list[str] = None,
130
+ logger: Logger = None) -> dict[str, str]:
116
131
  """
117
132
  Retrieve the authentication token for the user, from *iam_server*.
118
133
 
119
134
  The user is identified by the attribute *user-id* or *login*, provided in *args*.
120
135
 
136
+ On success, the returned *dict* will contain the following JSON:
137
+ {
138
+ "access-token": <token>,
139
+ "user-id": <user-identification
140
+ }
141
+
121
142
  :param iam_server: the reference registered *IAM* server
122
143
  :param args: the arguments passed when requesting the service
123
144
  :param errors: incidental error messages
124
145
  :param logger: optional logger
125
- :return: the token for user indicated, or *None* if error
146
+ :return: the user identification and token issued, or *None* if error
126
147
  """
127
148
  # initialize the return variable
128
- result: str | None = None
149
+ result: dict[str, str] | None = None
129
150
 
130
151
  # obtain the user's identification
131
152
  user_id: str = args.get("user-id") or args.get("login")
@@ -144,7 +165,10 @@ def action_token(iam_server: IamServer,
144
165
  access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
145
166
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
146
167
  if now < access_expiration:
147
- result = token
168
+ result = {
169
+ "access-token": token,
170
+ "user-id": user_id
171
+ }
148
172
  else:
149
173
  # access token has expired
150
174
  refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
@@ -172,7 +196,10 @@ def action_token(iam_server: IamServer,
172
196
  now=now,
173
197
  errors=errors,
174
198
  logger=logger)
175
- result = token_info[1]
199
+ result = {
200
+ "access-token": token_info[1],
201
+ "user-id": user_id
202
+ }
176
203
  else:
177
204
  # refresh token is no longer valid
178
205
  user_data[UserParam.REFRESH_TOKEN] = None
@@ -200,10 +227,10 @@ def action_token(iam_server: IamServer,
200
227
  return result
201
228
 
202
229
 
203
- def action_callback(iam_server: IamServer,
204
- args: dict[str, Any],
205
- errors: list[str] = None,
206
- logger: Logger = None) -> tuple[str, str] | None:
230
+ def iam_callback(iam_server: IamServer,
231
+ args: dict[str, Any],
232
+ errors: list[str] = None,
233
+ logger: Logger = None) -> tuple[str, str] | None:
207
234
  """
208
235
  Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
209
236
 
@@ -211,6 +238,10 @@ def action_callback(iam_server: IamServer,
211
238
  - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
212
239
  - *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
213
240
 
241
+ if *state* is postfixed with the string *#idp=<target-idp>*, this instructs *iam_server* to act as a broker,
242
+ forwarding the authentication process to the *IAM* server *target-idp*. This mechanism fully dispenses with
243
+ the flows 'callback-exchange', and 'callback' followed by 'exchange'.
244
+
214
245
  :param iam_server: the reference registered *IAM* server
215
246
  :param args: the arguments passed when requesting the service
216
247
  :param errors: incidental errors
@@ -240,6 +271,10 @@ def action_callback(iam_server: IamServer,
240
271
  if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
241
272
  errors.append("Operation timeout")
242
273
  else:
274
+ pos: int = oauth_state.rfind("#idp=")
275
+ target_idp: str = oauth_state[pos+4:] if pos > 0 else None
276
+ target_iam = IamServer(target_idp) if target_idp in IamServer else None
277
+ target_data: dict[str, Any] = user_data.copy() if target_iam else None
243
278
  users.pop(oauth_state)
244
279
  code: str = args.get("code")
245
280
  header_data: dict[str, str] = {
@@ -264,6 +299,33 @@ def action_callback(iam_server: IamServer,
264
299
  now=now,
265
300
  errors=errors,
266
301
  logger=logger)
302
+ if target_iam:
303
+ if logger:
304
+ logger.debug(msg=f"Requesting to IAM server '{iam_server}' "
305
+ f"the token issued by '{target_iam}' ")
306
+ registry: dict[str, Any] = _get_iam_registry(iam_server,
307
+ errors=errors,
308
+ logger=logger)
309
+ url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
310
+ url += f"/broker/{target_idp}/token"
311
+ header_data: dict[str, str] = {
312
+ "Authorization": f"Bearer {result[1]}",
313
+ "Content-Type": "application/json"
314
+ }
315
+ token_data = __get_for_data(url=url,
316
+ header_data=header_data,
317
+ params=None,
318
+ errors=errors,
319
+ logger=logger)
320
+ if not errors:
321
+ token_info: tuple[str, str] = __validate_and_store(iam_server=target_iam,
322
+ user_data=target_data,
323
+ token_data=token_data,
324
+ now=now,
325
+ errors=errors,
326
+ logger=logger)
327
+ if token_info and logger:
328
+ logger.debug(msg=f"Token obtained: {json.dumps(obj=token_info)}")
267
329
  else:
268
330
  msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
269
331
  if logger:
@@ -274,10 +336,10 @@ def action_callback(iam_server: IamServer,
274
336
  return result
275
337
 
276
338
 
277
- def action_exchange(iam_server: IamServer,
278
- args: dict[str, Any],
279
- errors: list[str] = None,
280
- logger: Logger = None) -> dict[str, Any]:
339
+ def iam_exchange(iam_server: IamServer,
340
+ args: dict[str, Any],
341
+ errors: list[str] = None,
342
+ logger: Logger = None) -> tuple[str, str]:
281
343
  """
282
344
  Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
283
345
 
@@ -298,22 +360,20 @@ def action_exchange(iam_server: IamServer,
298
360
  :param args: the arguments passed when requesting the service
299
361
  :param errors: incidental errors
300
362
  :param logger: optional logger
301
- :return: the data for the new token, or *None* if error
363
+ :return: a tuple containing the reference user identification and the token obtained, or *None* if error
302
364
  """
303
365
  # initialize the return variable
304
- result: dict[str, Any] | None = None
366
+ result: tuple[str, str] | None = None
305
367
 
306
368
  # obtain the user's identification
307
369
  user_id: str = args.get("user-id") or args.get("login")
308
370
 
309
371
  # obtain the token to be exchanged
310
372
  token: str = args.get("access-token") if user_id else None
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"],
373
+ token_issuer: tuple[str] = token_get_values(token=token,
374
+ keys=("iss",),
315
375
  errors=errors,
316
- logger=logger) if token_claims else None
376
+ logger=logger)
317
377
  if not errors:
318
378
  # HAZARD: only 'IAM_KEYCLOAK' is currently supported
319
379
  with _iam_lock:
@@ -326,6 +386,7 @@ def action_exchange(iam_server: IamServer,
326
386
  __assert_link(iam_server=iam_server,
327
387
  user_id=user_id,
328
388
  token=token,
389
+ token_issuer=token_issuer[0],
329
390
  errors=errors,
330
391
  logger=logger)
331
392
  if not errors:
@@ -371,6 +432,7 @@ def action_exchange(iam_server: IamServer,
371
432
  def __assert_link(iam_server: IamServer,
372
433
  user_id: str,
373
434
  token: str,
435
+ token_issuer: str,
374
436
  errors: list[str] | None,
375
437
  logger: Logger | None) -> None:
376
438
  """
@@ -398,7 +460,7 @@ def __assert_link(iam_server: IamServer,
398
460
  # obtain the internal user identification for 'user_id'
399
461
  if logger:
400
462
  logger.debug(msg="Obtaining internal identification "
401
- f"for user {user_id} in IAM server {iam_server}")
463
+ f"for user '{user_id}' in IAM server '{iam_server}'")
402
464
  url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
403
465
  header_data: dict[str, str] = {
404
466
  "Authorization": f"Bearer {admin_token}",
@@ -414,12 +476,12 @@ def __assert_link(iam_server: IamServer,
414
476
  errors=errors,
415
477
  logger=logger)
416
478
  if users:
417
- # verify whether the 'oidc' protocol is referred to in an
418
- # association between 'user_id' and the internal user identification
479
+ # verify whether the IAM server that issued the token is a federated identity provider
480
+ # in the associations between 'user_id' and the internal user identification
419
481
  internal_id: str = users[0].get("id")
420
482
  if logger:
421
- logger.debug(msg="Obtaining the providers federated with "
422
- f"IAM server '{iam_server}' for internal identification '{internal_id}'")
483
+ logger.debug(msg="Obtaining the providers federated in IAM server "
484
+ f"'{iam_server}', for internal identification '{internal_id}'")
423
485
  url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
424
486
  f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
425
487
  providers: list[dict[str, Any]] = __get_for_data(url=url,
@@ -428,13 +490,9 @@ def __assert_link(iam_server: IamServer,
428
490
  errors=errors,
429
491
  logger=logger)
430
492
  no_link: bool = True
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,
493
+ provider_name: str = _iam_server_from_issuer(issuer=token_issuer,
436
494
  errors=errors,
437
- logger=logger) if issuer else None
495
+ logger=logger)
438
496
  if provider_name:
439
497
  for provider in providers:
440
498
  if provider.get("identityProvider") == provider_name:
@@ -442,22 +500,22 @@ def __assert_link(iam_server: IamServer,
442
500
  break
443
501
  if no_link:
444
502
  # 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"]
503
+ token_sub: tuple[str] = token_get_values(token=token,
504
+ keys=("sub",),
505
+ errors=errors,
506
+ logger=logger)
507
+ if token_sub:
450
508
  if logger:
451
509
  logger.debug(msg="Creating an association between identifications "
452
- f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
510
+ f"'{user_id}' and '{token_sub}' in IAM server '{iam_server}'")
453
511
  url += f"/{provider_name}"
454
- body_data: dict[str, Any] = {
455
- "userId": token_sub,
512
+ json_data: dict[str, Any] = {
513
+ "userId": token_sub[0],
456
514
  "userName": user_id
457
515
  }
458
- __post_data(url=url,
516
+ __post_json(url=url,
459
517
  header_data=header_data,
460
- body_data=body_data,
518
+ json_data=json_data,
461
519
  errors=errors,
462
520
  logger=logger)
463
521
 
@@ -646,27 +704,27 @@ def __get_for_data(url: str,
646
704
  return result
647
705
 
648
706
 
649
- def __post_data(url: str,
707
+ def __post_json(url: str,
650
708
  header_data: dict[str, str],
651
- body_data: dict[str, Any],
709
+ json_data: dict[str, Any],
652
710
  errors: list[str] | None,
653
711
  logger: Logger | None) -> None:
654
712
  """
655
713
  Submit a *POST* request to *url*.
656
714
 
657
715
  :param header_data: the data to send in the header of the request
658
- :param body_data: the data to send in the body of the request
716
+ :param json_data: the JSON data to send in the request
659
717
  :param errors: incidental errors
660
718
  :param logger: optional logger
661
719
  """
662
720
  # log the POST
663
721
  if logger:
664
- logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
722
+ logger.debug(msg=f"POST {url}, {json.dumps(obj=json_data,
665
723
  ensure_ascii=False)}")
666
724
  try:
667
- response: requests.Response = requests.get(url=url,
668
- headers=header_data,
669
- data=body_data)
725
+ response: requests.Response = requests.post(url=url,
726
+ headers=header_data,
727
+ json=json_data)
670
728
  if response.status_code >= 400:
671
729
  # request failed, report the problem
672
730
  msg = f"POST failure, status {response.status_code}, reason {response.reason}"
@@ -839,6 +897,8 @@ def __validate_and_store(iam_server: IamServer,
839
897
  # initialize the return variable
840
898
  result: tuple[str, str] | None = None
841
899
 
900
+ if logger:
901
+ logger.debug(msg=f"Validating and storing the token")
842
902
  with _iam_lock:
843
903
  # retrieve the IAM server's registry
844
904
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
@@ -3,7 +3,10 @@ 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 TZ_LOCAL, exc_format
6
+ from pypomes_core import (
7
+ APP_PREFIX, TZ_LOCAL, exc_format,
8
+ env_get_str, env_get_int, env_get_enums
9
+ )
7
10
  from pypomes_crypto import crypto_jwk_convert
8
11
  from threading import RLock
9
12
  from typing import Any, Final
@@ -21,12 +24,14 @@ class IamParam(StrEnum):
21
24
  """
22
25
  Parameters for configuring *IAM* servers.
23
26
  """
27
+
24
28
  ADMIN_ID = "admin-id"
25
29
  ADMIN_SECRET = "admin-secret"
26
30
  CLIENT_ID = "client-id"
27
31
  CLIENT_REALM = "client-realm"
28
32
  CLIENT_SECRET = "client-secret"
29
33
  ENDPOINT_CALLBACK = "endpoint-callback"
34
+ ENDPOINT_CALLBACK_EXCHANGE = "endpoint-callback-exchange"
30
35
  ENDPOINT_LOGIN = "endpoint-login"
31
36
  ENDPOINT_LOGOUT = "endpoint_logout"
32
37
  ENDPOINT_TOKEN = "endpoint-token"
@@ -34,8 +39,9 @@ class IamParam(StrEnum):
34
39
  LOGIN_TIMEOUT = "login-timeout"
35
40
  PK_EXPIRATION = "pk-expiration"
36
41
  PK_LIFETIME = "pk-lifetime"
37
- PUBLIC_KEY = "public-key"
38
42
  RECIPIENT_ATTR = "recipient-attr"
43
+ # dynamic attributes
44
+ PUBLIC_KEY = "public-key"
39
45
  URL_BASE = "url-base"
40
46
  USERS = "users"
41
47
 
@@ -54,31 +60,65 @@ class UserParam(StrEnum):
54
60
  REDIRECT_URI = "redirect-uri"
55
61
 
56
62
 
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
63
+ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
64
+ """
65
+ Obtain the configuration data for select *IAM* servers.
66
+
67
+ The configuration parameters for the IAM servers are specified dynamically with environment variables,
68
+ or dynamically with calls to *iam_setup_server()*. Specifying configuration parameters with environment
69
+ variables can be done by following these steps:
70
+
71
+ 1. Specify *<APP_PREFIX>_IAM_SERVERS* with a list of names among the values found in *IamServer* class
72
+ (currently, *jusbr* and *keycloak* are supported), and the data set below for each server, where
73
+ *<IAM>* stands for the server's name as presented in *IamServer* class:
74
+ - *<APP_PREFIX>_<IAM>_ADMIN_ID* (optional, required if administrative duties are performed)
75
+ - *<APP_PREFIX>_<IAM>_ADMIN_PWD* (optional, required if administrative duties are performed)
76
+ - *<APP_PREFIX>_<IAM>_CLIENT_ID* (required)
77
+ - *<APP_PREFIX>_<IAM>_CLIENT_REALM* (required)
78
+ - *<APP_PREFIX>_<IAM>_CLIENT_SECRET* (required)
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. A group of special environment variables identifying endpoints for authentication services may be specified,
85
+ following the same scheme as presented in item *1* above. These are not part of the *IAM* server's setup,
86
+ but are meant to be used by function *iam_setup_endpoints()*, wherein the values in those variables
87
+ would represent default values for its parameters, respectively:
88
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_CALLBACK*
89
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_CALLBACK_EXCHANGE*
90
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_EXCHANGE*
91
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_LOGIN*
92
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_LOGOUT*
93
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_TOKEN*
94
+
95
+ :return: the configuration data for the select *IAM* servers.
96
+ """
97
+ # initialize the return variable
98
+ result: dict[IamServer, dict[IamParam, Any]] = {}
99
+
100
+ servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_IAM_SERVERS",
101
+ enum_class=IamServer) or []
102
+ for server in servers:
103
+ prefix = server.name
104
+ result[server] = {
105
+ IamParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
106
+ IamParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_SECRET"),
107
+ IamParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
108
+ IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
109
+ IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
110
+ IamParam.LOGIN_TIMEOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT"),
111
+ IamParam.PK_LIFETIME: env_get_int(key=f"{APP_PREFIX}_{prefix}_PUBLIC_KEY_LIFETIME"),
112
+ IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
113
+ IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH_BASE"),
114
+ # dynamically set
115
+ IamParam.PK_EXPIRATION: 0,
116
+ IamParam.PUBLIC_KEY: None,
117
+ IamParam.USERS: {}
118
+ }
119
+
120
+ return result
121
+
82
122
 
83
123
  # registry structure:
84
124
  # { <IamServer>:
@@ -91,6 +131,7 @@ class UserParam(StrEnum):
91
131
  # "client-realm": <str,
92
132
  # "client-timeout": <int>,
93
133
  # "recipient-attr": <str>,
134
+ # # dynamic attributes
94
135
  # "public-key": <str>,
95
136
  # "pk-lifetime": <int>,
96
137
  # "pk-expiration": <int>,
@@ -112,10 +153,10 @@ class UserParam(StrEnum):
112
153
  # },
113
154
  # ...
114
155
  # }
115
- _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = {}
156
+ _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
116
157
 
117
158
 
118
- # the lock protecting the data in '_IAM_SERVERS'
159
+ # the lock protecting the data in '_<IAM>_SERVERS'
119
160
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
120
161
  _iam_lock: Final[RLock] = RLock()
121
162