pypomes-iam 0.6.0__py3-none-any.whl → 0.7.4__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_actions.py +146 -89
- pypomes_iam/iam_common.py +8 -7
- pypomes_iam/iam_pomes.py +1 -1
- pypomes_iam/iam_services.py +9 -11
- pypomes_iam/provider_pomes.py +125 -64
- {pypomes_iam-0.6.0.dist-info → pypomes_iam-0.7.4.dist-info}/METADATA +1 -2
- pypomes_iam-0.7.4.dist-info/RECORD +11 -0
- pypomes_iam-0.6.0.dist-info/RECORD +0 -11
- {pypomes_iam-0.6.0.dist-info → pypomes_iam-0.7.4.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.6.0.dist-info → pypomes_iam-0.7.4.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_actions.py
CHANGED
|
@@ -10,8 +10,8 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
from .iam_common import (
|
|
12
12
|
IamServer, IamParam, UserParam, _iam_lock,
|
|
13
|
-
_get_iam_users, _get_iam_registry,
|
|
14
|
-
_get_login_timeout, _get_user_data
|
|
13
|
+
_get_iam_users, _get_iam_registry, _get_public_key,
|
|
14
|
+
_get_login_timeout, _get_user_data, _iam_server_from_issuer
|
|
15
15
|
)
|
|
16
16
|
from .token_pomes import token_get_claims, token_validate
|
|
17
17
|
|
|
@@ -59,7 +59,7 @@ def action_login(iam_server: IamServer,
|
|
|
59
59
|
errors=errors,
|
|
60
60
|
logger=logger)
|
|
61
61
|
if not errors:
|
|
62
|
-
user_data[UserParam.LOGIN_EXPIRATION] = int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout \
|
|
62
|
+
user_data[UserParam.LOGIN_EXPIRATION] = (int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout) \
|
|
63
63
|
if timeout else None
|
|
64
64
|
redirect_uri: str = args.get(UserParam.REDIRECT_URI)
|
|
65
65
|
user_data[UserParam.REDIRECT_URI] = redirect_uri
|
|
@@ -138,6 +138,7 @@ def action_token(iam_server: IamServer,
|
|
|
138
138
|
user_id=user_id,
|
|
139
139
|
errors=errors,
|
|
140
140
|
logger=logger)
|
|
141
|
+
# retrieve the stored access token
|
|
141
142
|
token: str = user_data[UserParam.ACCESS_TOKEN] if user_data else None
|
|
142
143
|
if token:
|
|
143
144
|
access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
|
|
@@ -148,7 +149,7 @@ def action_token(iam_server: IamServer,
|
|
|
148
149
|
# access token has expired
|
|
149
150
|
refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
|
|
150
151
|
if refresh_token:
|
|
151
|
-
refresh_expiration = user_data[UserParam.REFRESH_EXPIRATION]
|
|
152
|
+
refresh_expiration: int = user_data[UserParam.REFRESH_EXPIRATION]
|
|
152
153
|
if now < refresh_expiration:
|
|
153
154
|
header_data: dict[str, str] = {
|
|
154
155
|
"Content-Type": "application/json"
|
|
@@ -242,7 +243,7 @@ def action_callback(iam_server: IamServer,
|
|
|
242
243
|
users.pop(oauth_state)
|
|
243
244
|
code: str = args.get("code")
|
|
244
245
|
header_data: dict[str, str] = {
|
|
245
|
-
"Content-Type": "application/
|
|
246
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
246
247
|
}
|
|
247
248
|
body_data: dict[str, Any] = {
|
|
248
249
|
"grant_type": "authorization_code",
|
|
@@ -276,7 +277,7 @@ def action_callback(iam_server: IamServer,
|
|
|
276
277
|
def action_exchange(iam_server: IamServer,
|
|
277
278
|
args: dict[str, Any],
|
|
278
279
|
errors: list[str] = None,
|
|
279
|
-
logger: Logger = None) ->
|
|
280
|
+
logger: Logger = None) -> tuple[str, str]:
|
|
280
281
|
"""
|
|
281
282
|
Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
|
|
282
283
|
|
|
@@ -297,17 +298,23 @@ def action_exchange(iam_server: IamServer,
|
|
|
297
298
|
:param args: the arguments passed when requesting the service
|
|
298
299
|
:param errors: incidental errors
|
|
299
300
|
:param logger: optional logger
|
|
300
|
-
:return: the
|
|
301
|
+
:return: a tuple containing the reference user identification and the token obtained, or *None* if error
|
|
301
302
|
"""
|
|
302
303
|
# initialize the return variable
|
|
303
|
-
result:
|
|
304
|
+
result: tuple[str, str] | None = None
|
|
304
305
|
|
|
305
306
|
# obtain the user's identification
|
|
306
307
|
user_id: str = args.get("user-id") or args.get("login")
|
|
307
308
|
|
|
308
309
|
# obtain the token to be exchanged
|
|
309
310
|
token: str = args.get("access-token") if user_id else None
|
|
310
|
-
|
|
311
|
+
token_claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
|
|
312
|
+
errors=errors,
|
|
313
|
+
logger=logger) if token else None
|
|
314
|
+
token_issuer: str = _iam_server_from_issuer(issuer=token_claims["payload"]["iss"],
|
|
315
|
+
errors=errors,
|
|
316
|
+
logger=logger) if token_claims else None
|
|
317
|
+
if not errors:
|
|
311
318
|
# HAZARD: only 'IAM_KEYCLOAK' is currently supported
|
|
312
319
|
with _iam_lock:
|
|
313
320
|
# retrieve the IAM server's registry
|
|
@@ -323,6 +330,8 @@ def action_exchange(iam_server: IamServer,
|
|
|
323
330
|
logger=logger)
|
|
324
331
|
if not errors:
|
|
325
332
|
# exchange the token
|
|
333
|
+
if logger:
|
|
334
|
+
logger.debug(msg=f"Requesting the token exchange to IAM server '{iam_server}'")
|
|
326
335
|
header_data: dict[str, Any] = {
|
|
327
336
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
328
337
|
}
|
|
@@ -332,7 +341,7 @@ def action_exchange(iam_server: IamServer,
|
|
|
332
341
|
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
333
342
|
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
334
343
|
"audience": registry[IamParam.CLIENT_ID],
|
|
335
|
-
"subject_issuer":
|
|
344
|
+
"subject_issuer": token_issuer
|
|
336
345
|
}
|
|
337
346
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
338
347
|
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
@@ -367,7 +376,7 @@ def __assert_link(iam_server: IamServer,
|
|
|
367
376
|
"""
|
|
368
377
|
Make sure *iam_server* has a link associating *user_id* to an internal user identification.
|
|
369
378
|
This is a requirement for exchanging a token issued by a federated *IAM* server for an equivalent
|
|
370
|
-
one from *iam_server
|
|
379
|
+
one from *iam_server*.
|
|
371
380
|
|
|
372
381
|
:param iam_server: the reference *IAM* server
|
|
373
382
|
:param user_id: the reference user identification
|
|
@@ -375,6 +384,9 @@ def __assert_link(iam_server: IamServer,
|
|
|
375
384
|
:param errors: incidental errors
|
|
376
385
|
:param logger: optional logger
|
|
377
386
|
"""
|
|
387
|
+
if logger:
|
|
388
|
+
logger.debug(msg="Verifying associations for user "
|
|
389
|
+
f"'{user_id}' in IAM server '{iam_server}'")
|
|
378
390
|
# obtain a token with administrative rights
|
|
379
391
|
admin_token: str = __get_administrative_token(iam_server=iam_server,
|
|
380
392
|
errors=errors,
|
|
@@ -383,7 +395,10 @@ def __assert_link(iam_server: IamServer,
|
|
|
383
395
|
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
384
396
|
errors=errors,
|
|
385
397
|
logger=logger)
|
|
386
|
-
# obtain the internal user identification for '
|
|
398
|
+
# obtain the internal user identification for 'user_id'
|
|
399
|
+
if logger:
|
|
400
|
+
logger.debug(msg="Obtaining internal identification "
|
|
401
|
+
f"for user {user_id} in IAM server {iam_server}")
|
|
387
402
|
url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
|
|
388
403
|
header_data: dict[str, str] = {
|
|
389
404
|
"Authorization": f"Bearer {admin_token}",
|
|
@@ -393,15 +408,18 @@ def __assert_link(iam_server: IamServer,
|
|
|
393
408
|
"username": user_id,
|
|
394
409
|
"exact": "true"
|
|
395
410
|
}
|
|
396
|
-
users: dict[str, Any] = __get_for_data(url=url,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
411
|
+
users: list[dict[str, Any]] = __get_for_data(url=url,
|
|
412
|
+
header_data=header_data,
|
|
413
|
+
params=params,
|
|
414
|
+
errors=errors,
|
|
415
|
+
logger=logger)
|
|
401
416
|
if users:
|
|
402
417
|
# verify whether the 'oidc' protocol is referred to in an
|
|
403
|
-
# association between '
|
|
404
|
-
internal_id: str = users.get("id")
|
|
418
|
+
# association between 'user_id' and the internal user identification
|
|
419
|
+
internal_id: str = users[0].get("id")
|
|
420
|
+
if logger:
|
|
421
|
+
logger.debug(msg="Obtaining the providers federated with "
|
|
422
|
+
f"IAM server '{iam_server}' for internal identification '{internal_id}'")
|
|
405
423
|
url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
|
|
406
424
|
f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
|
|
407
425
|
providers: list[dict[str, Any]] = __get_for_data(url=url,
|
|
@@ -410,27 +428,38 @@ def __assert_link(iam_server: IamServer,
|
|
|
410
428
|
errors=errors,
|
|
411
429
|
logger=logger)
|
|
412
430
|
no_link: bool = True
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
431
|
+
claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
|
|
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,
|
|
436
|
+
errors=errors,
|
|
437
|
+
logger=logger) if issuer else None
|
|
438
|
+
if provider_name:
|
|
439
|
+
for provider in providers:
|
|
440
|
+
if provider.get("identityProvider") == provider_name:
|
|
441
|
+
no_link = False
|
|
442
|
+
break
|
|
443
|
+
if no_link:
|
|
444
|
+
# link the identities
|
|
445
|
+
claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
|
|
446
|
+
errors=errors,
|
|
447
|
+
logger=logger)
|
|
448
|
+
if claims:
|
|
449
|
+
token_sub: str = claims["payload"]["sub"]
|
|
450
|
+
if logger:
|
|
451
|
+
logger.debug(msg="Creating an association between identifications "
|
|
452
|
+
f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
|
|
453
|
+
url += f"/{provider_name}"
|
|
454
|
+
json_data: dict[str, Any] = {
|
|
455
|
+
"userId": token_sub,
|
|
456
|
+
"userName": user_id
|
|
457
|
+
}
|
|
458
|
+
__post_json(url=url,
|
|
459
|
+
header_data=header_data,
|
|
460
|
+
json_data=json_data,
|
|
461
|
+
errors=errors,
|
|
462
|
+
logger=logger)
|
|
434
463
|
|
|
435
464
|
|
|
436
465
|
def __get_administrative_token(iam_server: IamServer,
|
|
@@ -450,31 +479,47 @@ def __get_administrative_token(iam_server: IamServer,
|
|
|
450
479
|
# initialize the return variable
|
|
451
480
|
result: str | None = None
|
|
452
481
|
|
|
482
|
+
if logger:
|
|
483
|
+
logger.debug(msg="Requesting a token with "
|
|
484
|
+
f"administrative rights to IAM Server '{iam_server}'")
|
|
485
|
+
|
|
453
486
|
# obtain the IAM server's registry
|
|
454
487
|
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
455
488
|
errors=errors,
|
|
456
489
|
logger=logger)
|
|
457
|
-
if registry
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
490
|
+
if registry:
|
|
491
|
+
if registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
|
|
492
|
+
header_data: dict[str, str] = {
|
|
493
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
494
|
+
}
|
|
495
|
+
body_data: dict[str, str] = {
|
|
496
|
+
"grant_type": "password",
|
|
497
|
+
"username": registry[IamParam.ADMIN_ID],
|
|
498
|
+
"password": registry[IamParam.ADMIN_SECRET],
|
|
499
|
+
"client_id": "admin-cli"
|
|
500
|
+
}
|
|
501
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
502
|
+
header_data=header_data,
|
|
503
|
+
body_data=body_data,
|
|
504
|
+
errors=errors,
|
|
505
|
+
logger=logger)
|
|
506
|
+
if token_data:
|
|
507
|
+
# obtain the token
|
|
508
|
+
result = token_data["access_token"]
|
|
509
|
+
if logger:
|
|
510
|
+
logger.debug(msg="Administrative token obtained")
|
|
511
|
+
|
|
512
|
+
elif logger or isinstance(errors, list):
|
|
513
|
+
msg: str = ("Credentials for administrator of realm "
|
|
514
|
+
f"'{registry[IamParam.CLIENT_REALM]}' "
|
|
515
|
+
f"at IAM server '{iam_server}' not provided")
|
|
516
|
+
if logger:
|
|
517
|
+
logger.error(msg=msg)
|
|
518
|
+
if isinstance(errors, list):
|
|
519
|
+
errors.append(msg)
|
|
520
|
+
|
|
521
|
+
elif logger or isinstance(errors, list):
|
|
522
|
+
msg: str = f"Unknown IAM server {iam_server}"
|
|
478
523
|
if logger:
|
|
479
524
|
logger.error(msg=msg)
|
|
480
525
|
if isinstance(errors, list):
|
|
@@ -510,14 +555,19 @@ def __get_client_secret(iam_server: IamServer,
|
|
|
510
555
|
errors=errors,
|
|
511
556
|
logger=logger)
|
|
512
557
|
if token:
|
|
558
|
+
realm: str = registry[IamParam.CLIENT_REALM]
|
|
559
|
+
client_id: str = registry[IamParam.CLIENT_ID]
|
|
560
|
+
if logger:
|
|
561
|
+
logger.debug(msg=f"Obtaining the UUID for client '{client_id}', "
|
|
562
|
+
f"in realm '{realm}' at IAM server '{iam_server}'")
|
|
513
563
|
# obtain the client UUID
|
|
514
|
-
url: str = f"{registry[IamParam.URL_BASE]}/realms/{
|
|
564
|
+
url: str = f"{registry[IamParam.URL_BASE]}/realms/{realm}/clients"
|
|
515
565
|
header_data: dict[str, str] = {
|
|
516
566
|
"Authorization": f"Bearer {token}",
|
|
517
567
|
"Content-Type": "application/json"
|
|
518
568
|
}
|
|
519
569
|
params: dict[str, str] = {
|
|
520
|
-
"clientId":
|
|
570
|
+
"clientId": client_id
|
|
521
571
|
}
|
|
522
572
|
clients: list[dict[str, Any]] = __get_for_data(url=url,
|
|
523
573
|
header_data=header_data,
|
|
@@ -527,6 +577,9 @@ def __get_client_secret(iam_server: IamServer,
|
|
|
527
577
|
if clients:
|
|
528
578
|
# obtain the client's secret password
|
|
529
579
|
client_uuid: str = clients[0]["id"]
|
|
580
|
+
if logger:
|
|
581
|
+
logger.debug(msg=f"Obtaining the secret for client UUID '{client_uuid}', "
|
|
582
|
+
f"in realm '{realm}' at IAM server '{iam_server}'")
|
|
530
583
|
url += f"/{client_uuid}/client-secret"
|
|
531
584
|
reply: dict[str, Any] = __get_for_data(url=url,
|
|
532
585
|
header_data=header_data,
|
|
@@ -573,7 +626,7 @@ def __get_for_data(url: str,
|
|
|
573
626
|
logger.debug(msg=f"GET success, {json.dumps(obj=result,
|
|
574
627
|
ensure_ascii=False)}")
|
|
575
628
|
else:
|
|
576
|
-
# request
|
|
629
|
+
# request failed, report the problem
|
|
577
630
|
msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
|
|
578
631
|
if hasattr(response, "content") and response.content:
|
|
579
632
|
msg += f", content '{response.content}'"
|
|
@@ -593,29 +646,29 @@ def __get_for_data(url: str,
|
|
|
593
646
|
return result
|
|
594
647
|
|
|
595
648
|
|
|
596
|
-
def
|
|
649
|
+
def __post_json(url: str,
|
|
597
650
|
header_data: dict[str, str],
|
|
598
|
-
|
|
651
|
+
json_data: dict[str, Any],
|
|
599
652
|
errors: list[str] | None,
|
|
600
653
|
logger: Logger | None) -> None:
|
|
601
654
|
"""
|
|
602
655
|
Submit a *POST* request to *url*.
|
|
603
656
|
|
|
604
657
|
:param header_data: the data to send in the header of the request
|
|
605
|
-
:param
|
|
658
|
+
:param json_data: the JSON data to send in the request
|
|
606
659
|
:param errors: incidental errors
|
|
607
660
|
:param logger: optional logger
|
|
608
661
|
"""
|
|
609
662
|
# log the POST
|
|
610
663
|
if logger:
|
|
611
|
-
logger.debug(msg=f"POST {url}, {json.dumps(obj=
|
|
664
|
+
logger.debug(msg=f"POST {url}, {json.dumps(obj=json_data,
|
|
612
665
|
ensure_ascii=False)}")
|
|
613
666
|
try:
|
|
614
|
-
response: requests.Response = requests.
|
|
615
|
-
|
|
616
|
-
|
|
667
|
+
response: requests.Response = requests.post(url=url,
|
|
668
|
+
headers=header_data,
|
|
669
|
+
json=json_data)
|
|
617
670
|
if response.status_code >= 400:
|
|
618
|
-
# request
|
|
671
|
+
# request failed, report the problem
|
|
619
672
|
msg = f"POST failure, status {response.status_code}, reason {response.reason}"
|
|
620
673
|
if hasattr(response, "content") and response.content:
|
|
621
674
|
msg += f", content '{response.content}'"
|
|
@@ -665,9 +718,9 @@ def __post_for_token(iam_server: IamServer,
|
|
|
665
718
|
- "username": <realm-administrator-identification>
|
|
666
719
|
- "password": <realm-administrator-secret>
|
|
667
720
|
|
|
668
|
-
These attributes are then added to *body_data
|
|
721
|
+
These attributes are then added to *body_data*, except for acquiring administrative tokens:
|
|
669
722
|
- "client_id": <client-id>
|
|
670
|
-
- "client_secret": <client-secret>
|
|
723
|
+
- "client_secret": <client-secret>
|
|
671
724
|
|
|
672
725
|
If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
673
726
|
Otherwise, *errors* will contain the appropriate error message.
|
|
@@ -699,21 +752,25 @@ def __post_for_token(iam_server: IamServer,
|
|
|
699
752
|
logger=logger)
|
|
700
753
|
if registry:
|
|
701
754
|
# complete the data to send in body of request
|
|
702
|
-
body_data["
|
|
755
|
+
if body_data["grant_type"] != "password":
|
|
756
|
+
body_data["client_id"] = registry[IamParam.CLIENT_ID]
|
|
703
757
|
|
|
704
758
|
# build the URL
|
|
705
759
|
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
706
760
|
url: str = f"{base_url}/protocol/openid-connect/token"
|
|
707
|
-
|
|
708
|
-
|
|
761
|
+
# 'client_secret' data must not be shown in log
|
|
762
|
+
msg: str = f"POST {url}, {json.dumps(obj=body_data,
|
|
763
|
+
ensure_ascii=False)}"
|
|
764
|
+
if body_data["grant_type"] != "password":
|
|
765
|
+
# 'client_secret' not required for requesting tokens from staging environments
|
|
766
|
+
client_secret: str = __get_client_secret(iam_server=iam_server,
|
|
767
|
+
errors=None,
|
|
768
|
+
logger=logger)
|
|
769
|
+
if client_secret:
|
|
770
|
+
body_data["client_secret"] = client_secret
|
|
771
|
+
# log the POST
|
|
709
772
|
if logger:
|
|
710
|
-
logger.debug(msg=
|
|
711
|
-
ensure_ascii=False)}")
|
|
712
|
-
client_secret: str = __get_client_secret(iam_server=iam_server,
|
|
713
|
-
errors=errors,
|
|
714
|
-
logger=logger)
|
|
715
|
-
if body_data["grant_type"] != "password" and client_secret:
|
|
716
|
-
body_data["client_secret"] = client_secret
|
|
773
|
+
logger.debug(msg=msg)
|
|
717
774
|
|
|
718
775
|
# obtain the token
|
|
719
776
|
try:
|
|
@@ -735,7 +792,7 @@ def __post_for_token(iam_server: IamServer,
|
|
|
735
792
|
logger.debug(msg=f"POST success, {json.dumps(obj=result,
|
|
736
793
|
ensure_ascii=False)}")
|
|
737
794
|
else:
|
|
738
|
-
# request
|
|
795
|
+
# request failed, report the problem
|
|
739
796
|
err_msg = f"POST failure, status {response.status_code}, reason {response.reason}"
|
|
740
797
|
if hasattr(response, "content") and response.content:
|
|
741
798
|
err_msg += f", content '{response.content}'"
|
|
@@ -796,9 +853,9 @@ def __validate_and_store(iam_server: IamServer,
|
|
|
796
853
|
user_data["access-expiration"] = now + token_data.get("expires_in")
|
|
797
854
|
refresh_exp: int = user_data.get("refresh_expires_in")
|
|
798
855
|
user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
856
|
+
public_key: str = _get_public_key(iam_server=iam_server,
|
|
857
|
+
errors=errors,
|
|
858
|
+
logger=logger)
|
|
802
859
|
recipient_attr = registry[IamParam.RECIPIENT_ATTR]
|
|
803
860
|
login_id = user_data.pop("login-id", None)
|
|
804
861
|
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
@@ -806,7 +863,7 @@ def __validate_and_store(iam_server: IamServer,
|
|
|
806
863
|
issuer=base_url,
|
|
807
864
|
recipient_id=login_id,
|
|
808
865
|
recipient_attr=recipient_attr,
|
|
809
|
-
|
|
866
|
+
public_key=public_key,
|
|
810
867
|
errors=errors,
|
|
811
868
|
logger=logger)
|
|
812
869
|
if claims:
|
pypomes_iam/iam_common.py
CHANGED
|
@@ -58,16 +58,17 @@ class UserParam(StrEnum):
|
|
|
58
58
|
# Specifying configuration parameters with environment variables can be done in two ways:
|
|
59
59
|
#
|
|
60
60
|
# 1. for a single *IAM* server, specify the data set
|
|
61
|
-
# - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed
|
|
62
|
-
# - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed
|
|
61
|
+
# - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed if administrative duties are performed)
|
|
62
|
+
# - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed if administrative duties are performed)
|
|
63
63
|
# - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
|
|
64
64
|
# - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
|
|
65
65
|
# - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
|
|
66
|
-
# - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (
|
|
67
|
-
# - *<APP_PREFIX>
|
|
68
|
-
# - *<APP_PREFIX>
|
|
69
|
-
# - *<APP_PREFIX>
|
|
70
|
-
# - *<APP_PREFIX>
|
|
66
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (required)
|
|
67
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (required)
|
|
68
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (required)
|
|
69
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (required)
|
|
70
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_PROVIDER* (optional, needed if requesting tokens to providers)
|
|
71
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (required)
|
|
71
72
|
# - *<APP_PREFIX>_IAM_LOGIN_TIMEOUT* (optional, defaults to no timeout)
|
|
72
73
|
# - *<APP_PREFIX>_IAM_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
|
|
73
74
|
# - *<APP_PREFIX>_IAM_RECIPIENT_ATTR* (required)
|
pypomes_iam/iam_pomes.py
CHANGED
|
@@ -40,7 +40,7 @@ def iam_setup(flask_app: Flask,
|
|
|
40
40
|
the first time it is needed.
|
|
41
41
|
|
|
42
42
|
:param flask_app: the Flask application
|
|
43
|
-
:param iam_server: identifies the supported *IAM* server (*jusbr* or *keycloak*)
|
|
43
|
+
:param iam_server: identifies the supported *IAM* server (currently, *jusbr* or *keycloak*)
|
|
44
44
|
:param base_url: base URL to request services
|
|
45
45
|
:param client_id: the client's identification with the *IAM* server
|
|
46
46
|
:param client_realm: the client realm
|
pypomes_iam/iam_services.py
CHANGED
|
@@ -168,7 +168,7 @@ def service_login() -> Response:
|
|
|
168
168
|
# methods=["GET"])
|
|
169
169
|
def service_logout() -> Response:
|
|
170
170
|
"""
|
|
171
|
-
Entry point for the
|
|
171
|
+
Entry point for the IAM server's logout service.
|
|
172
172
|
|
|
173
173
|
The user is identified by the attribute *user-id* or "login", provided as a request parameter.
|
|
174
174
|
If successful, remove all data relating to the user from the *IAM* server's registry.
|
|
@@ -214,7 +214,7 @@ def service_logout() -> Response:
|
|
|
214
214
|
# methods=["POST"])
|
|
215
215
|
def service_callback() -> Response:
|
|
216
216
|
"""
|
|
217
|
-
Entry point for the callback from
|
|
217
|
+
Entry point for the callback from the IAM server on authentication operation.
|
|
218
218
|
|
|
219
219
|
This callback is invoked from a front-end application after a successful login at the
|
|
220
220
|
*IAM* server's login page, forwarding the data received. In a typical OAuth2 flow faction,
|
|
@@ -338,13 +338,10 @@ def service_exchange() -> Response:
|
|
|
338
338
|
If the exchange is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
339
339
|
Otherwise, *errors* will contain the appropriate error message.
|
|
340
340
|
|
|
341
|
-
On success, the
|
|
341
|
+
On success, the returned *Response* will contain the following JSON:
|
|
342
342
|
{
|
|
343
|
-
"
|
|
344
|
-
"
|
|
345
|
-
"expires_in": <number-of-seconds>,
|
|
346
|
-
"refresh_token": <str>,
|
|
347
|
-
"refesh_expires_in": <number-of-seconds>
|
|
343
|
+
"user-id": <reference-user-identification>,
|
|
344
|
+
"access-token": <token>
|
|
348
345
|
}
|
|
349
346
|
|
|
350
347
|
:return: *Response* containing the token data, or *BAD REQUEST*
|
|
@@ -360,10 +357,10 @@ def service_exchange() -> Response:
|
|
|
360
357
|
errors=errors,
|
|
361
358
|
logger=__IAM_LOGGER)
|
|
362
359
|
# exchange the token
|
|
363
|
-
|
|
360
|
+
token_info: tuple[str, str] | None = None
|
|
364
361
|
if iam_server:
|
|
365
362
|
errors: list[str] = []
|
|
366
|
-
|
|
363
|
+
token_info = action_exchange(iam_server=iam_server,
|
|
367
364
|
args=request.args,
|
|
368
365
|
errors=errors,
|
|
369
366
|
logger=__IAM_LOGGER)
|
|
@@ -372,7 +369,8 @@ def service_exchange() -> Response:
|
|
|
372
369
|
result = Response(response="; ".join(errors),
|
|
373
370
|
status=400)
|
|
374
371
|
else:
|
|
375
|
-
result = jsonify(
|
|
372
|
+
result = jsonify({"user-id": token_info[0],
|
|
373
|
+
"access-token": token_info[1]})
|
|
376
374
|
|
|
377
375
|
# log the response
|
|
378
376
|
if __IAM_LOGGER:
|
pypomes_iam/provider_pomes.py
CHANGED
|
@@ -106,76 +106,137 @@ def provider_get_token(provider_id: str,
|
|
|
106
106
|
# initialize the return variable
|
|
107
107
|
result: str | None = None
|
|
108
108
|
|
|
109
|
-
err_msg: str | None = None
|
|
110
109
|
with _provider_lock:
|
|
111
110
|
provider: dict[str, Any] = _provider_registry.get(provider_id)
|
|
112
111
|
if provider:
|
|
113
|
-
now:
|
|
114
|
-
if now
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
body_data[custom_auth[0]] = user
|
|
122
|
-
body_data[custom_auth[1]] = pwd
|
|
123
|
-
else:
|
|
124
|
-
enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
|
|
125
|
-
headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
|
|
112
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
113
|
+
if now < provider.get(ProviderParam.ACCESS_EXPIRATION):
|
|
114
|
+
# retrieve the stored access token
|
|
115
|
+
result = provider.get(ProviderParam.ACCESS_TOKEN)
|
|
116
|
+
else:
|
|
117
|
+
# access token has expired
|
|
118
|
+
header_data: dict[str, str] | None = None
|
|
119
|
+
body_data: dict[str, str] | None = None
|
|
126
120
|
url: str = provider.get(ProviderParam.URL)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
121
|
+
refresh_token: str = provider.get(ProviderParam.REFRESH_TOKEN)
|
|
122
|
+
if refresh_token:
|
|
123
|
+
# refresh token exists
|
|
124
|
+
refresh_expiration: int = provider.get(ProviderParam.REFRESH_EXPIRATION)
|
|
125
|
+
if now < refresh_expiration:
|
|
126
|
+
# refresh token has not expired
|
|
127
|
+
header_data: dict[str, str] = {
|
|
128
|
+
"Content-Type": "application/json"
|
|
129
|
+
}
|
|
130
|
+
body_data: dict[str, str] = {
|
|
131
|
+
"grant_type": "refresh_token",
|
|
132
|
+
"refresh_token": refresh_token
|
|
133
|
+
}
|
|
134
|
+
if not body_data:
|
|
135
|
+
# refresh token does not exist or has expired
|
|
136
|
+
user: str = provider.get(ProviderParam.USER)
|
|
137
|
+
pwd: str = provider.get(ProviderParam.PWD)
|
|
138
|
+
headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
|
|
139
|
+
body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
|
|
140
|
+
custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
|
|
141
|
+
if custom_auth:
|
|
142
|
+
body_data[custom_auth[0]] = user
|
|
143
|
+
body_data[custom_auth[1]] = pwd
|
|
148
144
|
else:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
errors
|
|
174
|
-
|
|
175
|
-
logger.error(msg=err_msg)
|
|
176
|
-
else:
|
|
177
|
-
result = provider.get(ProviderParam.ACCESS_TOKEN)
|
|
145
|
+
enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
|
|
146
|
+
headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
|
|
147
|
+
|
|
148
|
+
# obtain the token
|
|
149
|
+
token_data: dict[str, Any] = __post_for_token(url=url,
|
|
150
|
+
header_data=header_data,
|
|
151
|
+
body_data=body_data,
|
|
152
|
+
errors=errors,
|
|
153
|
+
logger=logger)
|
|
154
|
+
if token_data:
|
|
155
|
+
result = token_data.get("access_token")
|
|
156
|
+
provider[ProviderParam.ACCESS_TOKEN] = result
|
|
157
|
+
provider[ProviderParam.ACCESS_EXPIRATION] = now + token_data.get("expires_in")
|
|
158
|
+
refresh_token = token_data.get("refresh_token")
|
|
159
|
+
if refresh_token:
|
|
160
|
+
provider[ProviderParam.REFRESH_TOKEN] = refresh_token
|
|
161
|
+
refresh_exp: int = token_data.get("refresh_expires_in")
|
|
162
|
+
provider[ProviderParam.REFRESH_EXPIRATION] = (now + refresh_exp) \
|
|
163
|
+
if refresh_exp else sys.maxsize
|
|
164
|
+
|
|
165
|
+
elif logger or isinstance(errors, list):
|
|
166
|
+
msg: str = f"Unknown provider '{provider_id}'"
|
|
167
|
+
if logger:
|
|
168
|
+
logger.error(msg=msg)
|
|
169
|
+
if isinstance(errors, list):
|
|
170
|
+
errors.append(msg)
|
|
178
171
|
|
|
179
172
|
return result
|
|
180
173
|
|
|
181
174
|
|
|
175
|
+
def __post_for_token(url: str,
|
|
176
|
+
header_data: dict[str, str],
|
|
177
|
+
body_data: dict[str, Any],
|
|
178
|
+
errors: list[str] | None,
|
|
179
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
180
|
+
"""
|
|
181
|
+
Send a *POST* request to *url* and return the token data obtained.
|
|
182
|
+
|
|
183
|
+
Token acquisition and token refresh are the two types of requests contemplated herein.
|
|
184
|
+
For the former, *header_data* and *body_data* will have contents customized to the specific provider,
|
|
185
|
+
whereas the latter's *body_data* will contain these two attributes:
|
|
186
|
+
- "grant_type": "refresh_token"
|
|
187
|
+
- "refresh_token": <current-refresh-token>
|
|
188
|
+
|
|
189
|
+
The typical data set returned contains the following attributes:
|
|
190
|
+
{
|
|
191
|
+
"token_type": "Bearer",
|
|
192
|
+
"access_token": <str>,
|
|
193
|
+
"expires_in": <number-of-seconds>,
|
|
194
|
+
"refresh_token": <str>,
|
|
195
|
+
"refesh_expires_in": <number-of-seconds>
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
:param url: the target URL
|
|
199
|
+
:param header_data: the data to send in the header of the request
|
|
200
|
+
:param body_data: the data to send in the body of the request
|
|
201
|
+
:param errors: incidental errors
|
|
202
|
+
:param logger: optional logger
|
|
203
|
+
:return: the token data, or *None* if error
|
|
204
|
+
"""
|
|
205
|
+
# initialize the return variable
|
|
206
|
+
result: dict[str, Any] | None = None
|
|
207
|
+
|
|
208
|
+
# log the POST
|
|
209
|
+
if logger:
|
|
210
|
+
logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
|
|
211
|
+
ensure_ascii=False)}")
|
|
212
|
+
try:
|
|
213
|
+
response: requests.Response = requests.post(url=url,
|
|
214
|
+
data=body_data,
|
|
215
|
+
headers=header_data,
|
|
216
|
+
timeout=None)
|
|
217
|
+
if response.status_code == 200:
|
|
218
|
+
# request succeeded
|
|
219
|
+
result = response.json()
|
|
220
|
+
if logger:
|
|
221
|
+
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
222
|
+
else:
|
|
223
|
+
# request failed, report the problem
|
|
224
|
+
msg: str = (f"POST failure, "
|
|
225
|
+
f"status {response.status_code}, reason {response.reason}")
|
|
226
|
+
if hasattr(response, "content") and response.content:
|
|
227
|
+
msg += f", content '{response.content}'"
|
|
228
|
+
if logger:
|
|
229
|
+
logger.error(msg=msg)
|
|
230
|
+
if isinstance(errors, list):
|
|
231
|
+
errors.append(msg)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
# the operation raised an exception
|
|
234
|
+
err_msg = exc_format(exc=e,
|
|
235
|
+
exc_info=sys.exc_info())
|
|
236
|
+
msg: str = f"POST error, {err_msg}"
|
|
237
|
+
if logger:
|
|
238
|
+
logger.debug(msg=msg)
|
|
239
|
+
if isinstance(errors, list):
|
|
240
|
+
errors.append(msg)
|
|
241
|
+
|
|
242
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.4
|
|
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
|
|
@@ -10,7 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.12
|
|
13
|
-
Requires-Dist: cachetools>=6.2.1
|
|
14
13
|
Requires-Dist: flask>=3.1.2
|
|
15
14
|
Requires-Dist: pyjwt>=2.10.1
|
|
16
15
|
Requires-Dist: pypomes-core>=2.8.1
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
|
|
2
|
+
pypomes_iam/iam_actions.py,sha256=8cHtE6AU0sh8IBfO5w1IQj3HVZBSuTFMrrcOK0bnF9E,42774
|
|
3
|
+
pypomes_iam/iam_common.py,sha256=ki_-m6fqJqUbGjgTD41r9zaE-FOXgA_c_tLisIYYTfU,15457
|
|
4
|
+
pypomes_iam/iam_pomes.py,sha256=_kLnrZG25XhJsIv3wqDl_2sIJ2ho_2TIMKrPCyPmA7Q,7362
|
|
5
|
+
pypomes_iam/iam_services.py,sha256=AzrZux2Pt_FoCNcTcXfWphHb587vB3WIbKYG7RFf5zE,15821
|
|
6
|
+
pypomes_iam/provider_pomes.py,sha256=3mMj5LQs53YEINUEOfFBAxOwOP3aOR_szlE4daEBLK0,10523
|
|
7
|
+
pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
|
|
8
|
+
pypomes_iam-0.7.4.dist-info/METADATA,sha256=Si9dT8ORriAGUwCTrl4jYs3tsoz0XVgHATeqIVPv96g,661
|
|
9
|
+
pypomes_iam-0.7.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
pypomes_iam-0.7.4.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
11
|
+
pypomes_iam-0.7.4.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
|
|
2
|
-
pypomes_iam/iam_actions.py,sha256=Bmd8rBg3948Afsg10B6B1ZrFY4wYtbxi55rX4Rlqiyk,39167
|
|
3
|
-
pypomes_iam/iam_common.py,sha256=4sn9V4_fRXrljz41Dh7P6ng4aqFfp0pLnrq_rURDKt4,15363
|
|
4
|
-
pypomes_iam/iam_pomes.py,sha256=XkxpwwGivUR3Y1TKR6McrqLUnpFJhRvIrsEn9T_Ut9A,7351
|
|
5
|
-
pypomes_iam/iam_services.py,sha256=IkCjrKDX1Ix7BiHh-BL3VKz5xogcNC8prXkHyJzQoZ8,15862
|
|
6
|
-
pypomes_iam/provider_pomes.py,sha256=N0nL9_hgHmAjG9JKFoXC33zk8b1ckPG1veu1jTp-2JE,8045
|
|
7
|
-
pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
|
|
8
|
-
pypomes_iam-0.6.0.dist-info/METADATA,sha256=xl-01mkMhab71h8WMqADHpX5qxS5ngFfcMyhSouQwt0,694
|
|
9
|
-
pypomes_iam-0.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
-
pypomes_iam-0.6.0.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
11
|
-
pypomes_iam-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|