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.
@@ -9,8 +9,8 @@ from .iam_common import (
9
9
  _iam_server_from_endpoint, _iam_server_from_issuer
10
10
  )
11
11
  from .iam_actions import (
12
- action_login, action_logout,
13
- action_token, action_exchange, action_callback
12
+ iam_login, iam_logout, iam_callback,
13
+ iam_exchange, iam_get_token, iam_userinfo
14
14
  )
15
15
  from .token_pomes import token_get_claims, token_validate
16
16
 
@@ -23,7 +23,10 @@ def jwt_required(func: callable) -> callable:
23
23
  """
24
24
  Create a decorator to authenticate service endpoints with JWT tokens.
25
25
 
26
+ The decorated function must be a registered endpoint to a *Flask* application.
27
+
26
28
  :param func: the function being decorated
29
+ :return: the return from the call to *func*, or a *Response NOT AUTHORIZED* if the authentication failed
27
30
  """
28
31
  # ruff: noqa: ANN003 - Missing type annotation for *{name}
29
32
  def wrapper(*args, **kwargs) -> Response:
@@ -44,22 +47,20 @@ def __request_validate(request: Request) -> Response:
44
47
  Because this code has a high usage frequency, only authentication failures are logged.
45
48
 
46
49
  :param request: the *request* to be verified
47
- :return: *None* if the *request* is valid, otherwise a *Response* reporting the error
50
+ :return: *None* if the *request* is valid, otherwise a *Response NOT AUTHORIZED*
48
51
  """
49
52
  # initialize the return variable
50
53
  result: Response | None = None
51
54
 
52
- # retrieve the authorization from the request header
53
- auth_header: str = request.headers.get("Authorization")
54
-
55
55
  # validate the authorization token
56
56
  bad_token: bool = True
57
- if auth_header and auth_header.startswith("Bearer "):
58
- # extract and validate the JWT access token
59
- token: str = auth_header.split(" ")[1]
57
+ token: str = __get_bearer_token(request=request)
58
+ if token:
59
+ # extract token claims
60
60
  claims: dict[str, Any] = token_get_claims(token=token)
61
61
  if claims:
62
62
  issuer: str = claims["payload"].get("iss")
63
+ public_key: str | None = None
63
64
  recipient_attr: str | None = None
64
65
  recipient_id: str = request.values.get("user-id") or request.values.get("login")
65
66
  with _iam_lock:
@@ -74,17 +75,17 @@ def __request_validate(request: Request) -> Response:
74
75
  logger=__IAM_LOGGER)
75
76
  if registry:
76
77
  recipient_attr = registry[IamParam.RECIPIENT_ATTR]
77
- public_key: str = _get_public_key(iam_server=iam_server,
78
- errors=None,
79
- logger=__IAM_LOGGER)
78
+ public_key = _get_public_key(iam_server=iam_server,
79
+ errors=None,
80
+ logger=__IAM_LOGGER)
80
81
  # validate the token (log errors, only)
81
82
  errors: list[str] = []
82
- if public_key and token_validate(token=token,
83
- issuer=issuer,
84
- recipient_id=recipient_id,
85
- recipient_attr=recipient_attr,
86
- public_key=public_key,
87
- errors=errors):
83
+ if token_validate(token=token,
84
+ issuer=issuer,
85
+ recipient_id=recipient_id,
86
+ recipient_attr=recipient_attr,
87
+ public_key=public_key,
88
+ errors=errors):
88
89
  # token is valid
89
90
  bad_token = False
90
91
  elif __IAM_LOGGER:
@@ -99,7 +100,27 @@ def __request_validate(request: Request) -> Response:
99
100
  return result
100
101
 
101
102
 
102
- def logger_register(logger: Logger) -> None:
103
+ def __get_bearer_token(request: Request) -> str:
104
+ """
105
+ Retrieve the bearer token sent in the header of *request*.
106
+
107
+ This implementation assumes that HTTP requests are handled with the *Flask* framework.
108
+
109
+ :param request: the *request* to retrieve the token from
110
+ :return: the bearer token, or *None* if not found
111
+ """
112
+ # initialize the return variable
113
+ result: str | None = None
114
+
115
+ # retrieve the authorization from the request header
116
+ auth_header: str = request.headers.get("Authorization")
117
+ if auth_header and auth_header.startswith("Bearer "):
118
+ result: str = auth_header.split(" ")[1]
119
+
120
+ return result
121
+
122
+
123
+ def iam_setup_logger(logger: Logger) -> None:
103
124
  """
104
125
  Register the logger for HTTP services.
105
126
 
@@ -109,13 +130,66 @@ def logger_register(logger: Logger) -> None:
109
130
  __IAM_LOGGER = logger
110
131
 
111
132
 
112
- # @flask_app.route(rule=<login_endpoint>, # JUSBR_ENDPOINT_LOGIN
113
- # methods=["GET"])
114
- # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_ENDPOINT_LOGIN
133
+ # @flask_app.route(rule=<setup_server_endpoint>,
134
+ # methods=["POST"])
135
+ def service_setup_server() -> Response:
136
+ """
137
+ Entry point to setup a *IAM* server.
138
+
139
+ 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*)
141
+ - *admin_id*: identifies the realm administrator
142
+ - *admin_secret*: password for the realm administrator
143
+ - *client_id*: the client's identification with the *IAM* server
144
+ - *client_realm*: the client's realm
145
+ - *client_secret*: the client's password with the *IAM* server
146
+ - *login_timeout*: timeout for login authentication (in seconds,defaults to no timeout)
147
+ - *k_lifetime*: how long to use *IAM* server's public key, before refreshing it (in seconds)
148
+ - *recipient_attr*: attribute in the token's payload holding the token's subject
149
+ - *rl_base*: base URL to request services
150
+
151
+ For the parameters not effectively passed, an attempt is made to obtain a value from the corresponding
152
+ environment variables. Most parameters are required to have values, which must be assigned either
153
+ throught the function invocation, or from the corresponding environment variables.
154
+
155
+ The parameters *admin_id* and *admin_secret* are required only if performing administrative tasks is intended.
156
+ The optional parameter *ogin_timeout* refers to the maximum time in seconds allowed for the user
157
+ to login at the *IAM* server's login page, and defaults to no time limit.
158
+
159
+ The parameter *client_secret* is required in most requests to the *IAM* server. In the case
160
+ it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
161
+ the first time it is needed.
162
+
163
+ :return: *Response OK*
164
+ """
165
+ # retrieve the request arguments
166
+ args: dict[str, Any] = (dict(request.json) if request.is_json else dict(request.form)) or {}
167
+
168
+ # log the request
169
+ if __IAM_LOGGER:
170
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
171
+ ensure_ascii=False)}")
172
+ # setup the server
173
+ from .iam_pomes import iam_setup_server
174
+ iam_setup_server(**args)
175
+ result = Response(status=200)
176
+
177
+ # log the response
178
+ if __IAM_LOGGER:
179
+ __IAM_LOGGER.debug(msg=f"Response {result}")
180
+
181
+ return result
182
+
183
+
184
+ # @flask_app.route(rule=<login_endpoint>, # IAM_ENDPOINT_LOGIN
115
185
  # methods=["GET"])
116
186
  def service_login() -> Response:
117
187
  """
118
- Entry point for the IAM server's login service.
188
+ Entry point for the *IAM* server's login service.
189
+
190
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
191
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
192
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
119
193
 
120
194
  These are the expected request parameters:
121
195
  - user-id: optional, identifies the reference user (alias: 'login')
@@ -134,10 +208,13 @@ def service_login() -> Response:
134
208
  # declare the return variable
135
209
  result: Response | None = None
136
210
 
211
+ # retrieve the request arguments
212
+ args: dict[str, Any] = dict(request.args) or {}
213
+
137
214
  # log the request
138
215
  if __IAM_LOGGER:
139
- __IAM_LOGGER.debug(msg=_log_init(request=request))
140
-
216
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
217
+ ensure_ascii=False)}")
141
218
  errors: list[str] = []
142
219
  with _iam_lock:
143
220
  # retrieve the IAM server
@@ -146,10 +223,10 @@ def service_login() -> Response:
146
223
  logger=__IAM_LOGGER)
147
224
  if iam_server:
148
225
  # obtain the login URL
149
- login_url: str = action_login(iam_server=iam_server,
150
- args=request.args,
151
- errors=errors,
152
- logger=__IAM_LOGGER)
226
+ login_url: str = iam_login(iam_server=iam_server,
227
+ args=args,
228
+ errors=errors,
229
+ logger=__IAM_LOGGER)
153
230
  if login_url:
154
231
  result = jsonify({"login-url": login_url})
155
232
  if errors:
@@ -158,18 +235,21 @@ def service_login() -> Response:
158
235
 
159
236
  # log the response
160
237
  if __IAM_LOGGER:
161
- __IAM_LOGGER.debug(msg=f"Response {result}, {result.get_data(as_text=True)}")
238
+ __IAM_LOGGER.debug(msg=f"Response {result}; {result.get_data(as_text=True)}")
162
239
 
163
240
  return result
164
241
 
165
242
 
166
- # @flask_app.route(rule=<logout_endpoint>, # JUSBR_ENDPOINT_LOGOUT
167
- # methods=["GET"])
168
- # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_ENDPOINT_LOGOUT
169
- # methods=["GET"])
243
+ # @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
244
+ # methods=["POST"])
245
+ @jwt_required
170
246
  def service_logout() -> Response:
171
247
  """
172
- Entry point for the IAM server's logout service.
248
+ Entry point for the *IAM* server's logout service.
249
+
250
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
251
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
252
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
173
253
 
174
254
  The user is identified by the attribute *user-id* or "login", provided as a request parameter.
175
255
  If successful, remove all data relating to the user from the *IAM* server's registry.
@@ -180,10 +260,13 @@ def service_logout() -> Response:
180
260
  # declare the return variable
181
261
  result: Response | None
182
262
 
263
+ # retrieve the request arguments
264
+ args: dict[str, Any] = dict(request.args) or {}
265
+
183
266
  # log the request
184
267
  if __IAM_LOGGER:
185
- __IAM_LOGGER.debug(msg=_log_init(request=request))
186
-
268
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
269
+ ensure_ascii=False)}")
187
270
  errors: list[str] = []
188
271
  with _iam_lock:
189
272
  # retrieve the IAM server
@@ -192,30 +275,32 @@ def service_logout() -> Response:
192
275
  logger=__IAM_LOGGER)
193
276
  if iam_server:
194
277
  # logout the user
195
- action_logout(iam_server=iam_server,
196
- args=request.args,
197
- errors=errors,
198
- logger=__IAM_LOGGER)
278
+ iam_logout(iam_server=iam_server,
279
+ args=args,
280
+ errors=errors,
281
+ logger=__IAM_LOGGER)
199
282
  if errors:
200
283
  result = Response(response="; ".join(errors),
201
284
  status=400)
202
285
  else:
203
286
  result = Response(status=204)
204
287
 
205
- # log the response
206
288
  if __IAM_LOGGER:
289
+ # log the response
207
290
  __IAM_LOGGER.debug(msg=f"Response {result}")
208
291
 
209
292
  return result
210
293
 
211
294
 
212
- # @flask_app.route(rule=<callback_endpoint>, # JUSBR_ENDPOINT_CALLBACK
295
+ # @flask_app.route(rule=<callback_endpoint>, # IAM_ENDPOINT_CALLBACK
213
296
  # methods=["GET", "POST"])
214
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_CALLBACK
215
- # methods=["POST"])
216
297
  def service_callback() -> Response:
217
298
  """
218
- Entry point for the callback from the IAM server on authentication operation.
299
+ Entry point for the callback from the *IAM* server on authentication operation.
300
+
301
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
302
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
303
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
219
304
 
220
305
  This callback is invoked from a front-end application after a successful login at the
221
306
  *IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
@@ -233,10 +318,13 @@ def service_callback() -> Response:
233
318
 
234
319
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
235
320
  """
321
+ # retrieve the request arguments
322
+ args: dict[str, Any] = dict(request.args) or {}
323
+
236
324
  # log the request
237
325
  if __IAM_LOGGER:
238
- __IAM_LOGGER.debug(msg=_log_init(request=request))
239
-
326
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
327
+ ensure_ascii=False)}")
240
328
  errors: list[str] = []
241
329
  token_data: tuple[str, str] | None = None
242
330
  with _iam_lock:
@@ -246,10 +334,10 @@ def service_callback() -> Response:
246
334
  logger=__IAM_LOGGER)
247
335
  if iam_server:
248
336
  # process the callback operation
249
- token_data = action_callback(iam_server=iam_server,
250
- args=request.args,
251
- errors=errors,
252
- logger=__IAM_LOGGER)
337
+ token_data = iam_callback(iam_server=iam_server,
338
+ args=args,
339
+ errors=errors,
340
+ logger=__IAM_LOGGER)
253
341
  result: Response
254
342
  if errors:
255
343
  result = jsonify({"errors": "; ".join(errors)})
@@ -264,60 +352,137 @@ def service_callback() -> Response:
264
352
  return result
265
353
 
266
354
 
267
- # @flask_app.route(rule=<token_endpoint>, # JUSBR_ENDPOINT_TOKEN
268
- # methods=["GET"])
269
- # @flask_app.route(rule=<token_endpoint>, # KEYCLOAK_ENDPOINT_TOKEN
270
- # methods=["GET"])
271
- def service_token() -> Response:
355
+ # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
356
+ # methods=["POST"])
357
+ def service_exchange() -> Response:
272
358
  """
273
- Entry point for retrieving a token from the *IAM* server.
359
+ Entry point for requesting the *IAM* server to exchange the token.
274
360
 
275
- The user is identified by the attribute *user-id* or "login", provided as a request parameter.
361
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
362
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
363
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
364
+
365
+ If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
366
+ Otherwise, *errors* will contain the appropriate error message.
367
+
368
+ The expected request parameters are:
369
+ - user-id: identification for the reference user (alias: 'login')
370
+ - access-token: the token to be exchanged
276
371
 
277
372
  On success, the returned *Response* will contain the following JSON:
278
373
  {
279
374
  "user-id": <reference-user-identification>,
280
- "access-token": <token>
375
+ "access-token": <the-exchanged-token>
281
376
  }
282
377
 
283
- :return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
378
+ :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
284
379
  """
380
+ # retrieve the request arguments
381
+ args: dict[str, Any] = dict(request.args) or {}
382
+
285
383
  # log the request
286
384
  if __IAM_LOGGER:
287
- __IAM_LOGGER.debug(msg=_log_init(request=request))
288
-
289
- # obtain the user's identification
290
- args: dict[str, Any] = request.args
291
- user_id: str = args.get("user-id") or args.get("login")
292
-
385
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
386
+ ensure_ascii=False)}")
293
387
  errors: list[str] = []
294
- token: str | None = None
295
- if user_id:
296
- with _iam_lock:
297
- # retrieve the IAM server
298
- iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
299
- errors=errors,
300
- logger=__IAM_LOGGER)
301
- if iam_server:
302
- # retrieve the token
303
- errors: list[str] = []
304
- token: str = action_token(iam_server=iam_server,
305
- args=args,
306
- errors=errors,
307
- logger=__IAM_LOGGER)
308
- else:
309
- msg: str = "User identification not provided"
310
- errors.append(msg)
311
- if __IAM_LOGGER:
312
- __IAM_LOGGER.error(msg=msg)
313
-
388
+ with _iam_lock:
389
+ # retrieve the IAM server
390
+ iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
391
+ errors=errors,
392
+ logger=__IAM_LOGGER)
393
+ # exchange the token
394
+ token_info: tuple[str, str] | None = None
395
+ if iam_server:
396
+ errors: list[str] = []
397
+ token_info = iam_exchange(iam_server=iam_server,
398
+ args=args,
399
+ errors=errors,
400
+ logger=__IAM_LOGGER)
314
401
  result: Response
315
402
  if errors:
316
403
  result = Response(response="; ".join(errors),
317
404
  status=400)
318
405
  else:
319
- result = jsonify({"user-id": user_id,
320
- "access-token": token})
406
+ result = jsonify({"user-id": token_info[0],
407
+ "access-token": token_info[1]})
408
+ if __IAM_LOGGER:
409
+ # log the response (the returned data is not logged, as it contains the token)
410
+ __IAM_LOGGER.debug(msg=f"Response {result}; {result.get_data(as_text=True)}")
411
+
412
+ return result
413
+
414
+
415
+ # @flask_app.route(rule=/iam/jusbr:callback-exchange,
416
+ # methods=["GET"])
417
+ def service_callback_exchange() -> Response:
418
+ """
419
+ Entry point for the callback from the IAM server on authentication operation, with subsequent token exchange.
420
+
421
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
422
+ the name of the *IAM* server in charge of handling this service, and suffixed with the string *_to_*
423
+ followed by the name of the *IAM* server in charge of the token exchange. The prefixing, but not the suffixing,
424
+ is done automatically if the endpoint is established with a call to *iam_setup_endpoints()*.
425
+
426
+ This callback is invoked from a front-end application after a successful login at the
427
+ *IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
428
+ this data is then used to effectively obtain the token from the *IAM* server.
429
+ This token is stored and thereafter, a corresponding token is requested from another IAM *server*,
430
+ in a scheme known as "token exchange". This new token, along with the reference user identification,
431
+ are then stored. Note that the original token is the one actually returned.
432
+
433
+ The relevant expected request arguments are:
434
+ - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
435
+ - *code*: the temporary authorization code provided by the IAM server, to be exchanged for the token
436
+
437
+ On success, the returned *Response* will contain the following JSON:
438
+ {
439
+ "user-id": <reference-user-identification>,
440
+ "access-token": <the-original-token>
441
+ }
442
+
443
+ :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
444
+ """
445
+ # declare the return variable
446
+ result: Response | None = None
447
+
448
+ # retrieve the request arguments
449
+ args: dict[str, Any] = dict(request.args) or {}
450
+
451
+ # log the request
452
+ if __IAM_LOGGER:
453
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
454
+ ensure_ascii=False)}")
455
+ errors: list[str] = []
456
+ with _iam_lock:
457
+ # retrieve the IAM server
458
+ iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
459
+ errors=errors,
460
+ logger=__IAM_LOGGER)
461
+ # obtain the login URL
462
+ token_info: tuple[str, str] = iam_callback(iam_server=iam_server,
463
+ args=args,
464
+ errors=errors,
465
+ logger=__IAM_LOGGER)
466
+ if token_info:
467
+ args: dict[str, str] = {
468
+ "user-id": token_info[0],
469
+ "access-token": token_info[1]
470
+ }
471
+ # retrieve the exchange IAM server
472
+ pos: int = request.endpoint.index("_to_")
473
+ exchange_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint[pos+4],
474
+ errors=errors,
475
+ logger=__IAM_LOGGER)
476
+ token_info = iam_exchange(iam_server=exchange_server,
477
+ args=args,
478
+ logger=__IAM_LOGGER)
479
+ if token_info:
480
+ result = jsonify({"user-id": token_info[0],
481
+ "access-token": token_info[1]})
482
+ if errors:
483
+ result = Response("; ".join(errors))
484
+ result.status_code = 400
485
+
321
486
  if __IAM_LOGGER:
322
487
  # log the response (the returned data is not logged, as it contains the token)
323
488
  __IAM_LOGGER.debug(msg=f"Response {result}")
@@ -325,19 +490,17 @@ def service_token() -> Response:
325
490
  return result
326
491
 
327
492
 
328
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
329
- # methods=["POST"])
330
- def service_exchange() -> Response:
493
+ # @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_TOKEN
494
+ # methods=["GET"])
495
+ def service_get_token() -> Response:
331
496
  """
332
- Entry point for requesting the *IAM* server to exchange the token.
497
+ Entry point for retrieving a token from the *IAM* server.
333
498
 
334
- This is currently limited to the *KEYCLOAK* server. The token itself is stored in *KEYCLOAK*'s registry.
335
- The expected request parameters are:
336
- - user-id: identification for the reference user (alias: 'login')
337
- - access-token: the token to be exchanged
499
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
500
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
501
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
338
502
 
339
- If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
340
- Otherwise, *errors* will contain the appropriate error message.
503
+ The user is identified by the attribute *user-id* or "login", provided as a request parameter.
341
504
 
342
505
  On success, the returned *Response* will contain the following JSON:
343
506
  {
@@ -345,49 +508,91 @@ def service_exchange() -> Response:
345
508
  "access-token": <token>
346
509
  }
347
510
 
348
- :return: *Response* containing the token data, or *BAD REQUEST*
511
+ :return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
349
512
  """
513
+ # retrieve the request arguments
514
+ args: dict[str, Any] = dict(request.args) or {}
515
+
350
516
  # log the request
351
517
  if __IAM_LOGGER:
352
- __IAM_LOGGER.debug(msg=_log_init(request=request))
353
-
518
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
519
+ ensure_ascii=False)}")
354
520
  errors: list[str] = []
521
+ token_info: dict[str, str] | None = None
355
522
  with _iam_lock:
356
- # retrieve the IAM server (currently, only 'IAM_KEYCLOAK' is supported)
523
+ # retrieve the IAM server
357
524
  iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
358
525
  errors=errors,
359
526
  logger=__IAM_LOGGER)
360
- # exchange the token
361
- token_info: tuple[str, str] | None = None
362
527
  if iam_server:
528
+ # retrieve the token
363
529
  errors: list[str] = []
364
- token_info = action_exchange(iam_server=iam_server,
365
- args=request.args,
366
- errors=errors,
367
- logger=__IAM_LOGGER)
530
+ token_info = iam_get_token(iam_server=iam_server,
531
+ args=args,
532
+ errors=errors,
533
+ logger=__IAM_LOGGER)
368
534
  result: Response
369
535
  if errors:
370
536
  result = Response(response="; ".join(errors),
371
537
  status=400)
372
538
  else:
373
- result = jsonify({"user-id": token_info[0],
374
- "access-token": token_info[1]})
375
-
376
- # log the response
539
+ result = jsonify(token_info)
377
540
  if __IAM_LOGGER:
378
- __IAM_LOGGER.debug(msg=f"Response {result}, {result.get_data(as_text=True)}")
541
+ # log the response (the returned data is not logged, as it contains the token)
542
+ __IAM_LOGGER.debug(msg=f"Response {result}")
379
543
 
380
544
  return result
381
545
 
382
546
 
383
- def _log_init(request: Request) -> str:
547
+ # @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_USERINFO
548
+ # methods=["GET"])
549
+ @jwt_required
550
+ def service_userinfo() -> Response:
384
551
  """
385
- Build the messages for logging the request entry.
552
+ Entry point for retrieving user data from the *IAM* server.
553
+
554
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
555
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
556
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
386
557
 
387
- :param request: the Request object
388
- :return: the log message
558
+ The user is identified by the attribute *user-id* or "login", provided as a request parameter.
559
+
560
+ On success, the returned *Response* will contain a JSON with information kept by *iam_server* about *user_id*.
561
+
562
+ :return: *Response* containing user data, or *BAD REQUEST*
389
563
  """
564
+ # retrieve the request arguments
565
+ args: dict[str, Any] = dict(request.args) or {}
566
+
567
+ # log the request
568
+ if __IAM_LOGGER:
569
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
570
+ ensure_ascii=False)}")
571
+ # retrieve the bearer token
572
+ args["access-token"] = __get_bearer_token(request=request)
390
573
 
391
- params: str = json.dumps(obj=request.args,
392
- ensure_ascii=False)
393
- return f"Request {request.method}:{request.path}, params {params}"
574
+ errors: list[str] = []
575
+ user_info: dict[str, str] | None = None
576
+ with _iam_lock:
577
+ # retrieve the IAM server
578
+ iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
579
+ errors=errors,
580
+ logger=__IAM_LOGGER)
581
+ if iam_server:
582
+ # retrieve the token
583
+ errors: list[str] = []
584
+ user_info = iam_userinfo(iam_server=iam_server,
585
+ args=args,
586
+ errors=errors,
587
+ logger=__IAM_LOGGER)
588
+ result: Response
589
+ if errors:
590
+ result = Response(response="; ".join(errors),
591
+ status=400)
592
+ else:
593
+ result = jsonify(user_info)
594
+ if __IAM_LOGGER:
595
+ # log the response
596
+ __IAM_LOGGER.debug(msg=f"Response {result}; {json.dumps(obj=user_info,
597
+ ensure_ascii=False)}")
598
+ return result