pypomes-iam 0.6.9__tar.gz → 0.7.6__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.
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/PKG-INFO +1 -1
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/pyproject.toml +1 -1
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/src/pypomes_iam/iam_actions.py +66 -15
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/src/pypomes_iam/iam_services.py +8 -9
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/.gitignore +0 -0
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/LICENSE +0 -0
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/README.md +0 -0
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/src/__init__.py +0 -0
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/src/pypomes_iam/__init__.py +0 -0
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/src/pypomes_iam/iam_common.py +0 -0
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/src/pypomes_iam/iam_pomes.py +0 -0
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/src/pypomes_iam/provider_pomes.py +0 -0
- {pypomes_iam-0.6.9 → pypomes_iam-0.7.6}/src/pypomes_iam/token_pomes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.6
|
|
3
|
+
Version: 0.7.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
|
|
@@ -26,6 +26,7 @@ def action_login(iam_server: IamServer,
|
|
|
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
|
|
@@ -43,9 +44,14 @@ def action_login(iam_server: IamServer,
|
|
|
43
44
|
# obtain the optional user's identification
|
|
44
45
|
user_id: str = args.get("user-id") or args.get("login")
|
|
45
46
|
|
|
47
|
+
# obtain the optional target identity provider
|
|
48
|
+
target_idp: str = args.get("target-idp")
|
|
49
|
+
|
|
46
50
|
# build the user data
|
|
47
51
|
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
48
52
|
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
53
|
+
if target_idp:
|
|
54
|
+
oauth_state += f"idp={target_idp}"
|
|
49
55
|
|
|
50
56
|
with _iam_lock:
|
|
51
57
|
# retrieve the user data from the IAM server's registry
|
|
@@ -75,6 +81,10 @@ def action_login(iam_server: IamServer,
|
|
|
75
81
|
f"&client_id={registry[IamParam.CLIENT_ID]}"
|
|
76
82
|
f"&redirect_uri={redirect_uri}"
|
|
77
83
|
f"&state={oauth_state}")
|
|
84
|
+
if target_idp:
|
|
85
|
+
# HAZARD: the name 'kc_idp_hint' is Keycloak-specific
|
|
86
|
+
result += f"&kc_idp_hint={target_idp}"
|
|
87
|
+
|
|
78
88
|
return result
|
|
79
89
|
|
|
80
90
|
|
|
@@ -240,6 +250,10 @@ def action_callback(iam_server: IamServer,
|
|
|
240
250
|
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
241
251
|
errors.append("Operation timeout")
|
|
242
252
|
else:
|
|
253
|
+
pos: int = oauth_state.rfind("idp=")
|
|
254
|
+
target_idp: str = oauth_state[pos+4:] if pos > 0 else None
|
|
255
|
+
target_iam = IamServer(target_idp) if target_idp in IamServer else None
|
|
256
|
+
target_data: dict[str, Any] = user_data.copy() if target_iam else None
|
|
243
257
|
users.pop(oauth_state)
|
|
244
258
|
code: str = args.get("code")
|
|
245
259
|
header_data: dict[str, str] = {
|
|
@@ -264,6 +278,33 @@ def action_callback(iam_server: IamServer,
|
|
|
264
278
|
now=now,
|
|
265
279
|
errors=errors,
|
|
266
280
|
logger=logger)
|
|
281
|
+
if target_iam:
|
|
282
|
+
if logger:
|
|
283
|
+
logger.debug(msg=f"Requesting to IAM server '{iam_server}' "
|
|
284
|
+
f"the token issued by '{target_iam}' ")
|
|
285
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server,
|
|
286
|
+
errors=errors,
|
|
287
|
+
logger=logger)
|
|
288
|
+
url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
289
|
+
url += f"/broker/{target_idp}/token"
|
|
290
|
+
header_data: dict[str, str] = {
|
|
291
|
+
"Authorization": f"Bearer {result[1]}",
|
|
292
|
+
"Content-Type": "application/json"
|
|
293
|
+
}
|
|
294
|
+
token_data = __get_for_data(url=url,
|
|
295
|
+
header_data=header_data,
|
|
296
|
+
params=None,
|
|
297
|
+
errors=errors,
|
|
298
|
+
logger=logger)
|
|
299
|
+
if not errors:
|
|
300
|
+
token_info: tuple[str, str] = __validate_and_store(iam_server=target_iam,
|
|
301
|
+
user_data=target_data,
|
|
302
|
+
token_data=token_data,
|
|
303
|
+
now=now,
|
|
304
|
+
errors=errors,
|
|
305
|
+
logger=logger)
|
|
306
|
+
if token_info and logger:
|
|
307
|
+
logger.debug(msg=f"Token obtained: {json.dumps(obj=token_info)}")
|
|
267
308
|
else:
|
|
268
309
|
msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
|
|
269
310
|
if logger:
|
|
@@ -277,7 +318,7 @@ def action_callback(iam_server: IamServer,
|
|
|
277
318
|
def action_exchange(iam_server: IamServer,
|
|
278
319
|
args: dict[str, Any],
|
|
279
320
|
errors: list[str] = None,
|
|
280
|
-
logger: Logger = None) ->
|
|
321
|
+
logger: Logger = None) -> tuple[str, str]:
|
|
281
322
|
"""
|
|
282
323
|
Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
|
|
283
324
|
|
|
@@ -298,17 +339,23 @@ def action_exchange(iam_server: IamServer,
|
|
|
298
339
|
:param args: the arguments passed when requesting the service
|
|
299
340
|
:param errors: incidental errors
|
|
300
341
|
:param logger: optional logger
|
|
301
|
-
:return: the
|
|
342
|
+
:return: a tuple containing the reference user identification and the token obtained, or *None* if error
|
|
302
343
|
"""
|
|
303
344
|
# initialize the return variable
|
|
304
|
-
result:
|
|
345
|
+
result: tuple[str, str] | None = None
|
|
305
346
|
|
|
306
347
|
# obtain the user's identification
|
|
307
348
|
user_id: str = args.get("user-id") or args.get("login")
|
|
308
349
|
|
|
309
350
|
# obtain the token to be exchanged
|
|
310
351
|
token: str = args.get("access-token") if user_id else None
|
|
311
|
-
|
|
352
|
+
token_claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
|
|
353
|
+
errors=errors,
|
|
354
|
+
logger=logger) if token else None
|
|
355
|
+
token_issuer: str = _iam_server_from_issuer(issuer=token_claims["payload"]["iss"],
|
|
356
|
+
errors=errors,
|
|
357
|
+
logger=logger) if token_claims else None
|
|
358
|
+
if not errors:
|
|
312
359
|
# HAZARD: only 'IAM_KEYCLOAK' is currently supported
|
|
313
360
|
with _iam_lock:
|
|
314
361
|
# retrieve the IAM server's registry
|
|
@@ -324,6 +371,8 @@ def action_exchange(iam_server: IamServer,
|
|
|
324
371
|
logger=logger)
|
|
325
372
|
if not errors:
|
|
326
373
|
# exchange the token
|
|
374
|
+
if logger:
|
|
375
|
+
logger.debug(msg=f"Requesting the token exchange to IAM server '{iam_server}'")
|
|
327
376
|
header_data: dict[str, Any] = {
|
|
328
377
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
329
378
|
}
|
|
@@ -333,7 +382,7 @@ def action_exchange(iam_server: IamServer,
|
|
|
333
382
|
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
334
383
|
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
335
384
|
"audience": registry[IamParam.CLIENT_ID],
|
|
336
|
-
"subject_issuer":
|
|
385
|
+
"subject_issuer": token_issuer
|
|
337
386
|
}
|
|
338
387
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
339
388
|
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
@@ -443,13 +492,13 @@ def __assert_link(iam_server: IamServer,
|
|
|
443
492
|
logger.debug(msg="Creating an association between identifications "
|
|
444
493
|
f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
|
|
445
494
|
url += f"/{provider_name}"
|
|
446
|
-
|
|
495
|
+
json_data: dict[str, Any] = {
|
|
447
496
|
"userId": token_sub,
|
|
448
497
|
"userName": user_id
|
|
449
498
|
}
|
|
450
|
-
|
|
499
|
+
__post_json(url=url,
|
|
451
500
|
header_data=header_data,
|
|
452
|
-
|
|
501
|
+
json_data=json_data,
|
|
453
502
|
errors=errors,
|
|
454
503
|
logger=logger)
|
|
455
504
|
|
|
@@ -638,27 +687,27 @@ def __get_for_data(url: str,
|
|
|
638
687
|
return result
|
|
639
688
|
|
|
640
689
|
|
|
641
|
-
def
|
|
690
|
+
def __post_json(url: str,
|
|
642
691
|
header_data: dict[str, str],
|
|
643
|
-
|
|
692
|
+
json_data: dict[str, Any],
|
|
644
693
|
errors: list[str] | None,
|
|
645
694
|
logger: Logger | None) -> None:
|
|
646
695
|
"""
|
|
647
696
|
Submit a *POST* request to *url*.
|
|
648
697
|
|
|
649
698
|
:param header_data: the data to send in the header of the request
|
|
650
|
-
:param
|
|
699
|
+
:param json_data: the JSON data to send in the request
|
|
651
700
|
:param errors: incidental errors
|
|
652
701
|
:param logger: optional logger
|
|
653
702
|
"""
|
|
654
703
|
# log the POST
|
|
655
704
|
if logger:
|
|
656
|
-
logger.debug(msg=f"POST {url}, {json.dumps(obj=
|
|
705
|
+
logger.debug(msg=f"POST {url}, {json.dumps(obj=json_data,
|
|
657
706
|
ensure_ascii=False)}")
|
|
658
707
|
try:
|
|
659
|
-
response: requests.Response = requests.
|
|
660
|
-
|
|
661
|
-
|
|
708
|
+
response: requests.Response = requests.post(url=url,
|
|
709
|
+
headers=header_data,
|
|
710
|
+
json=json_data)
|
|
662
711
|
if response.status_code >= 400:
|
|
663
712
|
# request failed, report the problem
|
|
664
713
|
msg = f"POST failure, status {response.status_code}, reason {response.reason}"
|
|
@@ -831,6 +880,8 @@ def __validate_and_store(iam_server: IamServer,
|
|
|
831
880
|
# initialize the return variable
|
|
832
881
|
result: tuple[str, str] | None = None
|
|
833
882
|
|
|
883
|
+
if logger:
|
|
884
|
+
logger.debug(msg=f"Validating and storing the token")
|
|
834
885
|
with _iam_lock:
|
|
835
886
|
# retrieve the IAM server's registry
|
|
836
887
|
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
@@ -120,6 +120,7 @@ def service_login() -> Response:
|
|
|
120
120
|
These are the expected request parameters:
|
|
121
121
|
- user-id: optional, identifies the reference user (alias: 'login')
|
|
122
122
|
- redirect-uri: a parameter to be added to the query part of the returned URL
|
|
123
|
+
-target-idp: optionally, identify a target identity provider for the login operation
|
|
123
124
|
|
|
124
125
|
If provided, the user identification will be validated against the authorization data
|
|
125
126
|
returned by *iam_server* upon login. On success, the following JSON, containing the appropriate
|
|
@@ -338,13 +339,10 @@ def service_exchange() -> Response:
|
|
|
338
339
|
If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
339
340
|
Otherwise, *errors* will contain the appropriate error message.
|
|
340
341
|
|
|
341
|
-
On success, the
|
|
342
|
+
On success, the returned *Response* will contain the following JSON:
|
|
342
343
|
{
|
|
343
|
-
"
|
|
344
|
-
"
|
|
345
|
-
"expires_in": <number-of-seconds>,
|
|
346
|
-
"refresh_token": <str>,
|
|
347
|
-
"refesh_expires_in": <number-of-seconds>
|
|
344
|
+
"user-id": <reference-user-identification>,
|
|
345
|
+
"access-token": <token>
|
|
348
346
|
}
|
|
349
347
|
|
|
350
348
|
:return: *Response* containing the token data, or *BAD REQUEST*
|
|
@@ -360,10 +358,10 @@ def service_exchange() -> Response:
|
|
|
360
358
|
errors=errors,
|
|
361
359
|
logger=__IAM_LOGGER)
|
|
362
360
|
# exchange the token
|
|
363
|
-
|
|
361
|
+
token_info: tuple[str, str] | None = None
|
|
364
362
|
if iam_server:
|
|
365
363
|
errors: list[str] = []
|
|
366
|
-
|
|
364
|
+
token_info = action_exchange(iam_server=iam_server,
|
|
367
365
|
args=request.args,
|
|
368
366
|
errors=errors,
|
|
369
367
|
logger=__IAM_LOGGER)
|
|
@@ -372,7 +370,8 @@ def service_exchange() -> Response:
|
|
|
372
370
|
result = Response(response="; ".join(errors),
|
|
373
371
|
status=400)
|
|
374
372
|
else:
|
|
375
|
-
result = jsonify(
|
|
373
|
+
result = jsonify({"user-id": token_info[0],
|
|
374
|
+
"access-token": token_info[1]})
|
|
376
375
|
|
|
377
376
|
# log the response
|
|
378
377
|
if __IAM_LOGGER:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|