pypomes-iam 0.7.4__py3-none-any.whl → 0.8.2__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.

@@ -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,17 +130,71 @@ 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)
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')
122
196
  - redirect-uri: a parameter to be added to the query part of the returned URL
197
+ -target-idp: optionally, identify a target identity provider for the login operation
123
198
 
124
199
  If provided, the user identification will be validated against the authorization data
125
200
  returned by *iam_server* upon login. On success, the following JSON, containing the appropriate
@@ -133,10 +208,13 @@ def service_login() -> Response:
133
208
  # declare the return variable
134
209
  result: Response | None = None
135
210
 
211
+ # retrieve the request arguments
212
+ args: dict[str, Any] = dict(request.args)
213
+
136
214
  # log the request
137
215
  if __IAM_LOGGER:
138
- __IAM_LOGGER.debug(msg=_log_init(request=request))
139
-
216
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
217
+ ensure_ascii=False)}")
140
218
  errors: list[str] = []
141
219
  with _iam_lock:
142
220
  # retrieve the IAM server
@@ -145,10 +223,10 @@ def service_login() -> Response:
145
223
  logger=__IAM_LOGGER)
146
224
  if iam_server:
147
225
  # obtain the login URL
148
- login_url: str = action_login(iam_server=iam_server,
149
- args=request.args,
150
- errors=errors,
151
- logger=__IAM_LOGGER)
226
+ login_url: str = iam_login(iam_server=iam_server,
227
+ args=args,
228
+ errors=errors,
229
+ logger=__IAM_LOGGER)
152
230
  if login_url:
153
231
  result = jsonify({"login-url": login_url})
154
232
  if errors:
@@ -157,18 +235,21 @@ def service_login() -> Response:
157
235
 
158
236
  # log the response
159
237
  if __IAM_LOGGER:
160
- __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)}")
161
239
 
162
240
  return result
163
241
 
164
242
 
165
- # @flask_app.route(rule=<logout_endpoint>, # JUSBR_ENDPOINT_LOGOUT
166
- # methods=["GET"])
167
- # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_ENDPOINT_LOGOUT
168
- # methods=["GET"])
243
+ # @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
244
+ # methods=["POST"])
245
+ @jwt_required
169
246
  def service_logout() -> Response:
170
247
  """
171
- 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()*.
172
253
 
173
254
  The user is identified by the attribute *user-id* or "login", provided as a request parameter.
174
255
  If successful, remove all data relating to the user from the *IAM* server's registry.
@@ -179,10 +260,13 @@ def service_logout() -> Response:
179
260
  # declare the return variable
180
261
  result: Response | None
181
262
 
263
+ # retrieve the request arguments
264
+ args: dict[str, Any] = dict(request.args)
265
+
182
266
  # log the request
183
267
  if __IAM_LOGGER:
184
- __IAM_LOGGER.debug(msg=_log_init(request=request))
185
-
268
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
269
+ ensure_ascii=False)}")
186
270
  errors: list[str] = []
187
271
  with _iam_lock:
188
272
  # retrieve the IAM server
@@ -191,30 +275,32 @@ def service_logout() -> Response:
191
275
  logger=__IAM_LOGGER)
192
276
  if iam_server:
193
277
  # logout the user
194
- action_logout(iam_server=iam_server,
195
- args=request.args,
196
- errors=errors,
197
- logger=__IAM_LOGGER)
278
+ iam_logout(iam_server=iam_server,
279
+ args=args,
280
+ errors=errors,
281
+ logger=__IAM_LOGGER)
198
282
  if errors:
199
283
  result = Response(response="; ".join(errors),
200
284
  status=400)
201
285
  else:
202
286
  result = Response(status=204)
203
287
 
204
- # log the response
205
288
  if __IAM_LOGGER:
289
+ # log the response
206
290
  __IAM_LOGGER.debug(msg=f"Response {result}")
207
291
 
208
292
  return result
209
293
 
210
294
 
211
- # @flask_app.route(rule=<callback_endpoint>, # JUSBR_ENDPOINT_CALLBACK
295
+ # @flask_app.route(rule=<callback_endpoint>, # IAM_ENDPOINT_CALLBACK
212
296
  # methods=["GET", "POST"])
213
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_CALLBACK
214
- # methods=["POST"])
215
297
  def service_callback() -> Response:
216
298
  """
217
- 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()*.
218
304
 
219
305
  This callback is invoked from a front-end application after a successful login at the
220
306
  *IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
@@ -232,10 +318,13 @@ def service_callback() -> Response:
232
318
 
233
319
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
234
320
  """
321
+ # retrieve the request arguments
322
+ args: dict[str, Any] = dict(request.args)
323
+
235
324
  # log the request
236
325
  if __IAM_LOGGER:
237
- __IAM_LOGGER.debug(msg=_log_init(request=request))
238
-
326
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
327
+ ensure_ascii=False)}")
239
328
  errors: list[str] = []
240
329
  token_data: tuple[str, str] | None = None
241
330
  with _iam_lock:
@@ -245,10 +334,10 @@ def service_callback() -> Response:
245
334
  logger=__IAM_LOGGER)
246
335
  if iam_server:
247
336
  # process the callback operation
248
- token_data = action_callback(iam_server=iam_server,
249
- args=request.args,
250
- errors=errors,
251
- logger=__IAM_LOGGER)
337
+ token_data = iam_callback(iam_server=iam_server,
338
+ args=args,
339
+ errors=errors,
340
+ logger=__IAM_LOGGER)
252
341
  result: Response
253
342
  if errors:
254
343
  result = jsonify({"errors": "; ".join(errors)})
@@ -263,60 +352,137 @@ def service_callback() -> Response:
263
352
  return result
264
353
 
265
354
 
266
- # @flask_app.route(rule=<token_endpoint>, # JUSBR_ENDPOINT_TOKEN
267
- # methods=["GET"])
268
- # @flask_app.route(rule=<token_endpoint>, # KEYCLOAK_ENDPOINT_TOKEN
269
- # methods=["GET"])
270
- def service_token() -> Response:
355
+ # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
356
+ # methods=["POST"])
357
+ def service_exchange() -> Response:
271
358
  """
272
- Entry point for retrieving a token from the *IAM* server.
359
+ Entry point for requesting the *IAM* server to exchange the token.
273
360
 
274
- 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
275
371
 
276
372
  On success, the returned *Response* will contain the following JSON:
277
373
  {
278
374
  "user-id": <reference-user-identification>,
279
- "access-token": <token>
375
+ "access-token": <the-exchanged-token>
280
376
  }
281
377
 
282
- :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*
283
379
  """
380
+ # retrieve the request arguments
381
+ args: dict[str, Any] = dict(request.args)
382
+
284
383
  # log the request
285
384
  if __IAM_LOGGER:
286
- __IAM_LOGGER.debug(msg=_log_init(request=request))
287
-
288
- # obtain the user's identification
289
- args: dict[str, Any] = request.args
290
- user_id: str = args.get("user-id") or args.get("login")
291
-
385
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
386
+ ensure_ascii=False)}")
292
387
  errors: list[str] = []
293
- token: str | None = None
294
- if user_id:
295
- with _iam_lock:
296
- # retrieve the IAM server
297
- iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
298
- errors=errors,
299
- logger=__IAM_LOGGER)
300
- if iam_server:
301
- # retrieve the token
302
- errors: list[str] = []
303
- token: str = action_token(iam_server=iam_server,
304
- args=args,
305
- errors=errors,
306
- logger=__IAM_LOGGER)
307
- else:
308
- msg: str = "User identification not provided"
309
- errors.append(msg)
310
- if __IAM_LOGGER:
311
- __IAM_LOGGER.error(msg=msg)
312
-
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)
313
401
  result: Response
314
402
  if errors:
315
403
  result = Response(response="; ".join(errors),
316
404
  status=400)
317
405
  else:
318
- result = jsonify({"user-id": user_id,
319
- "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)
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
+
320
486
  if __IAM_LOGGER:
321
487
  # log the response (the returned data is not logged, as it contains the token)
322
488
  __IAM_LOGGER.debug(msg=f"Response {result}")
@@ -324,19 +490,17 @@ def service_token() -> Response:
324
490
  return result
325
491
 
326
492
 
327
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
328
- # methods=["POST"])
329
- def service_exchange() -> Response:
493
+ # @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_TOKEN
494
+ # methods=["GET"])
495
+ def service_get_token() -> Response:
330
496
  """
331
- Entry point for requesting the *IAM* server to exchange the token.
497
+ Entry point for retrieving a token from the *IAM* server.
332
498
 
333
- This is currently limited to the *KEYCLOAK* server. The token itself is stored in *KEYCLOAK*'s registry.
334
- The expected request parameters are:
335
- - user-id: identification for the reference user (alias: 'login')
336
- - 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()*.
337
502
 
338
- If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
339
- 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.
340
504
 
341
505
  On success, the returned *Response* will contain the following JSON:
342
506
  {
@@ -344,49 +508,91 @@ def service_exchange() -> Response:
344
508
  "access-token": <token>
345
509
  }
346
510
 
347
- :return: *Response* containing the token data, or *BAD REQUEST*
511
+ :return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
348
512
  """
513
+ # retrieve the request arguments
514
+ args: dict[str, Any] = dict(request.args)
515
+
349
516
  # log the request
350
517
  if __IAM_LOGGER:
351
- __IAM_LOGGER.debug(msg=_log_init(request=request))
352
-
518
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
519
+ ensure_ascii=False)}")
353
520
  errors: list[str] = []
521
+ token_info: dict[str, str] | None = None
354
522
  with _iam_lock:
355
- # retrieve the IAM server (currently, only 'IAM_KEYCLOAK' is supported)
523
+ # retrieve the IAM server
356
524
  iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
357
525
  errors=errors,
358
526
  logger=__IAM_LOGGER)
359
- # exchange the token
360
- token_info: tuple[str, str] | None = None
361
527
  if iam_server:
528
+ # retrieve the token
362
529
  errors: list[str] = []
363
- token_info = action_exchange(iam_server=iam_server,
364
- args=request.args,
365
- errors=errors,
366
- logger=__IAM_LOGGER)
530
+ token_info = iam_get_token(iam_server=iam_server,
531
+ args=args,
532
+ errors=errors,
533
+ logger=__IAM_LOGGER)
367
534
  result: Response
368
535
  if errors:
369
536
  result = Response(response="; ".join(errors),
370
537
  status=400)
371
538
  else:
372
- result = jsonify({"user-id": token_info[0],
373
- "access-token": token_info[1]})
374
-
375
- # log the response
539
+ result = jsonify(token_info)
376
540
  if __IAM_LOGGER:
377
- __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}")
378
543
 
379
544
  return result
380
545
 
381
546
 
382
- 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:
383
551
  """
384
- 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()*.
385
557
 
386
- :param request: the Request object
387
- :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*
388
563
  """
564
+ # retrieve the request arguments
565
+ args: dict[str, Any] = dict(request.args)
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)
389
573
 
390
- params: str = json.dumps(obj=request.args,
391
- ensure_ascii=False)
392
- 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