pypomes-iam 0.7.6__py3-none-any.whl → 0.8.5__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
@@ -1,37 +1,45 @@
1
1
  from .iam_actions import (
2
- action_callback, action_exchange,
3
- action_login, action_logout, action_token
2
+ iam_callback, iam_exchange,
3
+ iam_login, iam_logout, iam_get_token, iam_userinfo
4
4
  )
5
5
  from .iam_common import (
6
6
  IamServer, IamParam
7
7
  )
8
8
  from .iam_pomes import (
9
- iam_setup, iam_get_env_parameters, iam_get_token
9
+ iam_setup_server, iam_setup_endpoints
10
10
  )
11
11
  from .iam_services import (
12
- jwt_required, logger_register
12
+ jwt_required, iam_setup_logger,
13
+ service_setup_server, service_login, service_logout,
14
+ service_get_token, service_userinfo, service_callback,
15
+ service_exchange, service_callback_exchange
13
16
  )
14
17
  from .provider_pomes import (
15
- provider_register, provider_get_token
18
+ service_get_token, provider_get_token,
19
+ provider_setup_endpoint, provider_setup_logger, provider_setup_server
16
20
  )
17
21
  from .token_pomes import (
18
- token_validate
22
+ token_get_claims, token_get_values, token_validate
19
23
  )
20
24
 
21
25
  __all__ = [
22
26
  # iam_actions
23
- "action_callback", "action_exchange",
24
- "action_login", "action_logout", "action_token",
27
+ "iam_callback", "iam_exchange",
28
+ "iam_login", "iam_logout", "iam_get_token", "iam_userinfo",
25
29
  # iam_commons
26
30
  "IamServer", "IamParam",
27
31
  # iam_pomes
28
- "iam_setup", "iam_get_env_parameters", "iam_get_token",
32
+ "iam_setup_server", "iam_setup_endpoints",
29
33
  # iam_services
30
- "jwt_required", "logger_register",
34
+ "jwt_required", "iam_setup_logger",
35
+ "service_setup_server", "service_login", "service_logout",
36
+ "service_get_token", "service_userinfo", "service_callback",
37
+ "service_exchange", "service_callback_exchange",
31
38
  # provider_pomes
32
- "provider_register", "provider_get_token",
39
+ "provider_setup_server", "provider_get_token",
40
+ "provider_setup_endpoint", "provider_setup_logger", "provider_setup_server",
33
41
  # token_pomes
34
- "token_validate"
42
+ "token_get_claims", "token_get_values", "token_validate"
35
43
  ]
36
44
 
37
45
  from importlib.metadata import version
@@ -13,13 +13,13 @@ 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
 
@@ -32,6 +32,11 @@ def action_login(iam_server: IamServer,
32
32
  returned by *iam_server* upon login. On success, the appropriate URL for invoking
33
33
  the IAM server's authentication page is returned.
34
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
+
35
40
  :param iam_server: the reference registered *IAM* server
36
41
  :param args: the arguments passed when requesting the service
37
42
  :param errors: incidental error messages
@@ -51,7 +56,7 @@ def action_login(iam_server: IamServer,
51
56
  # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
52
57
  oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
53
58
  if target_idp:
54
- oauth_state += f"idp={target_idp}"
59
+ oauth_state += f"#idp={target_idp}"
55
60
 
56
61
  with _iam_lock:
57
62
  # retrieve the user data from the IAM server's registry
@@ -88,16 +93,16 @@ def action_login(iam_server: IamServer,
88
93
  return result
89
94
 
90
95
 
91
- def action_logout(iam_server: IamServer,
92
- args: dict[str, Any],
93
- errors: list[str] = None,
94
- 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:
95
100
  """
96
101
  Logout the user, by removing all data associating it from *iam_server*'s registry.
97
102
 
98
- The user is identified by the attribute *user-id* or "login", provided in *args*.
99
- If successful, remove all data relating to the user from the *IAM* server's registry.
100
- Otherwise, this operation fails silently, unless an error has ocurred.
103
+ The user is identified by the attribute *user-id* or *login*, provided in *args*.
104
+ A logout request is sent to *iam_server* and, if successful, remove all data relating to the user
105
+ from the *IAM* server's registry.
101
106
 
102
107
  :param iam_server: the reference registered *IAM* server
103
108
  :param args: the arguments passed when requesting the service
@@ -109,33 +114,90 @@ def action_logout(iam_server: IamServer,
109
114
 
110
115
  if user_id:
111
116
  with _iam_lock:
112
- # retrieve the data for all users in the IAM server's registry
113
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
114
- errors=errors,
115
- logger=logger) or {}
116
- if user_id in users:
117
- users.pop(user_id)
118
- if logger:
119
- logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
117
+ # retrieve the IAM server's registry and the data for all users therein
118
+ registry: dict[str, Any] = _get_iam_registry(iam_server,
119
+ errors=errors,
120
+ logger=logger)
121
+ users: dict[str, dict[str, Any]] = registry[IamParam.USERS] if registry else {}
122
+ user_data: dict[str, Any] = users.get(user_id)
123
+ if user_data:
124
+ # request the IAM server to logout 'client_id'
125
+ client_secret: str = __get_client_secret(iam_server=iam_server,
126
+ errors=errors,
127
+ logger=logger)
128
+ if client_secret:
129
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
130
+ "/protocol/openid-connect/logout")
131
+ header_data: dict[str, str] = {
132
+ "Content-Type": "application/x-www-form-urlencoded"
133
+ }
134
+ body_data: dict[str, Any] = {
135
+ "client_id": registry[IamParam.CLIENT_ID],
136
+ "client_secret": client_secret,
137
+ "refresh_token": user_data[UserParam.REFRESH_TOKEN]
138
+ }
139
+ # log the POST
140
+ if logger:
141
+ logger.debug(msg=f"POST {url}")
142
+ try:
143
+ response: requests.Response = requests.post(url=url,
144
+ headers=header_data,
145
+ data=body_data)
146
+ if response.status_code in [200, 204]:
147
+ # request succeeded
148
+ if logger:
149
+ logger.debug(msg=f"POST success")
150
+ else:
151
+ # request failed, report the problem
152
+ msg: str = f"POST failure, status {response.status_code}, reason {response.reason}"
153
+ if logger:
154
+ logger.error(msg=msg)
155
+ if isinstance(errors, list):
156
+ errors.append(msg)
157
+ except Exception as e:
158
+ # the operation raised an exception
159
+ msg: str = exc_format(exc=e,
160
+ exc_info=sys.exc_info())
161
+ if logger:
162
+ logger.error(msg=msg)
163
+ if isinstance(errors, list):
164
+ errors.append(msg)
120
165
 
166
+ if not errors and user_id in users:
167
+ users.pop(user_id)
168
+ if logger:
169
+ logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
170
+ else:
171
+ msg: str = "User identification not provided"
172
+ if logger:
173
+ logger.error(msg=msg)
174
+ if isinstance(errors, list):
175
+ errors.append(msg)
121
176
 
122
- def action_token(iam_server: IamServer,
123
- args: dict[str, Any],
124
- errors: list[str] = None,
125
- logger: Logger = None) -> str:
177
+
178
+ def iam_get_token(iam_server: IamServer,
179
+ args: dict[str, Any],
180
+ errors: list[str] = None,
181
+ logger: Logger = None) -> dict[str, str]:
126
182
  """
127
183
  Retrieve the authentication token for the user, from *iam_server*.
128
184
 
129
185
  The user is identified by the attribute *user-id* or *login*, provided in *args*.
130
186
 
187
+ On success, the returned *dict* will contain the following JSON:
188
+ {
189
+ "access-token": <token>,
190
+ "user-id": <user-identification
191
+ }
192
+
131
193
  :param iam_server: the reference registered *IAM* server
132
194
  :param args: the arguments passed when requesting the service
133
195
  :param errors: incidental error messages
134
196
  :param logger: optional logger
135
- :return: the token for user indicated, or *None* if error
197
+ :return: the user identification and token issued, or *None* if error
136
198
  """
137
199
  # initialize the return variable
138
- result: str | None = None
200
+ result: dict[str, str] | None = None
139
201
 
140
202
  # obtain the user's identification
141
203
  user_id: str = args.get("user-id") or args.get("login")
@@ -154,7 +216,10 @@ def action_token(iam_server: IamServer,
154
216
  access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
155
217
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
156
218
  if now < access_expiration:
157
- result = token
219
+ result = {
220
+ "access-token": token,
221
+ "user-id": user_id
222
+ }
158
223
  else:
159
224
  # access token has expired
160
225
  refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
@@ -162,7 +227,7 @@ def action_token(iam_server: IamServer,
162
227
  refresh_expiration: int = user_data[UserParam.REFRESH_EXPIRATION]
163
228
  if now < refresh_expiration:
164
229
  header_data: dict[str, str] = {
165
- "Content-Type": "application/json"
230
+ "Content-Type": "application/x-www-form-urlencoded"
166
231
  }
167
232
  body_data: dict[str, str] = {
168
233
  "grant_type": "refresh_token",
@@ -182,7 +247,10 @@ def action_token(iam_server: IamServer,
182
247
  now=now,
183
248
  errors=errors,
184
249
  logger=logger)
185
- result = token_info[1]
250
+ result = {
251
+ "access-token": token_info[1],
252
+ "user-id": user_id
253
+ }
186
254
  else:
187
255
  # refresh token is no longer valid
188
256
  user_data[UserParam.REFRESH_TOKEN] = None
@@ -210,10 +278,10 @@ def action_token(iam_server: IamServer,
210
278
  return result
211
279
 
212
280
 
213
- def action_callback(iam_server: IamServer,
214
- args: dict[str, Any],
215
- errors: list[str] = None,
216
- logger: Logger = None) -> tuple[str, str] | None:
281
+ def iam_callback(iam_server: IamServer,
282
+ args: dict[str, Any],
283
+ errors: list[str] = None,
284
+ logger: Logger = None) -> tuple[str, str] | None:
217
285
  """
218
286
  Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
219
287
 
@@ -221,6 +289,10 @@ def action_callback(iam_server: IamServer,
221
289
  - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
222
290
  - *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
223
291
 
292
+ if *state* is postfixed with the string *#idp=<target-idp>*, this instructs *iam_server* to act as a broker,
293
+ forwarding the authentication process to the *IAM* server *target-idp*. This mechanism fully dispenses with
294
+ the flows 'callback-exchange', and 'callback' followed by 'exchange'.
295
+
224
296
  :param iam_server: the reference registered *IAM* server
225
297
  :param args: the arguments passed when requesting the service
226
298
  :param errors: incidental errors
@@ -250,7 +322,7 @@ def action_callback(iam_server: IamServer,
250
322
  if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
251
323
  errors.append("Operation timeout")
252
324
  else:
253
- pos: int = oauth_state.rfind("idp=")
325
+ pos: int = oauth_state.rfind("#idp=")
254
326
  target_idp: str = oauth_state[pos+4:] if pos > 0 else None
255
327
  target_iam = IamServer(target_idp) if target_idp in IamServer else None
256
328
  target_data: dict[str, Any] = user_data.copy() if target_iam else None
@@ -285,8 +357,8 @@ def action_callback(iam_server: IamServer,
285
357
  registry: dict[str, Any] = _get_iam_registry(iam_server,
286
358
  errors=errors,
287
359
  logger=logger)
288
- url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
289
- url += f"/broker/{target_idp}/token"
360
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
361
+ f"{registry[IamParam.CLIENT_REALM]}/broker/{target_idp}/token")
290
362
  header_data: dict[str, str] = {
291
363
  "Authorization": f"Bearer {result[1]}",
292
364
  "Content-Type": "application/json"
@@ -315,10 +387,10 @@ def action_callback(iam_server: IamServer,
315
387
  return result
316
388
 
317
389
 
318
- def action_exchange(iam_server: IamServer,
319
- args: dict[str, Any],
320
- errors: list[str] = None,
321
- logger: Logger = None) -> tuple[str, str]:
390
+ def iam_exchange(iam_server: IamServer,
391
+ args: dict[str, Any],
392
+ errors: list[str] = None,
393
+ logger: Logger = None) -> tuple[str, str]:
322
394
  """
323
395
  Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
324
396
 
@@ -349,12 +421,10 @@ def action_exchange(iam_server: IamServer,
349
421
 
350
422
  # obtain the token to be exchanged
351
423
  token: str = args.get("access-token") if user_id else None
352
- token_claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
353
- errors=errors,
354
- logger=logger) if token else None
355
- token_issuer: str = _iam_server_from_issuer(issuer=token_claims["payload"]["iss"],
424
+ token_issuer: tuple[str] = token_get_values(token=token,
425
+ keys=("iss",),
356
426
  errors=errors,
357
- logger=logger) if token_claims else None
427
+ logger=logger)
358
428
  if not errors:
359
429
  # HAZARD: only 'IAM_KEYCLOAK' is currently supported
360
430
  with _iam_lock:
@@ -367,6 +437,7 @@ def action_exchange(iam_server: IamServer,
367
437
  __assert_link(iam_server=iam_server,
368
438
  user_id=user_id,
369
439
  token=token,
440
+ token_issuer=token_issuer[0],
370
441
  errors=errors,
371
442
  logger=logger)
372
443
  if not errors:
@@ -409,9 +480,64 @@ def action_exchange(iam_server: IamServer,
409
480
  return result
410
481
 
411
482
 
483
+ def iam_userinfo(iam_server: IamServer,
484
+ args: dict[str, Any],
485
+ errors: list[str] = None,
486
+ logger: Logger = None) -> dict[str, Any] | None:
487
+ """
488
+ Obtain user data from *iam_server*.
489
+
490
+ The user is identified by the attribute *user-id* or *login*, provided in *args*.
491
+
492
+ :param iam_server: the reference registered *IAM* server
493
+ :param args: the arguments passed when requesting the service
494
+ :param errors: incidental error messages
495
+ :param logger: optional logger
496
+ :return: the user information requested, or *None* if error
497
+ """
498
+ # initialize the return variable
499
+ result: dict[str, Any] | None = None
500
+
501
+ # obtain the user's identification
502
+ user_id: str = args.get("user-id") or args.get("login")
503
+
504
+ err_msg: str | None = None
505
+ if user_id:
506
+ with _iam_lock:
507
+ # retrieve the IAM server's registry and the user data therein
508
+ registry: dict[str, Any] = _get_iam_registry(iam_server,
509
+ errors=errors,
510
+ logger=logger)
511
+ user_data: dict[str, Any] = registry[IamParam.USERS].get(user_id)
512
+ if user_data:
513
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
514
+ "/protocol/openid-connect/userinfo")
515
+ header_data: dict[str, str] = {
516
+ "Authorization": f"Bearer {args.get('access-token')}"
517
+ }
518
+ result = __get_for_data(url=url,
519
+ header_data=header_data,
520
+ params=None,
521
+ errors=errors,
522
+ logger=logger)
523
+ else:
524
+ err_msg = f"Unknown user '{user_id}'"
525
+ else:
526
+ err_msg: str = "User identification not provided"
527
+
528
+ if err_msg:
529
+ if logger:
530
+ logger.error(msg=err_msg)
531
+ if isinstance(errors, list):
532
+ errors.append(err_msg)
533
+
534
+ return result
535
+
536
+
412
537
  def __assert_link(iam_server: IamServer,
413
538
  user_id: str,
414
539
  token: str,
540
+ token_issuer: str,
415
541
  errors: list[str] | None,
416
542
  logger: Logger | None) -> None:
417
543
  """
@@ -439,7 +565,7 @@ def __assert_link(iam_server: IamServer,
439
565
  # obtain the internal user identification for 'user_id'
440
566
  if logger:
441
567
  logger.debug(msg="Obtaining internal identification "
442
- f"for user {user_id} in IAM server {iam_server}")
568
+ f"for user '{user_id}' in IAM server '{iam_server}'")
443
569
  url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
444
570
  header_data: dict[str, str] = {
445
571
  "Authorization": f"Bearer {admin_token}",
@@ -455,12 +581,12 @@ def __assert_link(iam_server: IamServer,
455
581
  errors=errors,
456
582
  logger=logger)
457
583
  if users:
458
- # verify whether the 'oidc' protocol is referred to in an
459
- # association between 'user_id' and the internal user identification
584
+ # verify whether the IAM server that issued the token is a federated identity provider
585
+ # in the associations between 'user_id' and the internal user identification
460
586
  internal_id: str = users[0].get("id")
461
587
  if logger:
462
- logger.debug(msg="Obtaining the providers federated with "
463
- f"IAM server '{iam_server}' for internal identification '{internal_id}'")
588
+ logger.debug(msg="Obtaining the providers federated in IAM server "
589
+ f"'{iam_server}', for internal identification '{internal_id}'")
464
590
  url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
465
591
  f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
466
592
  providers: list[dict[str, Any]] = __get_for_data(url=url,
@@ -469,13 +595,9 @@ def __assert_link(iam_server: IamServer,
469
595
  errors=errors,
470
596
  logger=logger)
471
597
  no_link: bool = True
472
- claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
473
- errors=errors,
474
- logger=logger)
475
- issuer: str = claims["payload"]["iss"] if claims else None
476
- provider_name: str = _iam_server_from_issuer(issuer=issuer,
598
+ provider_name: str = _iam_server_from_issuer(issuer=token_issuer,
477
599
  errors=errors,
478
- logger=logger) if issuer else None
600
+ logger=logger)
479
601
  if provider_name:
480
602
  for provider in providers:
481
603
  if provider.get("identityProvider") == provider_name:
@@ -483,17 +605,17 @@ def __assert_link(iam_server: IamServer,
483
605
  break
484
606
  if no_link:
485
607
  # link the identities
486
- claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
487
- errors=errors,
488
- logger=logger)
489
- if claims:
490
- token_sub: str = claims["payload"]["sub"]
608
+ token_sub: tuple[str] = token_get_values(token=token,
609
+ keys=("sub",),
610
+ errors=errors,
611
+ logger=logger)
612
+ if token_sub:
491
613
  if logger:
492
614
  logger.debug(msg="Creating an association between identifications "
493
- f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
615
+ f"'{user_id}' and '{token_sub}' in IAM server '{iam_server}'")
494
616
  url += f"/{provider_name}"
495
617
  json_data: dict[str, Any] = {
496
- "userId": token_sub,
618
+ "userId": token_sub[0],
497
619
  "userName": user_id
498
620
  }
499
621
  __post_json(url=url,
@@ -797,8 +919,8 @@ def __post_for_token(iam_server: IamServer,
797
919
  body_data["client_id"] = registry[IamParam.CLIENT_ID]
798
920
 
799
921
  # build the URL
800
- base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
801
- url: str = f"{base_url}/protocol/openid-connect/token"
922
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
923
+ f"{registry[IamParam.CLIENT_REALM]}/protocol/openid-connect/token")
802
924
  # 'client_secret' data must not be shown in log
803
925
  msg: str = f"POST {url}, {json.dumps(obj=body_data,
804
926
  ensure_ascii=False)}"
pypomes_iam/iam_common.py CHANGED
@@ -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,66 @@ 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>_AUTH_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
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_USERINFO*
95
+
96
+ :return: the configuration data for the select *IAM* servers.
97
+ """
98
+ # initialize the return variable
99
+ result: dict[IamServer, dict[IamParam, Any]] = {}
100
+
101
+ servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_AUTH_SERVERS",
102
+ enum_class=IamServer) or []
103
+ for server in servers:
104
+ prefix = server.name
105
+ result[server] = {
106
+ IamParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
107
+ IamParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_SECRET"),
108
+ IamParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
109
+ IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
110
+ IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
111
+ IamParam.LOGIN_TIMEOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT"),
112
+ IamParam.PK_LIFETIME: env_get_int(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME"),
113
+ IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
114
+ IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH_BASE"),
115
+ # dynamically set
116
+ IamParam.PK_EXPIRATION: 0,
117
+ IamParam.PUBLIC_KEY: None,
118
+ IamParam.USERS: {}
119
+ }
120
+
121
+ return result
122
+
82
123
 
83
124
  # registry structure:
84
125
  # { <IamServer>:
@@ -91,6 +132,7 @@ class UserParam(StrEnum):
91
132
  # "client-realm": <str,
92
133
  # "client-timeout": <int>,
93
134
  # "recipient-attr": <str>,
135
+ # # dynamic attributes
94
136
  # "public-key": <str>,
95
137
  # "pk-lifetime": <int>,
96
138
  # "pk-expiration": <int>,
@@ -112,10 +154,10 @@ class UserParam(StrEnum):
112
154
  # },
113
155
  # ...
114
156
  # }
115
- _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = {}
157
+ _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
116
158
 
117
159
 
118
- # the lock protecting the data in '_IAM_SERVERS'
160
+ # the lock protecting the data in '_<IAM>_SERVERS'
119
161
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
120
162
  _iam_lock: Final[RLock] = RLock()
121
163