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.
- pypomes_iam/__init__.py +22 -12
- pypomes_iam/iam_actions.py +116 -56
- pypomes_iam/iam_common.py +70 -29
- pypomes_iam/iam_pomes.py +111 -98
- pypomes_iam/iam_services.py +228 -96
- pypomes_iam/provider_pomes.py +197 -30
- pypomes_iam/token_pomes.py +27 -0
- {pypomes_iam-0.7.2.dist-info → pypomes_iam-0.8.0.dist-info}/METADATA +2 -2
- pypomes_iam-0.8.0.dist-info/RECORD +11 -0
- pypomes_iam-0.7.2.dist-info/RECORD +0 -11
- {pypomes_iam-0.7.2.dist-info → pypomes_iam-0.8.0.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.7.2.dist-info → pypomes_iam-0.8.0.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/__init__.py
CHANGED
|
@@ -1,37 +1,47 @@
|
|
|
1
1
|
from .iam_actions import (
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
iam_callback, iam_exchange,
|
|
3
|
+
iam_login, iam_logout, iam_get_token
|
|
4
4
|
)
|
|
5
5
|
from .iam_common import (
|
|
6
6
|
IamServer, IamParam
|
|
7
7
|
)
|
|
8
8
|
from .iam_pomes import (
|
|
9
|
-
|
|
9
|
+
iam_setup_server, iam_setup_endpoints
|
|
10
10
|
)
|
|
11
11
|
from .iam_services import (
|
|
12
|
-
|
|
12
|
+
jwt_required, iam_setup_logger,
|
|
13
|
+
service_setup_server, service_get_token,
|
|
14
|
+
service_login, service_logout,
|
|
15
|
+
service_callback, service_exchange,
|
|
16
|
+
service_callback_exchange
|
|
13
17
|
)
|
|
14
18
|
from .provider_pomes import (
|
|
15
|
-
|
|
19
|
+
service_get_token, provider_get_token,
|
|
20
|
+
provider_setup_endpoint, provider_setup_logger, provider_setup_server
|
|
16
21
|
)
|
|
17
22
|
from .token_pomes import (
|
|
18
|
-
token_validate
|
|
23
|
+
token_get_claims, token_get_values, token_validate
|
|
19
24
|
)
|
|
20
25
|
|
|
21
26
|
__all__ = [
|
|
22
27
|
# iam_actions
|
|
23
|
-
"
|
|
24
|
-
"
|
|
28
|
+
"iam_callback", "iam_exchange",
|
|
29
|
+
"iam_login", "iam_logout", "iam_get_token",
|
|
25
30
|
# iam_commons
|
|
26
31
|
"IamServer", "IamParam",
|
|
27
32
|
# iam_pomes
|
|
28
|
-
"
|
|
33
|
+
"iam_setup_server", "iam_setup_endpoints",
|
|
29
34
|
# iam_services
|
|
30
|
-
"jwt_required", "
|
|
35
|
+
"jwt_required", "iam_setup_logger",
|
|
36
|
+
"service_setup_server", "service_get_token",
|
|
37
|
+
"service_login", "service_logout",
|
|
38
|
+
"service_callback", "service_exchange",
|
|
39
|
+
"service_callback_exchange",
|
|
31
40
|
# provider_pomes
|
|
32
|
-
"
|
|
41
|
+
"provider_setup_server", "provider_get_token",
|
|
42
|
+
"provider_setup_endpoint", "provider_setup_logger", "provider_setup_server",
|
|
33
43
|
# token_pomes
|
|
34
|
-
"token_validate"
|
|
44
|
+
"token_get_claims", "token_get_values", "token_validate"
|
|
35
45
|
]
|
|
36
46
|
|
|
37
47
|
from importlib.metadata import version
|
pypomes_iam/iam_actions.py
CHANGED
|
@@ -13,24 +13,30 @@ from .iam_common import (
|
|
|
13
13
|
_get_iam_users, _get_iam_registry, _get_public_key,
|
|
14
14
|
_get_login_timeout, _get_user_data, _iam_server_from_issuer
|
|
15
15
|
)
|
|
16
|
-
from .token_pomes import
|
|
16
|
+
from .token_pomes import token_get_values, token_validate
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
def iam_login(iam_server: IamServer,
|
|
20
|
+
args: dict[str, Any],
|
|
21
|
+
errors: list[str] = None,
|
|
22
|
+
logger: Logger = None) -> str:
|
|
23
23
|
"""
|
|
24
24
|
Build the URL for redirecting the request to *iam_server*'s authentication page.
|
|
25
25
|
|
|
26
26
|
These are the expected attributes in *args*:
|
|
27
27
|
- user-id: optional, identifies the reference user (alias: 'login')
|
|
28
28
|
- redirect-uri: a parameter to be added to the query part of the returned URL
|
|
29
|
+
-target-idp: optionally, identify a target identity provider for the login operation
|
|
29
30
|
|
|
30
31
|
If provided, the user identification will be validated against the authorization data
|
|
31
32
|
returned by *iam_server* upon login. On success, the appropriate URL for invoking
|
|
32
33
|
the IAM server's authentication page is returned.
|
|
33
34
|
|
|
35
|
+
if 'target_idp' is provided as an attribute in *args*, the OAuth2 state variable included in the
|
|
36
|
+
returned URL will be postfixed with the string *#idp=<target-idp>*. At the callback endpoint,
|
|
37
|
+
this instructs *iam_server* to act as a broker, forwading the authentication process to the
|
|
38
|
+
*IAM* server *target-idp*.
|
|
39
|
+
|
|
34
40
|
:param iam_server: the reference registered *IAM* server
|
|
35
41
|
:param args: the arguments passed when requesting the service
|
|
36
42
|
:param errors: incidental error messages
|
|
@@ -43,9 +49,14 @@ def action_login(iam_server: IamServer,
|
|
|
43
49
|
# obtain the optional user's identification
|
|
44
50
|
user_id: str = args.get("user-id") or args.get("login")
|
|
45
51
|
|
|
52
|
+
# obtain the optional target identity provider
|
|
53
|
+
target_idp: str = args.get("target-idp")
|
|
54
|
+
|
|
46
55
|
# build the user data
|
|
47
56
|
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
48
57
|
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
58
|
+
if target_idp:
|
|
59
|
+
oauth_state += f"#idp={target_idp}"
|
|
49
60
|
|
|
50
61
|
with _iam_lock:
|
|
51
62
|
# retrieve the user data from the IAM server's registry
|
|
@@ -75,13 +86,17 @@ def action_login(iam_server: IamServer,
|
|
|
75
86
|
f"&client_id={registry[IamParam.CLIENT_ID]}"
|
|
76
87
|
f"&redirect_uri={redirect_uri}"
|
|
77
88
|
f"&state={oauth_state}")
|
|
89
|
+
if target_idp:
|
|
90
|
+
# HAZARD: the name 'kc_idp_hint' is Keycloak-specific
|
|
91
|
+
result += f"&kc_idp_hint={target_idp}"
|
|
92
|
+
|
|
78
93
|
return result
|
|
79
94
|
|
|
80
95
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
def iam_logout(iam_server: IamServer,
|
|
97
|
+
args: dict[str, Any],
|
|
98
|
+
errors: list[str] = None,
|
|
99
|
+
logger: Logger = None) -> None:
|
|
85
100
|
"""
|
|
86
101
|
Logout the user, by removing all data associating it from *iam_server*'s registry.
|
|
87
102
|
|
|
@@ -109,23 +124,29 @@ def action_logout(iam_server: IamServer,
|
|
|
109
124
|
logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
|
|
110
125
|
|
|
111
126
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
def iam_get_token(iam_server: IamServer,
|
|
128
|
+
args: dict[str, Any],
|
|
129
|
+
errors: list[str] = None,
|
|
130
|
+
logger: Logger = None) -> dict[str, str]:
|
|
116
131
|
"""
|
|
117
132
|
Retrieve the authentication token for the user, from *iam_server*.
|
|
118
133
|
|
|
119
134
|
The user is identified by the attribute *user-id* or *login*, provided in *args*.
|
|
120
135
|
|
|
136
|
+
On success, the returned *dict* will contain the following JSON:
|
|
137
|
+
{
|
|
138
|
+
"access-token": <token>,
|
|
139
|
+
"user-id": <user-identification
|
|
140
|
+
}
|
|
141
|
+
|
|
121
142
|
:param iam_server: the reference registered *IAM* server
|
|
122
143
|
:param args: the arguments passed when requesting the service
|
|
123
144
|
:param errors: incidental error messages
|
|
124
145
|
:param logger: optional logger
|
|
125
|
-
:return: the
|
|
146
|
+
:return: the user identification and token issued, or *None* if error
|
|
126
147
|
"""
|
|
127
148
|
# initialize the return variable
|
|
128
|
-
result: str | None = None
|
|
149
|
+
result: dict[str, str] | None = None
|
|
129
150
|
|
|
130
151
|
# obtain the user's identification
|
|
131
152
|
user_id: str = args.get("user-id") or args.get("login")
|
|
@@ -144,7 +165,10 @@ def action_token(iam_server: IamServer,
|
|
|
144
165
|
access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
|
|
145
166
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
146
167
|
if now < access_expiration:
|
|
147
|
-
result =
|
|
168
|
+
result = {
|
|
169
|
+
"access-token": token,
|
|
170
|
+
"user-id": user_id
|
|
171
|
+
}
|
|
148
172
|
else:
|
|
149
173
|
# access token has expired
|
|
150
174
|
refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
|
|
@@ -172,7 +196,10 @@ def action_token(iam_server: IamServer,
|
|
|
172
196
|
now=now,
|
|
173
197
|
errors=errors,
|
|
174
198
|
logger=logger)
|
|
175
|
-
result =
|
|
199
|
+
result = {
|
|
200
|
+
"access-token": token_info[1],
|
|
201
|
+
"user-id": user_id
|
|
202
|
+
}
|
|
176
203
|
else:
|
|
177
204
|
# refresh token is no longer valid
|
|
178
205
|
user_data[UserParam.REFRESH_TOKEN] = None
|
|
@@ -200,10 +227,10 @@ def action_token(iam_server: IamServer,
|
|
|
200
227
|
return result
|
|
201
228
|
|
|
202
229
|
|
|
203
|
-
def
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
def iam_callback(iam_server: IamServer,
|
|
231
|
+
args: dict[str, Any],
|
|
232
|
+
errors: list[str] = None,
|
|
233
|
+
logger: Logger = None) -> tuple[str, str] | None:
|
|
207
234
|
"""
|
|
208
235
|
Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
|
|
209
236
|
|
|
@@ -211,6 +238,10 @@ def action_callback(iam_server: IamServer,
|
|
|
211
238
|
- *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
|
|
212
239
|
- *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
|
|
213
240
|
|
|
241
|
+
if *state* is postfixed with the string *#idp=<target-idp>*, this instructs *iam_server* to act as a broker,
|
|
242
|
+
forwarding the authentication process to the *IAM* server *target-idp*. This mechanism fully dispenses with
|
|
243
|
+
the flows 'callback-exchange', and 'callback' followed by 'exchange'.
|
|
244
|
+
|
|
214
245
|
:param iam_server: the reference registered *IAM* server
|
|
215
246
|
:param args: the arguments passed when requesting the service
|
|
216
247
|
:param errors: incidental errors
|
|
@@ -240,6 +271,10 @@ def action_callback(iam_server: IamServer,
|
|
|
240
271
|
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
241
272
|
errors.append("Operation timeout")
|
|
242
273
|
else:
|
|
274
|
+
pos: int = oauth_state.rfind("#idp=")
|
|
275
|
+
target_idp: str = oauth_state[pos+4:] if pos > 0 else None
|
|
276
|
+
target_iam = IamServer(target_idp) if target_idp in IamServer else None
|
|
277
|
+
target_data: dict[str, Any] = user_data.copy() if target_iam else None
|
|
243
278
|
users.pop(oauth_state)
|
|
244
279
|
code: str = args.get("code")
|
|
245
280
|
header_data: dict[str, str] = {
|
|
@@ -264,6 +299,33 @@ def action_callback(iam_server: IamServer,
|
|
|
264
299
|
now=now,
|
|
265
300
|
errors=errors,
|
|
266
301
|
logger=logger)
|
|
302
|
+
if target_iam:
|
|
303
|
+
if logger:
|
|
304
|
+
logger.debug(msg=f"Requesting to IAM server '{iam_server}' "
|
|
305
|
+
f"the token issued by '{target_iam}' ")
|
|
306
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server,
|
|
307
|
+
errors=errors,
|
|
308
|
+
logger=logger)
|
|
309
|
+
url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
310
|
+
url += f"/broker/{target_idp}/token"
|
|
311
|
+
header_data: dict[str, str] = {
|
|
312
|
+
"Authorization": f"Bearer {result[1]}",
|
|
313
|
+
"Content-Type": "application/json"
|
|
314
|
+
}
|
|
315
|
+
token_data = __get_for_data(url=url,
|
|
316
|
+
header_data=header_data,
|
|
317
|
+
params=None,
|
|
318
|
+
errors=errors,
|
|
319
|
+
logger=logger)
|
|
320
|
+
if not errors:
|
|
321
|
+
token_info: tuple[str, str] = __validate_and_store(iam_server=target_iam,
|
|
322
|
+
user_data=target_data,
|
|
323
|
+
token_data=token_data,
|
|
324
|
+
now=now,
|
|
325
|
+
errors=errors,
|
|
326
|
+
logger=logger)
|
|
327
|
+
if token_info and logger:
|
|
328
|
+
logger.debug(msg=f"Token obtained: {json.dumps(obj=token_info)}")
|
|
267
329
|
else:
|
|
268
330
|
msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
|
|
269
331
|
if logger:
|
|
@@ -274,10 +336,10 @@ def action_callback(iam_server: IamServer,
|
|
|
274
336
|
return result
|
|
275
337
|
|
|
276
338
|
|
|
277
|
-
def
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
339
|
+
def iam_exchange(iam_server: IamServer,
|
|
340
|
+
args: dict[str, Any],
|
|
341
|
+
errors: list[str] = None,
|
|
342
|
+
logger: Logger = None) -> tuple[str, str]:
|
|
281
343
|
"""
|
|
282
344
|
Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
|
|
283
345
|
|
|
@@ -308,12 +370,10 @@ def action_exchange(iam_server: IamServer,
|
|
|
308
370
|
|
|
309
371
|
# obtain the token to be exchanged
|
|
310
372
|
token: str = args.get("access-token") if user_id else None
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
logger=logger) if token else None
|
|
314
|
-
token_issuer: str = _iam_server_from_issuer(issuer=token_claims["payload"]["iss"],
|
|
373
|
+
token_issuer: tuple[str] = token_get_values(token=token,
|
|
374
|
+
keys=("iss",),
|
|
315
375
|
errors=errors,
|
|
316
|
-
logger=logger)
|
|
376
|
+
logger=logger)
|
|
317
377
|
if not errors:
|
|
318
378
|
# HAZARD: only 'IAM_KEYCLOAK' is currently supported
|
|
319
379
|
with _iam_lock:
|
|
@@ -326,6 +386,7 @@ def action_exchange(iam_server: IamServer,
|
|
|
326
386
|
__assert_link(iam_server=iam_server,
|
|
327
387
|
user_id=user_id,
|
|
328
388
|
token=token,
|
|
389
|
+
token_issuer=token_issuer[0],
|
|
329
390
|
errors=errors,
|
|
330
391
|
logger=logger)
|
|
331
392
|
if not errors:
|
|
@@ -371,6 +432,7 @@ def action_exchange(iam_server: IamServer,
|
|
|
371
432
|
def __assert_link(iam_server: IamServer,
|
|
372
433
|
user_id: str,
|
|
373
434
|
token: str,
|
|
435
|
+
token_issuer: str,
|
|
374
436
|
errors: list[str] | None,
|
|
375
437
|
logger: Logger | None) -> None:
|
|
376
438
|
"""
|
|
@@ -398,7 +460,7 @@ def __assert_link(iam_server: IamServer,
|
|
|
398
460
|
# obtain the internal user identification for 'user_id'
|
|
399
461
|
if logger:
|
|
400
462
|
logger.debug(msg="Obtaining internal identification "
|
|
401
|
-
f"for user {user_id} in IAM server {iam_server}")
|
|
463
|
+
f"for user '{user_id}' in IAM server '{iam_server}'")
|
|
402
464
|
url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
|
|
403
465
|
header_data: dict[str, str] = {
|
|
404
466
|
"Authorization": f"Bearer {admin_token}",
|
|
@@ -414,12 +476,12 @@ def __assert_link(iam_server: IamServer,
|
|
|
414
476
|
errors=errors,
|
|
415
477
|
logger=logger)
|
|
416
478
|
if users:
|
|
417
|
-
# verify whether the
|
|
418
|
-
#
|
|
479
|
+
# verify whether the IAM server that issued the token is a federated identity provider
|
|
480
|
+
# in the associations between 'user_id' and the internal user identification
|
|
419
481
|
internal_id: str = users[0].get("id")
|
|
420
482
|
if logger:
|
|
421
|
-
logger.debug(msg="Obtaining the providers federated
|
|
422
|
-
f"
|
|
483
|
+
logger.debug(msg="Obtaining the providers federated in IAM server "
|
|
484
|
+
f"'{iam_server}', for internal identification '{internal_id}'")
|
|
423
485
|
url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
|
|
424
486
|
f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
|
|
425
487
|
providers: list[dict[str, Any]] = __get_for_data(url=url,
|
|
@@ -428,13 +490,9 @@ def __assert_link(iam_server: IamServer,
|
|
|
428
490
|
errors=errors,
|
|
429
491
|
logger=logger)
|
|
430
492
|
no_link: bool = True
|
|
431
|
-
|
|
432
|
-
errors=errors,
|
|
433
|
-
logger=logger)
|
|
434
|
-
issuer: str = claims["payload"]["iss"] if claims else None
|
|
435
|
-
provider_name: str = _iam_server_from_issuer(issuer=issuer,
|
|
493
|
+
provider_name: str = _iam_server_from_issuer(issuer=token_issuer,
|
|
436
494
|
errors=errors,
|
|
437
|
-
logger=logger)
|
|
495
|
+
logger=logger)
|
|
438
496
|
if provider_name:
|
|
439
497
|
for provider in providers:
|
|
440
498
|
if provider.get("identityProvider") == provider_name:
|
|
@@ -442,22 +500,22 @@ def __assert_link(iam_server: IamServer,
|
|
|
442
500
|
break
|
|
443
501
|
if no_link:
|
|
444
502
|
# link the identities
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
503
|
+
token_sub: tuple[str] = token_get_values(token=token,
|
|
504
|
+
keys=("sub",),
|
|
505
|
+
errors=errors,
|
|
506
|
+
logger=logger)
|
|
507
|
+
if token_sub:
|
|
450
508
|
if logger:
|
|
451
509
|
logger.debug(msg="Creating an association between identifications "
|
|
452
|
-
f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
|
|
510
|
+
f"'{user_id}' and '{token_sub}' in IAM server '{iam_server}'")
|
|
453
511
|
url += f"/{provider_name}"
|
|
454
|
-
|
|
455
|
-
"userId": token_sub,
|
|
512
|
+
json_data: dict[str, Any] = {
|
|
513
|
+
"userId": token_sub[0],
|
|
456
514
|
"userName": user_id
|
|
457
515
|
}
|
|
458
|
-
|
|
516
|
+
__post_json(url=url,
|
|
459
517
|
header_data=header_data,
|
|
460
|
-
|
|
518
|
+
json_data=json_data,
|
|
461
519
|
errors=errors,
|
|
462
520
|
logger=logger)
|
|
463
521
|
|
|
@@ -646,27 +704,27 @@ def __get_for_data(url: str,
|
|
|
646
704
|
return result
|
|
647
705
|
|
|
648
706
|
|
|
649
|
-
def
|
|
707
|
+
def __post_json(url: str,
|
|
650
708
|
header_data: dict[str, str],
|
|
651
|
-
|
|
709
|
+
json_data: dict[str, Any],
|
|
652
710
|
errors: list[str] | None,
|
|
653
711
|
logger: Logger | None) -> None:
|
|
654
712
|
"""
|
|
655
713
|
Submit a *POST* request to *url*.
|
|
656
714
|
|
|
657
715
|
:param header_data: the data to send in the header of the request
|
|
658
|
-
:param
|
|
716
|
+
:param json_data: the JSON data to send in the request
|
|
659
717
|
:param errors: incidental errors
|
|
660
718
|
:param logger: optional logger
|
|
661
719
|
"""
|
|
662
720
|
# log the POST
|
|
663
721
|
if logger:
|
|
664
|
-
logger.debug(msg=f"POST {url}, {json.dumps(obj=
|
|
722
|
+
logger.debug(msg=f"POST {url}, {json.dumps(obj=json_data,
|
|
665
723
|
ensure_ascii=False)}")
|
|
666
724
|
try:
|
|
667
725
|
response: requests.Response = requests.post(url=url,
|
|
668
726
|
headers=header_data,
|
|
669
|
-
|
|
727
|
+
json=json_data)
|
|
670
728
|
if response.status_code >= 400:
|
|
671
729
|
# request failed, report the problem
|
|
672
730
|
msg = f"POST failure, status {response.status_code}, reason {response.reason}"
|
|
@@ -839,6 +897,8 @@ def __validate_and_store(iam_server: IamServer,
|
|
|
839
897
|
# initialize the return variable
|
|
840
898
|
result: tuple[str, str] | None = None
|
|
841
899
|
|
|
900
|
+
if logger:
|
|
901
|
+
logger.debug(msg=f"Validating and storing the token")
|
|
842
902
|
with _iam_lock:
|
|
843
903
|
# retrieve the IAM server's registry
|
|
844
904
|
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
pypomes_iam/iam_common.py
CHANGED
|
@@ -3,7 +3,10 @@ import sys
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import StrEnum, auto
|
|
5
5
|
from logging import Logger
|
|
6
|
-
from pypomes_core import
|
|
6
|
+
from pypomes_core import (
|
|
7
|
+
APP_PREFIX, TZ_LOCAL, exc_format,
|
|
8
|
+
env_get_str, env_get_int, env_get_enums
|
|
9
|
+
)
|
|
7
10
|
from pypomes_crypto import crypto_jwk_convert
|
|
8
11
|
from threading import RLock
|
|
9
12
|
from typing import Any, Final
|
|
@@ -21,12 +24,14 @@ class IamParam(StrEnum):
|
|
|
21
24
|
"""
|
|
22
25
|
Parameters for configuring *IAM* servers.
|
|
23
26
|
"""
|
|
27
|
+
|
|
24
28
|
ADMIN_ID = "admin-id"
|
|
25
29
|
ADMIN_SECRET = "admin-secret"
|
|
26
30
|
CLIENT_ID = "client-id"
|
|
27
31
|
CLIENT_REALM = "client-realm"
|
|
28
32
|
CLIENT_SECRET = "client-secret"
|
|
29
33
|
ENDPOINT_CALLBACK = "endpoint-callback"
|
|
34
|
+
ENDPOINT_CALLBACK_EXCHANGE = "endpoint-callback-exchange"
|
|
30
35
|
ENDPOINT_LOGIN = "endpoint-login"
|
|
31
36
|
ENDPOINT_LOGOUT = "endpoint_logout"
|
|
32
37
|
ENDPOINT_TOKEN = "endpoint-token"
|
|
@@ -34,8 +39,9 @@ class IamParam(StrEnum):
|
|
|
34
39
|
LOGIN_TIMEOUT = "login-timeout"
|
|
35
40
|
PK_EXPIRATION = "pk-expiration"
|
|
36
41
|
PK_LIFETIME = "pk-lifetime"
|
|
37
|
-
PUBLIC_KEY = "public-key"
|
|
38
42
|
RECIPIENT_ATTR = "recipient-attr"
|
|
43
|
+
# dynamic attributes
|
|
44
|
+
PUBLIC_KEY = "public-key"
|
|
39
45
|
URL_BASE = "url-base"
|
|
40
46
|
USERS = "users"
|
|
41
47
|
|
|
@@ -54,31 +60,65 @@ class UserParam(StrEnum):
|
|
|
54
60
|
REDIRECT_URI = "redirect-uri"
|
|
55
61
|
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
63
|
+
def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
|
|
64
|
+
"""
|
|
65
|
+
Obtain the configuration data for select *IAM* servers.
|
|
66
|
+
|
|
67
|
+
The configuration parameters for the IAM servers are specified dynamically with environment variables,
|
|
68
|
+
or dynamically with calls to *iam_setup_server()*. Specifying configuration parameters with environment
|
|
69
|
+
variables can be done by following these steps:
|
|
70
|
+
|
|
71
|
+
1. Specify *<APP_PREFIX>_IAM_SERVERS* with a list of names among the values found in *IamServer* class
|
|
72
|
+
(currently, *jusbr* and *keycloak* are supported), and the data set below for each server, where
|
|
73
|
+
*<IAM>* stands for the server's name as presented in *IamServer* class:
|
|
74
|
+
- *<APP_PREFIX>_<IAM>_ADMIN_ID* (optional, required if administrative duties are performed)
|
|
75
|
+
- *<APP_PREFIX>_<IAM>_ADMIN_PWD* (optional, required if administrative duties are performed)
|
|
76
|
+
- *<APP_PREFIX>_<IAM>_CLIENT_ID* (required)
|
|
77
|
+
- *<APP_PREFIX>_<IAM>_CLIENT_REALM* (required)
|
|
78
|
+
- *<APP_PREFIX>_<IAM>_CLIENT_SECRET* (required)
|
|
79
|
+
- *<APP_PREFIX>_<IAM>_LOGIN_TIMEOUT* (optional, defaults to no timeout)
|
|
80
|
+
- *<APP_PREFIX>_<IAM>_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
|
|
81
|
+
- *<APP_PREFIX>_<IAM>_RECIPIENT_ATTR* (required)
|
|
82
|
+
- *<APP_PREFIX>_<IAM>_URL_BASE* (required)
|
|
83
|
+
|
|
84
|
+
2. A group of special environment variables identifying endpoints for authentication services may be specified,
|
|
85
|
+
following the same scheme as presented in item *1* above. These are not part of the *IAM* server's setup,
|
|
86
|
+
but are meant to be used by function *iam_setup_endpoints()*, wherein the values in those variables
|
|
87
|
+
would represent default values for its parameters, respectively:
|
|
88
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_CALLBACK*
|
|
89
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_CALLBACK_EXCHANGE*
|
|
90
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_EXCHANGE*
|
|
91
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_LOGIN*
|
|
92
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_LOGOUT*
|
|
93
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_TOKEN*
|
|
94
|
+
|
|
95
|
+
:return: the configuration data for the select *IAM* servers.
|
|
96
|
+
"""
|
|
97
|
+
# initialize the return variable
|
|
98
|
+
result: dict[IamServer, dict[IamParam, Any]] = {}
|
|
99
|
+
|
|
100
|
+
servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_IAM_SERVERS",
|
|
101
|
+
enum_class=IamServer) or []
|
|
102
|
+
for server in servers:
|
|
103
|
+
prefix = server.name
|
|
104
|
+
result[server] = {
|
|
105
|
+
IamParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
|
|
106
|
+
IamParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_SECRET"),
|
|
107
|
+
IamParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
|
|
108
|
+
IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
|
|
109
|
+
IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
|
|
110
|
+
IamParam.LOGIN_TIMEOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT"),
|
|
111
|
+
IamParam.PK_LIFETIME: env_get_int(key=f"{APP_PREFIX}_{prefix}_PUBLIC_KEY_LIFETIME"),
|
|
112
|
+
IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
|
|
113
|
+
IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH_BASE"),
|
|
114
|
+
# dynamically set
|
|
115
|
+
IamParam.PK_EXPIRATION: 0,
|
|
116
|
+
IamParam.PUBLIC_KEY: None,
|
|
117
|
+
IamParam.USERS: {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
|
|
82
122
|
|
|
83
123
|
# registry structure:
|
|
84
124
|
# { <IamServer>:
|
|
@@ -91,6 +131,7 @@ class UserParam(StrEnum):
|
|
|
91
131
|
# "client-realm": <str,
|
|
92
132
|
# "client-timeout": <int>,
|
|
93
133
|
# "recipient-attr": <str>,
|
|
134
|
+
# # dynamic attributes
|
|
94
135
|
# "public-key": <str>,
|
|
95
136
|
# "pk-lifetime": <int>,
|
|
96
137
|
# "pk-expiration": <int>,
|
|
@@ -112,10 +153,10 @@ class UserParam(StrEnum):
|
|
|
112
153
|
# },
|
|
113
154
|
# ...
|
|
114
155
|
# }
|
|
115
|
-
_IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] =
|
|
156
|
+
_IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
|
|
116
157
|
|
|
117
158
|
|
|
118
|
-
# the lock protecting the data in '
|
|
159
|
+
# the lock protecting the data in '_<IAM>_SERVERS'
|
|
119
160
|
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
120
161
|
_iam_lock: Final[RLock] = RLock()
|
|
121
162
|
|