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.
- pypomes_iam/__init__.py +20 -12
- pypomes_iam/iam_actions.py +343 -122
- pypomes_iam/iam_common.py +71 -29
- pypomes_iam/iam_pomes.py +123 -100
- pypomes_iam/iam_services.py +330 -126
- pypomes_iam/provider_pomes.py +197 -30
- pypomes_iam/token_pomes.py +27 -0
- {pypomes_iam-0.6.2.dist-info → pypomes_iam-0.8.2.dist-info}/METADATA +2 -2
- pypomes_iam-0.8.2.dist-info/RECORD +11 -0
- pypomes_iam-0.6.2.dist-info/RECORD +0 -11
- {pypomes_iam-0.6.2.dist-info → pypomes_iam-0.8.2.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.6.2.dist-info → pypomes_iam-0.8.2.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_actions.py
CHANGED
|
@@ -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,
|
|
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
|
|
16
|
+
from .token_pomes import token_get_values, token_validate
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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 =
|
|
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/
|
|
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 =
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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":
|
|
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 '
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
404
|
-
#
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
token_sub: str =
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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/{
|
|
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":
|
|
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
|
|
812
|
+
def __post_json(url: str,
|
|
598
813
|
header_data: dict[str, str],
|
|
599
|
-
|
|
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
|
|
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=
|
|
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.
|
|
616
|
-
|
|
617
|
-
|
|
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>
|
|
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["
|
|
918
|
+
if body_data["grant_type"] != "password":
|
|
919
|
+
body_data["client_id"] = registry[IamParam.CLIENT_ID]
|
|
704
920
|
|
|
705
921
|
# build the URL
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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=
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
1031
|
+
public_key=public_key,
|
|
811
1032
|
errors=errors,
|
|
812
1033
|
logger=logger)
|
|
813
1034
|
if claims:
|