pypomes-iam 0.7.6__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.
@@ -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,76 +19,152 @@ 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_TOKEN = "url-token"
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>_AUTH_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>_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}_AUTH_PROVIDERS") or []
67
+ for server in servers:
68
+ prefix = server.upper()
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"),
72
+ ProviderParam.BODY_DATA: env_get_obj(key=f"{APP_PREFIX}_{prefix}_BODY_DATA"),
73
+ ProviderParam.CUSTOM_AUTH: env_get_strs(key=f"{APP_PREFIX}_{prefix}_CUSTOM_AUTH"),
74
+ ProviderParam.HEADER_DATA: env_get_obj(key=f"{APP_PREFIX}_{prefix}_HEADER_DATA"),
75
+ ProviderParam.URL_TOKEN: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_TOKEN"),
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:
30
86
  # {
31
87
  # <provider-id>: {
32
- # "url": <strl>,
33
- # "user": <str>,
34
- # "pwd": <str>,
35
- # "custom-auth": <bool>,
36
- # "headers-data": <dict[str, str]>,
37
88
  # "body-data": <dict[str, str],
89
+ # "custom-auth": <tuple[str, str]>,
90
+ # "headers-data": <dict[str, str]>,
91
+ # "user-id": <str>,
92
+ # "user-secret": <str>,
93
+ # "url-token": <strl>,
94
+ # # dinamically set
38
95
  # "access-token": <str>,
39
96
  # "access-expiration": <timestamp>,
40
97
  # "refresh-token": <str>,
41
98
  # "refresh-expiration": <timestamp>
42
99
  # }
43
100
  # }
44
- _provider_registry: Final[dict[str, dict[str, Any]]] = {}
101
+ _provider_registry: Final[dict[str, dict[str, Any]]] = __get_provider_data()
45
102
 
46
103
  # the lock protecting the data in '_provider_registry'
47
104
  # (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
48
105
  _provider_lock: Final[Lock] = Lock()
49
106
 
50
107
 
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:
108
+ @func_capture_params
109
+ def provider_setup_server(provider_id: str,
110
+ user_id: str = None,
111
+ user_secret: str = None,
112
+ custom_auth: tuple[str, str] = None,
113
+ header_data: dict[str, str] = None,
114
+ body_data: dict[str, str] = None,
115
+ url_token: str = None) -> None:
58
116
  """
59
- Register an external authentication token provider.
117
+ Setup the *JWT* provider *provider_id*.
118
+
119
+ For the parameters not effectively passed, an attempt is made to obtain a value from the corresponding
120
+ environment variable.
60
121
 
61
122
  If specified, *custom_auth* provides key names for sending credentials (username and password, in this order)
62
123
  as key-value pairs in the body of the request. Otherwise, the external provider *provider_id* uses the standard
63
124
  HTTP Basic Authorization scheme, wherein the credentials are B64-encoded and sent in the request headers.
64
125
 
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*.
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*.
68
130
 
69
131
  :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
132
+ :param user_id: the basic authorization user
133
+ :param user_secret: the basic authorization password
73
134
  :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
135
+ :param header_data: optional key-value pairs to be added to the request headers
75
136
  :param body_data: optional key-value pairs to be added to the request body
137
+ :param url_token: the url to request *JWT* tokens with
76
138
  """
77
139
  global _provider_registry
78
140
 
141
+ # obtain the defaulted parameters
142
+ defaulted_params: list[str] = func_defaulted_params.get()
143
+
144
+ # read from the environment variables
145
+ prefix: str = provider_id.upper()
146
+ if "user_id" in defaulted_params:
147
+ user_id = env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_ID")
148
+ if "user_secret" in defaulted_params:
149
+ user_secret = env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_SECRET")
150
+ if "custom_auth" in defaulted_params:
151
+ custom_auth = env_get_strs(key=f"{APP_PREFIX}_{prefix}_CUSTOM_AUTH")
152
+ if "header_data" in defaulted_params:
153
+ header_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_HEADER_DATA")
154
+ if "body_data" in defaulted_params:
155
+ body_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_BODY_DATA")
156
+ if "url_token" in defaulted_params:
157
+ url_token = env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_TOKEN")
158
+
79
159
  with _provider_lock:
80
160
  _provider_registry[provider_id] = {
81
- ProviderParam.URL: auth_url,
82
- ProviderParam.USER: auth_user,
83
- ProviderParam.PWD: auth_pwd,
84
- ProviderParam.CUSTOM_AUTH: custom_auth,
85
- ProviderParam.HEADER_DATA: headers_data,
86
161
  ProviderParam.BODY_DATA: body_data,
162
+ ProviderParam.CUSTOM_AUTH: custom_auth,
163
+ ProviderParam.HEADER_DATA: header_data,
164
+ ProviderParam.USER_ID: user_id,
165
+ ProviderParam.USER_SECRET: user_secret,
166
+ ProviderParam.URL_TOKEN: url_token,
167
+ # dynamically set
87
168
  ProviderParam.ACCESS_TOKEN: None,
88
169
  ProviderParam.ACCESS_EXPIRATION: 0,
89
170
  ProviderParam.REFRESH_TOKEN: None,
@@ -91,17 +172,107 @@ def provider_register(provider_id: str,
91
172
  }
92
173
 
93
174
 
175
+ @func_capture_params
176
+ def provider_setup_endpoint(flask_app: Flask,
177
+ provider_endpoint: str = None) -> None:
178
+ """
179
+ Setup the endpoint for requesting token from the registered *JWT* providers.
180
+
181
+ if *provider_endpoint* is not effectively passed, an attempt is made to obtain a value from the corresponding
182
+ environment variable.
183
+
184
+ :param flask_app: the Flask application
185
+ :param provider_endpoint: endpoint for requenting tokens to provider
186
+ """
187
+ # obtain the defaulted parameters
188
+ defaulted_params: list[str] = func_defaulted_params.get()
189
+
190
+ # read from the environment variable
191
+ if "provider_endpoint" in defaulted_params:
192
+ provider_endpoint = env_get_str(key=f"{APP_PREFIX}_PROVIDER_ENDPOINT_TOKEN")
193
+
194
+ # establish the endpoints
195
+ if provider_endpoint:
196
+ flask_app.add_url_rule(rule=provider_endpoint,
197
+ endpoint=f"provider-get-token",
198
+ view_func=service_get_token,
199
+ methods=["GET"])
200
+
201
+
202
+ def provider_setup_logger(logger: Logger) -> None:
203
+ """
204
+ Register the logger for HTTP services.
205
+
206
+ :param logger: the logger to be registered
207
+ """
208
+ global __JWT_LOGGER
209
+ __JWT_LOGGER = logger
210
+
211
+
212
+ # @flask_app.route(rule=<token_endpoint>, # IAM_PROVIDER_ENDPOINT_TOKEN
213
+ # methods=["GET"])
214
+ def service_get_token() -> Response:
215
+ """
216
+ Entry point for retrieving a token from the *JWT* provider.
217
+
218
+ The provider is identified by the request parameter *jwt-provider*.
219
+
220
+ On success, the returned *Response* will contain the following JSON:
221
+ {
222
+ "access-token": <token>
223
+ }
224
+
225
+ :return: *Response* containing the JWT token, or *BAD REQUEST*
226
+ """
227
+ # retrieve the request arguments
228
+ args: dict[str, Any] = dict(request.args) or {}
229
+
230
+ # log the request
231
+ if __JWT_LOGGER:
232
+ __JWT_LOGGER.debug(msg=f"Request {request.method}:{request.path}; {json.dumps(obj=args,
233
+ ensure_ascii=False)}")
234
+
235
+ # obtain the provider JWT
236
+ provider_id: str = args.get("jwt-provider")
237
+
238
+ # retrieve the token
239
+ token: str | None = None
240
+ errors: list[str] = []
241
+ if provider_id:
242
+ token: str = provider_get_token(provider_id=provider_id,
243
+ errors=errors,
244
+ logger=__JWT_LOGGER)
245
+ else:
246
+ msg: str = "JWT provider not informed"
247
+ errors.append(msg)
248
+ if __JWT_LOGGER:
249
+ __JWT_LOGGER.error(msg=msg)
250
+
251
+ result: Response
252
+ if errors:
253
+ result = Response(response="; ".join(errors),
254
+ status=400)
255
+ else:
256
+ result = jsonify({"access-token": token})
257
+ if __JWT_LOGGER:
258
+ # log the response (the returned data is not logged, as it contains the token)
259
+ __JWT_LOGGER.debug(msg=f"Response {result}")
260
+
261
+ return result
262
+
263
+
94
264
  def provider_get_token(provider_id: str,
95
265
  errors: list[str] = None,
96
266
  logger: Logger = None) -> str | None:
97
267
  """
98
- Obtain an authentication token from the external provider *provider_id*.
268
+ Obtain an JWT token from the external provider *provider_id*.
99
269
 
100
270
  :param provider_id: the provider's identification
101
271
  :param errors: incidental error messages
102
272
  :param logger: optional logger
273
+ :return: the JWT token, or *None* if error
103
274
  """
104
- global _provider_registry # noqa: PLW0602
275
+ global _provider_registry
105
276
 
106
277
  # initialize the return variable
107
278
  result: str | None = None
@@ -117,7 +288,7 @@ def provider_get_token(provider_id: str,
117
288
  # access token has expired
118
289
  header_data: dict[str, str] | None = None
119
290
  body_data: dict[str, str] | None = None
120
- url: str = provider.get(ProviderParam.URL)
291
+ url: str = provider.get(ProviderParam.URL_TOKEN)
121
292
  refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
122
293
  if refresh_token:
123
294
  # refresh token exists
@@ -131,11 +302,11 @@ def provider_get_token(provider_id: str,
131
302
  "grant_type": "refresh_token",
132
303
  "refresh_token": refresh_token
133
304
  }
134
- if not body_data:
305
+ if not header_data:
135
306
  # 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 {}
307
+ user: str = provider.get(ProviderParam.USER_ID)
308
+ pwd: str = provider.get(ProviderParam.USER_SECRET)
309
+ header_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
139
310
  body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
140
311
  custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
141
312
  if custom_auth:
@@ -143,7 +314,7 @@ def provider_get_token(provider_id: str,
143
314
  body_data[custom_auth[1]] = pwd
144
315
  else:
145
316
  enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
146
- headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
317
+ header_data["Authorization"] = f"Basic {enc_bytes.decode()}"
147
318
 
148
319
  # obtain the token
149
320
  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.6
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.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=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=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
2
- pypomes_iam/iam_actions.py,sha256=zoxAzcw9fBTMEt-y5INaw7Wjfa1R5o-z_ORxvo4kqGU,45612
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=HFK_ihY1n7I4JGptAwV44MxHMPsGLDU5ElsaFOqUDcc,15915
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.6.dist-info/METADATA,sha256=b6eajrnQCM0TsdjAbz1H4xf9EgrGV0ii_phYm51nOQI,661
9
- pypomes_iam-0.7.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- pypomes_iam-0.7.6.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
- pypomes_iam-0.7.6.dist-info/RECORD,,