pypomes-iam 0.6.2__py3-none-any.whl → 0.8.2__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.

@@ -10,27 +10,33 @@ 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, # _get_public_key,
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
- from .token_pomes import token_get_claims, token_validate
16
+ from .token_pomes import token_get_values, token_validate
17
17
 
18
18
 
19
- def action_login(iam_server: IamServer,
20
- args: dict[str, Any],
21
- errors: list[str] = None,
22
- logger: Logger = None) -> str:
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,19 +86,23 @@ 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 action_logout(iam_server: IamServer,
82
- args: dict[str, Any],
83
- errors: list[str] = None,
84
- logger: Logger = None) -> None:
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
 
88
- The user is identified by the attribute *user-id* or "login", provided in *args*.
89
- If successful, remove all data relating to the user from the *IAM* server's registry.
90
- Otherwise, this operation fails silently, unless an error has ocurred.
103
+ The user is identified by the attribute *user-id* or *login*, provided in *args*.
104
+ A logout request is sent to *iam_server* and, if successful, remove all data relating to the user
105
+ from the *IAM* server's registry.
91
106
 
92
107
  :param iam_server: the reference registered *IAM* server
93
108
  :param args: the arguments passed when requesting the service
@@ -99,33 +114,90 @@ def action_logout(iam_server: IamServer,
99
114
 
100
115
  if user_id:
101
116
  with _iam_lock:
102
- # retrieve the data for all users in the IAM server's registry
103
- users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
104
- errors=errors,
105
- logger=logger) or {}
106
- if user_id in users:
107
- users.pop(user_id)
108
- if logger:
109
- logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
117
+ # retrieve the IAM server's registry and the data for all users therein
118
+ registry: dict[str, Any] = _get_iam_registry(iam_server,
119
+ errors=errors,
120
+ logger=logger)
121
+ users: dict[str, dict[str, Any]] = registry[IamParam.USERS] if registry else {}
122
+ user_data: dict[str, Any] = users.get(user_id)
123
+ if user_data:
124
+ # request the IAM server to logout 'client_id'
125
+ client_secret: str = __get_client_secret(iam_server=iam_server,
126
+ errors=errors,
127
+ logger=logger)
128
+ if client_secret:
129
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
130
+ "/protocol/openid-connect/logout")
131
+ header_data: dict[str, str] = {
132
+ "Content-Type": "application/x-www-form-urlencoded"
133
+ }
134
+ body_data: dict[str, Any] = {
135
+ "client_id": registry[IamParam.CLIENT_ID],
136
+ "client_secret": client_secret,
137
+ "refresh_token": user_data[UserParam.REFRESH_TOKEN]
138
+ }
139
+ # log the POST
140
+ if logger:
141
+ logger.debug(msg=f"POST {url}")
142
+ try:
143
+ response: requests.Response = requests.post(url=url,
144
+ headers=header_data,
145
+ data=body_data)
146
+ if response.status_code in [200, 204]:
147
+ # request succeeded
148
+ if logger:
149
+ logger.debug(msg=f"POST success")
150
+ else:
151
+ # request failed, report the problem
152
+ msg: str = f"POST failure, status {response.status_code}, reason {response.reason}"
153
+ if logger:
154
+ logger.error(msg=msg)
155
+ if isinstance(errors, list):
156
+ errors.append(msg)
157
+ except Exception as e:
158
+ # the operation raised an exception
159
+ msg: str = exc_format(exc=e,
160
+ exc_info=sys.exc_info())
161
+ if logger:
162
+ logger.error(msg=msg)
163
+ if isinstance(errors, list):
164
+ errors.append(msg)
110
165
 
166
+ if not errors and user_id in users:
167
+ users.pop(user_id)
168
+ if logger:
169
+ logger.debug(msg=f"User '{user_id}' removed from {iam_server}'s registry")
170
+ else:
171
+ msg: str = "User identification not provided"
172
+ if logger:
173
+ logger.error(msg=msg)
174
+ if isinstance(errors, list):
175
+ errors.append(msg)
111
176
 
112
- def action_token(iam_server: IamServer,
113
- args: dict[str, Any],
114
- errors: list[str] = None,
115
- logger: Logger = None) -> str:
177
+
178
+ def iam_get_token(iam_server: IamServer,
179
+ args: dict[str, Any],
180
+ errors: list[str] = None,
181
+ logger: Logger = None) -> dict[str, str]:
116
182
  """
117
183
  Retrieve the authentication token for the user, from *iam_server*.
118
184
 
119
185
  The user is identified by the attribute *user-id* or *login*, provided in *args*.
120
186
 
187
+ On success, the returned *dict* will contain the following JSON:
188
+ {
189
+ "access-token": <token>,
190
+ "user-id": <user-identification
191
+ }
192
+
121
193
  :param iam_server: the reference registered *IAM* server
122
194
  :param args: the arguments passed when requesting the service
123
195
  :param errors: incidental error messages
124
196
  :param logger: optional logger
125
- :return: the token for user indicated, or *None* if error
197
+ :return: the user identification and token issued, or *None* if error
126
198
  """
127
199
  # initialize the return variable
128
- result: str | None = None
200
+ result: dict[str, str] | None = None
129
201
 
130
202
  # obtain the user's identification
131
203
  user_id: str = args.get("user-id") or args.get("login")
@@ -144,7 +216,10 @@ def action_token(iam_server: IamServer,
144
216
  access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
145
217
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
146
218
  if now < access_expiration:
147
- result = token
219
+ result = {
220
+ "access-token": token,
221
+ "user-id": user_id
222
+ }
148
223
  else:
149
224
  # access token has expired
150
225
  refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
@@ -152,7 +227,7 @@ def action_token(iam_server: IamServer,
152
227
  refresh_expiration: int = user_data[UserParam.REFRESH_EXPIRATION]
153
228
  if now < refresh_expiration:
154
229
  header_data: dict[str, str] = {
155
- "Content-Type": "application/json"
230
+ "Content-Type": "application/x-www-form-urlencoded"
156
231
  }
157
232
  body_data: dict[str, str] = {
158
233
  "grant_type": "refresh_token",
@@ -172,7 +247,10 @@ def action_token(iam_server: IamServer,
172
247
  now=now,
173
248
  errors=errors,
174
249
  logger=logger)
175
- result = token_info[1]
250
+ result = {
251
+ "access-token": token_info[1],
252
+ "user-id": user_id
253
+ }
176
254
  else:
177
255
  # refresh token is no longer valid
178
256
  user_data[UserParam.REFRESH_TOKEN] = None
@@ -200,10 +278,10 @@ def action_token(iam_server: IamServer,
200
278
  return result
201
279
 
202
280
 
203
- def action_callback(iam_server: IamServer,
204
- args: dict[str, Any],
205
- errors: list[str] = None,
206
- logger: Logger = None) -> tuple[str, str] | None:
281
+ def iam_callback(iam_server: IamServer,
282
+ args: dict[str, Any],
283
+ errors: list[str] = None,
284
+ logger: Logger = None) -> tuple[str, str] | None:
207
285
  """
208
286
  Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
209
287
 
@@ -211,6 +289,10 @@ def action_callback(iam_server: IamServer,
211
289
  - *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
212
290
  - *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
213
291
 
292
+ if *state* is postfixed with the string *#idp=<target-idp>*, this instructs *iam_server* to act as a broker,
293
+ forwarding the authentication process to the *IAM* server *target-idp*. This mechanism fully dispenses with
294
+ the flows 'callback-exchange', and 'callback' followed by 'exchange'.
295
+
214
296
  :param iam_server: the reference registered *IAM* server
215
297
  :param args: the arguments passed when requesting the service
216
298
  :param errors: incidental errors
@@ -240,6 +322,10 @@ def action_callback(iam_server: IamServer,
240
322
  if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
241
323
  errors.append("Operation timeout")
242
324
  else:
325
+ pos: int = oauth_state.rfind("#idp=")
326
+ target_idp: str = oauth_state[pos+4:] if pos > 0 else None
327
+ target_iam = IamServer(target_idp) if target_idp in IamServer else None
328
+ target_data: dict[str, Any] = user_data.copy() if target_iam else None
243
329
  users.pop(oauth_state)
244
330
  code: str = args.get("code")
245
331
  header_data: dict[str, str] = {
@@ -264,6 +350,33 @@ def action_callback(iam_server: IamServer,
264
350
  now=now,
265
351
  errors=errors,
266
352
  logger=logger)
353
+ if target_iam:
354
+ if logger:
355
+ logger.debug(msg=f"Requesting to IAM server '{iam_server}' "
356
+ f"the token issued by '{target_iam}' ")
357
+ registry: dict[str, Any] = _get_iam_registry(iam_server,
358
+ errors=errors,
359
+ logger=logger)
360
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
361
+ f"{registry[IamParam.CLIENT_REALM]}/broker/{target_idp}/token")
362
+ header_data: dict[str, str] = {
363
+ "Authorization": f"Bearer {result[1]}",
364
+ "Content-Type": "application/json"
365
+ }
366
+ token_data = __get_for_data(url=url,
367
+ header_data=header_data,
368
+ params=None,
369
+ errors=errors,
370
+ logger=logger)
371
+ if not errors:
372
+ token_info: tuple[str, str] = __validate_and_store(iam_server=target_iam,
373
+ user_data=target_data,
374
+ token_data=token_data,
375
+ now=now,
376
+ errors=errors,
377
+ logger=logger)
378
+ if token_info and logger:
379
+ logger.debug(msg=f"Token obtained: {json.dumps(obj=token_info)}")
267
380
  else:
268
381
  msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
269
382
  if logger:
@@ -274,10 +387,10 @@ def action_callback(iam_server: IamServer,
274
387
  return result
275
388
 
276
389
 
277
- def action_exchange(iam_server: IamServer,
278
- args: dict[str, Any],
279
- errors: list[str] = None,
280
- logger: Logger = None) -> dict[str, Any]:
390
+ def iam_exchange(iam_server: IamServer,
391
+ args: dict[str, Any],
392
+ errors: list[str] = None,
393
+ logger: Logger = None) -> tuple[str, str]:
281
394
  """
282
395
  Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
283
396
 
@@ -298,17 +411,21 @@ def action_exchange(iam_server: IamServer,
298
411
  :param args: the arguments passed when requesting the service
299
412
  :param errors: incidental errors
300
413
  :param logger: optional logger
301
- :return: the data for the new token, or *None* if error
414
+ :return: a tuple containing the reference user identification and the token obtained, or *None* if error
302
415
  """
303
416
  # initialize the return variable
304
- result: dict[str, Any] | None = None
417
+ result: tuple[str, str] | None = None
305
418
 
306
419
  # obtain the user's identification
307
420
  user_id: str = args.get("user-id") or args.get("login")
308
421
 
309
422
  # obtain the token to be exchanged
310
423
  token: str = args.get("access-token") if user_id else None
311
- if token:
424
+ token_issuer: tuple[str] = token_get_values(token=token,
425
+ keys=("iss",),
426
+ errors=errors,
427
+ logger=logger)
428
+ if not errors:
312
429
  # HAZARD: only 'IAM_KEYCLOAK' is currently supported
313
430
  with _iam_lock:
314
431
  # retrieve the IAM server's registry
@@ -320,10 +437,13 @@ def action_exchange(iam_server: IamServer,
320
437
  __assert_link(iam_server=iam_server,
321
438
  user_id=user_id,
322
439
  token=token,
440
+ token_issuer=token_issuer[0],
323
441
  errors=errors,
324
442
  logger=logger)
325
443
  if not errors:
326
444
  # exchange the token
445
+ if logger:
446
+ logger.debug(msg=f"Requesting the token exchange to IAM server '{iam_server}'")
327
447
  header_data: dict[str, Any] = {
328
448
  "Content-Type": "application/x-www-form-urlencoded"
329
449
  }
@@ -333,7 +453,7 @@ def action_exchange(iam_server: IamServer,
333
453
  "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
334
454
  "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
335
455
  "audience": registry[IamParam.CLIENT_ID],
336
- "subject_issuer": "oidc"
456
+ "subject_issuer": token_issuer
337
457
  }
338
458
  now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
339
459
  token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
@@ -360,15 +480,70 @@ def action_exchange(iam_server: IamServer,
360
480
  return result
361
481
 
362
482
 
483
+ def iam_userinfo(iam_server: IamServer,
484
+ args: dict[str, Any],
485
+ errors: list[str] = None,
486
+ logger: Logger = None) -> dict[str, Any] | None:
487
+ """
488
+ Obtain user data from *iam_server*.
489
+
490
+ The user is identified by the attribute *user-id* or *login*, provided in *args*.
491
+
492
+ :param iam_server: the reference registered *IAM* server
493
+ :param args: the arguments passed when requesting the service
494
+ :param errors: incidental error messages
495
+ :param logger: optional logger
496
+ :return: the user information requested, or *None* if error
497
+ """
498
+ # initialize the return variable
499
+ result: dict[str, Any] | None = None
500
+
501
+ # obtain the user's identification
502
+ user_id: str = args.get("user-id") or args.get("login")
503
+
504
+ err_msg: str | None = None
505
+ if user_id:
506
+ with _iam_lock:
507
+ # retrieve the IAM server's registry and the user data therein
508
+ registry: dict[str, Any] = _get_iam_registry(iam_server,
509
+ errors=errors,
510
+ logger=logger)
511
+ user_data: dict[str, Any] = registry[IamParam.USERS].get(user_id)
512
+ if user_data:
513
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
514
+ "/protocol/openid-connect/userinfo")
515
+ header_data: dict[str, str] = {
516
+ "Authorization": f"Bearer {args.get('access-token')}"
517
+ }
518
+ result = __get_for_data(url=url,
519
+ header_data=header_data,
520
+ params=None,
521
+ errors=errors,
522
+ logger=logger)
523
+ else:
524
+ err_msg = f"Unknown user '{user_id}'"
525
+ else:
526
+ err_msg: str = "User identification not provided"
527
+
528
+ if err_msg:
529
+ if logger:
530
+ logger.error(msg=err_msg)
531
+ if isinstance(errors, list):
532
+ errors.append(err_msg)
533
+
534
+ return result
535
+
536
+
363
537
  def __assert_link(iam_server: IamServer,
364
538
  user_id: str,
365
539
  token: str,
540
+ token_issuer: str,
366
541
  errors: list[str] | None,
367
542
  logger: Logger | None) -> None:
368
543
  """
369
544
  Make sure *iam_server* has a link associating *user_id* to an internal user identification.
370
545
  This is a requirement for exchanging a token issued by a federated *IAM* server for an equivalent
371
- one from *iam_server.
546
+ one from *iam_server*.
372
547
 
373
548
  :param iam_server: the reference *IAM* server
374
549
  :param user_id: the reference user identification
@@ -376,6 +551,9 @@ def __assert_link(iam_server: IamServer,
376
551
  :param errors: incidental errors
377
552
  :param logger: optional logger
378
553
  """
554
+ if logger:
555
+ logger.debug(msg="Verifying associations for user "
556
+ f"'{user_id}' in IAM server '{iam_server}'")
379
557
  # obtain a token with administrative rights
380
558
  admin_token: str = __get_administrative_token(iam_server=iam_server,
381
559
  errors=errors,
@@ -384,7 +562,10 @@ def __assert_link(iam_server: IamServer,
384
562
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
385
563
  errors=errors,
386
564
  logger=logger)
387
- # obtain the internal user identification for 'from_id'
565
+ # obtain the internal user identification for 'user_id'
566
+ if logger:
567
+ logger.debug(msg="Obtaining internal identification "
568
+ f"for user '{user_id}' in IAM server '{iam_server}'")
388
569
  url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
389
570
  header_data: dict[str, str] = {
390
571
  "Authorization": f"Bearer {admin_token}",
@@ -394,15 +575,18 @@ def __assert_link(iam_server: IamServer,
394
575
  "username": user_id,
395
576
  "exact": "true"
396
577
  }
397
- users: dict[str, Any] = __get_for_data(url=url,
398
- header_data=header_data,
399
- params=params,
400
- errors=errors,
401
- logger=logger)
578
+ users: list[dict[str, Any]] = __get_for_data(url=url,
579
+ header_data=header_data,
580
+ params=params,
581
+ errors=errors,
582
+ logger=logger)
402
583
  if users:
403
- # verify whether the 'oidc' protocol is referred to in an
404
- # association between 'from_id' and the internal user identification
405
- internal_id: str = users.get("id")
584
+ # verify whether the IAM server that issued the token is a federated identity provider
585
+ # in the associations between 'user_id' and the internal user identification
586
+ internal_id: str = users[0].get("id")
587
+ if logger:
588
+ logger.debug(msg="Obtaining the providers federated in IAM server "
589
+ f"'{iam_server}', for internal identification '{internal_id}'")
406
590
  url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
407
591
  f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
408
592
  providers: list[dict[str, Any]] = __get_for_data(url=url,
@@ -411,27 +595,34 @@ def __assert_link(iam_server: IamServer,
411
595
  errors=errors,
412
596
  logger=logger)
413
597
  no_link: bool = True
414
- for provider in providers:
415
- if provider.get("identityProvider") == "oidc":
416
- no_link = False
417
- break
418
- if no_link:
419
- # link the identities
420
- claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
421
- errors=errors,
422
- logger=logger)
423
- if claims:
424
- token_sub: str = claims["paylod"]["sub"]
425
- url += "/oidc"
426
- body_data: dict[str, Any] = {
427
- "userId": token_sub,
428
- "userName": user_id
429
- }
430
- __post_data(url=url,
431
- header_data=header_data,
432
- body_data=body_data,
433
- errors=errors,
434
- logger=logger)
598
+ provider_name: str = _iam_server_from_issuer(issuer=token_issuer,
599
+ errors=errors,
600
+ logger=logger)
601
+ if provider_name:
602
+ for provider in providers:
603
+ if provider.get("identityProvider") == provider_name:
604
+ no_link = False
605
+ break
606
+ if no_link:
607
+ # link the identities
608
+ token_sub: tuple[str] = token_get_values(token=token,
609
+ keys=("sub",),
610
+ errors=errors,
611
+ logger=logger)
612
+ if token_sub:
613
+ if logger:
614
+ logger.debug(msg="Creating an association between identifications "
615
+ f"'{user_id}' and '{token_sub}' in IAM server '{iam_server}'")
616
+ url += f"/{provider_name}"
617
+ json_data: dict[str, Any] = {
618
+ "userId": token_sub[0],
619
+ "userName": user_id
620
+ }
621
+ __post_json(url=url,
622
+ header_data=header_data,
623
+ json_data=json_data,
624
+ errors=errors,
625
+ logger=logger)
435
626
 
436
627
 
437
628
  def __get_administrative_token(iam_server: IamServer,
@@ -451,31 +642,47 @@ def __get_administrative_token(iam_server: IamServer,
451
642
  # initialize the return variable
452
643
  result: str | None = None
453
644
 
645
+ if logger:
646
+ logger.debug(msg="Requesting a token with "
647
+ f"administrative rights to IAM Server '{iam_server}'")
648
+
454
649
  # obtain the IAM server's registry
455
650
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
456
651
  errors=errors,
457
652
  logger=logger)
458
- if registry and registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
459
- header_data: dict[str, str] = {
460
- "Content-Type": "application/x-www-form-urlencoded"
461
- }
462
- body_data: dict[str, str] = {
463
- "grant-type": "password",
464
- "username": registry[IamParam.ADMIN_ID],
465
- "password": registry[IamParam.ADMIN_SECRET],
466
- "client_id": registry[IamParam.CLIENT_ID]
467
- }
468
- token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
469
- header_data=header_data,
470
- body_data=body_data,
471
- errors=errors,
472
- logger=logger)
473
- if token_data:
474
- # obtain the token
475
- result = token_data["access_token"]
476
- else:
477
- msg: str = (f"To obtain token with administrative rights from '{iam_server}', "
478
- f"the credentials for the realm administrator must be provided at setup time")
653
+ if registry:
654
+ if registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
655
+ header_data: dict[str, str] = {
656
+ "Content-Type": "application/x-www-form-urlencoded"
657
+ }
658
+ body_data: dict[str, str] = {
659
+ "grant_type": "password",
660
+ "username": registry[IamParam.ADMIN_ID],
661
+ "password": registry[IamParam.ADMIN_SECRET],
662
+ "client_id": "admin-cli"
663
+ }
664
+ token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
665
+ header_data=header_data,
666
+ body_data=body_data,
667
+ errors=errors,
668
+ logger=logger)
669
+ if token_data:
670
+ # obtain the token
671
+ result = token_data["access_token"]
672
+ if logger:
673
+ logger.debug(msg="Administrative token obtained")
674
+
675
+ elif logger or isinstance(errors, list):
676
+ msg: str = ("Credentials for administrator of realm "
677
+ f"'{registry[IamParam.CLIENT_REALM]}' "
678
+ f"at IAM server '{iam_server}' not provided")
679
+ if logger:
680
+ logger.error(msg=msg)
681
+ if isinstance(errors, list):
682
+ errors.append(msg)
683
+
684
+ elif logger or isinstance(errors, list):
685
+ msg: str = f"Unknown IAM server {iam_server}"
479
686
  if logger:
480
687
  logger.error(msg=msg)
481
688
  if isinstance(errors, list):
@@ -511,14 +718,19 @@ def __get_client_secret(iam_server: IamServer,
511
718
  errors=errors,
512
719
  logger=logger)
513
720
  if token:
721
+ realm: str = registry[IamParam.CLIENT_REALM]
722
+ client_id: str = registry[IamParam.CLIENT_ID]
723
+ if logger:
724
+ logger.debug(msg=f"Obtaining the UUID for client '{client_id}', "
725
+ f"in realm '{realm}' at IAM server '{iam_server}'")
514
726
  # obtain the client UUID
515
- url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}/clients"
727
+ url: str = f"{registry[IamParam.URL_BASE]}/realms/{realm}/clients"
516
728
  header_data: dict[str, str] = {
517
729
  "Authorization": f"Bearer {token}",
518
730
  "Content-Type": "application/json"
519
731
  }
520
732
  params: dict[str, str] = {
521
- "clientId": registry[IamParam.CLIENT_ID]
733
+ "clientId": client_id
522
734
  }
523
735
  clients: list[dict[str, Any]] = __get_for_data(url=url,
524
736
  header_data=header_data,
@@ -528,6 +740,9 @@ def __get_client_secret(iam_server: IamServer,
528
740
  if clients:
529
741
  # obtain the client's secret password
530
742
  client_uuid: str = clients[0]["id"]
743
+ if logger:
744
+ logger.debug(msg=f"Obtaining the secret for client UUID '{client_uuid}', "
745
+ f"in realm '{realm}' at IAM server '{iam_server}'")
531
746
  url += f"/{client_uuid}/client-secret"
532
747
  reply: dict[str, Any] = __get_for_data(url=url,
533
748
  header_data=header_data,
@@ -594,27 +809,27 @@ def __get_for_data(url: str,
594
809
  return result
595
810
 
596
811
 
597
- def __post_data(url: str,
812
+ def __post_json(url: str,
598
813
  header_data: dict[str, str],
599
- body_data: dict[str, Any],
814
+ json_data: dict[str, Any],
600
815
  errors: list[str] | None,
601
816
  logger: Logger | None) -> None:
602
817
  """
603
818
  Submit a *POST* request to *url*.
604
819
 
605
820
  :param header_data: the data to send in the header of the request
606
- :param body_data: the data to send in the body of the request
821
+ :param json_data: the JSON data to send in the request
607
822
  :param errors: incidental errors
608
823
  :param logger: optional logger
609
824
  """
610
825
  # log the POST
611
826
  if logger:
612
- logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
827
+ logger.debug(msg=f"POST {url}, {json.dumps(obj=json_data,
613
828
  ensure_ascii=False)}")
614
829
  try:
615
- response: requests.Response = requests.get(url=url,
616
- headers=header_data,
617
- data=body_data)
830
+ response: requests.Response = requests.post(url=url,
831
+ headers=header_data,
832
+ json=json_data)
618
833
  if response.status_code >= 400:
619
834
  # request failed, report the problem
620
835
  msg = f"POST failure, status {response.status_code}, reason {response.reason}"
@@ -666,9 +881,9 @@ def __post_for_token(iam_server: IamServer,
666
881
  - "username": <realm-administrator-identification>
667
882
  - "password": <realm-administrator-secret>
668
883
 
669
- These attributes are then added to *body_data*:
884
+ These attributes are then added to *body_data*, except for acquiring administrative tokens:
670
885
  - "client_id": <client-id>
671
- - "client_secret": <client-secret> <- except for acquiring administrative tokens
886
+ - "client_secret": <client-secret>
672
887
 
673
888
  If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
674
889
  Otherwise, *errors* will contain the appropriate error message.
@@ -700,21 +915,25 @@ def __post_for_token(iam_server: IamServer,
700
915
  logger=logger)
701
916
  if registry:
702
917
  # complete the data to send in body of request
703
- body_data["client_id"] = registry[IamParam.CLIENT_ID]
918
+ if body_data["grant_type"] != "password":
919
+ body_data["client_id"] = registry[IamParam.CLIENT_ID]
704
920
 
705
921
  # build the URL
706
- base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
707
- url: str = f"{base_url}/protocol/openid-connect/token"
708
-
709
- # log the POST ('client_secret' data must not be shown in log)
922
+ url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
923
+ f"{registry[IamParam.CLIENT_REALM]}/protocol/openid-connect/token")
924
+ # 'client_secret' data must not be shown in log
925
+ msg: str = f"POST {url}, {json.dumps(obj=body_data,
926
+ ensure_ascii=False)}"
927
+ if body_data["grant_type"] != "password":
928
+ # 'client_secret' not required for requesting tokens from staging environments
929
+ client_secret: str = __get_client_secret(iam_server=iam_server,
930
+ errors=None,
931
+ logger=logger)
932
+ if client_secret:
933
+ body_data["client_secret"] = client_secret
934
+ # log the POST
710
935
  if logger:
711
- logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
712
- ensure_ascii=False)}")
713
- client_secret: str = __get_client_secret(iam_server=iam_server,
714
- errors=errors,
715
- logger=logger)
716
- if body_data["grant_type"] != "password" and client_secret:
717
- body_data["client_secret"] = client_secret
936
+ logger.debug(msg=msg)
718
937
 
719
938
  # obtain the token
720
939
  try:
@@ -783,6 +1002,8 @@ def __validate_and_store(iam_server: IamServer,
783
1002
  # initialize the return variable
784
1003
  result: tuple[str, str] | None = None
785
1004
 
1005
+ if logger:
1006
+ logger.debug(msg=f"Validating and storing the token")
786
1007
  with _iam_lock:
787
1008
  # retrieve the IAM server's registry
788
1009
  registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
@@ -797,9 +1018,9 @@ def __validate_and_store(iam_server: IamServer,
797
1018
  user_data["access-expiration"] = now + token_data.get("expires_in")
798
1019
  refresh_exp: int = user_data.get("refresh_expires_in")
799
1020
  user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
800
- # public_key: str = _get_public_key(iam_server=iam_server,
801
- # errors=errors,
802
- # logger=logger)
1021
+ public_key: str = _get_public_key(iam_server=iam_server,
1022
+ errors=errors,
1023
+ logger=logger)
803
1024
  recipient_attr = registry[IamParam.RECIPIENT_ATTR]
804
1025
  login_id = user_data.pop("login-id", None)
805
1026
  base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
@@ -807,7 +1028,7 @@ def __validate_and_store(iam_server: IamServer,
807
1028
  issuer=base_url,
808
1029
  recipient_id=login_id,
809
1030
  recipient_attr=recipient_attr,
810
- # public_key=public_key,
1031
+ public_key=public_key,
811
1032
  errors=errors,
812
1033
  logger=logger)
813
1034
  if claims: