pypomes-iam 0.7.9__tar.gz → 0.8.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.7.9
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
@@ -12,6 +12,6 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.12
13
13
  Requires-Dist: flask>=3.1.2
14
14
  Requires-Dist: pyjwt>=2.10.1
15
- Requires-Dist: pypomes-core>=2.8.4
15
+ Requires-Dist: pypomes-core>=2.8.6
16
16
  Requires-Dist: pypomes-crypto>=0.4.8
17
17
  Requires-Dist: requests>=2.32.5
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_iam"
9
- version = "0.7.9"
9
+ version = "0.8.5"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -21,7 +21,7 @@ classifiers = [
21
21
  dependencies = [
22
22
  "Flask>=3.1.2",
23
23
  "PyJWT>=2.10.1",
24
- "pypomes-core>=2.8.4",
24
+ "pypomes-core>=2.8.6",
25
25
  "pypomes-crypto>=0.4.8",
26
26
  "requests>=2.32.5"
27
27
  ]
@@ -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)}"
@@ -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
@@ -1,7 +1,8 @@
1
1
  from flask import Flask
2
2
  from pypomes_core import (
3
3
  APP_PREFIX,
4
- env_get_int, env_get_str, func_get_defaulted_params
4
+ env_get_int, env_get_str,
5
+ func_capture_params, func_defaulted_params
5
6
  )
6
7
 
7
8
  from .iam_common import (
@@ -9,10 +10,12 @@ from .iam_common import (
9
10
  )
10
11
  from .iam_services import (
11
12
  service_login, service_logout,
12
- service_callback, service_callback_exchange, service_exchange, service_get_token
13
+ service_callback, service_callback_exchange,
14
+ service_exchange, service_get_token, service_userinfo
13
15
  )
14
16
 
15
17
 
18
+ @func_capture_params
16
19
  def iam_setup_server(iam_server: IamServer,
17
20
  admin_id: str = None,
18
21
  admin_secret: str = None,
@@ -50,7 +53,7 @@ def iam_setup_server(iam_server: IamServer,
50
53
  :param url_base: base URL to request services
51
54
  """
52
55
  # obtain the defaulted parameters
53
- defaulted_params: list[str] = func_get_defaulted_params()
56
+ defaulted_params: list[str] = func_defaulted_params.get()
54
57
 
55
58
  # read from the environment variables
56
59
  prefix: str = iam_server.name
@@ -92,6 +95,7 @@ def iam_setup_server(iam_server: IamServer,
92
95
  }
93
96
 
94
97
 
98
+ @func_capture_params
95
99
  def iam_setup_endpoints(flask_app: Flask,
96
100
  iam_server: IamServer,
97
101
  callback_endpoint: str = None,
@@ -99,7 +103,8 @@ def iam_setup_endpoints(flask_app: Flask,
99
103
  exchange_endpoint: str = None,
100
104
  login_endpoint: str = None,
101
105
  logout_endpoint: str = None,
102
- token_endpoint: str = None) -> None:
106
+ token_endpoint: str = None,
107
+ userinfo_endpoint: str = None) -> None:
103
108
  """
104
109
  Setup the endpoints for accessing the services provided by *iam_server*.
105
110
 
@@ -114,9 +119,10 @@ def iam_setup_endpoints(flask_app: Flask,
114
119
  :param login_endpoint: endpoint for redirecting user to the *IAM* server's login page
115
120
  :param logout_endpoint: endpoint for terminating user access
116
121
  :param token_endpoint: endpoint for retrieving authentication token
122
+ :param userinfo_endpoint: endpoint for retrieving user data
117
123
  """
118
124
  # obtain the defaulted parameters
119
- defaulted_params: list[str] = func_get_defaulted_params()
125
+ defaulted_params: list[str] = func_defaulted_params.get()
120
126
 
121
127
  # read from the environment variables
122
128
  prefix: str = iam_server.name
@@ -132,6 +138,8 @@ def iam_setup_endpoints(flask_app: Flask,
132
138
  logout_endpoint = env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGOUT")
133
139
  if "token_endpoint" in defaulted_params:
134
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")
135
143
 
136
144
  # establish the endpoints
137
145
  if callback_endpoint:
@@ -158,9 +166,14 @@ def iam_setup_endpoints(flask_app: Flask,
158
166
  flask_app.add_url_rule(rule=logout_endpoint,
159
167
  endpoint=f"{iam_server}-logout",
160
168
  view_func=service_logout,
161
- methods=["GET"])
169
+ methods=["POST"])
162
170
  if token_endpoint:
163
171
  flask_app.add_url_rule(rule=token_endpoint,
164
172
  endpoint=f"{iam_server}-token",
165
173
  view_func=service_get_token,
166
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,10 +9,9 @@ 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
- from .iam_pomes import iam_setup_server
16
15
  from .token_pomes import token_get_claims, token_validate
17
16
 
18
17
  # the logger for IAM service operations
@@ -24,7 +23,10 @@ def jwt_required(func: callable) -> callable:
24
23
  """
25
24
  Create a decorator to authenticate service endpoints with JWT tokens.
26
25
 
26
+ The decorated function must be a registered endpoint to a *Flask* application.
27
+
27
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
28
30
  """
29
31
  # ruff: noqa: ANN003 - Missing type annotation for *{name}
30
32
  def wrapper(*args, **kwargs) -> Response:
@@ -45,19 +47,16 @@ def __request_validate(request: Request) -> Response:
45
47
  Because this code has a high usage frequency, only authentication failures are logged.
46
48
 
47
49
  :param request: the *request* to be verified
48
- :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*
49
51
  """
50
52
  # initialize the return variable
51
53
  result: Response | None = None
52
54
 
53
- # retrieve the authorization from the request header
54
- auth_header: str = request.headers.get("Authorization")
55
-
56
55
  # validate the authorization token
57
56
  bad_token: bool = True
58
- if auth_header and auth_header.startswith("Bearer "):
59
- # extract and validate the JWT access token
60
- token: str = auth_header.split(" ")[1]
57
+ token: str = __get_bearer_token(request=request)
58
+ if token:
59
+ # extract token claims
61
60
  claims: dict[str, Any] = token_get_claims(token=token)
62
61
  if claims:
63
62
  issuer: str = claims["payload"].get("iss")
@@ -101,6 +100,26 @@ def __request_validate(request: Request) -> Response:
101
100
  return result
102
101
 
103
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
+
104
123
  def iam_setup_logger(logger: Logger) -> None:
105
124
  """
106
125
  Register the logger for HTTP services.
@@ -143,14 +162,15 @@ def service_setup_server() -> Response:
143
162
 
144
163
  :return: *Response OK*
145
164
  """
165
+ # retrieve the request arguments
166
+ args: dict[str, Any] = (dict(request.json) if request.is_json else dict(request.form)) or {}
167
+
146
168
  # log the request
147
169
  if __IAM_LOGGER:
148
- __IAM_LOGGER.debug(msg=f"{_log_init(request=request)}; {json.dumps(obj=request.args,
149
- ensure_ascii=False)}")
150
- # retrieve the arguments
151
- args: dict[str, Any] = request.json if request.is_json else request.form
152
-
170
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
171
+ ensure_ascii=False)}")
153
172
  # setup the server
173
+ from .iam_pomes import iam_setup_server
154
174
  iam_setup_server(**args)
155
175
  result = Response(status=200)
156
176
 
@@ -188,10 +208,13 @@ def service_login() -> Response:
188
208
  # declare the return variable
189
209
  result: Response | None = None
190
210
 
211
+ # retrieve the request arguments
212
+ args: dict[str, Any] = dict(request.args) or {}
213
+
191
214
  # log the request
192
215
  if __IAM_LOGGER:
193
- __IAM_LOGGER.debug(msg=_log_init(request=request))
194
-
216
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
217
+ ensure_ascii=False)}")
195
218
  errors: list[str] = []
196
219
  with _iam_lock:
197
220
  # retrieve the IAM server
@@ -201,7 +224,7 @@ def service_login() -> Response:
201
224
  if iam_server:
202
225
  # obtain the login URL
203
226
  login_url: str = iam_login(iam_server=iam_server,
204
- args=request.args,
227
+ args=args,
205
228
  errors=errors,
206
229
  logger=__IAM_LOGGER)
207
230
  if login_url:
@@ -218,7 +241,8 @@ def service_login() -> Response:
218
241
 
219
242
 
220
243
  # @flask_app.route(rule=<logout_endpoint>, # IAM_ENDPOINT_LOGOUT
221
- # methods=["GET"])
244
+ # methods=["POST"])
245
+ @jwt_required
222
246
  def service_logout() -> Response:
223
247
  """
224
248
  Entry point for the *IAM* server's logout service.
@@ -236,10 +260,13 @@ def service_logout() -> Response:
236
260
  # declare the return variable
237
261
  result: Response | None
238
262
 
263
+ # retrieve the request arguments
264
+ args: dict[str, Any] = dict(request.args) or {}
265
+
239
266
  # log the request
240
267
  if __IAM_LOGGER:
241
- __IAM_LOGGER.debug(msg=_log_init(request=request))
242
-
268
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
269
+ ensure_ascii=False)}")
243
270
  errors: list[str] = []
244
271
  with _iam_lock:
245
272
  # retrieve the IAM server
@@ -249,7 +276,7 @@ def service_logout() -> Response:
249
276
  if iam_server:
250
277
  # logout the user
251
278
  iam_logout(iam_server=iam_server,
252
- args=request.args,
279
+ args=args,
253
280
  errors=errors,
254
281
  logger=__IAM_LOGGER)
255
282
  if errors:
@@ -291,10 +318,13 @@ def service_callback() -> Response:
291
318
 
292
319
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
293
320
  """
321
+ # retrieve the request arguments
322
+ args: dict[str, Any] = dict(request.args) or {}
323
+
294
324
  # log the request
295
325
  if __IAM_LOGGER:
296
- __IAM_LOGGER.debug(msg=_log_init(request=request))
297
-
326
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
327
+ ensure_ascii=False)}")
298
328
  errors: list[str] = []
299
329
  token_data: tuple[str, str] | None = None
300
330
  with _iam_lock:
@@ -305,7 +335,7 @@ def service_callback() -> Response:
305
335
  if iam_server:
306
336
  # process the callback operation
307
337
  token_data = iam_callback(iam_server=iam_server,
308
- args=request.args,
338
+ args=args,
309
339
  errors=errors,
310
340
  logger=__IAM_LOGGER)
311
341
  result: Response
@@ -347,10 +377,13 @@ def service_exchange() -> Response:
347
377
 
348
378
  :return: *Response* containing the reference user identification and the token, or *BAD REQUEST*
349
379
  """
380
+ # retrieve the request arguments
381
+ args: dict[str, Any] = dict(request.args) or {}
382
+
350
383
  # log the request
351
384
  if __IAM_LOGGER:
352
- __IAM_LOGGER.debug(msg=_log_init(request=request))
353
-
385
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
386
+ ensure_ascii=False)}")
354
387
  errors: list[str] = []
355
388
  with _iam_lock:
356
389
  # retrieve the IAM server
@@ -362,7 +395,7 @@ def service_exchange() -> Response:
362
395
  if iam_server:
363
396
  errors: list[str] = []
364
397
  token_info = iam_exchange(iam_server=iam_server,
365
- args=request.args,
398
+ args=args,
366
399
  errors=errors,
367
400
  logger=__IAM_LOGGER)
368
401
  result: Response
@@ -412,10 +445,13 @@ def service_callback_exchange() -> Response:
412
445
  # declare the return variable
413
446
  result: Response | None = None
414
447
 
448
+ # retrieve the request arguments
449
+ args: dict[str, Any] = dict(request.args) or {}
450
+
415
451
  # log the request
416
452
  if __IAM_LOGGER:
417
- __IAM_LOGGER.debug(msg=_log_init(request=request))
418
-
453
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
454
+ ensure_ascii=False)}")
419
455
  errors: list[str] = []
420
456
  with _iam_lock:
421
457
  # retrieve the IAM server
@@ -424,7 +460,7 @@ def service_callback_exchange() -> Response:
424
460
  logger=__IAM_LOGGER)
425
461
  # obtain the login URL
426
462
  token_info: tuple[str, str] = iam_callback(iam_server=iam_server,
427
- args=request.args,
463
+ args=args,
428
464
  errors=errors,
429
465
  logger=__IAM_LOGGER)
430
466
  if token_info:
@@ -474,13 +510,13 @@ def service_get_token() -> Response:
474
510
 
475
511
  :return: *Response* containing the user reference identification and the token, or *BAD REQUEST*
476
512
  """
513
+ # retrieve the request arguments
514
+ args: dict[str, Any] = dict(request.args) or {}
515
+
477
516
  # log the request
478
517
  if __IAM_LOGGER:
479
- __IAM_LOGGER.debug(msg=_log_init(request=request))
480
-
481
- # obtain the request arguments
482
- args: dict[str, Any] = request.args
483
-
518
+ __IAM_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
519
+ ensure_ascii=False)}")
484
520
  errors: list[str] = []
485
521
  token_info: dict[str, str] | None = None
486
522
  with _iam_lock:
@@ -508,14 +544,55 @@ def service_get_token() -> Response:
508
544
  return result
509
545
 
510
546
 
511
- 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:
512
551
  """
513
- Build the messages for logging the request entry.
552
+ Entry point for retrieving user data from the *IAM* server.
514
553
 
515
- :param request: the Request object
516
- :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*
517
563
  """
564
+ # retrieve the request arguments
565
+ args: dict[str, Any] = dict(request.args) or {}
518
566
 
519
- params: str = json.dumps(obj=request.args,
520
- ensure_ascii=False)
521
- 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
@@ -8,8 +8,8 @@ from flask import Flask, Response, request, jsonify
8
8
  from logging import Logger
9
9
  from pypomes_core import (
10
10
  APP_PREFIX, TZ_LOCAL,
11
- env_get_str, env_get_strs, env_get_obj,
12
- exc_format, func_get_defaulted_params
11
+ env_get_str, env_get_strs, env_get_obj, exc_format,
12
+ func_capture_params, func_defaulted_params
13
13
  )
14
14
  from threading import Lock
15
15
  from typing import Any, Final
@@ -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>,
@@ -104,13 +105,14 @@ _provider_registry: Final[dict[str, dict[str, Any]]] = __get_provider_data()
104
105
  _provider_lock: Final[Lock] = Lock()
105
106
 
106
107
 
108
+ @func_capture_params
107
109
  def provider_setup_server(provider_id: str,
108
110
  user_id: str = None,
109
111
  user_secret: str = None,
110
112
  custom_auth: tuple[str, str] = None,
111
113
  header_data: dict[str, str] = None,
112
114
  body_data: dict[str, str] = None,
113
- url_auth: str = None) -> None:
115
+ url_token: str = None) -> None:
114
116
  """
115
117
  Setup the *JWT* provider *provider_id*.
116
118
 
@@ -121,9 +123,10 @@ def provider_setup_server(provider_id: str,
121
123
  as key-value pairs in the body of the request. Otherwise, the external provider *provider_id* uses the standard
122
124
  HTTP Basic Authorization scheme, wherein the credentials are B64-encoded and sent in the request headers.
123
125
 
124
- Optional constant key-value pairs (such as ['Content-Type', 'application/x-www-form-urlencoded']), to be
125
- added to the request headers, may be specified in *headers_data*. Likewise, optional constant key-value pairs
126
- (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*.
127
130
 
128
131
  :param provider_id: the provider's identification
129
132
  :param user_id: the basic authorization user
@@ -131,12 +134,12 @@ def provider_setup_server(provider_id: str,
131
134
  :param custom_auth: optional key names for sending the credentials as key-value pairs in the body of the request
132
135
  :param header_data: optional key-value pairs to be added to the request headers
133
136
  :param body_data: optional key-value pairs to be added to the request body
134
- :param url_auth: the url to request *JWT* tokens with
137
+ :param url_token: the url to request *JWT* tokens with
135
138
  """
136
139
  global _provider_registry
137
140
 
138
141
  # obtain the defaulted parameters
139
- defaulted_params: list[str] = func_get_defaulted_params()
142
+ defaulted_params: list[str] = func_defaulted_params.get()
140
143
 
141
144
  # read from the environment variables
142
145
  prefix: str = provider_id.upper()
@@ -150,17 +153,17 @@ def provider_setup_server(provider_id: str,
150
153
  header_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_HEADER_DATA")
151
154
  if "body_data" in defaulted_params:
152
155
  body_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_BODY_DATA")
153
- if "url_auth" in defaulted_params:
154
- 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")
155
158
 
156
159
  with _provider_lock:
157
160
  _provider_registry[provider_id] = {
158
- ProviderParam.URL_AUTH: url_auth,
159
- ProviderParam.USER_ID: user_id,
160
- ProviderParam.USER_SECRET: user_secret,
161
+ ProviderParam.BODY_DATA: body_data,
161
162
  ProviderParam.CUSTOM_AUTH: custom_auth,
162
163
  ProviderParam.HEADER_DATA: header_data,
163
- ProviderParam.BODY_DATA: body_data,
164
+ ProviderParam.USER_ID: user_id,
165
+ ProviderParam.USER_SECRET: user_secret,
166
+ ProviderParam.URL_TOKEN: url_token,
164
167
  # dynamically set
165
168
  ProviderParam.ACCESS_TOKEN: None,
166
169
  ProviderParam.ACCESS_EXPIRATION: 0,
@@ -169,6 +172,7 @@ def provider_setup_server(provider_id: str,
169
172
  }
170
173
 
171
174
 
175
+ @func_capture_params
172
176
  def provider_setup_endpoint(flask_app: Flask,
173
177
  provider_endpoint: str = None) -> None:
174
178
  """
@@ -181,16 +185,16 @@ def provider_setup_endpoint(flask_app: Flask,
181
185
  :param provider_endpoint: endpoint for requenting tokens to provider
182
186
  """
183
187
  # obtain the defaulted parameters
184
- defaulted_params: list[str] = func_get_defaulted_params()
188
+ defaulted_params: list[str] = func_defaulted_params.get()
185
189
 
186
190
  # read from the environment variable
187
191
  if "provider_endpoint" in defaulted_params:
188
- 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")
189
193
 
190
194
  # establish the endpoints
191
195
  if provider_endpoint:
192
196
  flask_app.add_url_rule(rule=provider_endpoint,
193
- endpoint=f"jwt-callback",
197
+ endpoint=f"provider-get-token",
194
198
  view_func=service_get_token,
195
199
  methods=["GET"])
196
200
 
@@ -220,14 +224,16 @@ def service_get_token() -> Response:
220
224
 
221
225
  :return: *Response* containing the JWT token, or *BAD REQUEST*
222
226
  """
227
+ # retrieve the request arguments
228
+ args: dict[str, Any] = dict(request.args) or {}
229
+
223
230
  # log the request
224
231
  if __JWT_LOGGER:
225
- params: str = json.dumps(obj=request.args,
226
- ensure_ascii=False)
227
- __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)}")
228
234
 
229
235
  # obtain the provider JWT
230
- provider_id: str = request.args.get("jwt-provider")
236
+ provider_id: str = args.get("jwt-provider")
231
237
 
232
238
  # retrieve the token
233
239
  token: str | None = None
@@ -282,7 +288,7 @@ def provider_get_token(provider_id: str,
282
288
  # access token has expired
283
289
  header_data: dict[str, str] | None = None
284
290
  body_data: dict[str, str] | None = None
285
- url: str = provider.get(ProviderParam.URL_AUTH)
291
+ url: str = provider.get(ProviderParam.URL_TOKEN)
286
292
  refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
287
293
  if refresh_token:
288
294
  # refresh token exists
@@ -296,7 +302,7 @@ def provider_get_token(provider_id: str,
296
302
  "grant_type": "refresh_token",
297
303
  "refresh_token": refresh_token
298
304
  }
299
- if not body_data:
305
+ if not header_data:
300
306
  # refresh token does not exist or has expired
301
307
  user: str = provider.get(ProviderParam.USER_ID)
302
308
  pwd: str = provider.get(ProviderParam.USER_SECRET)
File without changes
File without changes
File without changes
File without changes