pypomes-iam 0.7.2__py3-none-any.whl → 0.8.0__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,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* 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.
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,22 +97,26 @@ 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
@@ -67,23 +127,42 @@ def provider_register(provider_id: str,
67
127
  (such as ['grant_type', 'client_credentials']), to be added to the request body, may be specified in *body_data*.
68
128
 
69
129
  :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
130
+ :param user_id: the basic authorization user
131
+ :param user_secret: the basic authorization password
73
132
  :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
133
+ :param header_data: optional key-value pairs to be added to the request headers
75
134
  :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
76
136
  """
77
137
  global _provider_registry
78
138
 
139
+ # obtain the defaulted parameters
140
+ defaulted_params: list[str] = func_defaulted_params.get()
141
+
142
+ # read from the environment variables
143
+ prefix: str = provider_id.upper()
144
+ if "user_id" in defaulted_params:
145
+ user_id = env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_ID")
146
+ if "user_secret" in defaulted_params:
147
+ user_secret = env_get_str(key=f"{APP_PREFIX}_{prefix}_USER_SECRET")
148
+ if "custom_auth" in defaulted_params:
149
+ custom_auth = env_get_strs(key=f"{APP_PREFIX}_{prefix}_CUSTOM_AUTH")
150
+ if "header_data" in defaulted_params:
151
+ header_data = env_get_obj(key=f"{APP_PREFIX}_{prefix}_HEADER_DATA")
152
+ if "body_data" in defaulted_params:
153
+ 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
+
79
157
  with _provider_lock:
80
158
  _provider_registry[provider_id] = {
81
- ProviderParam.URL: auth_url,
82
- ProviderParam.USER: auth_user,
83
- ProviderParam.PWD: auth_pwd,
159
+ ProviderParam.URL_AUTH: url_auth,
160
+ ProviderParam.USER_ID: user_id,
161
+ ProviderParam.USER_SECRET: user_secret,
84
162
  ProviderParam.CUSTOM_AUTH: custom_auth,
85
- ProviderParam.HEADER_DATA: headers_data,
163
+ ProviderParam.HEADER_DATA: header_data,
86
164
  ProviderParam.BODY_DATA: body_data,
165
+ # dynamically set
87
166
  ProviderParam.ACCESS_TOKEN: None,
88
167
  ProviderParam.ACCESS_EXPIRATION: 0,
89
168
  ProviderParam.REFRESH_TOKEN: None,
@@ -91,17 +170,105 @@ def provider_register(provider_id: str,
91
170
  }
92
171
 
93
172
 
173
+ @func_capture_params
174
+ def provider_setup_endpoint(flask_app: Flask,
175
+ provider_endpoint: str = None) -> None:
176
+ """
177
+ Setup the endpoint for requesting token from the registered *JWT* providers.
178
+
179
+ if *provider_endpoint* is not effectively passed, an attempt is made to obtain a value from the corresponding
180
+ environment variable.
181
+
182
+ :param flask_app: the Flask application
183
+ :param provider_endpoint: endpoint for requenting tokens to provider
184
+ """
185
+ # obtain the defaulted parameters
186
+ defaulted_params: list[str] = func_defaulted_params.get()
187
+
188
+ # read from the environment variable
189
+ if "provider_endpoint" in defaulted_params:
190
+ provider_endpoint = env_get_str(key=f"{APP_PREFIX}_IAM_PROVIDER_ENDPOINT")
191
+
192
+ # establish the endpoints
193
+ if provider_endpoint:
194
+ flask_app.add_url_rule(rule=provider_endpoint,
195
+ endpoint=f"jwt-callback",
196
+ view_func=service_get_token,
197
+ methods=["GET"])
198
+
199
+
200
+ def provider_setup_logger(logger: Logger) -> None:
201
+ """
202
+ Register the logger for HTTP services.
203
+
204
+ :param logger: the logger to be registered
205
+ """
206
+ global __JWT_LOGGER
207
+ __JWT_LOGGER = logger
208
+
209
+
210
+ # @flask_app.route(rule=<token_endpoint>, # IAM_PROVIDER_ENDPOINT_TOKEN
211
+ # methods=["GET"])
212
+ def service_get_token() -> Response:
213
+ """
214
+ Entry point for retrieving a token from the *JWT* provider.
215
+
216
+ The provider is identified by the request parameter *jwt-provider*.
217
+
218
+ On success, the returned *Response* will contain the following JSON:
219
+ {
220
+ "access-token": <token>
221
+ }
222
+
223
+ :return: *Response* containing the JWT token, or *BAD REQUEST*
224
+ """
225
+ # log the request
226
+ 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}")
230
+
231
+ # obtain the provider JWT
232
+ provider_id: str = request.args.get("jwt-provider")
233
+
234
+ # retrieve the token
235
+ token: str | None = None
236
+ errors: list[str] = []
237
+ if provider_id:
238
+ token: str = provider_get_token(provider_id=provider_id,
239
+ errors=errors,
240
+ logger=__JWT_LOGGER)
241
+ else:
242
+ msg: str = "JWT provider not informed"
243
+ errors.append(msg)
244
+ if __JWT_LOGGER:
245
+ __JWT_LOGGER.error(msg=msg)
246
+
247
+ result: Response
248
+ if errors:
249
+ result = Response(response="; ".join(errors),
250
+ status=400)
251
+ else:
252
+ result = jsonify({"access-token": token})
253
+ if __JWT_LOGGER:
254
+ # log the response (the returned data is not logged, as it contains the token)
255
+ __JWT_LOGGER.debug(msg=f"Response {result}")
256
+
257
+ return result
258
+
259
+
94
260
  def provider_get_token(provider_id: str,
95
261
  errors: list[str] = None,
96
262
  logger: Logger = None) -> str | None:
97
263
  """
98
- Obtain an authentication token from the external provider *provider_id*.
264
+ Obtain an JWT token from the external provider *provider_id*.
99
265
 
100
266
  :param provider_id: the provider's identification
101
267
  :param errors: incidental error messages
102
268
  :param logger: optional logger
269
+ :return: the JWT token, or *None* if error
103
270
  """
104
- global _provider_registry # noqa: PLW0602
271
+ global _provider_registry
105
272
 
106
273
  # initialize the return variable
107
274
  result: str | None = None
@@ -117,7 +284,7 @@ def provider_get_token(provider_id: str,
117
284
  # access token has expired
118
285
  header_data: dict[str, str] | None = None
119
286
  body_data: dict[str, str] | None = None
120
- url: str = provider.get(ProviderParam.URL)
287
+ url: str = provider.get(ProviderParam.URL_AUTH)
121
288
  refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
122
289
  if refresh_token:
123
290
  # refresh token exists
@@ -133,9 +300,9 @@ def provider_get_token(provider_id: str,
133
300
  }
134
301
  if not body_data:
135
302
  # 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 {}
303
+ user: str = provider.get(ProviderParam.USER_ID)
304
+ pwd: str = provider.get(ProviderParam.USER_SECRET)
305
+ header_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
139
306
  body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
140
307
  custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
141
308
  if custom_auth:
@@ -143,7 +310,7 @@ def provider_get_token(provider_id: str,
143
310
  body_data[custom_auth[1]] = pwd
144
311
  else:
145
312
  enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
146
- headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
313
+ header_data["Authorization"] = f"Basic {enc_bytes.decode()}"
147
314
 
148
315
  # obtain the token
149
316
  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.2
3
+ Version: 0.8.0
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=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,,
@@ -1,11 +0,0 @@
1
- pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
2
- pypomes_iam/iam_actions.py,sha256=qUX8DdTBOUVfJpWtE7qg4rSBh6ZQNG_4VnMA7uebnV4,42781
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.2.dist-info/METADATA,sha256=7amJ095DkifMRQeC7uX5C6rsU7jzgCA7Bnn3uu6N6q4,661
9
- pypomes_iam-0.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
- pypomes_iam-0.7.2.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
11
- pypomes_iam-0.7.2.dist-info/RECORD,,