pypomes-iam 0.8.5__tar.gz → 0.9.1__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.8.5
3
+ Version: 0.9.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
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_iam"
9
- version = "0.8.5"
9
+ version = "0.9.1"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -3,7 +3,7 @@ from .iam_actions import (
3
3
  iam_login, iam_logout, iam_get_token, iam_userinfo
4
4
  )
5
5
  from .iam_common import (
6
- IamServer, IamParam
6
+ IamServer, ServerParam
7
7
  )
8
8
  from .iam_pomes import (
9
9
  iam_setup_server, iam_setup_endpoints
@@ -15,8 +15,9 @@ from .iam_services import (
15
15
  service_exchange, service_callback_exchange
16
16
  )
17
17
  from .provider_pomes import (
18
+ IamProvider, ProviderParam,
18
19
  service_get_token, provider_get_token,
19
- provider_setup_endpoint, provider_setup_logger, provider_setup_server
20
+ iam_setup_provider, provider_setup_endpoint, provider_setup_logger
20
21
  )
21
22
  from .token_pomes import (
22
23
  token_get_claims, token_get_values, token_validate
@@ -27,7 +28,7 @@ __all__ = [
27
28
  "iam_callback", "iam_exchange",
28
29
  "iam_login", "iam_logout", "iam_get_token", "iam_userinfo",
29
30
  # iam_commons
30
- "IamServer", "IamParam",
31
+ "IamServer", "ServerParam",
31
32
  # iam_pomes
32
33
  "iam_setup_server", "iam_setup_endpoints",
33
34
  # iam_services
@@ -36,8 +37,9 @@ __all__ = [
36
37
  "service_get_token", "service_userinfo", "service_callback",
37
38
  "service_exchange", "service_callback_exchange",
38
39
  # provider_pomes
39
- "provider_setup_server", "provider_get_token",
40
- "provider_setup_endpoint", "provider_setup_logger", "provider_setup_server",
40
+ "IamProvider", "ProviderParam",
41
+ "service_get_token", "provider_get_token",
42
+ "iam_setup_provider", "provider_setup_endpoint", "provider_setup_logger",
41
43
  # token_pomes
42
44
  "token_get_claims", "token_get_values", "token_validate"
43
45
  ]
@@ -9,7 +9,7 @@ from pypomes_core import TZ_LOCAL, exc_format
9
9
  from typing import Any
10
10
 
11
11
  from .iam_common import (
12
- IamServer, IamParam, UserParam, _iam_lock,
12
+ IamServer, ServerParam, UserParam, _iam_lock,
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
  )
@@ -56,7 +56,7 @@ def iam_login(iam_server: IamServer,
56
56
  # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
57
57
  oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
58
58
  if target_idp:
59
- oauth_state += f"#idp={target_idp}"
59
+ oauth_state += f"_{target_idp}"
60
60
 
61
61
  with _iam_lock:
62
62
  # retrieve the user data from the IAM server's registry
@@ -80,10 +80,10 @@ def iam_login(iam_server: IamServer,
80
80
  errors=errors,
81
81
  logger=logger)
82
82
  if registry:
83
- base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
83
+ base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
84
84
  result = (f"{base_url}/protocol/openid-connect/auth"
85
85
  f"?response_type=code&scope=openid"
86
- f"&client_id={registry[IamParam.CLIENT_ID]}"
86
+ f"&client_id={registry[ServerParam.CLIENT_ID]}"
87
87
  f"&redirect_uri={redirect_uri}"
88
88
  f"&state={oauth_state}")
89
89
  if target_idp:
@@ -118,7 +118,7 @@ def iam_logout(iam_server: IamServer,
118
118
  registry: dict[str, Any] = _get_iam_registry(iam_server,
119
119
  errors=errors,
120
120
  logger=logger)
121
- users: dict[str, dict[str, Any]] = registry[IamParam.USERS] if registry else {}
121
+ users: dict[str, dict[str, Any]] = registry[ServerParam.USERS] if registry else {}
122
122
  user_data: dict[str, Any] = users.get(user_id)
123
123
  if user_data:
124
124
  # request the IAM server to logout 'client_id'
@@ -126,13 +126,13 @@ def iam_logout(iam_server: IamServer,
126
126
  errors=errors,
127
127
  logger=logger)
128
128
  if client_secret:
129
- url: str = (f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
129
+ url: str = (f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
130
130
  "/protocol/openid-connect/logout")
131
131
  header_data: dict[str, str] = {
132
132
  "Content-Type": "application/x-www-form-urlencoded"
133
133
  }
134
134
  body_data: dict[str, Any] = {
135
- "client_id": registry[IamParam.CLIENT_ID],
135
+ "client_id": registry[ServerParam.CLIENT_ID],
136
136
  "client_secret": client_secret,
137
137
  "refresh_token": user_data[UserParam.REFRESH_TOKEN]
138
138
  }
@@ -322,9 +322,9 @@ def iam_callback(iam_server: IamServer,
322
322
  if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
323
323
  errors.append("Operation timeout")
324
324
  else:
325
- pos: int = oauth_state.rfind("#idp=")
326
- target_idp: str = oauth_state[pos+4:] if pos > 0 else None
327
- target_iam = IamServer(target_idp) if target_idp in IamServer else None
325
+ pos: int = oauth_state.rfind("_")
326
+ target_idp: str = oauth_state[pos+1:] if pos > 0 else None
327
+ target_iam: IamServer = IamServer(target_idp) if target_idp in IamServer else None
328
328
  target_data: dict[str, Any] = user_data.copy() if target_iam else None
329
329
  users.pop(oauth_state)
330
330
  code: str = args.get("code")
@@ -357,8 +357,8 @@ def iam_callback(iam_server: IamServer,
357
357
  registry: dict[str, Any] = _get_iam_registry(iam_server,
358
358
  errors=errors,
359
359
  logger=logger)
360
- url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
361
- f"{registry[IamParam.CLIENT_REALM]}/broker/{target_idp}/token")
360
+ url: str = (f"{registry[ServerParam.URL_BASE]}/realms/"
361
+ f"{registry[ServerParam.CLIENT_REALM]}/broker/{target_idp}/token")
362
362
  header_data: dict[str, str] = {
363
363
  "Authorization": f"Bearer {result[1]}",
364
364
  "Content-Type": "application/json"
@@ -426,7 +426,6 @@ def iam_exchange(iam_server: IamServer,
426
426
  errors=errors,
427
427
  logger=logger)
428
428
  if not errors:
429
- # HAZARD: only 'IAM_KEYCLOAK' is currently supported
430
429
  with _iam_lock:
431
430
  # retrieve the IAM server's registry
432
431
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
@@ -452,7 +451,7 @@ def iam_exchange(iam_server: IamServer,
452
451
  "subject_token": token,
453
452
  "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
454
453
  "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
455
- "audience": registry[IamParam.CLIENT_ID],
454
+ "audience": registry[ServerParam.CLIENT_ID],
456
455
  "subject_issuer": token_issuer
457
456
  }
458
457
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
@@ -508,9 +507,9 @@ def iam_userinfo(iam_server: IamServer,
508
507
  registry: dict[str, Any] = _get_iam_registry(iam_server,
509
508
  errors=errors,
510
509
  logger=logger)
511
- user_data: dict[str, Any] = registry[IamParam.USERS].get(user_id)
510
+ user_data: dict[str, Any] = registry[ServerParam.USERS].get(user_id)
512
511
  if user_data:
513
- url: str = (f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
512
+ url: str = (f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
514
513
  "/protocol/openid-connect/userinfo")
515
514
  header_data: dict[str, str] = {
516
515
  "Authorization": f"Bearer {args.get('access-token')}"
@@ -566,7 +565,7 @@ def __assert_link(iam_server: IamServer,
566
565
  if logger:
567
566
  logger.debug(msg="Obtaining internal identification "
568
567
  f"for user '{user_id}' in IAM server '{iam_server}'")
569
- url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
568
+ url: str = f"{registry[ServerParam.URL_BASE]}/admin/realms/{registry[ServerParam.CLIENT_REALM]}/users"
570
569
  header_data: dict[str, str] = {
571
570
  "Authorization": f"Bearer {admin_token}",
572
571
  "Content-Type": "application/json"
@@ -587,8 +586,8 @@ def __assert_link(iam_server: IamServer,
587
586
  if logger:
588
587
  logger.debug(msg="Obtaining the providers federated in IAM server "
589
588
  f"'{iam_server}', for internal identification '{internal_id}'")
590
- url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
591
- f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
589
+ url = (f"{registry[ServerParam.URL_BASE]}/admin/realms/"
590
+ f"{registry[ServerParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
592
591
  providers: list[dict[str, Any]] = __get_for_data(url=url,
593
592
  header_data=header_data,
594
593
  params=None,
@@ -651,14 +650,14 @@ def __get_administrative_token(iam_server: IamServer,
651
650
  errors=errors,
652
651
  logger=logger)
653
652
  if registry:
654
- if registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
653
+ if registry[ServerParam.ADMIN_ID] and registry[ServerParam.ADMIN_SECRET]:
655
654
  header_data: dict[str, str] = {
656
655
  "Content-Type": "application/x-www-form-urlencoded"
657
656
  }
658
657
  body_data: dict[str, str] = {
659
658
  "grant_type": "password",
660
- "username": registry[IamParam.ADMIN_ID],
661
- "password": registry[IamParam.ADMIN_SECRET],
659
+ "username": registry[ServerParam.ADMIN_ID],
660
+ "password": registry[ServerParam.ADMIN_SECRET],
662
661
  "client_id": "admin-cli"
663
662
  }
664
663
  token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
@@ -674,7 +673,7 @@ def __get_administrative_token(iam_server: IamServer,
674
673
 
675
674
  elif logger or isinstance(errors, list):
676
675
  msg: str = ("Credentials for administrator of realm "
677
- f"'{registry[IamParam.CLIENT_REALM]}' "
676
+ f"'{registry[ServerParam.CLIENT_REALM]}' "
678
677
  f"at IAM server '{iam_server}' not provided")
679
678
  if logger:
680
679
  logger.error(msg=msg)
@@ -710,7 +709,7 @@ def __get_client_secret(iam_server: IamServer,
710
709
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
711
710
  errors=errors,
712
711
  logger=logger)
713
- result: str = registry[IamParam.CLIENT_SECRET] if registry else None
712
+ result: str = registry[ServerParam.CLIENT_SECRET] if registry else None
714
713
 
715
714
  if not result and not errors:
716
715
  # obtain a token with administrative rights
@@ -718,13 +717,13 @@ def __get_client_secret(iam_server: IamServer,
718
717
  errors=errors,
719
718
  logger=logger)
720
719
  if token:
721
- realm: str = registry[IamParam.CLIENT_REALM]
722
- client_id: str = registry[IamParam.CLIENT_ID]
720
+ realm: str = registry[ServerParam.CLIENT_REALM]
721
+ client_id: str = registry[ServerParam.CLIENT_ID]
723
722
  if logger:
724
723
  logger.debug(msg=f"Obtaining the UUID for client '{client_id}', "
725
724
  f"in realm '{realm}' at IAM server '{iam_server}'")
726
725
  # obtain the client UUID
727
- url: str = f"{registry[IamParam.URL_BASE]}/realms/{realm}/clients"
726
+ url: str = f"{registry[ServerParam.URL_BASE]}/realms/{realm}/clients"
728
727
  header_data: dict[str, str] = {
729
728
  "Authorization": f"Bearer {token}",
730
729
  "Content-Type": "application/json"
@@ -752,7 +751,7 @@ def __get_client_secret(iam_server: IamServer,
752
751
  if reply:
753
752
  # store the client's secret password and return it
754
753
  result = reply["value"]
755
- registry[IamParam.CLIENT_ID] = result
754
+ registry[ServerParam.CLIENT_ID] = result
756
755
  return result
757
756
 
758
757
 
@@ -916,11 +915,11 @@ def __post_for_token(iam_server: IamServer,
916
915
  if registry:
917
916
  # complete the data to send in body of request
918
917
  if body_data["grant_type"] != "password":
919
- body_data["client_id"] = registry[IamParam.CLIENT_ID]
918
+ body_data["client_id"] = registry[ServerParam.CLIENT_ID]
920
919
 
921
920
  # build the URL
922
- url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
923
- f"{registry[IamParam.CLIENT_REALM]}/protocol/openid-connect/token")
921
+ url: str = (f"{registry[ServerParam.URL_BASE]}/realms/"
922
+ f"{registry[ServerParam.CLIENT_REALM]}/protocol/openid-connect/token")
924
923
  # 'client_secret' data must not be shown in log
925
924
  msg: str = f"POST {url}, {json.dumps(obj=body_data,
926
925
  ensure_ascii=False)}"
@@ -1021,9 +1020,9 @@ def __validate_and_store(iam_server: IamServer,
1021
1020
  public_key: str = _get_public_key(iam_server=iam_server,
1022
1021
  errors=errors,
1023
1022
  logger=logger)
1024
- recipient_attr = registry[IamParam.RECIPIENT_ATTR]
1023
+ recipient_attr = registry[ServerParam.RECIPIENT_ATTR]
1025
1024
  login_id = user_data.pop("login-id", None)
1026
- base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
1025
+ base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
1027
1026
  claims: dict[str, dict[str, Any]] = token_validate(token=token,
1028
1027
  issuer=base_url,
1029
1028
  recipient_id=login_id,
@@ -1,26 +1,23 @@
1
1
  import requests
2
2
  import sys
3
3
  from datetime import datetime
4
- from enum import StrEnum, auto
4
+ from enum import StrEnum
5
5
  from logging import Logger
6
6
  from pypomes_core import (
7
7
  APP_PREFIX, TZ_LOCAL, exc_format,
8
- env_get_str, env_get_int, env_get_enums
8
+ env_get_int, env_get_str, env_get_strs
9
9
  )
10
10
  from pypomes_crypto import crypto_jwk_convert
11
11
  from threading import RLock
12
12
  from typing import Any, Final
13
13
 
14
-
15
- class IamServer(StrEnum):
16
- """
17
- Supported IAM servers.
18
- """
19
- JUSBR = auto()
20
- KEYCLOAK = auto()
14
+ _members: dict[str, str] = {key.upper(): key.lower() for key in
15
+ env_get_strs(key=f"{APP_PREFIX}_AUTH_SERVERS")}
16
+ IamServer: type[StrEnum] = StrEnum("IamServer", _members)
17
+ del _members
21
18
 
22
19
 
23
- class IamParam(StrEnum):
20
+ class ServerParam(StrEnum):
24
21
  """
25
22
  Parameters for configuring *IAM* servers.
26
23
  """
@@ -60,7 +57,7 @@ class UserParam(StrEnum):
60
57
  REDIRECT_URI = "redirect-uri"
61
58
 
62
59
 
63
- def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
60
+ def __get_iam_data() -> dict[IamServer, dict[ServerParam, Any]]:
64
61
  """
65
62
  Obtain the configuration data for select *IAM* servers.
66
63
 
@@ -69,8 +66,8 @@ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
69
66
  variables can be done by following these steps:
70
67
 
71
68
  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:
69
+ and the data set below for each server, where *<IAM>* stands for the server's name as presented in
70
+ *IamServer* class:
74
71
  - *<APP_PREFIX>_<IAM>_ADMIN_ID* (optional, required if administrative duties are performed)
75
72
  - *<APP_PREFIX>_<IAM>_ADMIN_PWD* (optional, required if administrative duties are performed)
76
73
  - *<APP_PREFIX>_<IAM>_CLIENT_ID* (required)
@@ -96,26 +93,24 @@ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
96
93
  :return: the configuration data for the select *IAM* servers.
97
94
  """
98
95
  # initialize the return variable
99
- result: dict[IamServer, dict[IamParam, Any]] = {}
96
+ result: dict[IamServer, dict[ServerParam, Any]] = {}
100
97
 
101
- servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_AUTH_SERVERS",
102
- enum_class=IamServer) or []
103
- for server in servers:
98
+ for server in IamServer:
104
99
  prefix = server.name
105
100
  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"),
101
+ ServerParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
102
+ ServerParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_SECRET"),
103
+ ServerParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
104
+ ServerParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
105
+ ServerParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
106
+ ServerParam.LOGIN_TIMEOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT"),
107
+ ServerParam.PK_LIFETIME: env_get_int(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME"),
108
+ ServerParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
109
+ ServerParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_BASE"),
115
110
  # dynamically set
116
- IamParam.PK_EXPIRATION: 0,
117
- IamParam.PUBLIC_KEY: None,
118
- IamParam.USERS: {}
111
+ ServerParam.PK_EXPIRATION: 0,
112
+ ServerParam.PUBLIC_KEY: None,
113
+ ServerParam.USERS: {}
119
114
  }
120
115
 
121
116
  return result
@@ -154,7 +149,7 @@ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
154
149
  # },
155
150
  # ...
156
151
  # }
157
- _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
152
+ _IAM_SERVERS: Final[dict[IamServer, dict[ServerParam, Any]]] = __get_iam_data()
158
153
 
159
154
 
160
155
  # the lock protecting the data in '_<IAM>_SERVERS'
@@ -174,7 +169,7 @@ def _iam_server_from_endpoint(endpoint: str,
174
169
  :return: the corresponding *IAM* server, or *None* if one could not be obtained
175
170
  """
176
171
  # initialize the return variable
177
- result: IamServer | None = None
172
+ result: type(IamServer) | None = None
178
173
 
179
174
  for iam_server in _IAM_SERVERS:
180
175
  if endpoint.startswith(iam_server):
@@ -203,10 +198,10 @@ def _iam_server_from_issuer(issuer: str,
203
198
  :return: the corresponding *IAM* server, or *None* if one could not be obtained
204
199
  """
205
200
  # initialize the return variable
206
- result: IamServer | None = None
201
+ result: type(IamServer) | None = None
207
202
 
208
203
  for iam_server, registry in _IAM_SERVERS.items():
209
- base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
204
+ base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
210
205
  if base_url == issuer:
211
206
  result = IamServer(iam_server)
212
207
  break
@@ -269,9 +264,9 @@ def _get_public_key(iam_server: IamServer,
269
264
  logger=logger)
270
265
  if registry:
271
266
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
272
- if now > registry[IamParam.PK_EXPIRATION]:
267
+ if now > registry[ServerParam.PK_EXPIRATION]:
273
268
  # obtain the JWKS (JSON Web Key Set) from the token issuer
274
- base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
269
+ base_url: str = f"{registry[ServerParam.URL_BASE]}/realms/{registry[ServerParam.CLIENT_REALM]}"
275
270
  url: str = f"{base_url}/protocol/openid-connect/certs"
276
271
  if logger:
277
272
  logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
@@ -293,9 +288,9 @@ def _get_public_key(iam_server: IamServer,
293
288
  # convert from 'JWK' to 'PEM' and save it for further use
294
289
  result = crypto_jwk_convert(jwk=jwk,
295
290
  fmt="PEM")
296
- registry[IamParam.PUBLIC_KEY] = result
297
- lifetime: int = registry[IamParam.PK_LIFETIME] or 0
298
- registry[IamParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
291
+ registry[ServerParam.PUBLIC_KEY] = result
292
+ lifetime: int = registry[ServerParam.PK_LIFETIME] or 0
293
+ registry[ServerParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
299
294
  if logger:
300
295
  logger.debug("Public key obtained and saved")
301
296
  else:
@@ -320,7 +315,7 @@ def _get_public_key(iam_server: IamServer,
320
315
  if isinstance(errors, list):
321
316
  errors.append(msg)
322
317
  else:
323
- result = registry[IamParam.PUBLIC_KEY]
318
+ result = registry[ServerParam.PUBLIC_KEY]
324
319
 
325
320
  return result
326
321
 
@@ -427,4 +422,4 @@ def _get_iam_users(iam_server: IamServer,
427
422
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
428
423
  errors=errors,
429
424
  logger=logger)
430
- return registry[IamParam.USERS] if registry else None
425
+ return registry[ServerParam.USERS] if registry else None
@@ -6,7 +6,7 @@ from pypomes_core import (
6
6
  )
7
7
 
8
8
  from .iam_common import (
9
- _IAM_SERVERS, IamServer, IamParam, _iam_lock
9
+ _IAM_SERVERS, IamServer, ServerParam, _iam_lock
10
10
  )
11
11
  from .iam_services import (
12
12
  service_login, service_logout,
@@ -41,7 +41,7 @@ def iam_setup_server(iam_server: IamServer,
41
41
  it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
42
42
  the first time it is needed.
43
43
 
44
- :param iam_server: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
44
+ :param iam_server: identifies the supported *IAM* server
45
45
  :param admin_id: identifies the realm administrator
46
46
  :param admin_secret: password for the realm administrator
47
47
  :param client_id: the client's identification with the *IAM* server
@@ -70,28 +70,28 @@ def iam_setup_server(iam_server: IamServer,
70
70
  if "login_timeout" in defaulted_params:
71
71
  login_timeout = env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT")
72
72
  if "pk_lifetime" in defaulted_params:
73
- pk_lifetime = env_get_int(key=f"{APP_PREFIX}_{prefix}_PUBLIC_KEY_LIFETIME")
73
+ pk_lifetime = env_get_int(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME")
74
74
  if "recipient_attr" in defaulted_params:
75
75
  recipient_attr = env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR")
76
76
  if "url_base" in defaulted_params:
77
- url_base = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH_BASE")
77
+ url_base = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_BASE")
78
78
 
79
- # configure the Keycloak registry
79
+ # configure the IAM server's registry
80
80
  with _iam_lock:
81
81
  _IAM_SERVERS[iam_server] = {
82
- IamParam.CLIENT_ID: client_id,
83
- IamParam.CLIENT_REALM: client_realm,
84
- IamParam.CLIENT_SECRET: client_secret,
85
- IamParam.RECIPIENT_ATTR: recipient_attr,
86
- IamParam.ADMIN_ID: admin_id,
87
- IamParam.ADMIN_SECRET: admin_secret,
88
- IamParam.LOGIN_TIMEOUT: login_timeout,
89
- IamParam.PK_LIFETIME: pk_lifetime,
90
- IamParam.URL_BASE: url_base,
82
+ ServerParam.CLIENT_ID: client_id,
83
+ ServerParam.CLIENT_REALM: client_realm,
84
+ ServerParam.CLIENT_SECRET: client_secret,
85
+ ServerParam.RECIPIENT_ATTR: recipient_attr,
86
+ ServerParam.ADMIN_ID: admin_id,
87
+ ServerParam.ADMIN_SECRET: admin_secret,
88
+ ServerParam.LOGIN_TIMEOUT: login_timeout,
89
+ ServerParam.PK_LIFETIME: pk_lifetime,
90
+ ServerParam.URL_BASE: url_base,
91
91
  # dynamic attributes
92
- IamParam.PK_EXPIRATION: 0,
93
- IamParam.PUBLIC_KEY: None,
94
- IamParam.USERS: {}
92
+ ServerParam.PK_EXPIRATION: 0,
93
+ ServerParam.PUBLIC_KEY: None,
94
+ ServerParam.USERS: {}
95
95
  }
96
96
 
97
97
 
@@ -112,7 +112,7 @@ def iam_setup_endpoints(flask_app: Flask,
112
112
  environment variables.
113
113
 
114
114
  :param flask_app: the Flask application
115
- :param iam_server: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
115
+ :param iam_server: identifies the supported *IAM* server
116
116
  :param callback_endpoint: endpoint for the callback from the front end
117
117
  :param callback_exchange_endpoint: endpoint for the combination callback and exchange
118
118
  :param exchange_endpoint: endpoint for requesting token exchange
@@ -131,7 +131,7 @@ def iam_setup_endpoints(flask_app: Flask,
131
131
  if "callback_exchange_endpoint" in defaulted_params:
132
132
  callback_exchange_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_CALLBACK_EXCHANGE")
133
133
  if "exchange_endpoint" in defaulted_params:
134
- callback_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE")
134
+ exchange_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE")
135
135
  if "login_endpoint" in defaulted_params:
136
136
  login_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGIN")
137
137
  if "logout_endpoint" in defaulted_params:
@@ -4,7 +4,7 @@ from logging import Logger
4
4
  from typing import Any
5
5
 
6
6
  from .iam_common import (
7
- IamServer, IamParam, _iam_lock,
7
+ IamServer, ServerParam, _iam_lock,
8
8
  _get_iam_registry, _get_public_key,
9
9
  _iam_server_from_endpoint, _iam_server_from_issuer
10
10
  )
@@ -62,7 +62,9 @@ def __request_validate(request: Request) -> Response:
62
62
  issuer: str = claims["payload"].get("iss")
63
63
  public_key: str | None = None
64
64
  recipient_attr: str | None = None
65
- recipient_id: str = request.values.get("user-id") or request.values.get("login")
65
+ recipient_id: str = (request.values.get("user-id") or request.values.get("login") or
66
+ (request.get_json(silent=True) or {}).get("user-id") or
67
+ (request.get_json(silent=True) or {}).get("login"))
66
68
  with _iam_lock:
67
69
  iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
68
70
  errors=None,
@@ -74,7 +76,7 @@ def __request_validate(request: Request) -> Response:
74
76
  errors=None,
75
77
  logger=__IAM_LOGGER)
76
78
  if registry:
77
- recipient_attr = registry[IamParam.RECIPIENT_ATTR]
79
+ recipient_attr = registry[ServerParam.RECIPIENT_ATTR]
78
80
  public_key = _get_public_key(iam_server=iam_server,
79
81
  errors=None,
80
82
  logger=__IAM_LOGGER)
@@ -137,7 +139,7 @@ def service_setup_server() -> Response:
137
139
  Entry point to setup a *IAM* server.
138
140
 
139
141
  These are the expected parameters in the request's body, in a JSON or as form data:
140
- - *iam_server*: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
142
+ - *iam_server*: identifies the supported *IAM* server
141
143
  - *admin_id*: identifies the realm administrator
142
144
  - *admin_secret*: password for the realm administrator
143
145
  - *client_id*: the client's identification with the *IAM* server
@@ -163,7 +165,7 @@ def service_setup_server() -> Response:
163
165
  :return: *Response OK*
164
166
  """
165
167
  # retrieve the request arguments
166
- args: dict[str, Any] = (dict(request.json) if request.is_json else dict(request.form)) or {}
168
+ args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
167
169
 
168
170
  # log the request
169
171
  if __IAM_LOGGER:
@@ -181,7 +183,7 @@ def service_setup_server() -> Response:
181
183
  return result
182
184
 
183
185
 
184
- # @flask_app.route(rule=<login_endpoint>, # IAM_ENDPOINT_LOGIN
186
+ # @flask_app.route(rule=<login_endpoint>,
185
187
  # methods=["GET"])
186
188
  def service_login() -> Response:
187
189
  """
@@ -209,7 +211,7 @@ def service_login() -> Response:
209
211
  result: Response | None = None
210
212
 
211
213
  # retrieve the request arguments
212
- args: dict[str, Any] = dict(request.args) or {}
214
+ args: dict[str, Any] = dict(request.args or {})
213
215
 
214
216
  # log the request
215
217
  if __IAM_LOGGER:
@@ -240,7 +242,7 @@ def service_login() -> Response:
240
242
  return result
241
243
 
242
244
 
243
- # @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
245
+ # @flask_app.route(rule=<logout_endpoint>,
244
246
  # methods=["POST"])
245
247
  @jwt_required
246
248
  def service_logout() -> Response:
@@ -251,7 +253,7 @@ def service_logout() -> Response:
251
253
  the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
252
254
  if the endpoint is established with a call to *iam_setup_endpoints()*.
253
255
 
254
- The user is identified by the attribute *user-id* or "login", provided as a request parameter.
256
+ The user is identified by the attribute *user-id* or "login", provided in the body's *JSON*.
255
257
  If successful, remove all data relating to the user from the *IAM* server's registry.
256
258
  Otherwise, this operation fails silently, unless an error has ocurred.
257
259
 
@@ -261,7 +263,7 @@ def service_logout() -> Response:
261
263
  result: Response | None
262
264
 
263
265
  # retrieve the request arguments
264
- args: dict[str, Any] = dict(request.args) or {}
266
+ args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
265
267
 
266
268
  # log the request
267
269
  if __IAM_LOGGER:
@@ -292,8 +294,8 @@ def service_logout() -> Response:
292
294
  return result
293
295
 
294
296
 
295
- # @flask_app.route(rule=<callback_endpoint>, # IAM_ENDPOINT_CALLBACK
296
- # methods=["GET", "POST"])
297
+ # @flask_app.route(rule=<callback_endpoint>,
298
+ # methods=["GET"])
297
299
  def service_callback() -> Response:
298
300
  """
299
301
  Entry point for the callback from the *IAM* server on authentication operation.
@@ -319,7 +321,7 @@ def service_callback() -> Response:
319
321
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
320
322
  """
321
323
  # retrieve the request arguments
322
- args: dict[str, Any] = dict(request.args) or {}
324
+ args: dict[str, Any] = dict(request.args or {})
323
325
 
324
326
  # log the request
325
327
  if __IAM_LOGGER:
@@ -352,7 +354,7 @@ def service_callback() -> Response:
352
354
  return result
353
355
 
354
356
 
355
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
357
+ # @flask_app.route(rule=<callback_endpoint>,
356
358
  # methods=["POST"])
357
359
  def service_exchange() -> Response:
358
360
  """
@@ -365,7 +367,7 @@ def service_exchange() -> Response:
365
367
  If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
366
368
  Otherwise, *errors* will contain the appropriate error message.
367
369
 
368
- The expected request parameters are:
370
+ The expected request parameters, to be found in the body *JSON*, are:
369
371
  - user-id: identification for the reference user (alias: 'login')
370
372
  - access-token: the token to be exchanged
371
373
 
@@ -378,7 +380,7 @@ def service_exchange() -> Response:
378
380
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
379
381
  """
380
382
  # retrieve the request arguments
381
- args: dict[str, Any] = dict(request.args) or {}
383
+ args: dict[str, Any] = dict(request.get_json(silent=True) or request.form or {})
382
384
 
383
385
  # log the request
384
386
  if __IAM_LOGGER:
@@ -446,7 +448,7 @@ def service_callback_exchange() -> Response:
446
448
  result: Response | None = None
447
449
 
448
450
  # retrieve the request arguments
449
- args: dict[str, Any] = dict(request.args) or {}
451
+ args: dict[str, Any] = dict(request.args or {})
450
452
 
451
453
  # log the request
452
454
  if __IAM_LOGGER:
@@ -490,7 +492,7 @@ def service_callback_exchange() -> Response:
490
492
  return result
491
493
 
492
494
 
493
- # @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_TOKEN
495
+ # @flask_app.route(rule=<token_endpoint>,
494
496
  # methods=["GET"])
495
497
  def service_get_token() -> Response:
496
498
  """
@@ -544,7 +546,7 @@ def service_get_token() -> Response:
544
546
  return result
545
547
 
546
548
 
547
- # @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_USERINFO
549
+ # @flask_app.route(rule=<token_endpoint>,
548
550
  # methods=["GET"])
549
551
  @jwt_required
550
552
  def service_userinfo() -> Response:
@@ -14,6 +14,11 @@ from pypomes_core import (
14
14
  from threading import Lock
15
15
  from typing import Any, Final
16
16
 
17
+ _members: dict[str, str] = {key.upper(): key.lower() for key in
18
+ env_get_strs(key=f"{APP_PREFIX}_AUTH_PROVIDERS")}
19
+ IamProvider: type[StrEnum] = StrEnum("IamProvider", _members)
20
+ del _members
21
+
17
22
 
18
23
  class ProviderParam(StrEnum):
19
24
  """
@@ -36,37 +41,36 @@ class ProviderParam(StrEnum):
36
41
  __JWT_LOGGER: Logger | None = None
37
42
 
38
43
 
39
- def __get_provider_data() -> dict[str, dict[ProviderParam, Any]]:
44
+ def __get_provider_data() -> dict[IamProvider, dict[ProviderParam, Any]]:
40
45
  """
41
- Obtain the configuration data for select *JWT* providers.
46
+ Obtain the configuration data for select *IAM* providers.
42
47
 
43
- The configuration parameters for the JWT providers are specified with environment variables,
48
+ The configuration parameters for the *IAM* providers are specified with environment variables,
44
49
  or dynamically with *provider_setup_server()*. Specifying configuration parameters with
45
50
  environment variables can be done by following these steps:
46
51
 
47
52
  1. Specify *<APP_PREFIX>_AUTH_PROVIDERS* with a list of names (typically, in lower-case), and the data set
48
- below for each providers, where *<JWT>* stands for the provider's name in upper-case:
49
- - *<APP_PREFIX>_<JWT>_BODY_DATA* (optional)
50
- - *<APP_PREFIX>_<JWT>_CUSTOM_AUTH* (optional)
51
- - *<APP_PREFIX>_<JWT>_HEADER_DATA* (optional)
52
- - *<APP_PREFIX>_<JWT>_USER_ID* (required)
53
- - *<APP_PREFIX>_<JWT>_USER_SECRET* (required)
54
- - *<APP_PREFIX>_<JWT>_URL_TOKEN* (required)
53
+ below for each providers, where *<IAM>* stands for the provider's name in upper-case:
54
+ - *<APP_PREFIX>_<IAM>_BODY_DATA* (optional)
55
+ - *<APP_PREFIX>_<IAM>_CUSTOM_AUTH* (optional)
56
+ - *<APP_PREFIX>_<IAM>_HEADER_DATA* (optional)
57
+ - *<APP_PREFIX>_<IAM>_USER_ID* (required)
58
+ - *<APP_PREFIX>_<IAM>_USER_SECRET* (required)
59
+ - *<APP_PREFIX>_<IAM>_URL_TOKEN* (required)
55
60
 
56
61
  2. The special environment variable *<APP_PREFIX>_PROVIDER_ENDPOINT_TOKEN* identifies the endpoint
57
62
  from which to obtain JWT tokens. It is not part of the *JWT* providers' setup, but is meant to be
58
63
  used by function *provider_setup_endpoint()*, wherein the value in that variable would represent
59
64
  the default value for its parameter.
60
65
 
61
- :return: the configuration data for the select *JWT* providers.
66
+ :return: the configuration data for the select *IAM* providers.
62
67
  """
63
68
  # initialize the return variable
64
69
  result: dict[str, dict[ProviderParam, Any]] = {}
65
70
 
66
- servers: list[str] = env_get_strs(key=f"{APP_PREFIX}_AUTH_PROVIDERS") or []
67
- for server in servers:
68
- prefix = server.upper()
69
- result[server] = {
71
+ for provider in IamProvider:
72
+ prefix = provider.name
73
+ result[provider] = {
70
74
  ProviderParam.USER_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_ID"),
71
75
  ProviderParam.USER_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_SECRET"),
72
76
  ProviderParam.BODY_DATA: env_get_obj(key=f"{APP_PREFIX}_{prefix}_BODY_DATA"),
@@ -98,7 +102,7 @@ def __get_provider_data() -> dict[str, dict[ProviderParam, Any]]:
98
102
  # "refresh-expiration": <timestamp>
99
103
  # }
100
104
  # }
101
- _provider_registry: Final[dict[str, dict[str, Any]]] = __get_provider_data()
105
+ _provider_registry: Final[dict[IamProvider, dict[str, Any]]] = __get_provider_data()
102
106
 
103
107
  # the lock protecting the data in '_provider_registry'
104
108
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
@@ -106,15 +110,15 @@ _provider_lock: Final[Lock] = Lock()
106
110
 
107
111
 
108
112
  @func_capture_params
109
- def provider_setup_server(provider_id: str,
110
- user_id: str = None,
111
- user_secret: str = None,
112
- custom_auth: tuple[str, str] = None,
113
- header_data: dict[str, str] = None,
114
- body_data: dict[str, str] = None,
115
- url_token: str = None) -> None:
113
+ def iam_setup_provider(iam_provider: IamProvider,
114
+ user_id: str = None,
115
+ user_secret: str = None,
116
+ custom_auth: tuple[str, str] = None,
117
+ header_data: dict[str, str] = None,
118
+ body_data: dict[str, str] = None,
119
+ url_token: str = None) -> None:
116
120
  """
117
- Setup the *JWT* provider *provider_id*.
121
+ Setup the *IAM* provider *iam_provider*.
118
122
 
119
123
  For the parameters not effectively passed, an attempt is made to obtain a value from the corresponding
120
124
  environment variable.
@@ -128,7 +132,7 @@ def provider_setup_server(provider_id: str,
128
132
  key-value pairs (such as *['grant_type', 'client_credentials']*), to be added to the request body,
129
133
  may be specified in *body_data*.
130
134
 
131
- :param provider_id: the provider's identification
135
+ :param iam_provider: the provider's identification
132
136
  :param user_id: the basic authorization user
133
137
  :param user_secret: the basic authorization password
134
138
  :param custom_auth: optional key names for sending the credentials as key-value pairs in the body of the request
@@ -142,7 +146,7 @@ def provider_setup_server(provider_id: str,
142
146
  defaulted_params: list[str] = func_defaulted_params.get()
143
147
 
144
148
  # read from the environment variables
145
- prefix: str = provider_id.upper()
149
+ prefix: str = iam_provider.name
146
150
  if "user_id" in defaulted_params:
147
151
  user_id = env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_ID")
148
152
  if "user_secret" in defaulted_params:
@@ -157,7 +161,7 @@ def provider_setup_server(provider_id: str,
157
161
  url_token = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_TOKEN")
158
162
 
159
163
  with _provider_lock:
160
- _provider_registry[provider_id] = {
164
+ _provider_registry[iam_provider] = {
161
165
  ProviderParam.BODY_DATA: body_data,
162
166
  ProviderParam.CUSTOM_AUTH: custom_auth,
163
167
  ProviderParam.HEADER_DATA: header_data,
@@ -209,7 +213,7 @@ def provider_setup_logger(logger: Logger) -> None:
209
213
  __JWT_LOGGER = logger
210
214
 
211
215
 
212
- # @flask_app.route(rule=<token_endpoint>, # IAM_PROVIDER_ENDPOINT_TOKEN
216
+ # @flask_app.route(rule=<token_endpoint>,
213
217
  # methods=["GET"])
214
218
  def service_get_token() -> Response:
215
219
  """
@@ -233,17 +237,18 @@ def service_get_token() -> Response:
233
237
  ensure_ascii=False)}")
234
238
 
235
239
  # obtain the provider JWT
236
- provider_id: str = args.get("jwt-provider")
240
+ provider_id: str = args.get("iam-provider")
241
+ iam_provider: IamProvider = IamProvider(provider_id) if provider_id in IamProvider else None
237
242
 
238
243
  # retrieve the token
239
244
  token: str | None = None
240
245
  errors: list[str] = []
241
- if provider_id:
242
- token: str = provider_get_token(provider_id=provider_id,
246
+ if iam_provider:
247
+ token: str = provider_get_token(iam_provider=iam_provider,
243
248
  errors=errors,
244
249
  logger=__JWT_LOGGER)
245
250
  else:
246
- msg: str = "JWT provider not informed"
251
+ msg: str = "IAM provider unknown or not informed"
247
252
  errors.append(msg)
248
253
  if __JWT_LOGGER:
249
254
  __JWT_LOGGER.error(msg=msg)
@@ -261,13 +266,13 @@ def service_get_token() -> Response:
261
266
  return result
262
267
 
263
268
 
264
- def provider_get_token(provider_id: str,
269
+ def provider_get_token(iam_provider: IamProvider,
265
270
  errors: list[str] = None,
266
271
  logger: Logger = None) -> str | None:
267
272
  """
268
273
  Obtain an JWT token from the external provider *provider_id*.
269
274
 
270
- :param provider_id: the provider's identification
275
+ :param iam_provider: the provider's identification
271
276
  :param errors: incidental error messages
272
277
  :param logger: optional logger
273
278
  :return: the JWT token, or *None* if error
@@ -278,7 +283,7 @@ def provider_get_token(provider_id: str,
278
283
  result: str | None = None
279
284
 
280
285
  with _provider_lock:
281
- provider: dict[str, Any] = _provider_registry.get(provider_id)
286
+ provider: dict[str, Any] = _provider_registry.get(iam_provider)
282
287
  if provider:
283
288
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
284
289
  if now < provider.get(ProviderParam.ACCESS_EXPIRATION):
@@ -334,7 +339,7 @@ def provider_get_token(provider_id: str,
334
339
  if refresh_exp else sys.maxsize
335
340
 
336
341
  elif logger or isinstance(errors, list):
337
- msg: str = f"Unknown provider '{provider_id}'"
342
+ msg: str = f"Unknown provider '{iam_provider}'"
338
343
  if logger:
339
344
  logger.error(msg=msg)
340
345
  if isinstance(errors, list):
File without changes
File without changes
File without changes
File without changes