pypomes-iam 0.2.9__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pypomes-iam might be problematic. Click here for more details.

pypomes_iam/iam_common.py CHANGED
@@ -1,427 +1,388 @@
1
- import json
2
1
  import requests
3
- import secrets
4
- import string
5
2
  import sys
6
- from cachetools import Cache
7
3
  from datetime import datetime
8
- from enum import StrEnum
9
- from flask import Request
4
+ from enum import StrEnum, auto
10
5
  from logging import Logger
11
6
  from pypomes_core import TZ_LOCAL, exc_format
12
7
  from pypomes_crypto import crypto_jwk_convert
8
+ from threading import RLock
13
9
  from typing import Any, Final
14
10
 
15
11
 
16
12
  class IamServer(StrEnum):
17
- IAM_JUSRBR = "iam-jusbr",
18
- IAM_KEYCLOAK = "iam-keycloak"
13
+ """
14
+ Supported IAM servers.
15
+ """
16
+ JUSBR = auto()
17
+ KEYCLOAK = auto()
18
+
19
19
 
20
+ class IamParam(StrEnum):
21
+ """
22
+ Parameters for configuring *IAM* servers.
23
+ """
24
+ ADMIN_ID = "admin-id"
25
+ ADMIN_SECRET = "admin-secret"
26
+ CLIENT_ID = "client-id"
27
+ CLIENT_REALM = "client-realm"
28
+ CLIENT_SECRET = "client-secret"
29
+ ENDPOINT_CALLBACK = "endpoint-callback"
30
+ ENDPOINT_LOGIN = "endpoint-login"
31
+ ENDPOINT_LOGOUT = "endpoint_logout"
32
+ ENDPOINT_TOKEN = "endpoint-token"
33
+ ENDPOINT_EXCHANGE = "endpoint-exchange"
34
+ LOGIN_TIMEOUT = "login-timeout"
35
+ PK_EXPIRATION = "pk-expiration"
36
+ PK_LIFETIME = "pk-lifetime"
37
+ PUBLIC_KEY = "public-key"
38
+ RECIPIENT_ATTR = "recipient-attr"
39
+ URL_BASE = "url-base"
40
+ USERS = "users"
41
+
42
+
43
+ class UserParam(StrEnum):
44
+ """
45
+ Parameters for handling *IAM* users.
46
+ """
47
+ ACCESS_TOKEN = "access-token"
48
+ REFRESH_TOKEN = "refresh-token"
49
+ ACCESS_EXPIRATION = "access-expiration"
50
+ REFRESH_EXPIRATION = "refresh-expiration"
51
+ # transient attributes
52
+ LOGIN_EXPIRATION = "login-expiration"
53
+ LOGIN_ID = "login-id"
54
+ REDIRECT_URI = "redirect-uri"
55
+
56
+
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
20
82
 
21
83
  # registry structure:
22
84
  # { <IamServer>:
23
85
  # {
86
+ # "base-url": <str>,
87
+ # "admin-id": <str>,
88
+ # "admin-secret": <str>,
24
89
  # "client-id": <str>,
25
90
  # "client-secret": <str>,
91
+ # "client-realm": <str,
26
92
  # "client-timeout": <int>,
27
- # "public_key": <str>,
93
+ # "recipient-attr": <str>,
94
+ # "public-key": <str>,
28
95
  # "pk-lifetime": <int>,
29
96
  # "pk-expiration": <int>,
30
- # "base-url": <str>,
31
- # "logger": <Logger>,
32
- # "cache": <FIFOCache>,
33
- # "redirect-uri": <str> <-- transient
97
+ # "users": {}
34
98
  # },
35
99
  # ...
36
100
  # }
37
- # data in "cache":
101
+ # data in "users":
38
102
  # {
39
- # "users": {
40
- # "<user-id>": {
41
- # "access-token": <str>
42
- # "refresh-token": <str>
43
- # "access-expiration": <timestamp>,
44
- # "refresh-expiration": <timestamp>,
45
- # "login-expiration": <timestamp>, <-- transient
46
- # "login-id": <str>, <-- transient
47
- # }
48
- # },
103
+ # "<user-id>": {
104
+ # "access-token": <str>
105
+ # "refresh-token": <str>
106
+ # "access-expiration": <timestamp>,
107
+ # "refresh-expiration": <timestamp>,
108
+ # # transient attributes
109
+ # "login-expiration": <timestamp>,
110
+ # "login-id": <str>,
111
+ # "redirect-uri": <str>
112
+ # },
49
113
  # ...
50
114
  # }
51
- IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
115
+ _IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = {}
52
116
 
53
117
 
54
- def _service_login(registry: dict[str, Any],
55
- args: dict[str, Any],
56
- logger: Logger | None) -> dict[str, str]:
57
- """
58
- Build the callback URL for redirecting the request to the IAM's authentication page.
59
-
60
- :param registry: the registry holding the authentication data
61
- :param args: the arguments passed when requesting the service
62
- :param logger: optional logger
63
- :return: the callback URL, with the appropriate parameters
64
- """
65
- # retrieve user data
66
- oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
67
-
68
- # build the user data
69
- # ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
70
- user_data: dict[str, Any] = _get_user_data(registry=registry,
71
- user_id=oauth_state,
72
- logger=logger)
73
- user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
74
- user_data["login-id"] = user_id
75
- timeout: int = _get_login_timeout(registry=registry)
76
- user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout if timeout else None
77
- redirect_uri: str = args.get("redirect-uri")
78
- registry["redirect-uri"] = redirect_uri
79
-
80
- # build the login url
81
- return {
82
- "login-url": (f"{registry["base-url"]}/protocol/openid-connect/auth"
83
- f"?response_type=code&scope=openid"
84
- f"&client_id={registry["client-id"]}"
85
- f"&redirect_uri={redirect_uri}"
86
- f"&state={oauth_state}")
87
- }
88
-
89
-
90
- def _service_logout(registry: dict[str, Any],
91
- args: dict[str, Any],
92
- logger: Logger | None) -> None:
93
- """
94
- Remove all data associating *user_id* from *registry*.
95
-
96
- :param registry: the registry holding the authentication data
97
- :param args: the arguments passed when requesting the service
98
- :param logger: optional logger
99
- """
100
- # remove the user data
101
- user_id: str = args.get("user-id") or args.get("login")
102
- if user_id:
103
- cache: Cache = registry["cache"]
104
- users: dict[str, dict[str, Any]] = cache.get("users")
105
- if user_id in users:
106
- users.pop(user_id)
107
- if logger:
108
- logger.debug(msg=f"User '{user_id}' removed from the registry")
118
+ # the lock protecting the data in '_IAM_SERVERS'
119
+ # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
120
+ _iam_lock: Final[RLock] = RLock()
109
121
 
110
122
 
111
- def _service_callback(registry: dict[str, Any],
112
- args: dict[str, Any],
113
- errors: list[str],
114
- logger: Logger | None) -> tuple[str, str]:
123
+ def _iam_server_from_endpoint(endpoint: str,
124
+ errors: list[str] | None,
125
+ logger: Logger | None) -> IamServer | None:
115
126
  """
116
- Entry point for the callback from JusBR via the front-end application on authentication operation.
127
+ Retrieve the registered *IAM* server associated with the service's invocation *endpoint*.
117
128
 
118
- :param registry: the registry holding the authentication data
119
- :param args: the arguments passed when requesting the service
120
- :param errors: incidental errors
129
+ :param endpoint: the service's invocation endpoint
130
+ :param errors: incidental error messages
121
131
  :param logger: optional logger
132
+ :return: the corresponding *IAM* server, or *None* if one could not be obtained
122
133
  """
123
- from .token_pomes import token_validate
124
-
125
134
  # initialize the return variable
126
- result: tuple[str, str] | None = None
127
-
128
- # retrieve the users authentication data
129
- cache: Cache = registry["cache"]
130
- users: dict[str, dict[str, Any]] = cache.get("users")
131
-
132
- # validate the OAuth2 state
133
- oauth_state: str = args.get("state")
134
- user_data: dict[str, Any] | None = None
135
- if oauth_state:
136
- for user, data in users.items():
137
- if user == oauth_state:
138
- user_data = data
139
- break
140
-
141
- # exchange 'code' for the token
142
- if user_data:
143
- expiration: int = user_data["login-expiration"] or sys.maxsize
144
- if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
145
- errors.append("Operation timeout")
146
- else:
147
- users.pop(oauth_state)
148
- code: str = args.get("code")
149
- body_data: dict[str, Any] = {
150
- "grant_type": "authorization_code",
151
- "code": code,
152
- "redirect_uri": registry.get("redirect-uri"),
153
- }
154
- token = _post_for_token(registry=registry,
155
- user_data=user_data,
156
- body_data=body_data,
157
- errors=errors,
158
- logger=logger)
159
- # retrieve the token's claims
160
- if not errors:
161
- public_key: str = _get_public_key(registry=registry,
162
- logger=logger)
163
- token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
164
- issuer=registry["base-url"],
165
- public_key=public_key,
166
- errors=errors,
167
- logger=logger)
168
- if not errors:
169
- token_user: str = token_claims["payload"].get("preferred_username")
170
- login_id = user_data.pop("login-id", None)
171
- if not login_id or (login_id == token_user):
172
- users[token_user] = user_data
173
- result = (token_user, token)
174
- else:
175
- errors.append(f"Token was issued to user '{token_user}'")
176
- else:
177
- errors.append("Unknown state received")
135
+ result: IamServer | None = None
178
136
 
179
- if errors and logger:
180
- logger.error(msg="; ".join(errors))
137
+ for iam_server in _IAM_SERVERS:
138
+ if endpoint.startswith(iam_server):
139
+ result = iam_server
140
+ break
141
+
142
+ if not result:
143
+ msg: str = f"Unable to find a IAM server to service endpoint '{endpoint}'"
144
+ if logger:
145
+ logger.error(msg=msg)
146
+ if isinstance(errors, list):
147
+ errors.append(msg)
181
148
 
182
149
  return result
183
150
 
184
151
 
185
- def _service_token(registry: dict[str, Any],
186
- args: dict[str, Any],
187
- errors: list[str] = None,
188
- logger: Logger = None) -> str:
152
+ def _iam_server_from_issuer(issuer: str,
153
+ errors: list[str] | None,
154
+ logger: Logger | None) -> IamServer | None:
189
155
  """
190
- Retrieve the authentication token for user *user_id*.
156
+ Retrieve the registered *IAM* server associated with the token's *issuer*.
191
157
 
192
- :param registry: the registry holding the authentication data
193
- :param args: the arguments passed when requesting the service
158
+ :param issuer: the token's issuer
194
159
  :param errors: incidental error messages
195
160
  :param logger: optional logger
196
- :return: the token for *user_id*, or *None* if error
161
+ :return: the corresponding *IAM* server, or *None* if one could not be obtained
197
162
  """
198
163
  # initialize the return variable
199
- result: str | None = None
164
+ result: IamServer | None = None
200
165
 
201
- user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
202
- user_data: dict[str, Any] = _get_user_data(registry=registry,
203
- user_id=user_id,
204
- logger=logger)
205
- err_msg: str | None = None
206
- token: str = user_data["access-token"]
207
- if token:
208
- access_expiration: int = user_data.get("access-expiration")
209
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
210
- if now < access_expiration:
211
- result = token
212
- else:
213
- # access token has expired
214
- refresh_token: str = user_data["refresh-token"]
215
- if refresh_token:
216
- refresh_expiration = user_data["refresh-expiration"]
217
- if now < refresh_expiration:
218
- body_data: dict[str, str] = {
219
- "grant_type": "refresh_token",
220
- "refresh_token": refresh_token
221
- }
222
- result = _post_for_token(registry=registry,
223
- user_data=user_data,
224
- body_data=body_data,
225
- errors=errors,
226
- logger=logger)
227
- else:
228
- # refresh token has expired
229
- err_msg = "Access and refresh tokens expired"
230
- else:
231
- err_msg = "Access token expired, no refresh token available"
232
- else:
233
- err_msg = f"User '{user_id}' not authenticated"
234
-
235
- if err_msg and (logger or isinstance(errors, list)):
236
- err_msg: str = f"User '{user_id}' not authenticated"
237
- if isinstance(errors, list):
238
- errors.append(err_msg)
166
+ for iam_server, registry in _IAM_SERVERS.items():
167
+ base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
168
+ if base_url == issuer:
169
+ result = IamServer(iam_server)
170
+ break
171
+
172
+ if not result:
173
+ msg: str = f"Unable to find a IAM server associated with token issuer '{issuer}'"
239
174
  if logger:
240
- logger.error(msg=err_msg)
241
- logger.error(msg=err_msg)
175
+ logger.error(msg=msg)
176
+ if isinstance(errors, list):
177
+ errors.append(msg)
242
178
 
243
179
  return result
244
180
 
245
181
 
246
- def _get_public_key(registry: dict[str, Any],
182
+ def _get_public_key(iam_server: IamServer,
183
+ errors: list[str] | None,
247
184
  logger: Logger | None) -> str:
248
185
  """
249
- Obtain the public key used by the *IAM* to sign the authentication tokens.
186
+ Obtain the public key used by *iam_server* to sign the authentication tokens.
187
+
188
+ This is accomplished by requesting the token issuer for its *JWKS* (JSON Web Key Set),
189
+ containing the public keys used for various purposes, as indicated in the attribute *use*:
190
+ - *enc*: the key is intended for encryption
191
+ - *sig*: the key is intended for digital signature
192
+ - *wrap*: the key is intended for key wrapping
193
+
194
+ A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
195
+ {
196
+ "keys": [
197
+ {
198
+ "kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
199
+ "kty": "RSA",
200
+ "alg": "RSA256",
201
+ "use": "sig",
202
+ "n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
203
+ "e": "AQAB",
204
+ "x5c": [
205
+ "MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
206
+ ],
207
+ "x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
208
+ "x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
209
+ },
210
+ ...
211
+ ]
212
+ }
250
213
 
251
- The public key is saved in *registry*.
214
+ Once the signature key is obtained, it is converted from its original *JWK* (JSON Web Key) format
215
+ to *PEM* (Privacy-Enhanced Mail) format. The public key is saved in *iam_server*'s registry.
252
216
 
253
- :param registry: the registry holding the authentication data
254
- :return: the public key, in *PEM* format
217
+ :param iam_server: the reference registered *IAM* server
218
+ :param errors: incidental error messages
219
+ :param logger: optional logger
220
+ :return: the public key in *PEM* format, or *None* if error
255
221
  """
256
222
  # initialize the return variable
257
223
  result: str | None = None
258
224
 
259
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
260
- if now > registry["pk-expiration"]:
261
- # obtain a new public key
262
- url: str = f"{registry["base-url"]}/protocol/openid-connect/certs"
263
- if logger:
264
- logger.debug(msg=f"GET '{url}'")
265
- response: requests.Response = requests.get(url=url)
266
- if response.status_code == 200:
267
- # request succeeded
225
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
226
+ errors=errors,
227
+ logger=logger)
228
+ if registry:
229
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
230
+ if now > registry[IamParam.PK_EXPIRATION]:
231
+ # obtain the JWKS (JSON Web Key Set) from the token issuer
232
+ base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
233
+ url: str = f"{base_url}/protocol/openid-connect/certs"
268
234
  if logger:
269
- logger.debug(msg=f"GET success, status {response.status_code}")
270
- reply: dict[str, Any] = response.json()
271
- result = crypto_jwk_convert(jwk=reply["keys"][0],
272
- fmt="PEM")
273
- registry["public-key"] = result
274
- lifetime: int = registry["pk-lifetime"] or 0
275
- registry["pk-expiration"] = now + lifetime
276
- elif logger:
277
- msg: str = f"GET failure, status {response.status_code}, reason '{response.reason}'"
278
- if hasattr(response, "content") and response.content:
279
- msg += f", content '{response.content}'"
280
- logger.error(msg=msg)
281
- else:
282
- result = registry["public-key"]
235
+ logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
236
+ logger.debug(msg=f"GET {url}")
237
+ try:
238
+ response: requests.Response = requests.get(url=url)
239
+ if response.status_code == 200:
240
+ # request succeeded
241
+ if logger:
242
+ logger.debug(msg=f"GET success, status {response.status_code}")
243
+ # select the appropriate JWK
244
+ reply: dict[str, list[dict[str, str]]] = response.json()
245
+ jwk: dict[str, str] | None = None
246
+ for key in reply["keys"]:
247
+ if key.get("use") == "sig":
248
+ jwk = key
249
+ break
250
+ if jwk:
251
+ # convert from 'JWK' to 'PEM' and save it for further use
252
+ result = crypto_jwk_convert(jwk=jwk,
253
+ fmt="PEM")
254
+ registry[IamParam.PUBLIC_KEY] = result
255
+ lifetime: int = registry[IamParam.PK_LIFETIME] or 0
256
+ registry[IamParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
257
+ if logger:
258
+ logger.debug("Public key obtained and saved")
259
+ else:
260
+ msg = "Signature public key missing from the token issuer's JWKS"
261
+ if logger:
262
+ logger.error(msg=msg)
263
+ if isinstance(errors, list):
264
+ errors.append(msg)
265
+ elif logger:
266
+ msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
267
+ if hasattr(response, "content") and response.content:
268
+ msg += f", content {response.content}"
269
+ logger.error(msg=msg)
270
+ if isinstance(errors, list):
271
+ errors.append(msg)
272
+ except Exception as e:
273
+ # the operation raised an exception
274
+ msg = exc_format(exc=e,
275
+ exc_info=sys.exc_info())
276
+ if logger:
277
+ logger.error(msg=msg)
278
+ if isinstance(errors, list):
279
+ errors.append(msg)
280
+ else:
281
+ result = registry[IamParam.PUBLIC_KEY]
283
282
 
284
283
  return result
285
284
 
286
285
 
287
- def _get_login_timeout(registry: dict[str, Any]) -> int | None:
286
+ def _get_login_timeout(iam_server: IamServer,
287
+ errors: list[str] | None,
288
+ logger: Logger) -> int | None:
288
289
  """
289
- Retrieve from *registry* the timeout currently applicable for the login operation.
290
+ Retrieve the timeout currently applicable for the login operation.
290
291
 
291
- :param registry: the registry holding the authentication data
292
- :return: the current login timeout, or *None* if none has been set.
292
+ :param iam_server: the reference registered *IAM* server
293
+ :param errors: incidental error messages
294
+ :param logger: optional logger
295
+ :return: the current login timeout, or *None* if the server is unknown or none has been set.
293
296
  """
294
- timeout: int = registry.get("client-timeout")
295
- return timeout if isinstance(timeout, int) and timeout > 0 else None
297
+ # initialize the return variable
298
+ result: int | None = None
299
+
300
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
301
+ errors=errors,
302
+ logger=logger)
303
+ if registry:
304
+ timeout: int = registry.get("client-timeout")
305
+ if isinstance(timeout, int) and timeout > 0:
306
+ result = timeout
307
+
308
+ return result
296
309
 
297
310
 
298
- def _get_user_data(registry: dict[str, Any],
311
+ def _get_user_data(iam_server: IamServer,
299
312
  user_id: str,
300
- logger: Logger | None) -> dict[str, Any]:
313
+ errors: list[str] | None,
314
+ logger: Logger | None) -> dict[str, Any] | None:
301
315
  """
302
- Retrieve the data for *user_id* from *registry*.
316
+ Retrieve the data for *user_id* from *iam_server*'s registry.
303
317
 
304
318
  If an entry is not found for *user_id* in the registry, it is created.
305
319
  It will remain there until the user is logged out.
306
320
 
307
- :param registry: the registry holding the authentication data
308
- :return: the data for *user_id* in the registry
321
+ :param iam_server: the reference registered *IAM* server
322
+ :param errors: incidental error messages
323
+ :param logger: optional logger
324
+ :return: the data for *user_id* in *iam_server*'s registry, or *None* if the server is unknown
309
325
  """
310
- cache: Cache = registry["cache"]
311
- users: dict[str, dict[str, Any]] = cache.get("users")
312
- result: dict[str, Any] = users.get(user_id)
313
- if not result:
314
- result = {
315
- "access-token": None,
316
- "refresh-token": None,
317
- "access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
318
- "refresh-expiration": sys.maxsize
319
- }
320
- users[user_id] = result
321
- if logger:
322
- logger.debug(msg=f"Entry for '{user_id}' added to the registry")
323
- elif logger:
324
- logger.debug(msg=f"Entry for '{user_id}' obtained from the registry")
326
+ # initialize the return variable
327
+ result: dict[str, Any] | None = None
328
+
329
+ users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
330
+ errors=errors,
331
+ logger=logger)
332
+ if isinstance(users, dict):
333
+ result = users.get(user_id)
334
+ if not result:
335
+ result = {
336
+ UserParam.ACCESS_TOKEN: None,
337
+ UserParam.REFRESH_TOKEN: None,
338
+ UserParam.ACCESS_EXPIRATION: int(datetime.now(tz=TZ_LOCAL).timestamp()),
339
+ UserParam.REFRESH_EXPIRATION: sys.maxsize
340
+ }
341
+ users[user_id] = result
342
+ if logger:
343
+ logger.debug(msg=f"Entry for '{user_id}' added to {iam_server}'s registry")
344
+ elif logger:
345
+ logger.debug(msg=f"Entry for '{user_id}' obtained from {iam_server}'s registry")
325
346
 
326
347
  return result
327
348
 
328
349
 
329
- def _post_for_token(registry: dict[str, Any],
330
- user_data: dict[str, Any],
331
- body_data: dict[str, Any],
332
- errors: list[str] | None,
333
- logger: Logger | None) -> str | None:
350
+ def _get_iam_registry(iam_server: IamServer,
351
+ errors: list[str] | None,
352
+ logger: Logger | None) -> dict[str, Any]:
334
353
  """
335
- Send a POST request to obtain the authentication token data, and return the access token.
336
-
337
- For token exchange, *body_data* will have the attributes
338
- - "grant_type": "authorization_code"
339
- - "code": <16-character-random-code>
340
- - "redirect_uri": <redirect-uri>
341
- For token refresh, *body_data* will have the attributes
342
- - "grant_type": "refresh_token"
343
- - "refresh_token": <current-refresh-token>
344
-
345
- If the operation is successful, the token data is stored in the registry.
346
- Otherwise, *errors* will contain the appropriate error message.
347
-
348
- :param registry: the registry holding the authentication data
349
- :param user_data: the user's data in the registry
350
- :param body_data: the data to send in the body of the request
351
- :param errors: incidental errors
354
+ Retrieve the registry associated with *iam_server*.
355
+
356
+ :param iam_server: the reference registered *IAM* server
357
+ :param errors: incidental error messages
352
358
  :param logger: optional logger
353
- :return: the access token obtained, or *None* if error
359
+ :return: the registry associated with *iam_server*, or *None* if the server is unknown
354
360
  """
355
- # initialize the return variable
356
- result: str | None = None
361
+ # assign the return variable
362
+ result: dict[str, Any] = _IAM_SERVERS.get(iam_server)
357
363
 
358
- # complete the data to send in body of request
359
- body_data["client_id"] = registry["client-id"]
360
- client_secret: str = registry["client-secret"]
361
- if client_secret:
362
- body_data["client_secret"] = client_secret
363
-
364
- # obtain the token
365
- err_msg: str | None = None
366
- url: str = registry["base-url"] + "/protocol/openid-connect/token"
367
- now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
368
- if logger:
369
- logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
370
- ensure_ascii=False)}")
371
- try:
372
- # typical return on a token request:
373
- # {
374
- # "token_type": "Bearer",
375
- # "access_token": <str>,
376
- # "expires_in": <number-of-seconds>,
377
- # "refresh_token": <str>,
378
- # "refesh_expires_in": <number-of-seconds>
379
- # }
380
- response: requests.Response = requests.post(url=url,
381
- data=body_data)
382
- if response.status_code == 200:
383
- # request succeeded
384
- if logger:
385
- logger.debug(msg=f"POST success, status {response.status_code}")
386
- reply: dict[str, Any] = response.json()
387
- result = reply.get("access_token")
388
- user_data["access-token"] = result
389
- # on token refresh, keep current refresh token if a new one is not provided
390
- user_data["refresh-token"] = reply.get("refresh_token") or body_data.get("refresh_token")
391
- user_data["access-expiration"] = now + reply.get("expires_in")
392
- refresh_expiration: int = user_data.get("refresh_expires_in")
393
- user_data["refresh-expiration"] = (now + refresh_expiration) if refresh_expiration else sys.maxsize
394
- else:
395
- # request resulted in error
396
- err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
397
- if hasattr(response, "content") and response.content:
398
- err_msg += f", content '{response.content}'"
399
- if response.status_code == 400 and body_data.get("grant_type") == "refresh_token":
400
- # refresh token is no longer valid
401
- user_data["refresh-token"] = None
402
- except Exception as e:
403
- # the operation raised an exception
404
- err_msg = exc_format(exc=e,
405
- exc_info=sys.exc_info())
406
- err_msg = f"POST '{url}': error '{err_msg}'"
407
-
408
- if err_msg:
409
- if isinstance(errors, list):
410
- errors.append(err_msg)
364
+ if not result:
365
+ msg = f"Unknown IAM server '{iam_server}'"
411
366
  if logger:
412
- logger.error(msg=err_msg)
367
+ logger.error(msg=msg)
368
+ if isinstance(errors, list):
369
+ errors.append(msg)
413
370
 
414
371
  return result
415
372
 
416
373
 
417
- def _log_init(request: Request) -> str:
374
+ def _get_iam_users(iam_server: IamServer,
375
+ errors: list[str] | None,
376
+ logger: Logger | None) -> dict[str, dict[str, Any]]:
418
377
  """
419
- Build the messages for logging the request entry.
378
+ Retrieve the users data storage in *iam_server*'s registry.
420
379
 
421
- :param request: the Request object
422
- :return: the log message
380
+ :param iam_server: the reference registered *IAM* server
381
+ :param errors: incidental error messages
382
+ :param logger: optional logger
383
+ :return: the users data storage in *iam_server*'s registry, or *None* if the server is unknown
423
384
  """
424
-
425
- params: str = json.dumps(obj=request.args,
426
- ensure_ascii=False)
427
- return f"Request {request.method}:{request.path}, params {params}"
385
+ registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
386
+ errors=errors,
387
+ logger=logger)
388
+ return registry[IamParam.USERS] if registry else None