pypomes-iam 0.0.1__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.
@@ -0,0 +1,16 @@
1
+ .cache
2
+ .env
3
+ .venv
4
+ .idea
5
+ .vscode
6
+ env
7
+ venv
8
+ temp
9
+ tmp
10
+ **/__pycache__
11
+ *.py[cod]
12
+ *.sh
13
+ *.ps1
14
+ /dist
15
+ /env
16
+ deploy.txt
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 GT Nunes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: pypomes_iam
3
+ Version: 0.0.1
4
+ Summary: A collection of Python pomes, penyeach (IAM modules)
5
+ Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-IAM
6
+ Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-IAM/issues
7
+ Author-email: GT Nunes <wisecoder01@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.12
13
+ Requires-Dist: cachetools>=6.2.1
14
+ Requires-Dist: flask>=3.1.2
15
+ Requires-Dist: pypomes-core>=2.8.0
16
+ Requires-Dist: requests>=2.32.5
17
+ Requires-Dist: uuid>=1.3.0
File without changes
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = [
3
+ "hatchling>=1.27.0"
4
+ ]
5
+ build-backend = "hatchling.build"
6
+
7
+ [project]
8
+ name = "pypomes_iam"
9
+ version = "0.0.1"
10
+ authors = [
11
+ { name="GT Nunes", email="wisecoder01@gmail.com" }
12
+ ]
13
+ description = "A collection of Python pomes, penyeach (IAM modules)"
14
+ readme = "README.md"
15
+ requires-python = ">=3.12"
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent"
20
+ ]
21
+ dependencies = [
22
+ "cachetools>=6.2.1",
23
+ "Flask>=3.1.2",
24
+ "pypomes-core>=2.8.0",
25
+ "requests>=2.32.5",
26
+ "uuid>=1.3.0"
27
+ ]
28
+
29
+ [project.urls]
30
+ "Homepage" = "https://github.com/TheWiseCoder/PyPomes-IAM"
31
+ "Bug Tracker" = "https://github.com/TheWiseCoder/PyPomes-IAM/issues"
@@ -0,0 +1,17 @@
1
+ from iam_jusbr import (
2
+ jusbr_get_token
3
+ )
4
+ from .iam_provider import (
5
+ provider_register, provider_get_token
6
+ )
7
+
8
+ __all__ = [
9
+ # iam_jusbr
10
+ "jusbr_get_token",
11
+ # jwt_provider
12
+ "provider_register", "provider_get_token"
13
+ ]
14
+
15
+ from importlib.metadata import version
16
+ __version__ = version("pypomes_iam")
17
+ __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit())
@@ -0,0 +1,385 @@
1
+ import requests
2
+ import secrets
3
+ import string
4
+ import sys
5
+ from cachetools import Cache, FIFOCache, TTLCache
6
+ from datetime import datetime
7
+ from flask import Flask, Response, redirect, request, jsonify
8
+ from logging import Logger
9
+ from pypomes_core import (
10
+ APP_PREFIX, TZ_LOCAL, env_get_int, env_get_str, exc_format
11
+ )
12
+ from typing import Any, Final
13
+
14
+ JUSBR_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_ID")
15
+ JUSBR_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_SECRET")
16
+ JUSBR_LOGIN_TIMEOUT: Final[int] = env_get_int(key=f"{APP_PREFIX}_JUSBR_LOGIN_TIMEOUT")
17
+ JUSBR_CALLBACK_ENDPOINT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CALLBACK_ENDPOINT",
18
+ def_value="/iam/jusbr:callback")
19
+ JUSBR_TOKEN_ENDPOINT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_TOKEN_ENDPOINT",
20
+ def_value="/iam/jusbr:get-token")
21
+ JUSBR_LOGIN_ENDPOINT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_LOGIN_ENDPOINT",
22
+ def_value="/iam/jusbr:login")
23
+ JUSBR_LOGOUT_ENDPOINT: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_LOGOUT_ENDPOINT",
24
+ def_value="/iam/jusbr:logout")
25
+ JUSBR_AUTH_URL: Final[str] = env_get_str(
26
+ key=f"{APP_PREFIX}JUSBR_AUTH_URL",
27
+ def_value="https://sso.stg.cloud.pje.jus.br/auth/realms/pje/protocol/openid-connect/auth"
28
+ )
29
+ JUSBR_TOKEN_URL: Final[str] = env_get_str(
30
+ key=f"{APP_PREFIX}JUSBR_TOKEN_URL",
31
+ def_value="https://sso.stg.cloud.pje.jus.br/auth/realms/pje/protocol/openid-connect/token"
32
+ )
33
+ JUSBR_CALLBACK_URL: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CALLBACK_URL")
34
+
35
+ # safe memory cache - structure:
36
+ # {
37
+ # "client-id": <str>,
38
+ # "client-secret": <str>,
39
+ # "auth-url": <str>,
40
+ # "token-url": <str>,
41
+ # "login-timeout": <int>,
42
+ # "users": [
43
+ # "<user-id>": {
44
+ # "cache-obj": <Cache>,
45
+ # "oauth-scope": <str>,
46
+ # "access-expiration": <timestamp>,
47
+ # data in <TTLCache>:
48
+ # "oauth-state": <str>
49
+ # "access-token": <str>
50
+ # "refresh-token": <str>
51
+ # }
52
+ # ]
53
+ # }
54
+ _jusbr_registry: dict[str, Any] = {
55
+ "client-id": None,
56
+ "client-secret": None,
57
+ "login-timeout": None,
58
+ "auth-url": None,
59
+ "token-url": None,
60
+ "users": []
61
+ }
62
+
63
+ # dafault logger
64
+ _logger: Logger | None = None
65
+
66
+
67
+ def jusbr_config(flask_app: Flask,
68
+ client_id: str = JUSBR_CLIENT_ID,
69
+ client_secret: str = JUSBR_CLIENT_SECRET,
70
+ login_timeout: int = JUSBR_LOGIN_TIMEOUT,
71
+ callback_endpoint: str = JUSBR_CALLBACK_ENDPOINT,
72
+ token_endpoint: str = JUSBR_TOKEN_ENDPOINT,
73
+ login_endpoint: str = JUSBR_LOGIN_ENDPOINT,
74
+ logout_endpoint: str = JUSBR_LOGOUT_ENDPOINT,
75
+ auth_url: str = JUSBR_AUTH_URL,
76
+ token_url: str = JUSBR_TOKEN_URL,
77
+ logger: Logger = None) -> None:
78
+ """
79
+ Configure the JusBR IAM.
80
+
81
+ This should be invoked only once, before the first access to a JusBR service.
82
+
83
+ :param flask_app: the Flask application
84
+ :param client_id: the client's identification with JusBR
85
+ :param client_secret: the client's password with JusBR
86
+ :param login_timeout: timeout for login authentication (in seconds,defaults to no timeout)
87
+ :param callback_endpoint: endpoint for the callback from JusBR
88
+ :param token_endpoint: endpoint for retrieving the JusBR authentication token
89
+ :param login_endpoint: endpoint for redirecting user to JusBR login page
90
+ :param logout_endpoint: endpoint for terminating user access to JusBR
91
+ :param auth_url: URL to access the JusBR login page
92
+ :param token_url: URL for obtaing or refreshing the token
93
+ :param logger: optional logger
94
+ """
95
+ # establish the logger
96
+ global _logger
97
+ _logger = logger
98
+
99
+ # configure the JusBR registry
100
+ global _jusbr_registry # noqa: PLW0602
101
+ _jusbr_registry.update({
102
+ "client-id": client_id,
103
+ "client-secret": client_secret,
104
+ "login-timeout": login_timeout,
105
+ "auth-url": auth_url,
106
+ "token-url": token_url
107
+ })
108
+
109
+ # establish the endpoints
110
+ if token_endpoint:
111
+ flask_app.add_url_rule(rule=token_endpoint,
112
+ endpoint="jusbr-token",
113
+ view_func=jusbr_token,
114
+ methods=["GET"])
115
+ if login_endpoint:
116
+ flask_app.add_url_rule(rule=login_endpoint,
117
+ endpoint="jusbr-login",
118
+ view_func=jusbr_login,
119
+ methods=["GET"])
120
+ if logout_endpoint:
121
+ flask_app.add_url_rule(rule=logout_endpoint,
122
+ endpoint="jusbr-logout",
123
+ view_func=jusbr_logout,
124
+ methods=["GET"])
125
+ if callback_endpoint:
126
+ flask_app.add_url_rule(rule=callback_endpoint,
127
+ endpoint="jusbr-callback",
128
+ view_func=jusbr_callback,
129
+ methods=["POST"])
130
+
131
+
132
+ # @flask_app.route(rule=<login_endpoint>, # JUSBR_LOGIN_ENDPOINT: /iam/jusbr:login
133
+ # methods=["GET"])
134
+ def jusbr_login() -> Response:
135
+ """
136
+ Entry point for the JusBR login service.
137
+
138
+ Redirect the request to the JusBR authentication page, with the apprpriate parameters.
139
+
140
+ :return: the response from the redirect operation
141
+ """
142
+ # retrieve user id
143
+ input_params: dict[str, Any] = request.values
144
+ user_id: str = input_params.get("user-id") or input_params.get("login")
145
+
146
+ # retrieve user data
147
+ global _jusbr_registry
148
+ user_data: dict[str, Any] = _jusbr_registry["users"].get(user_id)
149
+ if not user_data:
150
+ user_data = {"access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp())}
151
+ _jusbr_registry["users"][user_id] = user_data
152
+
153
+ # build redirect url
154
+ oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
155
+ login_timeout: int = _jusbr_registry.get("login-timeout")
156
+ safe_cache: Cache
157
+ if isinstance(login_timeout, int) and login_timeout > 0:
158
+ safe_cache = TTLCache(maxsize=16,
159
+ ttl=600)
160
+ else:
161
+ safe_cache = FIFOCache(maxsize=16)
162
+ safe_cache["oauth-state"] = oauth_state
163
+ user_data["cache-obj"] = safe_cache
164
+ auth_url: str = (f"{_jusbr_registry["auth-url"]}?response_type=code"
165
+ f"&client_id={_jusbr_registry["client-id"]}"
166
+ f"&redirect_url={_jusbr_registry["redirect-url"]}"
167
+ f"&state={oauth_state}")
168
+ if user_data.get("oauth-scope"):
169
+ auth_url += f"&scope={user_data.get("oauth-scope")}"
170
+
171
+ # redirect request
172
+ return redirect(location=auth_url)
173
+
174
+
175
+ # @flask_app.route(rule=<login_endpoint>, # JUSBR_LOGIN_ENDPOINT: /iam/jusbr:logout
176
+ # methods=["GET"])
177
+ def jusbr_logout() -> Response:
178
+ """
179
+ Entry point for the JusBR logout service.
180
+
181
+ Delete all data associating the user with JusBR.
182
+
183
+ :return: the response from the redirect operation
184
+ """
185
+ # retrieve user id
186
+ input_params: dict[str, Any] = request.values
187
+ user_id: str = input_params.get("user-id") or input_params.get("login")
188
+
189
+ # retrieve user data
190
+ global _jusbr_registry
191
+ if user_id in _jusbr_registry.get("users"):
192
+ _jusbr_registry.pop(user_id)
193
+ logger: Logger = _jusbr_registry.get("logger")
194
+ if logger:
195
+ logger.debug(f"User '{user_id}' removed from the registry")
196
+
197
+ return Response(status=200)
198
+
199
+
200
+ # @flask_app.route(rule=<callback_endpoint>, # JUSBR_CALLBACK_ENDPOINT: /iam/jusbr:callback
201
+ # methods=["POST"])
202
+ def jusbr_callback() -> Response:
203
+ """
204
+ Entry point for the callback from JusBR on authentication.
205
+
206
+ :return: the response containing the token, or *NOT AUTHORIZED*
207
+ """
208
+ global _jusbr_registry
209
+
210
+ # validate the OAuth2 state
211
+ oauth_state: str = request.args.get("state")
212
+ user_data: dict[str, Any] | None = None
213
+ if oauth_state:
214
+ for user in _jusbr_registry.get("users"):
215
+ safe_cache: Cache = user_data.get("cache-obj")
216
+ if safe_cache and oauth_state == safe_cache.get("oauth-state"):
217
+ user_data = user
218
+ # 'oauth-state' is to be used only once
219
+ safe_cache["oauth-state"] = None
220
+ break
221
+
222
+ # exchange 'code' for the token
223
+ errors: list[str] = []
224
+ if user_data:
225
+ code: str = request.args.get("code")
226
+ body_data: dict[str, Any] = {
227
+ "grant_type": "authorization_code",
228
+ "code": code,
229
+ "redirec_url": _jusbr_registry.get("redirect-url"),
230
+ }
231
+ __post_jusbr(user_data=user_data,
232
+ body_data=body_data,
233
+ errors=errors,
234
+ logger=_jusbr_registry.get("logger"))
235
+ else:
236
+ # login operation timed-out
237
+ errors.append("Operation timeout")
238
+
239
+ result: Response
240
+ if errors:
241
+ result = jsonify({"errors": "; ".join(errors)})
242
+ result.status_code = 400
243
+ else:
244
+ result = Response(status=200)
245
+
246
+ return result
247
+
248
+
249
+ # @flask_app.route(rule=<token_endpoint>, # JUSBR_TOKEN_ENDPOINT: /iam/jusbr:get-token
250
+ # methods=["GET"])
251
+ def jusbr_token() -> Response:
252
+ """
253
+ Entry point for retrieving the JusBR token.
254
+
255
+ :return: the response containing the token, or *NOT AUTHORIZED*
256
+ """
257
+ # retrieve user id
258
+ input_params: dict[str, Any] = request.values
259
+ user_id: str = input_params.get("user-id") or input_params.get("login")
260
+
261
+ # retrieve the token
262
+ token: str = jusbr_get_token(user_id=user_id,
263
+ logger=_jusbr_registry.get("logger"))
264
+ result: Response
265
+ if token:
266
+ result = jsonify({"token": token})
267
+ else:
268
+ result = Response(status=401)
269
+
270
+ return result
271
+
272
+
273
+ def jusbr_get_token(user_id: str,
274
+ errors: list[str] = None,
275
+ logger: Logger = None) -> str:
276
+ """
277
+ Retrieve the authentication token for user *user_id*.
278
+
279
+ :param user_id: the user's identification
280
+ :param errors: incidental error messages
281
+ :param logger: optional logger
282
+ """
283
+ global _jusbr_registry
284
+
285
+ # initialize the return variable
286
+ result: str | None = None
287
+
288
+ user_data: dict[str, Any] = _jusbr_registry["users"].get(user_id)
289
+ safe_cache: Cache = user_data["cache-obj"] if user_data else None
290
+ if user_data and safe_cache:
291
+ access_expiration: int = user_data.get("access-expiration")
292
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
293
+ if now < access_expiration:
294
+ result = safe_cache.get("access-token")
295
+ else:
296
+ # access token has expired
297
+ safe_cache["access-token"] = None
298
+ refresh_token: str = safe_cache.get("refresh-token")
299
+ if refresh_token:
300
+ body_data: dict[str, str] = {
301
+ "grant_type": "refresh_token",
302
+ "refresh_token": refresh_token
303
+ }
304
+ __post_jusbr(user_data=user_data,
305
+ body_data=body_data,
306
+ errors=errors,
307
+ logger=logger)
308
+ if not errors:
309
+ result = safe_cache.get("access_token")
310
+
311
+ elif logger or isinstance(errors, list):
312
+ err_msg: str = f"User '{user_id}' not authenticated with JusBR"
313
+ if isinstance(errors, list):
314
+ errors.append(err_msg)
315
+ if logger:
316
+ logger.error(msg=err_msg)
317
+
318
+ return result
319
+
320
+
321
+ def __post_jusbr(user_data: dict[str, Any],
322
+ body_data: dict[str, Any],
323
+ errors: list[str] | None,
324
+ logger: Logger | None) -> None:
325
+ """
326
+ Send a POST request to JusBR to obtain the authorization token.
327
+
328
+ If successful, the token data is stored in the registry, and the token itself is returned.
329
+ Otherwise, *errors* will contain the appropriate error message.
330
+
331
+ :param user_data: the user data in the registry
332
+ :param body_data: the data to send in the body of the request
333
+ :param errors: incidental errors
334
+ :param logger: optional logger
335
+ """
336
+ global _jusbr_registry
337
+
338
+ # complete the data to send in body of request
339
+ body_data["client_id"] = _jusbr_registry.get("client-id")
340
+ client_secret: str = _jusbr_registry.get("client-secret")
341
+ if client_secret:
342
+ body_data["client_secret"] = client_secret
343
+
344
+ err_msg: str | None = None
345
+ safe_cache: Cache = user_data.get("cache-obj")
346
+ url: str = _jusbr_registry.get("auth-url")
347
+ now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
348
+ try:
349
+ # JusBR return on a token request:
350
+ # {
351
+ # "token_type": "Bearer",
352
+ # "access_token": <str>,
353
+ # "expires_in": <number-of-seconds>,
354
+ # "refresh_token": <str>,
355
+ # }
356
+ response: requests.Response = requests.post(url=url,
357
+ data=body_data)
358
+ if response.status_code < 200 or response.status_code >= 300:
359
+ # request resulted in error, report the problem
360
+ err_msg = (f"POST '{url}': failed, "
361
+ f"status {response.status_code}, reason '{response.reason}'")
362
+ if response.status_code == 401 and "refresh_token" in body_data:
363
+ # refresh token is no longer valid
364
+ safe_cache["refresh-token"] = None
365
+ else:
366
+ reply: dict[str, Any] = response.json()
367
+ result = reply.get("access_token")
368
+ safe_cache: Cache = FIFOCache(maxsize=1024)
369
+ safe_cache["access-token"] = result
370
+ safe_cache["refresh-token"] = reply.get("refresh_token")
371
+ user_data["cache-obj"] = safe_cache
372
+ user_data["access-expiration"] = now + reply.get("expires_in")
373
+ if logger:
374
+ logger.debug(msg=f"POST '{url}': status {response.status_code}")
375
+ except Exception as e:
376
+ # the operation raised an exception
377
+ err_msg = exc_format(exc=e,
378
+ exc_info=sys.exc_info())
379
+ err_msg = f"POST '{url}': error, '{err_msg}'"
380
+
381
+ if err_msg:
382
+ if isinstance(errors, list):
383
+ errors.append(err_msg)
384
+ if logger:
385
+ logger.error(msg=err_msg)
@@ -0,0 +1,139 @@
1
+ import requests
2
+ import sys
3
+ from base64 import b64encode
4
+ from datetime import datetime
5
+ from logging import Logger
6
+ from pypomes_core import TZ_LOCAL, exc_format
7
+ from typing import Any
8
+
9
+ # structure:
10
+ # {
11
+ # <provider-id>: {
12
+ # "url": <strl>,
13
+ # "user": <str>,
14
+ # "pwd": <str>,
15
+ # "basic-auth": <bool>,
16
+ # "headers-data": <dict[str, str]>,
17
+ # "body-data": <dict[str, str],
18
+ # "token": <str>,
19
+ # "expiration": <timestamp>
20
+ # }
21
+ # }
22
+ _provider_registry: dict[str, dict[str, Any]] = {}
23
+
24
+
25
+ def provider_register(provider_id: str,
26
+ auth_url: str,
27
+ auth_user: str,
28
+ auth_pwd: str,
29
+ custom_auth: tuple[str, str] = None,
30
+ headers_data: dict[str, str] = None,
31
+ body_data: dict[str, str] = None) -> None:
32
+ """
33
+ Register an external authentication token provider.
34
+
35
+ If specified, *custom_auth* provides key names for sending credentials (username and password, in this order)
36
+ as key-value pairs in the body of the request. Otherwise, the external provider *provider_id* uses the standard
37
+ HTTP Basic Authorization scheme, wherein the credentials are B64-encoded and sent in the request headers.
38
+
39
+ Optional constant key-value pairs (such as ['Content-Type', 'application/x-www-form-urlencoded']), to be
40
+ added to the request headers, may be specified in *headers_data*. Likewise, optional constant key-value pairs
41
+ (such as ['grant_type', 'client_credentials']), to be added to the request body, may be specified in *body_data*.
42
+
43
+ :param provider_id: the provider's identification
44
+ :param auth_url: the url to request authentication tokens with
45
+ :param auth_user: the basic authorization user
46
+ :param auth_pwd: the basic authorization password
47
+ :param custom_auth: optional key names for sending the credentials as key-value pairs in the body of the request
48
+ :param headers_data: optional key-value pairs to be added to the request headers
49
+ :param body_data: optional key-value pairs to be added to the request body
50
+ """
51
+ global _provider_registry # noqa: PLW0602
52
+
53
+ _provider_registry[provider_id] = {
54
+ "url": auth_url,
55
+ "user": auth_user,
56
+ "pwd": auth_pwd,
57
+ "custom-auth": custom_auth,
58
+ "headers-data": headers_data,
59
+ "body-data": body_data,
60
+ "token": None,
61
+ "expiration": datetime.now(tz=TZ_LOCAL).timestamp()
62
+ }
63
+
64
+
65
+ def provider_get_token(provider_id: str,
66
+ errors: list[str] = None,
67
+ logger: Logger = None) -> str | None:
68
+ """
69
+ Obtain an authentication token from the external provider *provider_id*.
70
+
71
+ :param provider_id: the provider's identification
72
+ :param errors: incidental error messages
73
+ :param logger: optional logger
74
+ """
75
+ global _provider_registry # noqa: PLW0602
76
+
77
+ # initialize the return variable
78
+ result: str | None = None
79
+
80
+ err_msg: str | None = None
81
+ provider: dict[str, Any] = _provider_registry.get(provider_id)
82
+ if provider:
83
+ now: float = datetime.now(tz=TZ_LOCAL).timestamp()
84
+ if now > provider.get("expiration"):
85
+ user: str = provider.get("user")
86
+ pwd: str = provider.get("pwd")
87
+ headers_data: dict[str, str] = provider.get("headers-data") or {}
88
+ body_data: dict[str, str] = provider.get("body-data") or {}
89
+ custom_auth: tuple[str, str] = provider.get("custom-auth")
90
+ if custom_auth:
91
+ body_data[custom_auth[0]] = user
92
+ body_data[custom_auth[1]] = pwd
93
+ else:
94
+ enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
95
+ headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
96
+ url: str = provider.get("url")
97
+ try:
98
+ # typical return on a token request:
99
+ # {
100
+ # "token_type": "Bearer",
101
+ # "access_token": <str>,
102
+ # "expires_in": <number-of-seconds>,
103
+ # optional data:
104
+ # "refresh_token": <str>,
105
+ # "refresh_expires_in": <number-of-seconds>
106
+ # }
107
+ response: requests.Response = requests.post(url=url,
108
+ data=body_data,
109
+ headers=headers_data,
110
+ timeout=None)
111
+ if response.status_code < 200 or response.status_code >= 300:
112
+ # request resulted in error, report the problem
113
+ err_msg = (f"POST '{url}': failed, "
114
+ f"status {response.status_code}, reason '{response.reason}'")
115
+ else:
116
+ reply: dict[str, Any] = response.json()
117
+ provider["token"] = reply.get("access_token")
118
+ provider["expiration"] = now + int(reply.get("expires_in"))
119
+ if logger:
120
+ logger.debug(msg=f"POST '{url}': status {response.status_code}")
121
+ except Exception as e:
122
+ # the operation raised an exception
123
+ err_msg = exc_format(exc=e,
124
+ exc_info=sys.exc_info())
125
+ err_msg = f"POST '{url}': error, '{err_msg}'"
126
+ else:
127
+ err_msg: str = f"Provider '{provider_id}' not registered"
128
+
129
+ if err_msg:
130
+ if isinstance(errors, list):
131
+ errors.append(err_msg)
132
+ if logger:
133
+ logger.error(msg=err_msg)
134
+ else:
135
+ result = provider.get("token")
136
+
137
+ return result
138
+
139
+