pypomes-iam 0.7.2__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,17 +113,71 @@ 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')
122
179
  - redirect-uri: a parameter to be added to the query part of the returned URL
180
+ -target-idp: optionally, identify a target identity provider for the login operation
123
181
 
124
182
  If provided, the user identification will be validated against the authorization data
125
183
  returned by *iam_server* upon login. On success, the following JSON, containing the appropriate
@@ -145,10 +203,10 @@ def service_login() -> Response:
145
203
  logger=__IAM_LOGGER)
146
204
  if iam_server:
147
205
  # 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)
206
+ login_url: str = iam_login(iam_server=iam_server,
207
+ args=request.args,
208
+ errors=errors,
209
+ logger=__IAM_LOGGER)
152
210
  if login_url:
153
211
  result = jsonify({"login-url": login_url})
154
212
  if errors:
@@ -157,18 +215,20 @@ def service_login() -> Response:
157
215
 
158
216
  # log the response
159
217
  if __IAM_LOGGER:
160
- __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)}")
161
219
 
162
220
  return result
163
221
 
164
222
 
165
- # @flask_app.route(rule=<logout_endpoint>, # JUSBR_ENDPOINT_LOGOUT
166
- # methods=["GET"])
167
- # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_ENDPOINT_LOGOUT
223
+ # @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
168
224
  # methods=["GET"])
169
225
  def service_logout() -> Response:
170
226
  """
171
- 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()*.
172
232
 
173
233
  The user is identified by the attribute *user-id* or "login", provided as a request parameter.
174
234
  If successful, remove all data relating to the user from the *IAM* server's registry.
@@ -191,30 +251,32 @@ def service_logout() -> Response:
191
251
  logger=__IAM_LOGGER)
192
252
  if iam_server:
193
253
  # logout the user
194
- action_logout(iam_server=iam_server,
195
- args=request.args,
196
- errors=errors,
197
- logger=__IAM_LOGGER)
254
+ iam_logout(iam_server=iam_server,
255
+ args=request.args,
256
+ errors=errors,
257
+ logger=__IAM_LOGGER)
198
258
  if errors:
199
259
  result = Response(response="; ".join(errors),
200
260
  status=400)
201
261
  else:
202
262
  result = Response(status=204)
203
263
 
204
- # log the response
205
264
  if __IAM_LOGGER:
265
+ # log the response
206
266
  __IAM_LOGGER.debug(msg=f"Response {result}")
207
267
 
208
268
  return result
209
269
 
210
270
 
211
- # @flask_app.route(rule=<callback_endpoint>, # JUSBR_ENDPOINT_CALLBACK
271
+ # @flask_app.route(rule=<callback_endpoint>, # IAM_ENDPOINT_CALLBACK
212
272
  # methods=["GET", "POST"])
213
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_CALLBACK
214
- # methods=["POST"])
215
273
  def service_callback() -> Response:
216
274
  """
217
- 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()*.
218
280
 
219
281
  This callback is invoked from a front-end application after a successful login at the
220
282
  *IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
@@ -245,10 +307,10 @@ def service_callback() -> Response:
245
307
  logger=__IAM_LOGGER)
246
308
  if iam_server:
247
309
  # process the callback operation
248
- token_data = action_callback(iam_server=iam_server,
249
- args=request.args,
250
- errors=errors,
251
- logger=__IAM_LOGGER)
310
+ token_data = iam_callback(iam_server=iam_server,
311
+ args=request.args,
312
+ errors=errors,
313
+ logger=__IAM_LOGGER)
252
314
  result: Response
253
315
  if errors:
254
316
  result = jsonify({"errors": "; ".join(errors)})
@@ -263,60 +325,131 @@ def service_callback() -> Response:
263
325
  return result
264
326
 
265
327
 
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:
328
+ # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
329
+ # methods=["POST"])
330
+ def service_exchange() -> Response:
271
331
  """
272
- Entry point for retrieving a token from the *IAM* server.
332
+ Entry point for requesting the *IAM* server to exchange the token.
273
333
 
274
- 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
275
344
 
276
345
  On success, the returned *Response* will contain the following JSON:
277
346
  {
278
347
  "user-id": <reference-user-identification>,
279
- "access-token": <token>
348
+ "access-token": <the-exchanged-token>
280
349
  }
281
350
 
282
- :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*
283
352
  """
284
353
  # log the request
285
354
  if __IAM_LOGGER:
286
355
  __IAM_LOGGER.debug(msg=_log_init(request=request))
287
356
 
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
-
292
357
  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
-
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)
313
371
  result: Response
314
372
  if errors:
315
373
  result = Response(response="; ".join(errors),
316
374
  status=400)
317
375
  else:
318
- result = jsonify({"user-id": user_id,
319
- "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
+
320
453
  if __IAM_LOGGER:
321
454
  # log the response (the returned data is not logged, as it contains the token)
322
455
  __IAM_LOGGER.debug(msg=f"Response {result}")
@@ -324,19 +457,17 @@ def service_token() -> Response:
324
457
  return result
325
458
 
326
459
 
327
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
328
- # methods=["POST"])
329
- def service_exchange() -> Response:
460
+ # @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_TOKEN
461
+ # methods=["GET"])
462
+ def service_get_token() -> Response:
330
463
  """
331
- Entry point for requesting the *IAM* server to exchange the token.
464
+ Entry point for retrieving a token from the *IAM* server.
332
465
 
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
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()*.
337
469
 
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.
470
+ The user is identified by the attribute *user-id* or "login", provided as a request parameter.
340
471
 
341
472
  On success, the returned *Response* will contain the following JSON:
342
473
  {
@@ -344,37 +475,38 @@ def service_exchange() -> Response:
344
475
  "access-token": <token>
345
476
  }
346
477
 
347
- :return: *Response* containing the token data, or *BAD REQUEST*
478
+ :return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
348
479
  """
349
480
  # log the request
350
481
  if __IAM_LOGGER:
351
482
  __IAM_LOGGER.debug(msg=_log_init(request=request))
352
483
 
484
+ # obtain the request arguments
485
+ args: dict[str, Any] = request.args
486
+
353
487
  errors: list[str] = []
488
+ token_info: dict[str, str] | None = None
354
489
  with _iam_lock:
355
- # retrieve the IAM server (currently, only 'IAM_KEYCLOAK' is supported)
490
+ # retrieve the IAM server
356
491
  iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
357
492
  errors=errors,
358
493
  logger=__IAM_LOGGER)
359
- # exchange the token
360
- token_info: tuple[str, str] | None = None
361
494
  if iam_server:
495
+ # retrieve the token
362
496
  errors: list[str] = []
363
- token_info = action_exchange(iam_server=iam_server,
364
- args=request.args,
365
- errors=errors,
366
- logger=__IAM_LOGGER)
497
+ token_info = iam_get_token(iam_server=iam_server,
498
+ args=args,
499
+ errors=errors,
500
+ logger=__IAM_LOGGER)
367
501
  result: Response
368
502
  if errors:
369
503
  result = Response(response="; ".join(errors),
370
504
  status=400)
371
505
  else:
372
- result = jsonify({"user-id": token_info[0],
373
- "access-token": token_info[1]})
374
-
375
- # log the response
506
+ result = jsonify(token_info)
376
507
  if __IAM_LOGGER:
377
- __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}")
378
510
 
379
511
  return result
380
512