pypomes-iam 0.7.4__py3-none-any.whl → 0.8.3__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.

Potentially problematic release.


This version of pypomes-iam might be problematic. Click here for more details.

@@ -4,8 +4,13 @@ import sys
4
4
  from base64 import b64encode
5
5
  from datetime import datetime
6
6
  from enum import StrEnum
7
+ from flask import Flask, Response, request, jsonify
7
8
  from logging import Logger
8
- from pypomes_core import TZ_LOCAL, exc_format
9
+ from pypomes_core import (
10
+ APP_PREFIX, TZ_LOCAL,
11
+ env_get_str, env_get_strs, env_get_obj, exc_format,
12
+ func_capture_params, func_defaulted_params
13
+ )
9
14
  from threading import Lock
10
15
  from typing import Any, Final
11
16
 
@@ -14,16 +19,67 @@ class ProviderParam(StrEnum):
14
19
  """
15
20
  Parameters for configuring a *JWT* token provider.
16
21
  """
17
- URL = "url"
18
- USER = "user"
19
- PWD = "pwd"
22
+ BODY_DATA = "body-data"
20
23
  CUSTOM_AUTH = "custom-auth"
21
24
  HEADER_DATA = "headers-data"
22
- BODY_DATA = "body-data"
25
+ USER_ID = "user-id"
26
+ USER_SECRET = "user-secret"
23
27
  ACCESS_TOKEN = "access-token"
24
28
  ACCESS_EXPIRATION = "access-expiration"
25
29
  REFRESH_TOKEN = "refresh-token"
26
30
  REFRESH_EXPIRATION = "refresh-expiration"
31
+ URL_AUTH = "url-auth"
32
+
33
+
34
+ # the logger for IAM service operations
35
+ # (used exclusively at the HTTP endpoints - all other functions receive the logger as parameter)
36
+ __JWT_LOGGER: Logger | None = None
37
+
38
+
39
+ def __get_provider_data() -> dict[str, dict[ProviderParam, Any]]:
40
+ """
41
+ Obtain the configuration data for select *JWT* providers.
42
+
43
+ The configuration parameters for the JWT providers are specified with environment variables,
44
+ or dynamically with *provider_setup_server()*. Specifying configuration parameters with
45
+ environment variables can be done by following these steps:
46
+
47
+ 1. Specify *<APP_PREFIX>_IAM_PROVIDERS* with a list of names (typically, in lower-case), and the data set
48
+ below for each providers, where *<JWT>* stands for the provider's name in upper-case:
49
+ - *<APP_PREFIX>_<JWT>_BODY_DATA* (optional)
50
+ - *<APP_PREFIX>_<JWT>_CUSTOM_AUTH* (optional)
51
+ - *<APP_PREFIX>_<JWT>_HEADER_DATA* (optional)
52
+ - *<APP_PREFIX>_<JWT>_USER_ID* (required)
53
+ - *<APP_PREFIX>_<JWT>_USER_SECRET* (required)
54
+ - *<APP_PREFIX>_<JWT>_URL_TOKEN* (required)
55
+
56
+ 2. The special environment variable *<APP_PREFIX>_IAM_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
+
61
+ :return: the configuration data for the select *JWT* providers.
62
+ """
63
+ # initialize the return variable
64
+ result: dict[str, dict[ProviderParam, Any]] = {}
65
+
66
+ servers: list[str] = env_get_strs(key=f"{APP_PREFIX}_IAM_PROVIDERS") or []
67
+ for server in servers:
68
+ prefix = server.upper()
69
+ result[server] = {
70
+ ProviderParam.BODY_DATA: env_get_obj(key=f"{APP_PREFIX}_{prefix}_BODY_DATA"),
71
+ ProviderParam.CUSTOM_AUTH: env_get_strs(key=f"{APP_PREFIX}_{prefix}_CUSTOM_AUTH"),
72
+ 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"),
76
+ ProviderParam.ACCESS_TOKEN: None,
77
+ ProviderParam.ACCESS_EXPIRATION: 0,
78
+ ProviderParam.REFRESH_TOKEN: None,
79
+ ProviderParam.REFRESH_EXPIRATION: 0
80
+ }
81
+
82
+ return result
27
83
 
28
84
 
29
85
  # structure:
@@ -32,7 +88,7 @@ class ProviderParam(StrEnum):
32
88
  # "url": <strl>,
33
89
  # "user": <str>,
34
90
  # "pwd": <str>,
35
- # "custom-auth": <bool>,
91
+ # "custom-auth": <tuple[str, str]>,
36
92
  # "headers-data": <dict[str, str]>,
37
93
  # "body-data": <dict[str, str],
38
94
  # "access-token": <str>,
@@ -41,49 +97,73 @@ class ProviderParam(StrEnum):
41
97
  # "refresh-expiration": <timestamp>
42
98
  # }
43
99
  # }
44
- _provider_registry: Final[dict[str, dict[str, Any]]] = {}
100
+ _provider_registry: Final[dict[str, dict[str, Any]]] = __get_provider_data()
45
101
 
46
102
  # the lock protecting the data in '_provider_registry'
47
103
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
48
104
  _provider_lock: Final[Lock] = Lock()
49
105
 
50
106
 
51
- def provider_register(provider_id: str,
52
- auth_url: str,
53
- auth_user: str,
54
- auth_pwd: str,
55
- custom_auth: tuple[str, str] = None,
56
- headers_data: dict[str, str] = None,
57
- body_data: dict[str, str] = None) -> None:
107
+ @func_capture_params
108
+ def provider_setup_server(provider_id: str,
109
+ user_id: str = None,
110
+ user_secret: str = None,
111
+ custom_auth: tuple[str, str] = None,
112
+ header_data: dict[str, str] = None,
113
+ body_data: dict[str, str] = None,
114
+ url_auth: str = None) -> None:
58
115
  """
59
- Register an external authentication token provider.
116
+ Setup the *JWT* provider *provider_id*.
117
+
118
+ For the parameters not effectively passed, an attempt is made to obtain a value from the corresponding
119
+ environment variable.
60
120
 
61
121
  If specified, *custom_auth* provides key names for sending credentials (username and password, in this order)
62
122
  as key-value pairs in the body of the request. Otherwise, the external provider *provider_id* uses the standard
63
123
  HTTP Basic Authorization scheme, wherein the credentials are B64-encoded and sent in the request headers.
64
124
 
65
- Optional constant key-value pairs (such as ['Content-Type', 'application/x-www-form-urlencoded']), to be
66
- added to the request headers, may be specified in *headers_data*. Likewise, optional constant key-value pairs
67
- (such as ['grant_type', 'client_credentials']), to be added to the request body, may be specified in *body_data*.
125
+ Optional constant key-value pairs (such as *['Content-Type', 'application/x-www-form-urlencoded']*),
126
+ to be added to the request headers, may be specified in *headers_data*. Likewise, optional constant
127
+ key-value pairs (such as *['grant_type', 'client_credentials']*), to be added to the request body,
128
+ may be specified in *body_data*.
68
129
 
69
130
  :param provider_id: the provider's identification
70
- :param auth_url: the url to request authentication tokens with
71
- :param auth_user: the basic authorization user
72
- :param auth_pwd: the basic authorization password
131
+ :param user_id: the basic authorization user
132
+ :param user_secret: the basic authorization password
73
133
  :param custom_auth: optional key names for sending the credentials as key-value pairs in the body of the request
74
- :param headers_data: optional key-value pairs to be added to the request headers
134
+ :param header_data: optional key-value pairs to be added to the request headers
75
135
  :param body_data: optional key-value pairs to be added to the request body
136
+ :param url_auth: the url to request *JWT* tokens with
76
137
  """
77
138
  global _provider_registry
78
139
 
140
+ # obtain the defaulted parameters
141
+ defaulted_params: list[str] = func_defaulted_params.get()
142
+
143
+ # read from the environment variables
144
+ prefix: str = provider_id.upper()
145
+ if "user_id" in defaulted_params:
146
+ user_id = env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_ID")
147
+ if "user_secret" in defaulted_params:
148
+ user_secret = env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_SECRET")
149
+ if "custom_auth" in defaulted_params:
150
+ custom_auth = env_get_strs(key=f"{APP_PREFIX}_{prefix}_CUSTOM_AUTH")
151
+ if "header_data" in defaulted_params:
152
+ header_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_HEADER_DATA")
153
+ if "body_data" in defaulted_params:
154
+ body_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_BODY_DATA")
155
+ if "url_auth" in defaulted_params:
156
+ url_auth = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH")
157
+
79
158
  with _provider_lock:
80
159
  _provider_registry[provider_id] = {
81
- ProviderParam.URL: auth_url,
82
- ProviderParam.USER: auth_user,
83
- ProviderParam.PWD: auth_pwd,
160
+ ProviderParam.URL_AUTH: url_auth,
161
+ ProviderParam.USER_ID: user_id,
162
+ ProviderParam.USER_SECRET: user_secret,
84
163
  ProviderParam.CUSTOM_AUTH: custom_auth,
85
- ProviderParam.HEADER_DATA: headers_data,
164
+ ProviderParam.HEADER_DATA: header_data,
86
165
  ProviderParam.BODY_DATA: body_data,
166
+ # dynamically set
87
167
  ProviderParam.ACCESS_TOKEN: None,
88
168
  ProviderParam.ACCESS_EXPIRATION: 0,
89
169
  ProviderParam.REFRESH_TOKEN: None,
@@ -91,17 +171,107 @@ def provider_register(provider_id: str,
91
171
  }
92
172
 
93
173
 
174
+ @func_capture_params
175
+ def provider_setup_endpoint(flask_app: Flask,
176
+ provider_endpoint: str = None) -> None:
177
+ """
178
+ Setup the endpoint for requesting token from the registered *JWT* providers.
179
+
180
+ if *provider_endpoint* is not effectively passed, an attempt is made to obtain a value from the corresponding
181
+ environment variable.
182
+
183
+ :param flask_app: the Flask application
184
+ :param provider_endpoint: endpoint for requenting tokens to provider
185
+ """
186
+ # obtain the defaulted parameters
187
+ defaulted_params: list[str] = func_defaulted_params.get()
188
+
189
+ # read from the environment variable
190
+ if "provider_endpoint" in defaulted_params:
191
+ provider_endpoint = env_get_str(key=f"{APP_PREFIX}_IAM_PROVIDER_ENDPOINT_TOKEN")
192
+
193
+ # establish the endpoints
194
+ if provider_endpoint:
195
+ flask_app.add_url_rule(rule=provider_endpoint,
196
+ endpoint=f"jwt-callback",
197
+ view_func=service_get_token,
198
+ methods=["GET"])
199
+
200
+
201
+ def provider_setup_logger(logger: Logger) -> None:
202
+ """
203
+ Register the logger for HTTP services.
204
+
205
+ :param logger: the logger to be registered
206
+ """
207
+ global __JWT_LOGGER
208
+ __JWT_LOGGER = logger
209
+
210
+
211
+ # @flask_app.route(rule=<token_endpoint>, # IAM_PROVIDER_ENDPOINT_TOKEN
212
+ # methods=["GET"])
213
+ def service_get_token() -> Response:
214
+ """
215
+ Entry point for retrieving a token from the *JWT* provider.
216
+
217
+ The provider is identified by the request parameter *jwt-provider*.
218
+
219
+ On success, the returned *Response* will contain the following JSON:
220
+ {
221
+ "access-token": <token>
222
+ }
223
+
224
+ :return: *Response* containing the JWT token, or *BAD REQUEST*
225
+ """
226
+ # retrieve the request arguments
227
+ args: dict[str, Any] = dict(request.args) or {}
228
+
229
+ # log the request
230
+ if __JWT_LOGGER:
231
+ __JWT_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
232
+ ensure_ascii=False)}")
233
+
234
+ # obtain the provider JWT
235
+ provider_id: str = args.get("jwt-provider")
236
+
237
+ # retrieve the token
238
+ token: str | None = None
239
+ errors: list[str] = []
240
+ if provider_id:
241
+ token: str = provider_get_token(provider_id=provider_id,
242
+ errors=errors,
243
+ logger=__JWT_LOGGER)
244
+ else:
245
+ msg: str = "JWT provider not informed"
246
+ errors.append(msg)
247
+ if __JWT_LOGGER:
248
+ __JWT_LOGGER.error(msg=msg)
249
+
250
+ result: Response
251
+ if errors:
252
+ result = Response(response="; ".join(errors),
253
+ status=400)
254
+ else:
255
+ result = jsonify({"access-token": token})
256
+ if __JWT_LOGGER:
257
+ # log the response (the returned data is not logged, as it contains the token)
258
+ __JWT_LOGGER.debug(msg=f"Response {result}")
259
+
260
+ return result
261
+
262
+
94
263
  def provider_get_token(provider_id: str,
95
264
  errors: list[str] = None,
96
265
  logger: Logger = None) -> str | None:
97
266
  """
98
- Obtain an authentication token from the external provider *provider_id*.
267
+ Obtain an JWT token from the external provider *provider_id*.
99
268
 
100
269
  :param provider_id: the provider's identification
101
270
  :param errors: incidental error messages
102
271
  :param logger: optional logger
272
+ :return: the JWT token, or *None* if error
103
273
  """
104
- global _provider_registry # noqa: PLW0602
274
+ global _provider_registry
105
275
 
106
276
  # initialize the return variable
107
277
  result: str | None = None
@@ -117,7 +287,7 @@ def provider_get_token(provider_id: str,
117
287
  # access token has expired
118
288
  header_data: dict[str, str] | None = None
119
289
  body_data: dict[str, str] | None = None
120
- url: str = provider.get(ProviderParam.URL)
290
+ url: str = provider.get(ProviderParam.URL_AUTH)
121
291
  refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
122
292
  if refresh_token:
123
293
  # refresh token exists
@@ -133,9 +303,9 @@ def provider_get_token(provider_id: str,
133
303
  }
134
304
  if not body_data:
135
305
  # refresh token does not exist or has expired
136
- user: str = provider.get(ProviderParam.USER)
137
- pwd: str = provider.get(ProviderParam.PWD)
138
- headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
306
+ user: str = provider.get(ProviderParam.USER_ID)
307
+ pwd: str = provider.get(ProviderParam.USER_SECRET)
308
+ header_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
139
309
  body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
140
310
  custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
141
311
  if custom_auth:
@@ -143,7 +313,7 @@ def provider_get_token(provider_id: str,
143
313
  body_data[custom_auth[1]] = pwd
144
314
  else:
145
315
  enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
146
- headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
316
+ header_data["Authorization"] = f"Basic {enc_bytes.decode()}"
147
317
 
148
318
  # obtain the token
149
319
  token_data: dict[str, Any] = __post_for_token(url=url,
@@ -46,6 +46,33 @@ def token_get_claims(token: str,
46
46
  return result
47
47
 
48
48
 
49
+ def token_get_values(token: str,
50
+ keys: tuple[str, ...],
51
+ errors: list[str] = None,
52
+ logger: Logger = None) -> tuple:
53
+ """
54
+ Retrieve the values of *keys* in the token's payload.
55
+
56
+ Ther values are returned in the same order as requested in *keys*.
57
+ For a claim not found, *None* is returned in its position.
58
+
59
+ :param token: the reference token
60
+ :param keys: the names of the claims whose values are to be returned
61
+ :param errors: incidental errors
62
+ :param logger: optiona logger
63
+ :return: a tuple containing the respective values of *claims* in *token*.
64
+ """
65
+ token_claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
66
+ errors=errors,
67
+ logger=logger)
68
+ payload: dict[str, Any] = token_claims["payload"]
69
+ values: list[Any] = []
70
+ for key in keys:
71
+ values.append(payload.get(key))
72
+
73
+ return tuple(values)
74
+
75
+
49
76
  def token_validate(token: str,
50
77
  issuer: str = None,
51
78
  recipient_id: str = None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.7.4
3
+ Version: 0.8.3
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.1
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
@@ -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=f8MGez9Eyia30FAXdNiHjeHnljnMQOOERr_BOxtoirY,17575
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=yude0n63gzArUP5IRFXnkBATHA0NMuQm7-vRIGZuzuc,17648
7
+ pypomes_iam/token_pomes.py,sha256=KiTlBNj3HURbZS_Rmti2RC6hny8VFPpbXeIO--HZ-fI,7703
8
+ pypomes_iam-0.8.3.dist-info/METADATA,sha256=zHhCJ6Xjcf6oGtdBU9nvDVbQo1xSGA0UIogdyqFwGFc,661
9
+ pypomes_iam-0.8.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ pypomes_iam-0.8.3.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
+ pypomes_iam-0.8.3.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
2
- pypomes_iam/iam_actions.py,sha256=8cHtE6AU0sh8IBfO5w1IQj3HVZBSuTFMrrcOK0bnF9E,42774
3
- pypomes_iam/iam_common.py,sha256=ki_-m6fqJqUbGjgTD41r9zaE-FOXgA_c_tLisIYYTfU,15457
4
- pypomes_iam/iam_pomes.py,sha256=_kLnrZG25XhJsIv3wqDl_2sIJ2ho_2TIMKrPCyPmA7Q,7362
5
- pypomes_iam/iam_services.py,sha256=AzrZux2Pt_FoCNcTcXfWphHb587vB3WIbKYG7RFf5zE,15821
6
- pypomes_iam/provider_pomes.py,sha256=3mMj5LQs53YEINUEOfFBAxOwOP3aOR_szlE4daEBLK0,10523
7
- pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
8
- pypomes_iam-0.7.4.dist-info/METADATA,sha256=Si9dT8ORriAGUwCTrl4jYs3tsoz0XVgHATeqIVPv96g,661
9
- pypomes_iam-0.7.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- pypomes_iam-0.7.4.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
- pypomes_iam-0.7.4.dist-info/RECORD,,