pypomes-iam 0.3.5__py3-none-any.whl → 0.3.6__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.
- pypomes_iam/iam_common.py +10 -1
- pypomes_iam/iam_pomes.py +219 -199
- pypomes_iam/iam_services.py +94 -70
- pypomes_iam/jusbr_pomes.py +23 -17
- pypomes_iam/keycloak_pomes.py +23 -17
- pypomes_iam/provider_pomes.py +66 -59
- {pypomes_iam-0.3.5.dist-info → pypomes_iam-0.3.6.dist-info}/METADATA +1 -1
- pypomes_iam-0.3.6.dist-info/RECORD +12 -0
- pypomes_iam-0.3.5.dist-info/RECORD +0 -12
- {pypomes_iam-0.3.5.dist-info → pypomes_iam-0.3.6.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.3.5.dist-info → pypomes_iam-0.3.6.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_common.py
CHANGED
|
@@ -5,6 +5,7 @@ from enum import StrEnum
|
|
|
5
5
|
from logging import Logger
|
|
6
6
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
7
7
|
from pypomes_crypto import crypto_jwk_convert
|
|
8
|
+
from threading import Lock
|
|
8
9
|
from typing import Any, Final
|
|
9
10
|
|
|
10
11
|
|
|
@@ -16,7 +17,8 @@ class IamServer(StrEnum):
|
|
|
16
17
|
IAM_KEYCLOAK = "iam-keycloak"
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
# the logger for IAM operations
|
|
20
|
+
# the logger for IAM service operations
|
|
21
|
+
# (used exclusively at the HTTP endpoint - all other functions receive the lgger as parameter)
|
|
20
22
|
__IAM_LOGGER: Logger | None = None
|
|
21
23
|
|
|
22
24
|
# registry structure:
|
|
@@ -51,11 +53,18 @@ __IAM_LOGGER: Logger | None = None
|
|
|
51
53
|
# }
|
|
52
54
|
_IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
|
|
53
55
|
|
|
56
|
+
# the lock protecting the data in '_IAM_SERVER'
|
|
57
|
+
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
58
|
+
_iam_lock: Final[Lock] = Lock()
|
|
59
|
+
|
|
54
60
|
|
|
55
61
|
def _get_logger() -> Logger | None:
|
|
56
62
|
"""
|
|
57
63
|
Retrieve the registered logger for *IAM* operations.
|
|
58
64
|
|
|
65
|
+
This function is invoked exclusively from the HTTP endpoints.
|
|
66
|
+
All other functions receive the logger as parameter.
|
|
67
|
+
|
|
59
68
|
:return: the registered logger for *IAM* operations.
|
|
60
69
|
"""
|
|
61
70
|
return __IAM_LOGGER
|
pypomes_iam/iam_pomes.py
CHANGED
|
@@ -3,14 +3,13 @@ import requests
|
|
|
3
3
|
import secrets
|
|
4
4
|
import string
|
|
5
5
|
import sys
|
|
6
|
-
from cachetools import Cache
|
|
7
6
|
from datetime import datetime
|
|
8
7
|
from logging import Logger
|
|
9
8
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
10
9
|
from typing import Any
|
|
11
10
|
|
|
12
11
|
from .iam_common import (
|
|
13
|
-
IamServer,
|
|
12
|
+
IamServer, _iam_lock,
|
|
14
13
|
_register_logger, _get_iam_users, _get_iam_registry,
|
|
15
14
|
_get_login_timeout, _get_user_data, _get_public_key
|
|
16
15
|
)
|
|
@@ -48,30 +47,34 @@ def user_login(iam_server: IamServer,
|
|
|
48
47
|
# build the user data
|
|
49
48
|
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
50
49
|
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
registry[
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
50
|
+
|
|
51
|
+
with _iam_lock:
|
|
52
|
+
# retrieve the user data from the IAM server's registry
|
|
53
|
+
user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
|
|
54
|
+
user_id=oauth_state,
|
|
55
|
+
errors=errors,
|
|
56
|
+
logger=logger)
|
|
57
|
+
if user_data:
|
|
58
|
+
user_data["login-id"] = user_id
|
|
59
|
+
timeout: int = _get_login_timeout(iam_server=iam_server,
|
|
60
|
+
errors=errors,
|
|
61
|
+
logger=logger)
|
|
62
|
+
if not errors:
|
|
63
|
+
user_data["login-expiration"] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
|
|
64
|
+
if timeout else None
|
|
65
|
+
redirect_uri: str = args.get("redirect-uri")
|
|
66
|
+
|
|
67
|
+
# build the login url
|
|
68
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
69
|
+
errors=errors,
|
|
70
|
+
logger=logger)
|
|
71
|
+
if registry:
|
|
72
|
+
registry["redirect-uri"] = redirect_uri
|
|
73
|
+
result = {"login-url": (f"{registry["base-url"]}/protocol/openid-connect/auth"
|
|
74
|
+
f"?response_type=code&scope=openid"
|
|
75
|
+
f"&client_id={registry["client-id"]}"
|
|
76
|
+
f"&redirect_uri={redirect_uri}"
|
|
77
|
+
f"&state={oauth_state}")}
|
|
75
78
|
return result
|
|
76
79
|
|
|
77
80
|
|
|
@@ -94,14 +97,15 @@ def user_logout(iam_server: IamServer,
|
|
|
94
97
|
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
95
98
|
|
|
96
99
|
if user_id:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
users
|
|
103
|
-
|
|
104
|
-
logger
|
|
100
|
+
with _iam_lock:
|
|
101
|
+
# retrieve the data for all users in the IAM server's registry
|
|
102
|
+
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
103
|
+
errors=errors,
|
|
104
|
+
logger=logger) or {}
|
|
105
|
+
if user_id in users:
|
|
106
|
+
users.pop(user_id)
|
|
107
|
+
if logger:
|
|
108
|
+
logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
|
|
105
109
|
|
|
106
110
|
|
|
107
111
|
def user_token(iam_server: IamServer,
|
|
@@ -111,7 +115,7 @@ def user_token(iam_server: IamServer,
|
|
|
111
115
|
"""
|
|
112
116
|
Retrieve the authentication token for the user, from *iam_server*.
|
|
113
117
|
|
|
114
|
-
The user is identified by the attribute *user-id*, *user_id*, or
|
|
118
|
+
The user is identified by the attribute *user-id*, *user_id*, or *login*, provided in *args*.
|
|
115
119
|
|
|
116
120
|
:param iam_server: the reference registered *IAM* server
|
|
117
121
|
:param args: the arguments passed when requesting the service
|
|
@@ -127,56 +131,58 @@ def user_token(iam_server: IamServer,
|
|
|
127
131
|
|
|
128
132
|
err_msg: str | None = None
|
|
129
133
|
if user_id:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
134
|
+
with _iam_lock:
|
|
135
|
+
# retrieve the user data in the IAM server's registry
|
|
136
|
+
user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
|
|
137
|
+
user_id=user_id,
|
|
138
|
+
errors=errors,
|
|
139
|
+
logger=logger)
|
|
140
|
+
token: str = user_data["access-token"] if user_data else None
|
|
141
|
+
if token:
|
|
142
|
+
access_expiration: int = user_data.get("access-expiration")
|
|
143
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
144
|
+
if now < access_expiration:
|
|
145
|
+
result = token
|
|
146
|
+
else:
|
|
147
|
+
# access token has expired
|
|
148
|
+
refresh_token: str = user_data["refresh-token"]
|
|
149
|
+
if refresh_token:
|
|
150
|
+
refresh_expiration = user_data["refresh-expiration"]
|
|
151
|
+
if now < refresh_expiration:
|
|
152
|
+
body_data: dict[str, str] = {
|
|
153
|
+
"grant_type": "refresh_token",
|
|
154
|
+
"refresh_token": refresh_token
|
|
155
|
+
}
|
|
156
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
157
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
158
|
+
body_data=body_data,
|
|
159
|
+
errors=errors,
|
|
160
|
+
logger=logger)
|
|
161
|
+
# validate and store the token data
|
|
162
|
+
if token_data:
|
|
163
|
+
token_info: tuple[str, str] = __validate_and_store(iam_server=iam_server,
|
|
164
|
+
user_data=user_data,
|
|
165
|
+
token_data=token_data,
|
|
166
|
+
now=now,
|
|
167
|
+
errors=errors,
|
|
168
|
+
logger=logger)
|
|
169
|
+
result = token_info[1]
|
|
170
|
+
else:
|
|
171
|
+
# refresh token is no longer valid
|
|
172
|
+
user_data["refresh-token"] = None
|
|
164
173
|
else:
|
|
165
|
-
# refresh token
|
|
166
|
-
|
|
174
|
+
# refresh token has expired
|
|
175
|
+
err_msg = "Access and refresh tokens expired"
|
|
176
|
+
if logger:
|
|
177
|
+
logger.error(msg=err_msg)
|
|
167
178
|
else:
|
|
168
|
-
|
|
169
|
-
err_msg = "Access and refresh tokens expired"
|
|
179
|
+
err_msg = "Access token expired, no refresh token available"
|
|
170
180
|
if logger:
|
|
171
181
|
logger.error(msg=err_msg)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
else:
|
|
177
|
-
err_msg = f"User '{user_id}' not authenticated"
|
|
178
|
-
if logger:
|
|
179
|
-
logger.error(msg=err_msg)
|
|
182
|
+
else:
|
|
183
|
+
err_msg = f"User '{user_id}' not authenticated"
|
|
184
|
+
if logger:
|
|
185
|
+
logger.error(msg=err_msg)
|
|
180
186
|
else:
|
|
181
187
|
err_msg = "User identification not provided"
|
|
182
188
|
if logger:
|
|
@@ -193,7 +199,11 @@ def login_callback(iam_server: IamServer,
|
|
|
193
199
|
errors: list[str] = None,
|
|
194
200
|
logger: Logger = None) -> tuple[str, str] | None:
|
|
195
201
|
"""
|
|
196
|
-
Entry point for the callback from *iam_server* via the front-end application, on authentication
|
|
202
|
+
Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
|
|
203
|
+
|
|
204
|
+
The relevant arguments received are:
|
|
205
|
+
- *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
|
|
206
|
+
- *code*: the temporary authorization code, to be exchanged for the token
|
|
197
207
|
|
|
198
208
|
:param iam_server: the reference registered *IAM* server
|
|
199
209
|
:param args: the arguments passed when requesting the service
|
|
@@ -204,15 +214,13 @@ def login_callback(iam_server: IamServer,
|
|
|
204
214
|
# initialize the return variable
|
|
205
215
|
result: tuple[str, str] | None = None
|
|
206
216
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
# validate the OAuth2 state
|
|
217
|
+
with _iam_lock:
|
|
218
|
+
# retrieve the IAM server's registry and the data for all users therein
|
|
219
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
220
|
+
errors=errors,
|
|
221
|
+
logger=logger)
|
|
222
|
+
users: dict[str, dict[str, Any]] = (registry["cache"]["users"] or {}) if registry else {}
|
|
223
|
+
# retrieve the OAuth2 state
|
|
216
224
|
oauth_state: str = args.get("state")
|
|
217
225
|
user_data: dict[str, Any] | None = None
|
|
218
226
|
if oauth_state:
|
|
@@ -221,7 +229,7 @@ def login_callback(iam_server: IamServer,
|
|
|
221
229
|
user_data = data
|
|
222
230
|
break
|
|
223
231
|
|
|
224
|
-
# exchange 'code' for the token
|
|
232
|
+
# exchange 'code' received for the token
|
|
225
233
|
if user_data:
|
|
226
234
|
expiration: int = user_data["login-expiration"] or sys.maxsize
|
|
227
235
|
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
@@ -268,6 +276,15 @@ def token_exchange(iam_server: IamServer,
|
|
|
268
276
|
- client-id: identification for the reference user (aliases: 'client_id', 'login')
|
|
269
277
|
- token: the token to be exchanged
|
|
270
278
|
|
|
279
|
+
The typical data set returned contains the following attributes:
|
|
280
|
+
{
|
|
281
|
+
"token_type": "Bearer",
|
|
282
|
+
"access_token": <str>,
|
|
283
|
+
"expires_in": <number-of-seconds>,
|
|
284
|
+
"refresh_token": <str>,
|
|
285
|
+
"refesh_expires_in": <number-of-seconds>
|
|
286
|
+
}
|
|
287
|
+
|
|
271
288
|
:param iam_server: the reference registered *IAM* server
|
|
272
289
|
:param args: the arguments passed when requesting the service
|
|
273
290
|
:param errors: incidental errors
|
|
@@ -280,40 +297,41 @@ def token_exchange(iam_server: IamServer,
|
|
|
280
297
|
# obtain the user's identification
|
|
281
298
|
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
282
299
|
|
|
283
|
-
#
|
|
300
|
+
# obtain the token to be exchanges
|
|
284
301
|
token: str = args.get("token")
|
|
285
302
|
|
|
286
303
|
if user_id and token:
|
|
287
304
|
# HAZARD: only 'IAM_KEYCLOAK' is currently supported
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
305
|
+
with _iam_lock:
|
|
306
|
+
# retrieve the IAM server's registry
|
|
307
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
308
|
+
errors=errors,
|
|
309
|
+
logger=logger)
|
|
310
|
+
if registry:
|
|
311
|
+
body_data: dict[str, str] = {
|
|
312
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
313
|
+
"subject_token": token,
|
|
314
|
+
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
315
|
+
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
316
|
+
"audience": registry["client-id"],
|
|
317
|
+
"subject_issuer": "oidc"
|
|
318
|
+
}
|
|
319
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
320
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=IamServer.IAM_KEYCLOAK,
|
|
321
|
+
body_data=body_data,
|
|
322
|
+
errors=errors,
|
|
323
|
+
logger=logger)
|
|
324
|
+
# validate and store the token data
|
|
325
|
+
if token_data:
|
|
326
|
+
user_data: dict[str, Any] = {}
|
|
327
|
+
result = __validate_and_store(iam_server=iam_server,
|
|
328
|
+
user_data=user_data,
|
|
329
|
+
token_data=token_data,
|
|
330
|
+
now=now,
|
|
331
|
+
errors=errors,
|
|
332
|
+
logger=logger)
|
|
315
333
|
else:
|
|
316
|
-
msg: str = "User identification
|
|
334
|
+
msg: str = "User identification or token not provided"
|
|
317
335
|
if logger:
|
|
318
336
|
logger.error(msg=msg)
|
|
319
337
|
if isinstance(errors, list):
|
|
@@ -353,7 +371,7 @@ def __post_for_token(iam_server: IamServer,
|
|
|
353
371
|
If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
354
372
|
Otherwise, *errors* will contain the appropriate error message.
|
|
355
373
|
|
|
356
|
-
The typical data returned contains the following attributes:
|
|
374
|
+
The typical data set returned contains the following attributes:
|
|
357
375
|
{
|
|
358
376
|
"token_type": "Bearer",
|
|
359
377
|
"access_token": <str>,
|
|
@@ -371,52 +389,53 @@ def __post_for_token(iam_server: IamServer,
|
|
|
371
389
|
# initialize the return variable
|
|
372
390
|
result: dict[str, Any] | None = None
|
|
373
391
|
|
|
374
|
-
# PBTAIN THE iam SERVER'S REGISTRY
|
|
375
|
-
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
376
|
-
errors=errors,
|
|
377
|
-
logger=logger)
|
|
378
392
|
err_msg: str | None = None
|
|
379
|
-
|
|
380
|
-
#
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
393
|
+
with _iam_lock:
|
|
394
|
+
# retrieve the IAM server's registry
|
|
395
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
396
|
+
errors=errors,
|
|
397
|
+
logger=logger)
|
|
398
|
+
if registry:
|
|
399
|
+
# complete the data to send in body of request
|
|
400
|
+
body_data["client_id"] = registry["client-id"]
|
|
401
|
+
client_secret: str = registry["client-secret"]
|
|
402
|
+
if client_secret:
|
|
403
|
+
body_data["client_secret"] = client_secret
|
|
404
|
+
|
|
405
|
+
# obtain the token
|
|
406
|
+
url: str = registry["base-url"] + "/protocol/openid-connect/token"
|
|
407
|
+
if logger:
|
|
408
|
+
logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
|
|
409
|
+
ensure_ascii=False)}")
|
|
410
|
+
try:
|
|
411
|
+
# typical return on a token request:
|
|
412
|
+
# {
|
|
413
|
+
# "token_type": "Bearer",
|
|
414
|
+
# "access_token": <str>,
|
|
415
|
+
# "expires_in": <number-of-seconds>,
|
|
416
|
+
# "refresh_token": <str>,
|
|
417
|
+
# "refesh_expires_in": <number-of-seconds>
|
|
418
|
+
# }
|
|
419
|
+
response: requests.Response = requests.post(url=url,
|
|
420
|
+
data=body_data)
|
|
421
|
+
if response.status_code == 200:
|
|
422
|
+
# request succeeded
|
|
423
|
+
if logger:
|
|
424
|
+
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
425
|
+
result = response.json()
|
|
426
|
+
else:
|
|
427
|
+
# request resulted in error
|
|
428
|
+
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
429
|
+
if hasattr(response, "content") and response.content:
|
|
430
|
+
err_msg += f", content '{response.content}'"
|
|
431
|
+
if logger:
|
|
432
|
+
logger.error(msg=err_msg)
|
|
433
|
+
except Exception as e:
|
|
434
|
+
# the operation raised an exception
|
|
435
|
+
err_msg = exc_format(exc=e,
|
|
436
|
+
exc_info=sys.exc_info())
|
|
412
437
|
if logger:
|
|
413
438
|
logger.error(msg=err_msg)
|
|
414
|
-
except Exception as e:
|
|
415
|
-
# the operation raised an exception
|
|
416
|
-
err_msg = exc_format(exc=e,
|
|
417
|
-
exc_info=sys.exc_info())
|
|
418
|
-
if logger:
|
|
419
|
-
logger.error(msg=err_msg)
|
|
420
439
|
|
|
421
440
|
if err_msg and isinstance(errors, list):
|
|
422
441
|
errors.append(err_msg)
|
|
@@ -452,38 +471,39 @@ def __validate_and_store(iam_server: IamServer,
|
|
|
452
471
|
# initialize the return variable
|
|
453
472
|
result: tuple[str, str] | None = None
|
|
454
473
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
474
|
+
with _iam_lock:
|
|
475
|
+
# retrieve the IAM server's registry
|
|
476
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
477
|
+
errors=errors,
|
|
478
|
+
logger=logger)
|
|
479
|
+
if registry:
|
|
480
|
+
token: str = token_data.get("access_token")
|
|
481
|
+
user_data["access-token"] = token
|
|
482
|
+
# keep current refresh token if a new one is not provided
|
|
483
|
+
if token_data.get("refresh_token"):
|
|
484
|
+
user_data["refresh-token"] = token_data.get("refresh_token")
|
|
485
|
+
user_data["access-expiration"] = now + token_data.get("expires_in")
|
|
486
|
+
refresh_exp: int = user_data.get("refresh_expires_in")
|
|
487
|
+
user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
|
|
488
|
+
public_key: str = _get_public_key(iam_server=iam_server,
|
|
489
|
+
errors=errors,
|
|
490
|
+
logger=logger)
|
|
491
|
+
if public_key:
|
|
492
|
+
recipient_attr = registry["recipient_attr"]
|
|
493
|
+
login_id = user_data.pop("login-id", None)
|
|
494
|
+
claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
495
|
+
issuer=registry["base-url"],
|
|
496
|
+
recipient_id=login_id,
|
|
497
|
+
recipient_attr=recipient_attr,
|
|
498
|
+
public_key=public_key,
|
|
499
|
+
errors=errors,
|
|
500
|
+
logger=logger)
|
|
501
|
+
if claims:
|
|
502
|
+
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
503
|
+
errors=errors,
|
|
504
|
+
logger=logger)
|
|
505
|
+
if users:
|
|
506
|
+
user_id: str = login_id if login_id else claims["payload"][recipient_attr]
|
|
507
|
+
users[user_id] = user_data
|
|
508
|
+
result = (user_id, token)
|
|
489
509
|
return result
|
pypomes_iam/iam_services.py
CHANGED
|
@@ -3,8 +3,14 @@ from flask import Request, Response, request, jsonify
|
|
|
3
3
|
from logging import Logger
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from .iam_common import
|
|
7
|
-
|
|
6
|
+
from .iam_common import (
|
|
7
|
+
IamServer, _iam_lock,
|
|
8
|
+
_get_logger, _get_iam_server
|
|
9
|
+
)
|
|
10
|
+
from .iam_pomes import (
|
|
11
|
+
user_login, user_logout,
|
|
12
|
+
user_token, token_exchange, login_callback
|
|
13
|
+
)
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
# @flask_app.route(rule=<login_endpoint>, # JUSBR_ENDPOINT_LOGIN
|
|
@@ -28,19 +34,20 @@ def service_login() -> Response:
|
|
|
28
34
|
# log the request
|
|
29
35
|
logger.debug(msg=_log_init(request=request))
|
|
30
36
|
|
|
31
|
-
# retrieve the IAM server
|
|
32
37
|
errors: list[str] = []
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
with _iam_lock:
|
|
39
|
+
# retrieve the IAM server
|
|
40
|
+
iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
|
|
41
|
+
errors=errors,
|
|
42
|
+
logger=logger)
|
|
43
|
+
if iam_server:
|
|
44
|
+
# obtain the login URL
|
|
45
|
+
login_data: dict[str, str] = user_login(iam_server=iam_server,
|
|
46
|
+
args=request.args,
|
|
47
|
+
errors=errors,
|
|
48
|
+
logger=logger)
|
|
49
|
+
if login_data:
|
|
50
|
+
result = jsonify(login_data)
|
|
44
51
|
|
|
45
52
|
if errors:
|
|
46
53
|
result = Response("; ".join(errors))
|
|
@@ -74,17 +81,18 @@ def service_logout() -> Response:
|
|
|
74
81
|
# log the request
|
|
75
82
|
logger.debug(msg=_log_init(request=request))
|
|
76
83
|
|
|
77
|
-
# retrieve the IAM server
|
|
78
84
|
errors: list[str] = []
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
with _iam_lock:
|
|
86
|
+
# retrieve the IAM server
|
|
87
|
+
iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
|
|
88
|
+
errors=errors,
|
|
89
|
+
logger=logger)
|
|
90
|
+
if iam_server:
|
|
91
|
+
# logout the user
|
|
92
|
+
user_logout(iam_server=iam_server,
|
|
93
|
+
args=request.args,
|
|
94
|
+
errors=errors,
|
|
95
|
+
logger=logger)
|
|
88
96
|
if errors:
|
|
89
97
|
result = Response("; ".join(errors))
|
|
90
98
|
result.status_code = 400
|
|
@@ -106,10 +114,11 @@ def service_callback() -> Response:
|
|
|
106
114
|
"""
|
|
107
115
|
Entry point for the callback from JusBR on authentication operation.
|
|
108
116
|
|
|
109
|
-
This callback is
|
|
110
|
-
|
|
117
|
+
This callback is invoked from a front-end application after a successful login at the
|
|
118
|
+
*IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
|
|
119
|
+
this data is then used to effectively obtain the token from the *IAM* server.
|
|
111
120
|
|
|
112
|
-
:return: the
|
|
121
|
+
:return: the *Response* containing the reference user identification and the token, or *BAD REQUEST*
|
|
113
122
|
"""
|
|
114
123
|
# retrieve the operations's logger
|
|
115
124
|
logger: Logger = _get_logger()
|
|
@@ -117,18 +126,19 @@ def service_callback() -> Response:
|
|
|
117
126
|
# log the request
|
|
118
127
|
logger.debug(msg=_log_init(request=request))
|
|
119
128
|
|
|
120
|
-
# retrieve the IAM server
|
|
121
129
|
errors: list[str] = []
|
|
122
130
|
token_data: tuple[str, str] | None = None
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
with _iam_lock:
|
|
132
|
+
# retrieve the IAM server
|
|
133
|
+
iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
|
|
134
|
+
errors=errors,
|
|
135
|
+
logger=logger)
|
|
136
|
+
if iam_server:
|
|
137
|
+
# process the callback operation
|
|
138
|
+
token_data = login_callback(iam_server=iam_server,
|
|
139
|
+
args=request.args,
|
|
140
|
+
errors=errors,
|
|
141
|
+
logger=logger)
|
|
132
142
|
result: Response
|
|
133
143
|
if errors:
|
|
134
144
|
result = jsonify({"errors": "; ".join(errors)})
|
|
@@ -136,10 +146,8 @@ def service_callback() -> Response:
|
|
|
136
146
|
if logger:
|
|
137
147
|
logger.error(msg=json.dumps(obj=result))
|
|
138
148
|
else:
|
|
139
|
-
result = jsonify({
|
|
140
|
-
|
|
141
|
-
"access-token": token_data[1]})
|
|
142
|
-
|
|
149
|
+
result = jsonify({"user-id": token_data[0],
|
|
150
|
+
"access-token": token_data[1]})
|
|
143
151
|
# log the response
|
|
144
152
|
if logger:
|
|
145
153
|
logger.debug(msg=f"Response {result}")
|
|
@@ -148,14 +156,14 @@ def service_callback() -> Response:
|
|
|
148
156
|
|
|
149
157
|
|
|
150
158
|
# @flask_app.route(rule=<token_endpoint>, # JUSBR_ENDPOINT_TOKEN
|
|
151
|
-
# @flask_app.route(rule=<token_endpoint>, # KEYCLOAK_ENDPOINT_TOKEN
|
|
152
159
|
# methods=["GET"])
|
|
160
|
+
# @flask_app.route(rule=<token_endpoint>, # KEYCLOAK_ENDPOINT_TOKEN
|
|
153
161
|
# methods=["GET"])
|
|
154
162
|
def service_token() -> Response:
|
|
155
163
|
"""
|
|
156
|
-
Entry point for retrieving token from the *IAM* server.
|
|
164
|
+
Entry point for retrieving a token from the *IAM* server.
|
|
157
165
|
|
|
158
|
-
:return: the
|
|
166
|
+
:return: the *Response* containing the user reference identification and the token, or *BAD REQUEST*
|
|
159
167
|
"""
|
|
160
168
|
# retrieve the operations's logger
|
|
161
169
|
logger: Logger = _get_logger()
|
|
@@ -163,26 +171,38 @@ def service_token() -> Response:
|
|
|
163
171
|
# log the request
|
|
164
172
|
logger.debug(msg=_log_init(request=request))
|
|
165
173
|
|
|
166
|
-
#
|
|
174
|
+
# obtain the user's identification
|
|
175
|
+
args: dict[str, Any] = request.args
|
|
176
|
+
user_id: str = args.get("user-id") or args.get("user_id") or args.get("login")
|
|
177
|
+
|
|
167
178
|
errors: list[str] = []
|
|
168
|
-
iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
|
|
169
|
-
errors=errors,
|
|
170
|
-
logger=logger)
|
|
171
|
-
# retrieve the token
|
|
172
179
|
token: str | None = None
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
if user_id:
|
|
181
|
+
with _iam_lock:
|
|
182
|
+
# retrieve the IAM server
|
|
183
|
+
iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
|
|
184
|
+
errors=errors,
|
|
185
|
+
logger=logger)
|
|
186
|
+
if iam_server:
|
|
187
|
+
# retrieve the token
|
|
188
|
+
errors: list[str] = []
|
|
189
|
+
token: str = user_token(iam_server=iam_server,
|
|
190
|
+
args=args,
|
|
191
|
+
errors=errors,
|
|
192
|
+
logger=logger)
|
|
193
|
+
else:
|
|
194
|
+
msg: str = "User identification not provided"
|
|
195
|
+
errors.append(msg)
|
|
196
|
+
if logger:
|
|
197
|
+
logger.error(msg=msg)
|
|
198
|
+
|
|
179
199
|
result: Response
|
|
180
200
|
if errors:
|
|
181
201
|
result = Response("; ".join(errors))
|
|
182
202
|
result.status_code = 400
|
|
183
203
|
else:
|
|
184
|
-
result = jsonify({"
|
|
185
|
-
|
|
204
|
+
result = jsonify({"user-id": user_id,
|
|
205
|
+
"token": token})
|
|
186
206
|
# log the response
|
|
187
207
|
if logger:
|
|
188
208
|
logger.debug(msg=f"Response {result}")
|
|
@@ -213,24 +233,28 @@ def service_exchange() -> Response:
|
|
|
213
233
|
"refesh_expires_in": <number-of-seconds>
|
|
214
234
|
}
|
|
215
235
|
|
|
216
|
-
:return: the
|
|
236
|
+
:return: the *Response* containing the token data, or *UNAUTHORIZED*
|
|
217
237
|
"""
|
|
218
238
|
# retrieve the operations's logger
|
|
219
239
|
logger: Logger = _get_logger()
|
|
240
|
+
if logger:
|
|
241
|
+
# log the request
|
|
242
|
+
logger.debug(msg=_log_init(request=request))
|
|
220
243
|
|
|
221
|
-
# retrieve the IAM server (currently, only 'Keycloak' is supported)
|
|
222
244
|
errors: list[str] = []
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
245
|
+
with _iam_lock:
|
|
246
|
+
# retrieve the IAM server (currently, only 'IAM_KEYCLOAK' is supported)
|
|
247
|
+
iam_server: IamServer = _get_iam_server(endpoint=request.endpoint,
|
|
248
|
+
errors=errors,
|
|
249
|
+
logger=logger)
|
|
250
|
+
# exchange the token
|
|
251
|
+
token_data: dict[str, Any] | None = None
|
|
252
|
+
if iam_server:
|
|
253
|
+
errors: list[str] = []
|
|
254
|
+
token_data = token_exchange(iam_server=iam_server,
|
|
255
|
+
args=request.args,
|
|
256
|
+
errors=errors,
|
|
257
|
+
logger=logger)
|
|
234
258
|
result: Response
|
|
235
259
|
if errors:
|
|
236
260
|
result = Response("; ".join(errors))
|
pypomes_iam/jusbr_pomes.py
CHANGED
|
@@ -7,7 +7,7 @@ from pypomes_core import (
|
|
|
7
7
|
)
|
|
8
8
|
from typing import Any, Final
|
|
9
9
|
|
|
10
|
-
from .iam_common import _IAM_SERVERS, IamServer
|
|
10
|
+
from .iam_common import _IAM_SERVERS, IamServer, _iam_lock
|
|
11
11
|
from .iam_pomes import user_token
|
|
12
12
|
|
|
13
13
|
JUSBR_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_CLIENT_ID")
|
|
@@ -65,18 +65,19 @@ def jusbr_setup(flask_app: Flask,
|
|
|
65
65
|
# configure the JusBR registry
|
|
66
66
|
cache: Cache = FIFOCache(maxsize=1048576)
|
|
67
67
|
cache["users"] = {}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
with _iam_lock:
|
|
69
|
+
_IAM_SERVERS[IamServer.IAM_JUSRBR] = {
|
|
70
|
+
"client-id": client_id,
|
|
71
|
+
"client-secret": client_secret,
|
|
72
|
+
"client-timeout": client_timeout,
|
|
73
|
+
"recipient-attr": recipient_attribute,
|
|
74
|
+
"base-url": base_url,
|
|
75
|
+
"pk-expiration": sys.maxsize,
|
|
76
|
+
"pk-lifetime": public_key_lifetime,
|
|
77
|
+
"cache": cache,
|
|
78
|
+
"logger": logger,
|
|
79
|
+
"redirect-uri": None
|
|
80
|
+
}
|
|
80
81
|
|
|
81
82
|
# establish the endpoints
|
|
82
83
|
if login_endpoint:
|
|
@@ -112,9 +113,14 @@ def jusbr_get_token(user_id: str,
|
|
|
112
113
|
:param logger: optional logger
|
|
113
114
|
:return: the uthentication tokem
|
|
114
115
|
"""
|
|
116
|
+
# declare the return variable
|
|
117
|
+
result: str
|
|
118
|
+
|
|
115
119
|
# retrieve the token
|
|
116
120
|
args: dict[str, Any] = {"user-id": user_id}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
with _iam_lock:
|
|
122
|
+
result = user_token(iam_server=IamServer.IAM_JUSRBR,
|
|
123
|
+
args=args,
|
|
124
|
+
errors=errors,
|
|
125
|
+
logger=logger)
|
|
126
|
+
return result
|
pypomes_iam/keycloak_pomes.py
CHANGED
|
@@ -7,7 +7,7 @@ from pypomes_core import (
|
|
|
7
7
|
)
|
|
8
8
|
from typing import Any, Final
|
|
9
9
|
|
|
10
|
-
from .iam_common import _IAM_SERVERS, IamServer
|
|
10
|
+
from .iam_common import _IAM_SERVERS, IamServer, _iam_lock
|
|
11
11
|
from .iam_pomes import user_token
|
|
12
12
|
|
|
13
13
|
KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
|
|
@@ -74,18 +74,19 @@ def keycloak_setup(flask_app: Flask,
|
|
|
74
74
|
# configure the Keycloak registry
|
|
75
75
|
cache: Cache = FIFOCache(maxsize=1048576)
|
|
76
76
|
cache["users"] = {}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
77
|
+
with _iam_lock:
|
|
78
|
+
_IAM_SERVERS[IamServer.IAM_KEYCLOAK] = {
|
|
79
|
+
"client-id": client_id,
|
|
80
|
+
"client-secret": client_secret,
|
|
81
|
+
"client-timeout": client_timeout,
|
|
82
|
+
"recipient-attr": recipient_attribute,
|
|
83
|
+
"base-url": f"{base_url}/realms/{realm}",
|
|
84
|
+
"pk-expiration": sys.maxsize,
|
|
85
|
+
"pk-lifetime": public_key_lifetime,
|
|
86
|
+
"cache": cache,
|
|
87
|
+
"logger": logger,
|
|
88
|
+
"redirect-uri": None
|
|
89
|
+
}
|
|
89
90
|
|
|
90
91
|
# establish the endpoints
|
|
91
92
|
if login_endpoint:
|
|
@@ -126,9 +127,14 @@ def keycloak_get_token(user_id: str,
|
|
|
126
127
|
:param logger: optional logger
|
|
127
128
|
:return: the uthentication tokem
|
|
128
129
|
"""
|
|
130
|
+
# declare the return variable
|
|
131
|
+
result: str
|
|
132
|
+
|
|
129
133
|
# retrieve the token
|
|
130
134
|
args: dict[str, Any] = {"user-id": user_id}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
with _iam_lock:
|
|
136
|
+
result = user_token(iam_server=IamServer.IAM_KEYCLOAK,
|
|
137
|
+
args=args,
|
|
138
|
+
errors=errors,
|
|
139
|
+
logger=logger)
|
|
140
|
+
return result
|
pypomes_iam/provider_pomes.py
CHANGED
|
@@ -4,7 +4,8 @@ from base64 import b64encode
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from logging import Logger
|
|
6
6
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
7
|
-
from
|
|
7
|
+
from threading import Lock
|
|
8
|
+
from typing import Any, Final
|
|
8
9
|
|
|
9
10
|
# structure:
|
|
10
11
|
# {
|
|
@@ -19,7 +20,11 @@ from typing import Any
|
|
|
19
20
|
# "expiration": <timestamp>
|
|
20
21
|
# }
|
|
21
22
|
# }
|
|
22
|
-
_provider_registry: dict[str, dict[str, Any]] = {}
|
|
23
|
+
_provider_registry: Final[dict[str, dict[str, Any]]] = {}
|
|
24
|
+
|
|
25
|
+
# the lock protecting the data in '_provider_registry'
|
|
26
|
+
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
27
|
+
_provider_lock: Final[Lock] = Lock()
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
def provider_register(provider_id: str,
|
|
@@ -48,18 +53,19 @@ def provider_register(provider_id: str,
|
|
|
48
53
|
:param headers_data: optional key-value pairs to be added to the request headers
|
|
49
54
|
:param body_data: optional key-value pairs to be added to the request body
|
|
50
55
|
"""
|
|
51
|
-
global _provider_registry
|
|
56
|
+
global _provider_registry
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
with _provider_lock:
|
|
59
|
+
_provider_registry[provider_id] = {
|
|
60
|
+
"url": auth_url,
|
|
61
|
+
"user": auth_user,
|
|
62
|
+
"pwd": auth_pwd,
|
|
63
|
+
"custom-auth": custom_auth,
|
|
64
|
+
"headers-data": headers_data,
|
|
65
|
+
"body-data": body_data,
|
|
66
|
+
"token": None,
|
|
67
|
+
"expiration": datetime.now(tz=TZ_LOCAL).timestamp()
|
|
68
|
+
}
|
|
63
69
|
|
|
64
70
|
|
|
65
71
|
def provider_get_token(provider_id: str,
|
|
@@ -78,53 +84,54 @@ def provider_get_token(provider_id: str,
|
|
|
78
84
|
result: str | None = None
|
|
79
85
|
|
|
80
86
|
err_msg: str | None = None
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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}'")
|
|
87
|
+
with _provider_lock:
|
|
88
|
+
provider: dict[str, Any] = _provider_registry.get(provider_id)
|
|
89
|
+
if provider:
|
|
90
|
+
now: float = datetime.now(tz=TZ_LOCAL).timestamp()
|
|
91
|
+
if now > provider.get("expiration"):
|
|
92
|
+
user: str = provider.get("user")
|
|
93
|
+
pwd: str = provider.get("pwd")
|
|
94
|
+
headers_data: dict[str, str] = provider.get("headers-data") or {}
|
|
95
|
+
body_data: dict[str, str] = provider.get("body-data") or {}
|
|
96
|
+
custom_auth: tuple[str, str] = provider.get("custom-auth")
|
|
97
|
+
if custom_auth:
|
|
98
|
+
body_data[custom_auth[0]] = user
|
|
99
|
+
body_data[custom_auth[1]] = pwd
|
|
115
100
|
else:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
101
|
+
enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
|
|
102
|
+
headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
|
|
103
|
+
url: str = provider.get("url")
|
|
104
|
+
try:
|
|
105
|
+
# typical return on a token request:
|
|
106
|
+
# {
|
|
107
|
+
# "token_type": "Bearer",
|
|
108
|
+
# "access_token": <str>,
|
|
109
|
+
# "expires_in": <number-of-seconds>,
|
|
110
|
+
# optional data:
|
|
111
|
+
# "refresh_token": <str>,
|
|
112
|
+
# "refresh_expires_in": <number-of-seconds>
|
|
113
|
+
# }
|
|
114
|
+
response: requests.Response = requests.post(url=url,
|
|
115
|
+
data=body_data,
|
|
116
|
+
headers=headers_data,
|
|
117
|
+
timeout=None)
|
|
118
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
119
|
+
# request resulted in error, report the problem
|
|
120
|
+
err_msg = (f"POST '{url}': failed, "
|
|
121
|
+
f"status {response.status_code}, reason '{response.reason}'")
|
|
122
|
+
else:
|
|
123
|
+
reply: dict[str, Any] = response.json()
|
|
124
|
+
provider["token"] = reply.get("access_token")
|
|
125
|
+
provider["expiration"] = now + int(reply.get("expires_in"))
|
|
126
|
+
if logger:
|
|
127
|
+
logger.debug(msg=f"POST '{url}': status {response.status_code}")
|
|
128
|
+
except Exception as e:
|
|
129
|
+
# the operation raised an exception
|
|
130
|
+
err_msg = exc_format(exc=e,
|
|
131
|
+
exc_info=sys.exc_info())
|
|
132
|
+
err_msg = f"POST '{url}': error, '{err_msg}'"
|
|
133
|
+
else:
|
|
134
|
+
err_msg: str = f"Provider '{provider_id}' not registered"
|
|
128
135
|
|
|
129
136
|
if err_msg:
|
|
130
137
|
if isinstance(errors, list):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pypomes_iam/__init__.py,sha256=H7rUCaUEJBLNJv2rtdmBxwcAB28OItdEPenpv_UEOVw,965
|
|
2
|
+
pypomes_iam/iam_common.py,sha256=f74FUDcnMM2cgzJg-AF17GwCKwbfoezS8LppwxYwPys,10049
|
|
3
|
+
pypomes_iam/iam_pomes.py,sha256=gvDpgff6arB4_Y8AAf6QH2CEWRmdy-kcnQLyD0hx4Y4,23966
|
|
4
|
+
pypomes_iam/iam_services.py,sha256=Ae_hLz5luRjK-l_rhBcuuY03Ov7n7o67UYgBb5rbBys,10002
|
|
5
|
+
pypomes_iam/jusbr_pomes.py,sha256=0qbjJ6EGnlx17K-4Lqh5XkfH58y0joVZiD6HykbwpoE,5823
|
|
6
|
+
pypomes_iam/keycloak_pomes.py,sha256=5ZfpncofF20C1IB5ndO31vfrvfa8Ffy7FJxkGoKKoQQ,6836
|
|
7
|
+
pypomes_iam/provider_pomes.py,sha256=3Rui68hmj8zwY0tnw4aWurz-yQ-niacJFQpi6nWzh-M,6355
|
|
8
|
+
pypomes_iam/token_pomes.py,sha256=1g6PMNNMbmdwLrsvSXvpO8-zdRhso1IFnwAyndNmV4Q,5332
|
|
9
|
+
pypomes_iam-0.3.6.dist-info/METADATA,sha256=q53TFkBnU4mUAZgsJ_r_730kASXLiIyCwW_5mkFz8TU,694
|
|
10
|
+
pypomes_iam-0.3.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
pypomes_iam-0.3.6.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
12
|
+
pypomes_iam-0.3.6.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
pypomes_iam/__init__.py,sha256=H7rUCaUEJBLNJv2rtdmBxwcAB28OItdEPenpv_UEOVw,965
|
|
2
|
-
pypomes_iam/iam_common.py,sha256=1NgXFTiD4qpbVqLfYsCfHrE0khEaczp-nR3AYXmzmvU,9608
|
|
3
|
-
pypomes_iam/iam_pomes.py,sha256=qmnHX88iaiMGaGeZfbs4VT-G_XMTpRT6wqRZeCOOKbQ,22294
|
|
4
|
-
pypomes_iam/iam_services.py,sha256=qdPPfwR9jIdGak-wr4t2NkdfhVaMqauBdvmVJvmFqyg,8914
|
|
5
|
-
pypomes_iam/jusbr_pomes.py,sha256=M47h_PUUgbCmFQyKz2sN1H9T00BC5v_oPgwl5ATWMSA,5625
|
|
6
|
-
pypomes_iam/keycloak_pomes.py,sha256=GtXJb4TZb-a_5b9ExYdJGetBcU1pEP96ONO6prA_vDo,6638
|
|
7
|
-
pypomes_iam/provider_pomes.py,sha256=eP8XzjTUEpwejTkO0wmDiqKjqbIEOzRNCR2ju5E15og,5856
|
|
8
|
-
pypomes_iam/token_pomes.py,sha256=1g6PMNNMbmdwLrsvSXvpO8-zdRhso1IFnwAyndNmV4Q,5332
|
|
9
|
-
pypomes_iam-0.3.5.dist-info/METADATA,sha256=fGRhn3H98wOkQSrxMh9FFTiheHez3TMIpX6R91wysoU,694
|
|
10
|
-
pypomes_iam-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
-
pypomes_iam-0.3.5.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
12
|
-
pypomes_iam-0.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|