pypomes-iam 0.7.6__py3-none-any.whl → 0.8.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.
@@ -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,
13
+ iam_get_token, iam_exchange, iam_callback
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:
@@ -60,6 +63,7 @@ def __request_validate(request: Request) -> Response:
60
63
  claims: dict[str, Any] = token_get_claims(token=token)
61
64
  if claims:
62
65
  issuer: str = claims["payload"].get("iss")
66
+ public_key: str | None = None
63
67
  recipient_attr: str | None = None
64
68
  recipient_id: str = request.values.get("user-id") or request.values.get("login")
65
69
  with _iam_lock:
@@ -74,17 +78,17 @@ def __request_validate(request: Request) -> Response:
74
78
  logger=__IAM_LOGGER)
75
79
  if registry:
76
80
  recipient_attr = registry[IamParam.RECIPIENT_ATTR]
77
- public_key: str = _get_public_key(iam_server=iam_server,
78
- errors=None,
79
- logger=__IAM_LOGGER)
81
+ public_key = _get_public_key(iam_server=iam_server,
82
+ errors=None,
83
+ logger=__IAM_LOGGER)
80
84
  # validate the token (log errors, only)
81
85
  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):
86
+ if token_validate(token=token,
87
+ issuer=issuer,
88
+ recipient_id=recipient_id,
89
+ recipient_attr=recipient_attr,
90
+ public_key=public_key,
91
+ errors=errors):
88
92
  # token is valid
89
93
  bad_token = False
90
94
  elif __IAM_LOGGER:
@@ -99,7 +103,7 @@ def __request_validate(request: Request) -> Response:
99
103
  return result
100
104
 
101
105
 
102
- def logger_register(logger: Logger) -> None:
106
+ def iam_setup_logger(logger: Logger) -> None:
103
107
  """
104
108
  Register the logger for HTTP services.
105
109
 
@@ -109,13 +113,66 @@ def logger_register(logger: Logger) -> None:
109
113
  __IAM_LOGGER = logger
110
114
 
111
115
 
112
- # @flask_app.route(rule=<login_endpoint>, # JUSBR_ENDPOINT_LOGIN
113
- # methods=["GET"])
114
- # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_ENDPOINT_LOGIN
116
+ # @flask_app.route(rule=<setup_server_endpoint>,
117
+ # methods=["POST"])
118
+ def service_setup_server() -> Response:
119
+ """
120
+ Entry point to setup a *IAM* server.
121
+
122
+ These are the expected parameters in the request's body, in a JSON or as form data:
123
+ - *iam_server*: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
124
+ - *admin_id*: identifies the realm administrator
125
+ - *admin_secret*: password for the realm administrator
126
+ - *client_id*: the client's identification with the *IAM* server
127
+ - *client_realm*: the client's realm
128
+ - *client_secret*: the client's password with the *IAM* server
129
+ - *login_timeout*: timeout for login authentication (in seconds,defaults to no timeout)
130
+ - *k_lifetime*: how long to use *IAM* server's public key, before refreshing it (in seconds)
131
+ - *recipient_attr*: attribute in the token's payload holding the token's subject
132
+ - *rl_base*: base URL to request services
133
+
134
+ For the parameters not effectively passed, an attempt is made to obtain a value from the corresponding
135
+ environment variables. Most parameters are required to have values, which must be assigned either
136
+ throught the function invocation, or from the corresponding environment variables.
137
+
138
+ The parameters *admin_id* and *admin_secret* are required only if performing administrative tasks is intended.
139
+ The optional parameter *ogin_timeout* refers to the maximum time in seconds allowed for the user
140
+ to login at the *IAM* server's login page, and defaults to no time limit.
141
+
142
+ The parameter *client_secret* is required in most requests to the *IAM* server. In the case
143
+ it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
144
+ the first time it is needed.
145
+
146
+ :return: *Response OK*
147
+ """
148
+ # log the request
149
+ if __IAM_LOGGER:
150
+ __IAM_LOGGER.debug(msg=f"{_log_init(request=request)}; {json.dumps(obj=request.args,
151
+ ensure_ascii=False)}")
152
+ # retrieve the arguments
153
+ args: dict[str, Any] = request.json if request.is_json else request.form
154
+
155
+ # setup the server
156
+ from .iam_pomes import iam_setup_server
157
+ iam_setup_server(**args)
158
+ result = Response(status=200)
159
+
160
+ # log the response
161
+ if __IAM_LOGGER:
162
+ __IAM_LOGGER.debug(msg=f"Response {result}")
163
+
164
+ return result
165
+
166
+
167
+ # @flask_app.route(rule=<login_endpoint>, # IAM_ENDPOINT_LOGIN
115
168
  # methods=["GET"])
116
169
  def service_login() -> Response:
117
170
  """
118
- Entry point for the IAM server's login service.
171
+ Entry point for the *IAM* server's login service.
172
+
173
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
174
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
175
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
119
176
 
120
177
  These are the expected request parameters:
121
178
  - user-id: optional, identifies the reference user (alias: 'login')
@@ -146,10 +203,10 @@ def service_login() -> Response:
146
203
  logger=__IAM_LOGGER)
147
204
  if iam_server:
148
205
  # 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)
206
+ login_url: str = iam_login(iam_server=iam_server,
207
+ args=request.args,
208
+ errors=errors,
209
+ logger=__IAM_LOGGER)
153
210
  if login_url:
154
211
  result = jsonify({"login-url": login_url})
155
212
  if errors:
@@ -158,18 +215,20 @@ def service_login() -> Response:
158
215
 
159
216
  # log the response
160
217
  if __IAM_LOGGER:
161
- __IAM_LOGGER.debug(msg=f"Response {result}, {result.get_data(as_text=True)}")
218
+ __IAM_LOGGER.debug(msg=f"Response {result}; {result.get_data(as_text=True)}")
162
219
 
163
220
  return result
164
221
 
165
222
 
166
- # @flask_app.route(rule=<logout_endpoint>, # JUSBR_ENDPOINT_LOGOUT
167
- # methods=["GET"])
168
- # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_ENDPOINT_LOGOUT
223
+ # @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
169
224
  # methods=["GET"])
170
225
  def service_logout() -> Response:
171
226
  """
172
- Entry point for the IAM server's logout service.
227
+ Entry point for the *IAM* server's logout service.
228
+
229
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
230
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
231
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
173
232
 
174
233
  The user is identified by the attribute *user-id* or "login", provided as a request parameter.
175
234
  If successful, remove all data relating to the user from the *IAM* server's registry.
@@ -192,30 +251,32 @@ def service_logout() -> Response:
192
251
  logger=__IAM_LOGGER)
193
252
  if iam_server:
194
253
  # logout the user
195
- action_logout(iam_server=iam_server,
196
- args=request.args,
197
- errors=errors,
198
- logger=__IAM_LOGGER)
254
+ iam_logout(iam_server=iam_server,
255
+ args=request.args,
256
+ errors=errors,
257
+ logger=__IAM_LOGGER)
199
258
  if errors:
200
259
  result = Response(response="; ".join(errors),
201
260
  status=400)
202
261
  else:
203
262
  result = Response(status=204)
204
263
 
205
- # log the response
206
264
  if __IAM_LOGGER:
265
+ # log the response
207
266
  __IAM_LOGGER.debug(msg=f"Response {result}")
208
267
 
209
268
  return result
210
269
 
211
270
 
212
- # @flask_app.route(rule=<callback_endpoint>, # JUSBR_ENDPOINT_CALLBACK
271
+ # @flask_app.route(rule=<callback_endpoint>, # IAM_ENDPOINT_CALLBACK
213
272
  # methods=["GET", "POST"])
214
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_CALLBACK
215
- # methods=["POST"])
216
273
  def service_callback() -> Response:
217
274
  """
218
- Entry point for the callback from the IAM server on authentication operation.
275
+ Entry point for the callback from the *IAM* server on authentication operation.
276
+
277
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
278
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
279
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
219
280
 
220
281
  This callback is invoked from a front-end application after a successful login at the
221
282
  *IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
@@ -246,10 +307,10 @@ def service_callback() -> Response:
246
307
  logger=__IAM_LOGGER)
247
308
  if iam_server:
248
309
  # process the callback operation
249
- token_data = action_callback(iam_server=iam_server,
250
- args=request.args,
251
- errors=errors,
252
- logger=__IAM_LOGGER)
310
+ token_data = iam_callback(iam_server=iam_server,
311
+ args=request.args,
312
+ errors=errors,
313
+ logger=__IAM_LOGGER)
253
314
  result: Response
254
315
  if errors:
255
316
  result = jsonify({"errors": "; ".join(errors)})
@@ -264,60 +325,131 @@ def service_callback() -> Response:
264
325
  return result
265
326
 
266
327
 
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:
328
+ # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
329
+ # methods=["POST"])
330
+ def service_exchange() -> Response:
272
331
  """
273
- Entry point for retrieving a token from the *IAM* server.
332
+ Entry point for requesting the *IAM* server to exchange the token.
274
333
 
275
- The user is identified by the attribute *user-id* or "login", provided as a request parameter.
334
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
335
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
336
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
337
+
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.
340
+
341
+ The expected request parameters are:
342
+ - user-id: identification for the reference user (alias: 'login')
343
+ - access-token: the token to be exchanged
276
344
 
277
345
  On success, the returned *Response* will contain the following JSON:
278
346
  {
279
347
  "user-id": <reference-user-identification>,
280
- "access-token": <token>
348
+ "access-token": <the-exchanged-token>
281
349
  }
282
350
 
283
- :return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
351
+ :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
284
352
  """
285
353
  # log the request
286
354
  if __IAM_LOGGER:
287
355
  __IAM_LOGGER.debug(msg=_log_init(request=request))
288
356
 
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
-
293
357
  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
-
358
+ with _iam_lock:
359
+ # retrieve the IAM server
360
+ iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
361
+ errors=errors,
362
+ logger=__IAM_LOGGER)
363
+ # exchange the token
364
+ token_info: tuple[str, str] | None = None
365
+ if iam_server:
366
+ errors: list[str] = []
367
+ token_info = iam_exchange(iam_server=iam_server,
368
+ args=request.args,
369
+ errors=errors,
370
+ logger=__IAM_LOGGER)
314
371
  result: Response
315
372
  if errors:
316
373
  result = Response(response="; ".join(errors),
317
374
  status=400)
318
375
  else:
319
- result = jsonify({"user-id": user_id,
320
- "access-token": token})
376
+ result = jsonify({"user-id": token_info[0],
377
+ "access-token": token_info[1]})
378
+ if __IAM_LOGGER:
379
+ # log the response (the returned data is not logged, as it contains the token)
380
+ __IAM_LOGGER.debug(msg=f"Response {result}; {result.get_data(as_text=True)}")
381
+
382
+ return result
383
+
384
+
385
+ # @flask_app.route(rule=/iam/jusbr:callback-exchange,
386
+ # methods=["GET"])
387
+ def service_callback_exchange() -> Response:
388
+ """
389
+ Entry point for the callback from the IAM server on authentication operation, with subsequent token exchange.
390
+
391
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
392
+ the name of the *IAM* server in charge of handling this service, and suffixed with the string *_to_*
393
+ followed by the name of the *IAM* server in charge of the token exchange. The prefixing, but not the suffixing,
394
+ is done automatically if the endpoint is established with a call to *iam_setup_endpoints()*.
395
+
396
+ This callback is invoked from a front-end application after a successful login at the
397
+ *IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
398
+ this data is then used to effectively obtain the token from the *IAM* server.
399
+ This token is stored and thereafter, a corresponding token is requested from another IAM *server*,
400
+ in a scheme known as "token exchange". This new token, along with the reference user identification,
401
+ are then stored. Note that the original token is the one actually returned.
402
+
403
+ The relevant expected request arguments are:
404
+ - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
405
+ - *code*: the temporary authorization code provided by the IAM server, to be exchanged for the token
406
+
407
+ On success, the returned *Response* will contain the following JSON:
408
+ {
409
+ "user-id": <reference-user-identification>,
410
+ "access-token": <the-original-token>
411
+ }
412
+
413
+ :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
414
+ """
415
+ # declare the return variable
416
+ result: Response | None = None
417
+
418
+ # log the request
419
+ if __IAM_LOGGER:
420
+ __IAM_LOGGER.debug(msg=_log_init(request=request))
421
+
422
+ errors: list[str] = []
423
+ with _iam_lock:
424
+ # retrieve the IAM server
425
+ iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
426
+ errors=errors,
427
+ logger=__IAM_LOGGER)
428
+ # obtain the login URL
429
+ token_info: tuple[str, str] = iam_callback(iam_server=iam_server,
430
+ args=request.args,
431
+ errors=errors,
432
+ logger=__IAM_LOGGER)
433
+ if token_info:
434
+ args: dict[str, str] = {
435
+ "user-id": token_info[0],
436
+ "access-token": token_info[1]
437
+ }
438
+ # retrieve the exchange IAM server
439
+ pos: int = request.endpoint.index("_to_")
440
+ exchange_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint[pos+4],
441
+ errors=errors,
442
+ logger=__IAM_LOGGER)
443
+ token_info = iam_exchange(iam_server=exchange_server,
444
+ args=args,
445
+ logger=__IAM_LOGGER)
446
+ if token_info:
447
+ result = jsonify({"user-id": token_info[0],
448
+ "access-token": token_info[1]})
449
+ if errors:
450
+ result = Response("; ".join(errors))
451
+ result.status_code = 400
452
+
321
453
  if __IAM_LOGGER:
322
454
  # log the response (the returned data is not logged, as it contains the token)
323
455
  __IAM_LOGGER.debug(msg=f"Response {result}")
@@ -325,19 +457,17 @@ def service_token() -> Response:
325
457
  return result
326
458
 
327
459
 
328
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
329
- # methods=["POST"])
330
- def service_exchange() -> Response:
460
+ # @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_TOKEN
461
+ # methods=["GET"])
462
+ def service_get_token() -> Response:
331
463
  """
332
- Entry point for requesting the *IAM* server to exchange the token.
464
+ Entry point for retrieving a token from the *IAM* server.
333
465
 
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
466
+ When registering this endpoint, the name used in *Flask*'s *endpoint* parameter must be prefixed with
467
+ the name of the *IAM* server in charge of handling this service. This prefixing is done automatically
468
+ if the endpoint is established with a call to *iam_setup_endpoints()*.
338
469
 
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.
470
+ The user is identified by the attribute *user-id* or "login", provided as a request parameter.
341
471
 
342
472
  On success, the returned *Response* will contain the following JSON:
343
473
  {
@@ -345,37 +475,38 @@ def service_exchange() -> Response:
345
475
  "access-token": <token>
346
476
  }
347
477
 
348
- :return: *Response* containing the token data, or *BAD REQUEST*
478
+ :return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
349
479
  """
350
480
  # log the request
351
481
  if __IAM_LOGGER:
352
482
  __IAM_LOGGER.debug(msg=_log_init(request=request))
353
483
 
484
+ # obtain the request arguments
485
+ args: dict[str, Any] = request.args
486
+
354
487
  errors: list[str] = []
488
+ token_info: dict[str, str] | None = None
355
489
  with _iam_lock:
356
- # retrieve the IAM server (currently, only 'IAM_KEYCLOAK' is supported)
490
+ # retrieve the IAM server
357
491
  iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
358
492
  errors=errors,
359
493
  logger=__IAM_LOGGER)
360
- # exchange the token
361
- token_info: tuple[str, str] | None = None
362
494
  if iam_server:
495
+ # retrieve the token
363
496
  errors: list[str] = []
364
- token_info = action_exchange(iam_server=iam_server,
365
- args=request.args,
366
- errors=errors,
367
- logger=__IAM_LOGGER)
497
+ token_info = iam_get_token(iam_server=iam_server,
498
+ args=args,
499
+ errors=errors,
500
+ logger=__IAM_LOGGER)
368
501
  result: Response
369
502
  if errors:
370
503
  result = Response(response="; ".join(errors),
371
504
  status=400)
372
505
  else:
373
- result = jsonify({"user-id": token_info[0],
374
- "access-token": token_info[1]})
375
-
376
- # log the response
506
+ result = jsonify(token_info)
377
507
  if __IAM_LOGGER:
378
- __IAM_LOGGER.debug(msg=f"Response {result}, {result.get_data(as_text=True)}")
508
+ # log the response (the returned data is not logged, as it contains the token)
509
+ __IAM_LOGGER.debug(msg=f"Response {result}")
379
510
 
380
511
  return result
381
512