pypomes-iam 0.8.0__py3-none-any.whl → 0.8.9__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.
pypomes_iam/iam_pomes.py CHANGED
@@ -6,11 +6,12 @@ from pypomes_core import (
6
6
  )
7
7
 
8
8
  from .iam_common import (
9
- _IAM_SERVERS, IamServer, IamParam, _iam_lock
9
+ _IAM_SERVERS, IamServer, ServerParam, _iam_lock
10
10
  )
11
11
  from .iam_services import (
12
12
  service_login, service_logout,
13
- service_callback, service_callback_exchange, service_exchange, service_get_token
13
+ service_callback, service_callback_exchange,
14
+ service_exchange, service_get_token, service_userinfo
14
15
  )
15
16
 
16
17
 
@@ -40,7 +41,7 @@ def iam_setup_server(iam_server: IamServer,
40
41
  it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
41
42
  the first time it is needed.
42
43
 
43
- :param iam_server: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
44
+ :param iam_server: identifies the supported *IAM* server
44
45
  :param admin_id: identifies the realm administrator
45
46
  :param admin_secret: password for the realm administrator
46
47
  :param client_id: the client's identification with the *IAM* server
@@ -69,28 +70,28 @@ def iam_setup_server(iam_server: IamServer,
69
70
  if "login_timeout" in defaulted_params:
70
71
  login_timeout = env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT")
71
72
  if "pk_lifetime" in defaulted_params:
72
- pk_lifetime = env_get_int(key=f"{APP_PREFIX}_{prefix}_PUBLIC_KEY_LIFETIME")
73
+ pk_lifetime = env_get_int(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME")
73
74
  if "recipient_attr" in defaulted_params:
74
75
  recipient_attr = env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR")
75
76
  if "url_base" in defaulted_params:
76
- url_base = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH_BASE")
77
+ url_base = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_BASE")
77
78
 
78
- # configure the Keycloak registry
79
+ # configure the IAM server's registry
79
80
  with _iam_lock:
80
81
  _IAM_SERVERS[iam_server] = {
81
- IamParam.CLIENT_ID: client_id,
82
- IamParam.CLIENT_REALM: client_realm,
83
- IamParam.CLIENT_SECRET: client_secret,
84
- IamParam.RECIPIENT_ATTR: recipient_attr,
85
- IamParam.ADMIN_ID: admin_id,
86
- IamParam.ADMIN_SECRET: admin_secret,
87
- IamParam.LOGIN_TIMEOUT: login_timeout,
88
- IamParam.PK_LIFETIME: pk_lifetime,
89
- IamParam.URL_BASE: url_base,
82
+ ServerParam.CLIENT_ID: client_id,
83
+ ServerParam.CLIENT_REALM: client_realm,
84
+ ServerParam.CLIENT_SECRET: client_secret,
85
+ ServerParam.RECIPIENT_ATTR: recipient_attr,
86
+ ServerParam.ADMIN_ID: admin_id,
87
+ ServerParam.ADMIN_SECRET: admin_secret,
88
+ ServerParam.LOGIN_TIMEOUT: login_timeout,
89
+ ServerParam.PK_LIFETIME: pk_lifetime,
90
+ ServerParam.URL_BASE: url_base,
90
91
  # dynamic attributes
91
- IamParam.PK_EXPIRATION: 0,
92
- IamParam.PUBLIC_KEY: None,
93
- IamParam.USERS: {}
92
+ ServerParam.PK_EXPIRATION: 0,
93
+ ServerParam.PUBLIC_KEY: None,
94
+ ServerParam.USERS: {}
94
95
  }
95
96
 
96
97
 
@@ -102,7 +103,8 @@ def iam_setup_endpoints(flask_app: Flask,
102
103
  exchange_endpoint: str = None,
103
104
  login_endpoint: str = None,
104
105
  logout_endpoint: str = None,
105
- token_endpoint: str = None) -> None:
106
+ token_endpoint: str = None,
107
+ userinfo_endpoint: str = None) -> None:
106
108
  """
107
109
  Setup the endpoints for accessing the services provided by *iam_server*.
108
110
 
@@ -110,13 +112,14 @@ def iam_setup_endpoints(flask_app: Flask,
110
112
  environment variables.
111
113
 
112
114
  :param flask_app: the Flask application
113
- :param iam_server: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
115
+ :param iam_server: identifies the supported *IAM* server
114
116
  :param callback_endpoint: endpoint for the callback from the front end
115
117
  :param callback_exchange_endpoint: endpoint for the combination callback and exchange
116
118
  :param exchange_endpoint: endpoint for requesting token exchange
117
119
  :param login_endpoint: endpoint for redirecting user to the *IAM* server's login page
118
120
  :param logout_endpoint: endpoint for terminating user access
119
121
  :param token_endpoint: endpoint for retrieving authentication token
122
+ :param userinfo_endpoint: endpoint for retrieving user data
120
123
  """
121
124
  # obtain the defaulted parameters
122
125
  defaulted_params: list[str] = func_defaulted_params.get()
@@ -128,20 +131,22 @@ def iam_setup_endpoints(flask_app: Flask,
128
131
  if "callback_exchange_endpoint" in defaulted_params:
129
132
  callback_exchange_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_CALLBACK_EXCHANGE")
130
133
  if "exchange_endpoint" in defaulted_params:
131
- callback_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE")
134
+ exchange_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE")
132
135
  if "login_endpoint" in defaulted_params:
133
136
  login_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGIN")
134
137
  if "logout_endpoint" in defaulted_params:
135
138
  logout_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGOUT")
136
139
  if "token_endpoint" in defaulted_params:
137
140
  token_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_TOKEN")
141
+ if "userinfo_endpoint" in defaulted_params:
142
+ userinfo_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_USERINFO")
138
143
 
139
144
  # establish the endpoints
140
145
  if callback_endpoint:
141
146
  flask_app.add_url_rule(rule=callback_endpoint,
142
147
  endpoint=f"{iam_server}-callback",
143
148
  view_func=service_callback,
144
- methods=["GET"])
149
+ methods=["GET", "POST"])
145
150
  if callback_exchange_endpoint:
146
151
  flask_app.add_url_rule(rule=callback_exchange_endpoint,
147
152
  endpoint=f"{iam_server}-callback-exchange",
@@ -161,9 +166,14 @@ def iam_setup_endpoints(flask_app: Flask,
161
166
  flask_app.add_url_rule(rule=logout_endpoint,
162
167
  endpoint=f"{iam_server}-logout",
163
168
  view_func=service_logout,
164
- methods=["GET"])
169
+ methods=["POST"])
165
170
  if token_endpoint:
166
171
  flask_app.add_url_rule(rule=token_endpoint,
167
172
  endpoint=f"{iam_server}-token",
168
173
  view_func=service_get_token,
169
174
  methods=["GET"])
175
+ if userinfo_endpoint:
176
+ flask_app.add_url_rule(rule=userinfo_endpoint,
177
+ endpoint=f"{iam_server}-userinfo",
178
+ view_func=service_userinfo,
179
+ methods=["GET"])
@@ -4,13 +4,13 @@ from logging import Logger
4
4
  from typing import Any
5
5
 
6
6
  from .iam_common import (
7
- IamServer, IamParam, _iam_lock,
7
+ IamServer, ServerParam, _iam_lock,
8
8
  _get_iam_registry, _get_public_key,
9
9
  _iam_server_from_endpoint, _iam_server_from_issuer
10
10
  )
11
11
  from .iam_actions import (
12
- iam_login, iam_logout,
13
- iam_get_token, iam_exchange, iam_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
 
@@ -47,19 +47,16 @@ def __request_validate(request: Request) -> Response:
47
47
  Because this code has a high usage frequency, only authentication failures are logged.
48
48
 
49
49
  :param request: the *request* to be verified
50
- :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*
51
51
  """
52
52
  # initialize the return variable
53
53
  result: Response | None = None
54
54
 
55
- # retrieve the authorization from the request header
56
- auth_header: str = request.headers.get("Authorization")
57
-
58
55
  # validate the authorization token
59
56
  bad_token: bool = True
60
- if auth_header and auth_header.startswith("Bearer "):
61
- # extract and validate the JWT access token
62
- token: str = auth_header.split(" ")[1]
57
+ token: str = __get_bearer_token(request=request)
58
+ if token:
59
+ # extract token claims
63
60
  claims: dict[str, Any] = token_get_claims(token=token)
64
61
  if claims:
65
62
  issuer: str = claims["payload"].get("iss")
@@ -77,7 +74,7 @@ def __request_validate(request: Request) -> Response:
77
74
  errors=None,
78
75
  logger=__IAM_LOGGER)
79
76
  if registry:
80
- recipient_attr = registry[IamParam.RECIPIENT_ATTR]
77
+ recipient_attr = registry[ServerParam.RECIPIENT_ATTR]
81
78
  public_key = _get_public_key(iam_server=iam_server,
82
79
  errors=None,
83
80
  logger=__IAM_LOGGER)
@@ -103,6 +100,26 @@ def __request_validate(request: Request) -> Response:
103
100
  return result
104
101
 
105
102
 
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
+
106
123
  def iam_setup_logger(logger: Logger) -> None:
107
124
  """
108
125
  Register the logger for HTTP services.
@@ -120,7 +137,7 @@ def service_setup_server() -> Response:
120
137
  Entry point to setup a *IAM* server.
121
138
 
122
139
  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*)
140
+ - *iam_server*: identifies the supported *IAM* server
124
141
  - *admin_id*: identifies the realm administrator
125
142
  - *admin_secret*: password for the realm administrator
126
143
  - *client_id*: the client's identification with the *IAM* server
@@ -145,13 +162,13 @@ def service_setup_server() -> Response:
145
162
 
146
163
  :return: *Response OK*
147
164
  """
165
+ # retrieve the request arguments
166
+ args: dict[str, Any] = (dict(request.json) if request.is_json else dict(request.form)) or {}
167
+
148
168
  # log the request
149
169
  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
-
170
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
171
+ ensure_ascii=False)}")
155
172
  # setup the server
156
173
  from .iam_pomes import iam_setup_server
157
174
  iam_setup_server(**args)
@@ -164,7 +181,7 @@ def service_setup_server() -> Response:
164
181
  return result
165
182
 
166
183
 
167
- # @flask_app.route(rule=<login_endpoint>, # IAM_ENDPOINT_LOGIN
184
+ # @flask_app.route(rule=<login_endpoint>,
168
185
  # methods=["GET"])
169
186
  def service_login() -> Response:
170
187
  """
@@ -191,10 +208,13 @@ def service_login() -> Response:
191
208
  # declare the return variable
192
209
  result: Response | None = None
193
210
 
211
+ # retrieve the request arguments
212
+ args: dict[str, Any] = dict(request.args) or {}
213
+
194
214
  # log the request
195
215
  if __IAM_LOGGER:
196
- __IAM_LOGGER.debug(msg=_log_init(request=request))
197
-
216
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
217
+ ensure_ascii=False)}")
198
218
  errors: list[str] = []
199
219
  with _iam_lock:
200
220
  # retrieve the IAM server
@@ -204,7 +224,7 @@ def service_login() -> Response:
204
224
  if iam_server:
205
225
  # obtain the login URL
206
226
  login_url: str = iam_login(iam_server=iam_server,
207
- args=request.args,
227
+ args=args,
208
228
  errors=errors,
209
229
  logger=__IAM_LOGGER)
210
230
  if login_url:
@@ -220,8 +240,9 @@ def service_login() -> Response:
220
240
  return result
221
241
 
222
242
 
223
- # @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
224
- # methods=["GET"])
243
+ # @flask_app.route(rule=<logout_endpoint>,
244
+ # methods=["POST"])
245
+ @jwt_required
225
246
  def service_logout() -> Response:
226
247
  """
227
248
  Entry point for the *IAM* server's logout service.
@@ -239,10 +260,13 @@ def service_logout() -> Response:
239
260
  # declare the return variable
240
261
  result: Response | None
241
262
 
263
+ # retrieve the request arguments
264
+ args: dict[str, Any] = dict(request.args) or {}
265
+
242
266
  # log the request
243
267
  if __IAM_LOGGER:
244
- __IAM_LOGGER.debug(msg=_log_init(request=request))
245
-
268
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
269
+ ensure_ascii=False)}")
246
270
  errors: list[str] = []
247
271
  with _iam_lock:
248
272
  # retrieve the IAM server
@@ -252,7 +276,7 @@ def service_logout() -> Response:
252
276
  if iam_server:
253
277
  # logout the user
254
278
  iam_logout(iam_server=iam_server,
255
- args=request.args,
279
+ args=args,
256
280
  errors=errors,
257
281
  logger=__IAM_LOGGER)
258
282
  if errors:
@@ -268,7 +292,7 @@ def service_logout() -> Response:
268
292
  return result
269
293
 
270
294
 
271
- # @flask_app.route(rule=<callback_endpoint>, # IAM_ENDPOINT_CALLBACK
295
+ # @flask_app.route(rule=<callback_endpoint>,
272
296
  # methods=["GET", "POST"])
273
297
  def service_callback() -> Response:
274
298
  """
@@ -294,10 +318,13 @@ def service_callback() -> Response:
294
318
 
295
319
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
296
320
  """
321
+ # retrieve the request arguments
322
+ args: dict[str, Any] = dict(request.args) or {}
323
+
297
324
  # log the request
298
325
  if __IAM_LOGGER:
299
- __IAM_LOGGER.debug(msg=_log_init(request=request))
300
-
326
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
327
+ ensure_ascii=False)}")
301
328
  errors: list[str] = []
302
329
  token_data: tuple[str, str] | None = None
303
330
  with _iam_lock:
@@ -308,7 +335,7 @@ def service_callback() -> Response:
308
335
  if iam_server:
309
336
  # process the callback operation
310
337
  token_data = iam_callback(iam_server=iam_server,
311
- args=request.args,
338
+ args=args,
312
339
  errors=errors,
313
340
  logger=__IAM_LOGGER)
314
341
  result: Response
@@ -325,7 +352,7 @@ def service_callback() -> Response:
325
352
  return result
326
353
 
327
354
 
328
- # @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_ENDPOINT_EXCHANGE
355
+ # @flask_app.route(rule=<callback_endpoint>,
329
356
  # methods=["POST"])
330
357
  def service_exchange() -> Response:
331
358
  """
@@ -350,10 +377,13 @@ def service_exchange() -> Response:
350
377
 
351
378
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
352
379
  """
380
+ # retrieve the request arguments
381
+ args: dict[str, Any] = dict(request.args) or {}
382
+
353
383
  # log the request
354
384
  if __IAM_LOGGER:
355
- __IAM_LOGGER.debug(msg=_log_init(request=request))
356
-
385
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
386
+ ensure_ascii=False)}")
357
387
  errors: list[str] = []
358
388
  with _iam_lock:
359
389
  # retrieve the IAM server
@@ -365,7 +395,7 @@ def service_exchange() -> Response:
365
395
  if iam_server:
366
396
  errors: list[str] = []
367
397
  token_info = iam_exchange(iam_server=iam_server,
368
- args=request.args,
398
+ args=args,
369
399
  errors=errors,
370
400
  logger=__IAM_LOGGER)
371
401
  result: Response
@@ -415,10 +445,13 @@ def service_callback_exchange() -> Response:
415
445
  # declare the return variable
416
446
  result: Response | None = None
417
447
 
448
+ # retrieve the request arguments
449
+ args: dict[str, Any] = dict(request.args) or {}
450
+
418
451
  # log the request
419
452
  if __IAM_LOGGER:
420
- __IAM_LOGGER.debug(msg=_log_init(request=request))
421
-
453
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
454
+ ensure_ascii=False)}")
422
455
  errors: list[str] = []
423
456
  with _iam_lock:
424
457
  # retrieve the IAM server
@@ -427,7 +460,7 @@ def service_callback_exchange() -> Response:
427
460
  logger=__IAM_LOGGER)
428
461
  # obtain the login URL
429
462
  token_info: tuple[str, str] = iam_callback(iam_server=iam_server,
430
- args=request.args,
463
+ args=args,
431
464
  errors=errors,
432
465
  logger=__IAM_LOGGER)
433
466
  if token_info:
@@ -457,7 +490,7 @@ def service_callback_exchange() -> Response:
457
490
  return result
458
491
 
459
492
 
460
- # @flask_app.route(rule=<token_endpoint>, # IAM_ENDPOINT_TOKEN
493
+ # @flask_app.route(rule=<token_endpoint>,
461
494
  # methods=["GET"])
462
495
  def service_get_token() -> Response:
463
496
  """
@@ -477,13 +510,13 @@ def service_get_token() -> Response:
477
510
 
478
511
  :return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
479
512
  """
513
+ # retrieve the request arguments
514
+ args: dict[str, Any] = dict(request.args) or {}
515
+
480
516
  # log the request
481
517
  if __IAM_LOGGER:
482
- __IAM_LOGGER.debug(msg=_log_init(request=request))
483
-
484
- # obtain the request arguments
485
- args: dict[str, Any] = request.args
486
-
518
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
519
+ ensure_ascii=False)}")
487
520
  errors: list[str] = []
488
521
  token_info: dict[str, str] | None = None
489
522
  with _iam_lock:
@@ -511,14 +544,55 @@ def service_get_token() -> Response:
511
544
  return result
512
545
 
513
546
 
514
- def _log_init(request: Request) -> str:
547
+ # @flask_app.route(rule=<token_endpoint>,
548
+ # methods=["GET"])
549
+ @jwt_required
550
+ def service_userinfo() -> Response:
515
551
  """
516
- Build the messages for logging the request entry.
552
+ Entry point for retrieving user data from the *IAM* server.
517
553
 
518
- :param request: the Request object
519
- :return: the log message
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()*.
557
+
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*
520
563
  """
564
+ # retrieve the request arguments
565
+ args: dict[str, Any] = dict(request.args) or {}
521
566
 
522
- params: str = json.dumps(obj=request.args,
523
- ensure_ascii=False)
524
- return f"Request {request.method}:{request.path}, params {params}"
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)
573
+
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