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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pypomes_iam
3
- Version: 0.6.9
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.6.9"
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,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 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")
308
349
 
309
350
  # obtain the token to be exchanged
310
351
  token: str = args.get("access-token") if user_id else None
311
- if token:
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": "oidc"
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
- body_data: dict[str, Any] = {
495
+ json_data: dict[str, Any] = {
447
496
  "userId": token_sub,
448
497
  "userName": user_id
449
498
  }
450
- __post_data(url=url,
499
+ __post_json(url=url,
451
500
  header_data=header_data,
452
- body_data=body_data,
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 __post_data(url: str,
690
+ def __post_json(url: str,
642
691
  header_data: dict[str, str],
643
- body_data: dict[str, Any],
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 body_data: the data to send in the body of the request
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=body_data,
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.get(url=url,
660
- headers=header_data,
661
- data=body_data)
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 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