pypomes-iam 0.8.0__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.
pypomes_iam/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from .iam_actions import (
2
2
  iam_callback, iam_exchange,
3
- iam_login, iam_logout, iam_get_token
3
+ iam_login, iam_logout, iam_get_token, iam_userinfo
4
4
  )
5
5
  from .iam_common import (
6
6
  IamServer, IamParam
@@ -10,10 +10,9 @@ from .iam_pomes import (
10
10
  )
11
11
  from .iam_services import (
12
12
  jwt_required, iam_setup_logger,
13
- service_setup_server, service_get_token,
14
- service_login, service_logout,
15
- service_callback, service_exchange,
16
- service_callback_exchange
13
+ service_setup_server, service_login, service_logout,
14
+ service_get_token, service_userinfo, service_callback,
15
+ service_exchange, service_callback_exchange
17
16
  )
18
17
  from .provider_pomes import (
19
18
  service_get_token, provider_get_token,
@@ -26,17 +25,16 @@ from .token_pomes import (
26
25
  __all__ = [
27
26
  # iam_actions
28
27
  "iam_callback", "iam_exchange",
29
- "iam_login", "iam_logout", "iam_get_token",
28
+ "iam_login", "iam_logout", "iam_get_token", "iam_userinfo",
30
29
  # iam_commons
31
30
  "IamServer", "IamParam",
32
31
  # iam_pomes
33
32
  "iam_setup_server", "iam_setup_endpoints",
34
33
  # iam_services
35
34
  "jwt_required", "iam_setup_logger",
36
- "service_setup_server", "service_get_token",
37
- "service_login", "service_logout",
38
- "service_callback", "service_exchange",
39
- "service_callback_exchange",
35
+ "service_setup_server", "service_login", "service_logout",
36
+ "service_get_token", "service_userinfo", "service_callback",
37
+ "service_exchange", "service_callback_exchange",
40
38
  # provider_pomes
41
39
  "provider_setup_server", "provider_get_token",
42
40
  "provider_setup_endpoint", "provider_setup_logger", "provider_setup_server",
@@ -100,9 +100,9 @@ def iam_logout(iam_server: IamServer,
100
100
  """
101
101
  Logout the user, by removing all data associating it from *iam_server*'s registry.
102
102
 
103
- The user is identified by the attribute *user-id* or "login", provided in *args*.
104
- If successful, remove all data relating to the user from the *IAM* server's registry.
105
- Otherwise, this operation fails silently, unless an error has ocurred.
103
+ The user is identified by the attribute *user-id* or *login*, provided in *args*.
104
+ A logout request is sent to *iam_server* and, if successful, remove all data relating to the user
105
+ from the *IAM* server's registry.
106
106
 
107
107
  :param iam_server: the reference registered *IAM* server
108
108
  :param args: the arguments passed when requesting the service
@@ -114,14 +114,65 @@ def iam_logout(iam_server: IamServer,
114
114
 
115
115
  if user_id:
116
116
  with _iam_lock:
117
- # retrieve the data for all users in the IAM server's registry
118
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
119
- errors=errors,
120
- logger=logger) or {}
121
- if user_id in users:
122
- users.pop(user_id)
123
- if logger:
124
- logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
117
+ # retrieve the IAM server's registry and the data for all users therein
118
+ registry: dict[str, Any] = _get_iam_registry(iam_server,
119
+ errors=errors,
120
+ logger=logger)
121
+ users: dict[str, dict[str, Any]] = registry[IamParam.USERS] if registry else {}
122
+ user_data: dict[str, Any] = users.get(user_id)
123
+ if user_data:
124
+ # request the IAM server to logout 'client_id'
125
+ client_secret: str = __get_client_secret(iam_server=iam_server,
126
+ errors=errors,
127
+ logger=logger)
128
+ if client_secret:
129
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
130
+ "/protocol/openid-connect/logout")
131
+ header_data: dict[str, str] = {
132
+ "Content-Type": "application/x-www-form-urlencoded"
133
+ }
134
+ body_data: dict[str, Any] = {
135
+ "client_id": registry[IamParam.CLIENT_ID],
136
+ "client_secret": client_secret,
137
+ "refresh_token": user_data[UserParam.REFRESH_TOKEN]
138
+ }
139
+ # log the POST
140
+ if logger:
141
+ logger.debug(msg=f"POST {url}")
142
+ try:
143
+ response: requests.Response = requests.post(url=url,
144
+ headers=header_data,
145
+ data=body_data)
146
+ if response.status_code in [200, 204]:
147
+ # request succeeded
148
+ if logger:
149
+ logger.debug(msg=f"POST success")
150
+ else:
151
+ # request failed, report the problem
152
+ msg: str = f"POST failure, status {response.status_code}, reason {response.reason}"
153
+ if logger:
154
+ logger.error(msg=msg)
155
+ if isinstance(errors, list):
156
+ errors.append(msg)
157
+ except Exception as e:
158
+ # the operation raised an exception
159
+ msg: str = exc_format(exc=e,
160
+ exc_info=sys.exc_info())
161
+ if logger:
162
+ logger.error(msg=msg)
163
+ if isinstance(errors, list):
164
+ errors.append(msg)
165
+
166
+ if not errors and user_id in users:
167
+ users.pop(user_id)
168
+ if logger:
169
+ logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
170
+ else:
171
+ msg: str = "User identification not provided"
172
+ if logger:
173
+ logger.error(msg=msg)
174
+ if isinstance(errors, list):
175
+ errors.append(msg)
125
176
 
126
177
 
127
178
  def iam_get_token(iam_server: IamServer,
@@ -176,7 +227,7 @@ def iam_get_token(iam_server: IamServer,
176
227
  refresh_expiration: int = user_data[UserParam.REFRESH_EXPIRATION]
177
228
  if now < refresh_expiration:
178
229
  header_data: dict[str, str] = {
179
- "Content-Type": "application/json"
230
+ "Content-Type": "application/x-www-form-urlencoded"
180
231
  }
181
232
  body_data: dict[str, str] = {
182
233
  "grant_type": "refresh_token",
@@ -306,8 +357,8 @@ def iam_callback(iam_server: IamServer,
306
357
  registry: dict[str, Any] = _get_iam_registry(iam_server,
307
358
  errors=errors,
308
359
  logger=logger)
309
- url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
310
- url += f"/broker/{target_idp}/token"
360
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
361
+ f"{registry[IamParam.CLIENT_REALM]}/broker/{target_idp}/token")
311
362
  header_data: dict[str, str] = {
312
363
  "Authorization": f"Bearer {result[1]}",
313
364
  "Content-Type": "application/json"
@@ -429,6 +480,60 @@ def iam_exchange(iam_server: IamServer,
429
480
  return result
430
481
 
431
482
 
483
+ def iam_userinfo(iam_server: IamServer,
484
+ args: dict[str, Any],
485
+ errors: list[str] = None,
486
+ logger: Logger = None) -> dict[str, Any] | None:
487
+ """
488
+ Obtain user data from *iam_server*.
489
+
490
+ The user is identified by the attribute *user-id* or *login*, provided in *args*.
491
+
492
+ :param iam_server: the reference registered *IAM* server
493
+ :param args: the arguments passed when requesting the service
494
+ :param errors: incidental error messages
495
+ :param logger: optional logger
496
+ :return: the user information requested, or *None* if error
497
+ """
498
+ # initialize the return variable
499
+ result: dict[str, Any] | None = None
500
+
501
+ # obtain the user's identification
502
+ user_id: str = args.get("user-id") or args.get("login")
503
+
504
+ err_msg: str | None = None
505
+ if user_id:
506
+ with _iam_lock:
507
+ # retrieve the IAM server's registry and the user data therein
508
+ registry: dict[str, Any] = _get_iam_registry(iam_server,
509
+ errors=errors,
510
+ logger=logger)
511
+ user_data: dict[str, Any] = registry[IamParam.USERS].get(user_id)
512
+ if user_data:
513
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
514
+ "/protocol/openid-connect/userinfo")
515
+ header_data: dict[str, str] = {
516
+ "Authorization": f"Bearer {args.get('access-token')}"
517
+ }
518
+ result = __get_for_data(url=url,
519
+ header_data=header_data,
520
+ params=None,
521
+ errors=errors,
522
+ logger=logger)
523
+ else:
524
+ err_msg = f"Unknown user '{user_id}'"
525
+ else:
526
+ err_msg: str = "User identification not provided"
527
+
528
+ if err_msg:
529
+ if logger:
530
+ logger.error(msg=err_msg)
531
+ if isinstance(errors, list):
532
+ errors.append(err_msg)
533
+
534
+ return result
535
+
536
+
432
537
  def __assert_link(iam_server: IamServer,
433
538
  user_id: str,
434
539
  token: str,
@@ -814,8 +919,8 @@ def __post_for_token(iam_server: IamServer,
814
919
  body_data["client_id"] = registry[IamParam.CLIENT_ID]
815
920
 
816
921
  # build the URL
817
- base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
818
- url: str = f"{base_url}/protocol/openid-connect/token"
922
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
923
+ f"{registry[IamParam.CLIENT_REALM]}/protocol/openid-connect/token")
819
924
  # 'client_secret' data must not be shown in log
820
925
  msg: str = f"POST {url}, {json.dumps(obj=body_data,
821
926
  ensure_ascii=False)}"
pypomes_iam/iam_common.py CHANGED
@@ -68,7 +68,7 @@ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
68
68
  or dynamically with calls to *iam_setup_server()*. Specifying configuration parameters with environment
69
69
  variables can be done by following these steps:
70
70
 
71
- 1. Specify *<APP_PREFIX>_IAM_SERVERS* with a list of names among the values found in *IamServer* class
71
+ 1. Specify *<APP_PREFIX>_AUTH_SERVERS* with a list of names among the values found in *IamServer* class
72
72
  (currently, *jusbr* and *keycloak* are supported), and the data set below for each server, where
73
73
  *<IAM>* stands for the server's name as presented in *IamServer* class:
74
74
  - *<APP_PREFIX>_<IAM>_ADMIN_ID* (optional, required if administrative duties are performed)
@@ -91,13 +91,14 @@ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
91
91
  - *<APP_PREFIX>_<IAM>_ENDPOINT_LOGIN*
92
92
  - *<APP_PREFIX>_<IAM>_ENDPOINT_LOGOUT*
93
93
  - *<APP_PREFIX>_<IAM>_ENDPOINT_TOKEN*
94
+ - *<APP_PREFIX>_<IAM>_ENDPOINT_USERINFO*
94
95
 
95
96
  :return: the configuration data for the select *IAM* servers.
96
97
  """
97
98
  # initialize the return variable
98
99
  result: dict[IamServer, dict[IamParam, Any]] = {}
99
100
 
100
- servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_IAM_SERVERS",
101
+ servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_AUTH_SERVERS",
101
102
  enum_class=IamServer) or []
102
103
  for server in servers:
103
104
  prefix = server.name
@@ -108,7 +109,7 @@ def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
108
109
  IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
109
110
  IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
110
111
  IamParam.LOGIN_TIMEOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT"),
111
- IamParam.PK_LIFETIME: env_get_int(key=f"{APP_PREFIX}_{prefix}_PUBLIC_KEY_LIFETIME"),
112
+ IamParam.PK_LIFETIME: env_get_int(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME"),
112
113
  IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
113
114
  IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH_BASE"),
114
115
  # dynamically set
pypomes_iam/iam_pomes.py CHANGED
@@ -10,7 +10,8 @@ from .iam_common import (
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
 
@@ -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
 
@@ -117,6 +119,7 @@ def iam_setup_endpoints(flask_app: Flask,
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()
@@ -135,6 +138,8 @@ def iam_setup_endpoints(flask_app: Flask,
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:
@@ -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"])
@@ -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
- 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")
@@ -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.
@@ -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)
@@ -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:
@@ -221,7 +241,8 @@ def service_login() -> Response:
221
241
 
222
242
 
223
243
  # @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
224
- # methods=["GET"])
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:
@@ -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
@@ -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:
@@ -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>, # IAM_ENDPOINT_USERINFO
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
@@ -28,7 +28,7 @@ class ProviderParam(StrEnum):
28
28
  ACCESS_EXPIRATION = "access-expiration"
29
29
  REFRESH_TOKEN = "refresh-token"
30
30
  REFRESH_EXPIRATION = "refresh-expiration"
31
- URL_AUTH = "url-auth"
31
+ URL_TOKEN = "url-token"
32
32
 
33
33
 
34
34
  # the logger for IAM service operations
@@ -44,7 +44,7 @@ def __get_provider_data() -> dict[str, dict[ProviderParam, Any]]:
44
44
  or dynamically with *provider_setup_server()*. Specifying configuration parameters with
45
45
  environment variables can be done by following these steps:
46
46
 
47
- 1. Specify *<APP_PREFIX>_IAM_PROVIDERS* with a list of names (typically, in lower-case), and the data set
47
+ 1. Specify *<APP_PREFIX>_AUTH_PROVIDERS* with a list of names (typically, in lower-case), and the data set
48
48
  below for each providers, where *<JWT>* stands for the provider's name in upper-case:
49
49
  - *<APP_PREFIX>_<JWT>_BODY_DATA* (optional)
50
50
  - *<APP_PREFIX>_<JWT>_CUSTOM_AUTH* (optional)
@@ -53,26 +53,26 @@ def __get_provider_data() -> dict[str, dict[ProviderParam, Any]]:
53
53
  - *<APP_PREFIX>_<JWT>_USER_SECRET* (required)
54
54
  - *<APP_PREFIX>_<JWT>_URL_TOKEN* (required)
55
55
 
56
- 2. The special environment variable *<APP_PREFIX>_IAM_PROVIDER_ENDPOINT* identifies the endpoint from which
57
- to obtain JWT tokens. It is not part of the *JWT* providers' setup, but is meant to be used
58
- by function *provider_setup_endpoint()*, wherein the value in that variable would represent the
59
- default value for its parameter.
56
+ 2. The special environment variable *<APP_PREFIX>_PROVIDER_ENDPOINT_TOKEN* identifies the endpoint
57
+ from which to obtain JWT tokens. It is not part of the *JWT* providers' setup, but is meant to be
58
+ used by function *provider_setup_endpoint()*, wherein the value in that variable would represent
59
+ the default value for its parameter.
60
60
 
61
61
  :return: the configuration data for the select *JWT* providers.
62
62
  """
63
63
  # initialize the return variable
64
64
  result: dict[str, dict[ProviderParam, Any]] = {}
65
65
 
66
- servers: list[str] = env_get_strs(key=f"{APP_PREFIX}_IAM_PROVIDERS") or []
66
+ servers: list[str] = env_get_strs(key=f"{APP_PREFIX}_AUTH_PROVIDERS") or []
67
67
  for server in servers:
68
68
  prefix = server.upper()
69
69
  result[server] = {
70
+ ProviderParam.USER_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_ID"),
71
+ ProviderParam.USER_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_SECRET"),
70
72
  ProviderParam.BODY_DATA: env_get_obj(key=f"{APP_PREFIX}_{prefix}_BODY_DATA"),
71
73
  ProviderParam.CUSTOM_AUTH: env_get_strs(key=f"{APP_PREFIX}_{prefix}_CUSTOM_AUTH"),
72
74
  ProviderParam.HEADER_DATA: env_get_obj(key=f"{APP_PREFIX}_{prefix}_HEADER_DATA"),
73
- ProviderParam.USER_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_ID"),
74
- ProviderParam.USER_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_SECRET"),
75
- ProviderParam.URL_AUTH: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH"),
75
+ ProviderParam.URL_TOKEN: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_TOKEN"),
76
76
  ProviderParam.ACCESS_TOKEN: None,
77
77
  ProviderParam.ACCESS_EXPIRATION: 0,
78
78
  ProviderParam.REFRESH_TOKEN: None,
@@ -85,12 +85,13 @@ def __get_provider_data() -> dict[str, dict[ProviderParam, Any]]:
85
85
  # structure:
86
86
  # {
87
87
  # <provider-id>: {
88
- # "url": <strl>,
89
- # "user": <str>,
90
- # "pwd": <str>,
88
+ # "body-data": <dict[str, str],
91
89
  # "custom-auth": <tuple[str, str]>,
92
90
  # "headers-data": <dict[str, str]>,
93
- # "body-data": <dict[str, str],
91
+ # "user-id": <str>,
92
+ # "user-secret": <str>,
93
+ # "url-token": <strl>,
94
+ # # dinamically set
94
95
  # "access-token": <str>,
95
96
  # "access-expiration": <timestamp>,
96
97
  # "refresh-token": <str>,
@@ -111,7 +112,7 @@ def provider_setup_server(provider_id: str,
111
112
  custom_auth: tuple[str, str] = None,
112
113
  header_data: dict[str, str] = None,
113
114
  body_data: dict[str, str] = None,
114
- url_auth: str = None) -> None:
115
+ url_token: str = None) -> None:
115
116
  """
116
117
  Setup the *JWT* provider *provider_id*.
117
118
 
@@ -122,9 +123,10 @@ def provider_setup_server(provider_id: str,
122
123
  as key-value pairs in the body of the request. Otherwise, the external provider *provider_id* uses the standard
123
124
  HTTP Basic Authorization scheme, wherein the credentials are B64-encoded and sent in the request headers.
124
125
 
125
- Optional constant key-value pairs (such as ['Content-Type', 'application/x-www-form-urlencoded']), to be
126
- added to the request headers, may be specified in *headers_data*. Likewise, optional constant key-value pairs
127
- (such as ['grant_type', 'client_credentials']), to be added to the request body, may be specified in *body_data*.
126
+ Optional constant key-value pairs (such as *['Content-Type', 'application/x-www-form-urlencoded']*),
127
+ to be added to the request headers, may be specified in *headers_data*. Likewise, optional constant
128
+ key-value pairs (such as *['grant_type', 'client_credentials']*), to be added to the request body,
129
+ may be specified in *body_data*.
128
130
 
129
131
  :param provider_id: the provider's identification
130
132
  :param user_id: the basic authorization user
@@ -132,7 +134,7 @@ def provider_setup_server(provider_id: str,
132
134
  :param custom_auth: optional key names for sending the credentials as key-value pairs in the body of the request
133
135
  :param header_data: optional key-value pairs to be added to the request headers
134
136
  :param body_data: optional key-value pairs to be added to the request body
135
- :param url_auth: the url to request *JWT* tokens with
137
+ :param url_token: the url to request *JWT* tokens with
136
138
  """
137
139
  global _provider_registry
138
140
 
@@ -151,17 +153,17 @@ def provider_setup_server(provider_id: str,
151
153
  header_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_HEADER_DATA")
152
154
  if "body_data" in defaulted_params:
153
155
  body_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_BODY_DATA")
154
- if "url_auth" in defaulted_params:
155
- url_auth = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH")
156
+ if "url_token" in defaulted_params:
157
+ url_token = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_TOKEN")
156
158
 
157
159
  with _provider_lock:
158
160
  _provider_registry[provider_id] = {
159
- ProviderParam.URL_AUTH: url_auth,
160
- ProviderParam.USER_ID: user_id,
161
- ProviderParam.USER_SECRET: user_secret,
161
+ ProviderParam.BODY_DATA: body_data,
162
162
  ProviderParam.CUSTOM_AUTH: custom_auth,
163
163
  ProviderParam.HEADER_DATA: header_data,
164
- ProviderParam.BODY_DATA: body_data,
164
+ ProviderParam.USER_ID: user_id,
165
+ ProviderParam.USER_SECRET: user_secret,
166
+ ProviderParam.URL_TOKEN: url_token,
165
167
  # dynamically set
166
168
  ProviderParam.ACCESS_TOKEN: None,
167
169
  ProviderParam.ACCESS_EXPIRATION: 0,
@@ -187,12 +189,12 @@ def provider_setup_endpoint(flask_app: Flask,
187
189
 
188
190
  # read from the environment variable
189
191
  if "provider_endpoint" in defaulted_params:
190
- provider_endpoint = env_get_str(key=f"{APP_PREFIX}_IAM_PROVIDER_ENDPOINT")
192
+ provider_endpoint = env_get_str(key=f"{APP_PREFIX}_PROVIDER_ENDPOINT_TOKEN")
191
193
 
192
194
  # establish the endpoints
193
195
  if provider_endpoint:
194
196
  flask_app.add_url_rule(rule=provider_endpoint,
195
- endpoint=f"jwt-callback",
197
+ endpoint=f"provider-get-token",
196
198
  view_func=service_get_token,
197
199
  methods=["GET"])
198
200
 
@@ -222,14 +224,16 @@ def service_get_token() -> Response:
222
224
 
223
225
  :return: *Response* containing the JWT token, or *BAD REQUEST*
224
226
  """
227
+ # retrieve the request arguments
228
+ args: dict[str, Any] = dict(request.args) or {}
229
+
225
230
  # log the request
226
231
  if __JWT_LOGGER:
227
- params: str = json.dumps(obj=request.args,
228
- ensure_ascii=False)
229
- __JWT_LOGGER.debug(msg=f"Request {request.method}:{request.path}, params {params}")
232
+ __JWT_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
233
+ ensure_ascii=False)}")
230
234
 
231
235
  # obtain the provider JWT
232
- provider_id: str = request.args.get("jwt-provider")
236
+ provider_id: str = args.get("jwt-provider")
233
237
 
234
238
  # retrieve the token
235
239
  token: str | None = None
@@ -284,7 +288,7 @@ def provider_get_token(provider_id: str,
284
288
  # access token has expired
285
289
  header_data: dict[str, str] | None = None
286
290
  body_data: dict[str, str] | None = None
287
- url: str = provider.get(ProviderParam.URL_AUTH)
291
+ url: str = provider.get(ProviderParam.URL_TOKEN)
288
292
  refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
289
293
  if refresh_token:
290
294
  # refresh token exists
@@ -298,7 +302,7 @@ def provider_get_token(provider_id: str,
298
302
  "grant_type": "refresh_token",
299
303
  "refresh_token": refresh_token
300
304
  }
301
- if not body_data:
305
+ if not header_data:
302
306
  # refresh token does not exist or has expired
303
307
  user: str = provider.get(ProviderParam.USER_ID)
304
308
  pwd: str = provider.get(ProviderParam.USER_SECRET)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.8.0
3
+ Version: 0.8.5
4
4
  Summary: A collection of Python pomes, penyeach (IAM modules)
5
5
  Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-IAM
6
6
  Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-IAM/issues
@@ -0,0 +1,11 @@
1
+ pypomes_iam/__init__.py,sha256=kkHvF3P79h21dNBmJ566Mp-L27oejhBcJa2VquyVsdg,1619
2
+ pypomes_iam/iam_actions.py,sha256=ORuHoiuMPnrMabvnCUcMeqHI4xfqbTErED1LydOPBCg,51191
3
+ pypomes_iam/iam_common.py,sha256=lJAx0J7xjAyzaMI9WXUXRq2qO7bUGIUP85h1hNE2-RE,17569
4
+ pypomes_iam/iam_pomes.py,sha256=VwqK3FoGj76SHKLARuBmIhziYnd_hoMWoUteMGRjuSc,8963
5
+ pypomes_iam/iam_services.py,sha256=_oAAk3y6iw_2gxDkbcNJDmj6Mk8HByhqX8fUT6Qg9kU,26865
6
+ pypomes_iam/provider_pomes.py,sha256=e2AFGQgEajDOvr47LJYAqJ9Eaf0G0MrBAKKC4JK2Jp0,17705
7
+ pypomes_iam/token_pomes.py,sha256=KiTlBNj3HURbZS_Rmti2RC6hny8VFPpbXeIO--HZ-fI,7703
8
+ pypomes_iam-0.8.5.dist-info/METADATA,sha256=z0_4gG_jLZVliMWrQzcRw9AJD58V703MZjT4ZIfKXEo,661
9
+ pypomes_iam-0.8.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ pypomes_iam-0.8.5.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
+ pypomes_iam-0.8.5.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- pypomes_iam/__init__.py,sha256=CbAH4lmonMxJTa3mWsWqA-pztmMb5-4Vsb4yA7HJ-hw,1561
2
- pypomes_iam/iam_actions.py,sha256=Le0uzKU9OfI9a5U0q2FvGDGrVwskMuV_e_kb5ZOveFQ,46227
3
- pypomes_iam/iam_common.py,sha256=xT1OFxX8wdRUN7GURL2kr6yBsh5j_Zb3iyqKykWEMYA,17523
4
- pypomes_iam/iam_pomes.py,sha256=IUwxMevYvDsH-PVNkbOF-b0U_W_5ACi-REBjm1ZT2PE,8421
5
- pypomes_iam/iam_services.py,sha256=bWPxzIsZFD9fP7bkLKqgi7Z23h107Qd_GKgL2KjQzuE,22995
6
- pypomes_iam/provider_pomes.py,sha256=gkE0LvOzfPASDCilPRhqSql4LhU6sCilucE5zbM3lF8,17534
7
- pypomes_iam/token_pomes.py,sha256=KiTlBNj3HURbZS_Rmti2RC6hny8VFPpbXeIO--HZ-fI,7703
8
- pypomes_iam-0.8.0.dist-info/METADATA,sha256=olIXbMd6htlsfw0TuEd4aSYEFwNuL_dne4tphUxwILM,661
9
- pypomes_iam-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- pypomes_iam-0.8.0.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
- pypomes_iam-0.8.0.dist-info/RECORD,,