pypomes-iam 0.7.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.7.0
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
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "pypomes_iam"
9
- version = "0.7.0"
9
+ version = "0.7.6"
10
10
  authors = [
11
11
  { name="GT Nunes", email="wisecoder01@gmail.com" }
12
12
  ]
@@ -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) -> dict[str, Any]:
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,10 +339,10 @@ 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 data for the new token, or *None* if error
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: dict[str, Any] | None = None
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")
@@ -451,13 +492,13 @@ def __assert_link(iam_server: IamServer,
451
492
  logger.debug(msg="Creating an association between identifications "
452
493
  f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
453
494
  url += f"/{provider_name}"
454
- body_data: dict[str, Any] = {
495
+ json_data: dict[str, Any] = {
455
496
  "userId": token_sub,
456
497
  "userName": user_id
457
498
  }
458
- __post_data(url=url,
499
+ __post_json(url=url,
459
500
  header_data=header_data,
460
- body_data=body_data,
501
+ json_data=json_data,
461
502
  errors=errors,
462
503
  logger=logger)
463
504
 
@@ -646,27 +687,27 @@ def __get_for_data(url: str,
646
687
  return result
647
688
 
648
689
 
649
- def __post_data(url: str,
690
+ def __post_json(url: str,
650
691
  header_data: dict[str, str],
651
- body_data: dict[str, Any],
692
+ json_data: dict[str, Any],
652
693
  errors: list[str] | None,
653
694
  logger: Logger | None) -> None:
654
695
  """
655
696
  Submit a *POST* request to *url*.
656
697
 
657
698
  :param header_data: the data to send in the header of the request
658
- :param body_data: the data to send in the body of the request
699
+ :param json_data: the JSON data to send in the request
659
700
  :param errors: incidental errors
660
701
  :param logger: optional logger
661
702
  """
662
703
  # log the POST
663
704
  if logger:
664
- logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
705
+ logger.debug(msg=f"POST {url}, {json.dumps(obj=json_data,
665
706
  ensure_ascii=False)}")
666
707
  try:
667
- response: requests.Response = requests.get(url=url,
668
- headers=header_data,
669
- data=body_data)
708
+ response: requests.Response = requests.post(url=url,
709
+ headers=header_data,
710
+ json=json_data)
670
711
  if response.status_code >= 400:
671
712
  # request failed, report the problem
672
713
  msg = f"POST failure, status {response.status_code}, reason {response.reason}"
@@ -839,6 +880,8 @@ def __validate_and_store(iam_server: IamServer,
839
880
  # initialize the return variable
840
881
  result: tuple[str, str] | None = None
841
882
 
883
+ if logger:
884
+ logger.debug(msg=f"Validating and storing the token")
842
885
  with _iam_lock:
843
886
  # retrieve the IAM server's registry
844
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 typical *Response* returned will contain the following attributes:
342
+ On success, the returned *Response* will contain the following JSON:
342
343
  {
343
- "token_type": "Bearer",
344
- "access_token": <str>,
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
- token_data: dict[str, Any] | None = None
361
+ token_info: tuple[str, str] | None = None
364
362
  if iam_server:
365
363
  errors: list[str] = []
366
- token_data = action_exchange(iam_server=iam_server,
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(token_data)
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