pypomes-iam 0.5.1__py3-none-any.whl → 0.6.9__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.
- pypomes_iam/__init__.py +16 -18
- pypomes_iam/iam_actions.py +870 -0
- pypomes_iam/iam_common.py +217 -83
- pypomes_iam/iam_pomes.py +131 -486
- pypomes_iam/iam_services.py +138 -53
- pypomes_iam/provider_pomes.py +160 -79
- pypomes_iam/token_pomes.py +39 -2
- {pypomes_iam-0.5.1.dist-info → pypomes_iam-0.6.9.dist-info}/METADATA +1 -2
- pypomes_iam-0.6.9.dist-info/RECORD +11 -0
- pypomes_iam/jusbr_pomes.py +0 -122
- pypomes_iam/keycloak_pomes.py +0 -136
- pypomes_iam-0.5.1.dist-info/RECORD +0 -12
- {pypomes_iam-0.5.1.dist-info → pypomes_iam-0.6.9.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.1.dist-info → pypomes_iam-0.6.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
import secrets
|
|
4
|
+
import string
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from logging import Logger
|
|
8
|
+
from pypomes_core import TZ_LOCAL, exc_format
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .iam_common import (
|
|
12
|
+
IamServer, IamParam, UserParam, _iam_lock,
|
|
13
|
+
_get_iam_users, _get_iam_registry, _get_public_key,
|
|
14
|
+
_get_login_timeout, _get_user_data, _iam_server_from_issuer
|
|
15
|
+
)
|
|
16
|
+
from .token_pomes import token_get_claims, token_validate
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def action_login(iam_server: IamServer,
|
|
20
|
+
args: dict[str, Any],
|
|
21
|
+
errors: list[str] = None,
|
|
22
|
+
logger: Logger = None) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Build the URL for redirecting the request to *iam_server*'s authentication page.
|
|
25
|
+
|
|
26
|
+
These are the expected attributes in *args*:
|
|
27
|
+
- user-id: optional, identifies the reference user (alias: 'login')
|
|
28
|
+
- redirect-uri: a parameter to be added to the query part of the returned URL
|
|
29
|
+
|
|
30
|
+
If provided, the user identification will be validated against the authorization data
|
|
31
|
+
returned by *iam_server* upon login. On success, the appropriate URL for invoking
|
|
32
|
+
the IAM server's authentication page is returned.
|
|
33
|
+
|
|
34
|
+
:param iam_server: the reference registered *IAM* server
|
|
35
|
+
:param args: the arguments passed when requesting the service
|
|
36
|
+
:param errors: incidental error messages
|
|
37
|
+
:param logger: optional logger
|
|
38
|
+
:return: the callback URL, with the appropriate parameters, of *None* if error
|
|
39
|
+
"""
|
|
40
|
+
# initialize the return variable
|
|
41
|
+
result: str | None = None
|
|
42
|
+
|
|
43
|
+
# obtain the optional user's identification
|
|
44
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
45
|
+
|
|
46
|
+
# build the user data
|
|
47
|
+
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
48
|
+
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
49
|
+
|
|
50
|
+
with _iam_lock:
|
|
51
|
+
# retrieve the user data from the IAM server's registry
|
|
52
|
+
user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
|
|
53
|
+
user_id=oauth_state,
|
|
54
|
+
errors=errors,
|
|
55
|
+
logger=logger)
|
|
56
|
+
if user_data:
|
|
57
|
+
user_data[UserParam.LOGIN_ID] = user_id
|
|
58
|
+
timeout: int = _get_login_timeout(iam_server=iam_server,
|
|
59
|
+
errors=errors,
|
|
60
|
+
logger=logger)
|
|
61
|
+
if not errors:
|
|
62
|
+
user_data[UserParam.LOGIN_EXPIRATION] = (int(datetime.now(tz=TZ_LOCAL).timestamp()) + timeout) \
|
|
63
|
+
if timeout else None
|
|
64
|
+
redirect_uri: str = args.get(UserParam.REDIRECT_URI)
|
|
65
|
+
user_data[UserParam.REDIRECT_URI] = redirect_uri
|
|
66
|
+
|
|
67
|
+
# build the login url
|
|
68
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
69
|
+
errors=errors,
|
|
70
|
+
logger=logger)
|
|
71
|
+
if registry:
|
|
72
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
73
|
+
result = (f"{base_url}/protocol/openid-connect/auth"
|
|
74
|
+
f"?response_type=code&scope=openid"
|
|
75
|
+
f"&client_id={registry[IamParam.CLIENT_ID]}"
|
|
76
|
+
f"&redirect_uri={redirect_uri}"
|
|
77
|
+
f"&state={oauth_state}")
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def action_logout(iam_server: IamServer,
|
|
82
|
+
args: dict[str, Any],
|
|
83
|
+
errors: list[str] = None,
|
|
84
|
+
logger: Logger = None) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Logout the user, by removing all data associating it from *iam_server*'s registry.
|
|
87
|
+
|
|
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.
|
|
91
|
+
|
|
92
|
+
:param iam_server: the reference registered *IAM* server
|
|
93
|
+
:param args: the arguments passed when requesting the service
|
|
94
|
+
:param errors: incidental error messages
|
|
95
|
+
:param logger: optional logger
|
|
96
|
+
"""
|
|
97
|
+
# obtain the user's identification
|
|
98
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
99
|
+
|
|
100
|
+
if user_id:
|
|
101
|
+
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")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def action_token(iam_server: IamServer,
|
|
113
|
+
args: dict[str, Any],
|
|
114
|
+
errors: list[str] = None,
|
|
115
|
+
logger: Logger = None) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Retrieve the authentication token for the user, from *iam_server*.
|
|
118
|
+
|
|
119
|
+
The user is identified by the attribute *user-id* or *login*, provided in *args*.
|
|
120
|
+
|
|
121
|
+
:param iam_server: the reference registered *IAM* server
|
|
122
|
+
:param args: the arguments passed when requesting the service
|
|
123
|
+
:param errors: incidental error messages
|
|
124
|
+
:param logger: optional logger
|
|
125
|
+
:return: the token for user indicated, or *None* if error
|
|
126
|
+
"""
|
|
127
|
+
# initialize the return variable
|
|
128
|
+
result: str | None = None
|
|
129
|
+
|
|
130
|
+
# obtain the user's identification
|
|
131
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
132
|
+
|
|
133
|
+
err_msg: str | None = None
|
|
134
|
+
if user_id:
|
|
135
|
+
with _iam_lock:
|
|
136
|
+
# retrieve the user data in the IAM server's registry
|
|
137
|
+
user_data: dict[str, Any] = _get_user_data(iam_server=iam_server,
|
|
138
|
+
user_id=user_id,
|
|
139
|
+
errors=errors,
|
|
140
|
+
logger=logger)
|
|
141
|
+
# retrieve the stored access token
|
|
142
|
+
token: str = user_data[UserParam.ACCESS_TOKEN] if user_data else None
|
|
143
|
+
if token:
|
|
144
|
+
access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
|
|
145
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
146
|
+
if now < access_expiration:
|
|
147
|
+
result = token
|
|
148
|
+
else:
|
|
149
|
+
# access token has expired
|
|
150
|
+
refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
|
|
151
|
+
if refresh_token:
|
|
152
|
+
refresh_expiration: int = user_data[UserParam.REFRESH_EXPIRATION]
|
|
153
|
+
if now < refresh_expiration:
|
|
154
|
+
header_data: dict[str, str] = {
|
|
155
|
+
"Content-Type": "application/json"
|
|
156
|
+
}
|
|
157
|
+
body_data: dict[str, str] = {
|
|
158
|
+
"grant_type": "refresh_token",
|
|
159
|
+
"refresh_token": refresh_token
|
|
160
|
+
}
|
|
161
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
162
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
163
|
+
header_data=header_data,
|
|
164
|
+
body_data=body_data,
|
|
165
|
+
errors=errors,
|
|
166
|
+
logger=logger)
|
|
167
|
+
# validate and store the token data
|
|
168
|
+
if token_data:
|
|
169
|
+
token_info: tuple[str, str] = __validate_and_store(iam_server=iam_server,
|
|
170
|
+
user_data=user_data,
|
|
171
|
+
token_data=token_data,
|
|
172
|
+
now=now,
|
|
173
|
+
errors=errors,
|
|
174
|
+
logger=logger)
|
|
175
|
+
result = token_info[1]
|
|
176
|
+
else:
|
|
177
|
+
# refresh token is no longer valid
|
|
178
|
+
user_data[UserParam.REFRESH_TOKEN] = None
|
|
179
|
+
else:
|
|
180
|
+
# refresh token has expired
|
|
181
|
+
err_msg = "Access and refresh tokens expired"
|
|
182
|
+
if logger:
|
|
183
|
+
logger.error(msg=err_msg)
|
|
184
|
+
else:
|
|
185
|
+
err_msg = "Access token expired, no refresh token available"
|
|
186
|
+
if logger:
|
|
187
|
+
logger.error(msg=err_msg)
|
|
188
|
+
else:
|
|
189
|
+
err_msg = f"User '{user_id}' not authenticated"
|
|
190
|
+
if logger:
|
|
191
|
+
logger.error(msg=err_msg)
|
|
192
|
+
else:
|
|
193
|
+
err_msg = "User identification not provided"
|
|
194
|
+
if logger:
|
|
195
|
+
logger.error(msg=err_msg)
|
|
196
|
+
|
|
197
|
+
if err_msg and isinstance(errors, list):
|
|
198
|
+
errors.append(err_msg)
|
|
199
|
+
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
|
|
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:
|
|
207
|
+
"""
|
|
208
|
+
Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
|
|
209
|
+
|
|
210
|
+
The relevant expected arguments in *args* are:
|
|
211
|
+
- *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
|
|
212
|
+
- *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
|
|
213
|
+
|
|
214
|
+
:param iam_server: the reference registered *IAM* server
|
|
215
|
+
:param args: the arguments passed when requesting the service
|
|
216
|
+
:param errors: incidental errors
|
|
217
|
+
:param logger: optional logger
|
|
218
|
+
:return: a tuple containing the reference user identification and the token obtained, or *None* if error
|
|
219
|
+
"""
|
|
220
|
+
# initialize the return variable
|
|
221
|
+
result: tuple[str, str] | None = None
|
|
222
|
+
|
|
223
|
+
with _iam_lock:
|
|
224
|
+
# retrieve the IAM server's data for all users
|
|
225
|
+
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
226
|
+
errors=errors,
|
|
227
|
+
logger=logger) or {}
|
|
228
|
+
# retrieve the OAuth2 state
|
|
229
|
+
oauth_state: str = args.get("state")
|
|
230
|
+
user_data: dict[str, Any] | None = None
|
|
231
|
+
if oauth_state:
|
|
232
|
+
for user, data in users.items():
|
|
233
|
+
if user == oauth_state:
|
|
234
|
+
user_data = data
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
# exchange 'code' received for the token
|
|
238
|
+
if user_data:
|
|
239
|
+
expiration: int = user_data["login-expiration"] or sys.maxsize
|
|
240
|
+
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
241
|
+
errors.append("Operation timeout")
|
|
242
|
+
else:
|
|
243
|
+
users.pop(oauth_state)
|
|
244
|
+
code: str = args.get("code")
|
|
245
|
+
header_data: dict[str, str] = {
|
|
246
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
247
|
+
}
|
|
248
|
+
body_data: dict[str, Any] = {
|
|
249
|
+
"grant_type": "authorization_code",
|
|
250
|
+
"code": code,
|
|
251
|
+
"redirect_uri": user_data.pop("redirect-uri")
|
|
252
|
+
}
|
|
253
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
254
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
255
|
+
header_data=header_data,
|
|
256
|
+
body_data=body_data,
|
|
257
|
+
errors=errors,
|
|
258
|
+
logger=logger)
|
|
259
|
+
# validate and store the token data
|
|
260
|
+
if token_data:
|
|
261
|
+
result = __validate_and_store(iam_server=iam_server,
|
|
262
|
+
user_data=user_data,
|
|
263
|
+
token_data=token_data,
|
|
264
|
+
now=now,
|
|
265
|
+
errors=errors,
|
|
266
|
+
logger=logger)
|
|
267
|
+
else:
|
|
268
|
+
msg: str = f"State '{oauth_state}' not found in {iam_server}'s registry"
|
|
269
|
+
if logger:
|
|
270
|
+
logger.error(msg=msg)
|
|
271
|
+
if isinstance(errors, list):
|
|
272
|
+
errors.append(msg)
|
|
273
|
+
|
|
274
|
+
return result
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def action_exchange(iam_server: IamServer,
|
|
278
|
+
args: dict[str, Any],
|
|
279
|
+
errors: list[str] = None,
|
|
280
|
+
logger: Logger = None) -> dict[str, Any]:
|
|
281
|
+
"""
|
|
282
|
+
Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
|
|
283
|
+
|
|
284
|
+
The expected parameters in *args* are:
|
|
285
|
+
- user-id: identification for the reference user (alias: 'login')
|
|
286
|
+
- token: the token to be exchanged
|
|
287
|
+
|
|
288
|
+
The typical data set returned contains the following attributes:
|
|
289
|
+
{
|
|
290
|
+
"token_type": "Bearer",
|
|
291
|
+
"access_token": <str>,
|
|
292
|
+
"expires_in": <number-of-seconds>,
|
|
293
|
+
"refresh_token": <str>,
|
|
294
|
+
"refesh_expires_in": <number-of-seconds>
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
:param iam_server: the reference registered *IAM* server
|
|
298
|
+
:param args: the arguments passed when requesting the service
|
|
299
|
+
:param errors: incidental errors
|
|
300
|
+
:param logger: optional logger
|
|
301
|
+
:return: the data for the new token, or *None* if error
|
|
302
|
+
"""
|
|
303
|
+
# initialize the return variable
|
|
304
|
+
result: dict[str, Any] | None = None
|
|
305
|
+
|
|
306
|
+
# obtain the user's identification
|
|
307
|
+
user_id: str = args.get("user-id") or args.get("login")
|
|
308
|
+
|
|
309
|
+
# obtain the token to be exchanged
|
|
310
|
+
token: str = args.get("access-token") if user_id else None
|
|
311
|
+
if token:
|
|
312
|
+
# HAZARD: only 'IAM_KEYCLOAK' is currently supported
|
|
313
|
+
with _iam_lock:
|
|
314
|
+
# retrieve the IAM server's registry
|
|
315
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
316
|
+
errors=errors,
|
|
317
|
+
logger=logger)
|
|
318
|
+
if registry:
|
|
319
|
+
# make sure 'client_id' is linked to the token's 'token_sub' at the IAM server
|
|
320
|
+
__assert_link(iam_server=iam_server,
|
|
321
|
+
user_id=user_id,
|
|
322
|
+
token=token,
|
|
323
|
+
errors=errors,
|
|
324
|
+
logger=logger)
|
|
325
|
+
if not errors:
|
|
326
|
+
# exchange the token
|
|
327
|
+
header_data: dict[str, Any] = {
|
|
328
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
329
|
+
}
|
|
330
|
+
body_data: dict[str, str] = {
|
|
331
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
332
|
+
"subject_token": token,
|
|
333
|
+
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
334
|
+
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
335
|
+
"audience": registry[IamParam.CLIENT_ID],
|
|
336
|
+
"subject_issuer": "oidc"
|
|
337
|
+
}
|
|
338
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
339
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
340
|
+
header_data=header_data,
|
|
341
|
+
body_data=body_data,
|
|
342
|
+
errors=errors,
|
|
343
|
+
logger=logger)
|
|
344
|
+
# validate and store the token data
|
|
345
|
+
if token_data:
|
|
346
|
+
user_data: dict[str, Any] = {}
|
|
347
|
+
result = __validate_and_store(iam_server=iam_server,
|
|
348
|
+
user_data=user_data,
|
|
349
|
+
token_data=token_data,
|
|
350
|
+
now=now,
|
|
351
|
+
errors=errors,
|
|
352
|
+
logger=logger)
|
|
353
|
+
else:
|
|
354
|
+
msg: str = "User identification or token not provided"
|
|
355
|
+
if logger:
|
|
356
|
+
logger.error(msg=msg)
|
|
357
|
+
if isinstance(errors, list):
|
|
358
|
+
errors.append(msg)
|
|
359
|
+
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def __assert_link(iam_server: IamServer,
|
|
364
|
+
user_id: str,
|
|
365
|
+
token: str,
|
|
366
|
+
errors: list[str] | None,
|
|
367
|
+
logger: Logger | None) -> None:
|
|
368
|
+
"""
|
|
369
|
+
Make sure *iam_server* has a link associating *user_id* to an internal user identification.
|
|
370
|
+
This is a requirement for exchanging a token issued by a federated *IAM* server for an equivalent
|
|
371
|
+
one from *iam_server*.
|
|
372
|
+
|
|
373
|
+
:param iam_server: the reference *IAM* server
|
|
374
|
+
:param user_id: the reference user identification
|
|
375
|
+
:param token: the reference token
|
|
376
|
+
:param errors: incidental errors
|
|
377
|
+
:param logger: optional logger
|
|
378
|
+
"""
|
|
379
|
+
if logger:
|
|
380
|
+
logger.debug(msg="Verifying associations for user "
|
|
381
|
+
f"'{user_id}' in IAM server '{iam_server}'")
|
|
382
|
+
# obtain a token with administrative rights
|
|
383
|
+
admin_token: str = __get_administrative_token(iam_server=iam_server,
|
|
384
|
+
errors=errors,
|
|
385
|
+
logger=logger)
|
|
386
|
+
if admin_token:
|
|
387
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
388
|
+
errors=errors,
|
|
389
|
+
logger=logger)
|
|
390
|
+
# obtain the internal user identification for 'user_id'
|
|
391
|
+
if logger:
|
|
392
|
+
logger.debug(msg="Obtaining internal identification "
|
|
393
|
+
f"for user {user_id} in IAM server {iam_server}")
|
|
394
|
+
url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
|
|
395
|
+
header_data: dict[str, str] = {
|
|
396
|
+
"Authorization": f"Bearer {admin_token}",
|
|
397
|
+
"Content-Type": "application/json"
|
|
398
|
+
}
|
|
399
|
+
params: dict[str, str] = {
|
|
400
|
+
"username": user_id,
|
|
401
|
+
"exact": "true"
|
|
402
|
+
}
|
|
403
|
+
users: list[dict[str, Any]] = __get_for_data(url=url,
|
|
404
|
+
header_data=header_data,
|
|
405
|
+
params=params,
|
|
406
|
+
errors=errors,
|
|
407
|
+
logger=logger)
|
|
408
|
+
if users:
|
|
409
|
+
# verify whether the 'oidc' protocol is referred to in an
|
|
410
|
+
# association between 'user_id' and the internal user identification
|
|
411
|
+
internal_id: str = users[0].get("id")
|
|
412
|
+
if logger:
|
|
413
|
+
logger.debug(msg="Obtaining the providers federated with "
|
|
414
|
+
f"IAM server '{iam_server}' for internal identification '{internal_id}'")
|
|
415
|
+
url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
|
|
416
|
+
f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
|
|
417
|
+
providers: list[dict[str, Any]] = __get_for_data(url=url,
|
|
418
|
+
header_data=header_data,
|
|
419
|
+
params=None,
|
|
420
|
+
errors=errors,
|
|
421
|
+
logger=logger)
|
|
422
|
+
no_link: bool = True
|
|
423
|
+
claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
|
|
424
|
+
errors=errors,
|
|
425
|
+
logger=logger)
|
|
426
|
+
issuer: str = claims["payload"]["iss"] if claims else None
|
|
427
|
+
provider_name: str = _iam_server_from_issuer(issuer=issuer,
|
|
428
|
+
errors=errors,
|
|
429
|
+
logger=logger) if issuer else None
|
|
430
|
+
if provider_name:
|
|
431
|
+
for provider in providers:
|
|
432
|
+
if provider.get("identityProvider") == provider_name:
|
|
433
|
+
no_link = False
|
|
434
|
+
break
|
|
435
|
+
if no_link:
|
|
436
|
+
# link the identities
|
|
437
|
+
claims: dict[str, dict[str, Any]] = token_get_claims(token=token,
|
|
438
|
+
errors=errors,
|
|
439
|
+
logger=logger)
|
|
440
|
+
if claims:
|
|
441
|
+
token_sub: str = claims["payload"]["sub"]
|
|
442
|
+
if logger:
|
|
443
|
+
logger.debug(msg="Creating an association between identifications "
|
|
444
|
+
f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
|
|
445
|
+
url += f"/{provider_name}"
|
|
446
|
+
body_data: dict[str, Any] = {
|
|
447
|
+
"userId": token_sub,
|
|
448
|
+
"userName": user_id
|
|
449
|
+
}
|
|
450
|
+
__post_data(url=url,
|
|
451
|
+
header_data=header_data,
|
|
452
|
+
body_data=body_data,
|
|
453
|
+
errors=errors,
|
|
454
|
+
logger=logger)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def __get_administrative_token(iam_server: IamServer,
|
|
458
|
+
errors: list[str] | None,
|
|
459
|
+
logger: Logger | None) -> str:
|
|
460
|
+
"""
|
|
461
|
+
Obtain a token with administrative rights from *iam_server*'s reference realm.
|
|
462
|
+
|
|
463
|
+
The reference realm is the realm specified at *iam_server*'s setup time. This operation requires
|
|
464
|
+
the realm administrator's identification and secret password to have also been provided.
|
|
465
|
+
|
|
466
|
+
:param iam_server: the reference *IAM* server
|
|
467
|
+
:param errors: incidental errors
|
|
468
|
+
:param logger: optional logger
|
|
469
|
+
:return: a token with administrative rights for the reference realm
|
|
470
|
+
"""
|
|
471
|
+
# initialize the return variable
|
|
472
|
+
result: str | None = None
|
|
473
|
+
|
|
474
|
+
if logger:
|
|
475
|
+
logger.debug(msg="Requesting a token with "
|
|
476
|
+
f"administrative rights to IAM Server '{iam_server}'")
|
|
477
|
+
|
|
478
|
+
# obtain the IAM server's registry
|
|
479
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
480
|
+
errors=errors,
|
|
481
|
+
logger=logger)
|
|
482
|
+
if registry:
|
|
483
|
+
if registry[IamParam.ADMIN_ID] and registry[IamParam.ADMIN_SECRET]:
|
|
484
|
+
header_data: dict[str, str] = {
|
|
485
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
486
|
+
}
|
|
487
|
+
body_data: dict[str, str] = {
|
|
488
|
+
"grant_type": "password",
|
|
489
|
+
"username": registry[IamParam.ADMIN_ID],
|
|
490
|
+
"password": registry[IamParam.ADMIN_SECRET],
|
|
491
|
+
"client_id": "admin-cli"
|
|
492
|
+
}
|
|
493
|
+
token_data: dict[str, Any] = __post_for_token(iam_server=iam_server,
|
|
494
|
+
header_data=header_data,
|
|
495
|
+
body_data=body_data,
|
|
496
|
+
errors=errors,
|
|
497
|
+
logger=logger)
|
|
498
|
+
if token_data:
|
|
499
|
+
# obtain the token
|
|
500
|
+
result = token_data["access_token"]
|
|
501
|
+
if logger:
|
|
502
|
+
logger.debug(msg="Administrative token obtained")
|
|
503
|
+
|
|
504
|
+
elif logger or isinstance(errors, list):
|
|
505
|
+
msg: str = ("Credentials for administrator of realm "
|
|
506
|
+
f"'{registry[IamParam.CLIENT_REALM]}' "
|
|
507
|
+
f"at IAM server '{iam_server}' not provided")
|
|
508
|
+
if logger:
|
|
509
|
+
logger.error(msg=msg)
|
|
510
|
+
if isinstance(errors, list):
|
|
511
|
+
errors.append(msg)
|
|
512
|
+
|
|
513
|
+
elif logger or isinstance(errors, list):
|
|
514
|
+
msg: str = f"Unknown IAM server {iam_server}"
|
|
515
|
+
if logger:
|
|
516
|
+
logger.error(msg=msg)
|
|
517
|
+
if isinstance(errors, list):
|
|
518
|
+
errors.append(msg)
|
|
519
|
+
|
|
520
|
+
return result
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def __get_client_secret(iam_server: IamServer,
|
|
524
|
+
errors: list[str] | None,
|
|
525
|
+
logger: Logger | None) -> str:
|
|
526
|
+
"""
|
|
527
|
+
Retrieve the client's secret password.
|
|
528
|
+
|
|
529
|
+
If it has not been provided at *iam_server*'s setup time, an attempt is made to obtain it
|
|
530
|
+
from the *IAM* server itself. This would require the realm administrator's identification and
|
|
531
|
+
secret password to have been provided, instead.
|
|
532
|
+
|
|
533
|
+
:param iam_server: the reference *IAM* server
|
|
534
|
+
:param errors: incidental errors
|
|
535
|
+
:param logger: optional logger
|
|
536
|
+
:return: the client's secret password, or *None* if error
|
|
537
|
+
"""
|
|
538
|
+
# retrieve client's secret password stored in the IAM server's registry
|
|
539
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
540
|
+
errors=errors,
|
|
541
|
+
logger=logger)
|
|
542
|
+
result: str = registry[IamParam.CLIENT_SECRET] if registry else None
|
|
543
|
+
|
|
544
|
+
if not result and not errors:
|
|
545
|
+
# obtain a token with administrative rights
|
|
546
|
+
token: str = __get_administrative_token(iam_server=iam_server,
|
|
547
|
+
errors=errors,
|
|
548
|
+
logger=logger)
|
|
549
|
+
if token:
|
|
550
|
+
realm: str = registry[IamParam.CLIENT_REALM]
|
|
551
|
+
client_id: str = registry[IamParam.CLIENT_ID]
|
|
552
|
+
if logger:
|
|
553
|
+
logger.debug(msg=f"Obtaining the UUID for client '{client_id}', "
|
|
554
|
+
f"in realm '{realm}' at IAM server '{iam_server}'")
|
|
555
|
+
# obtain the client UUID
|
|
556
|
+
url: str = f"{registry[IamParam.URL_BASE]}/realms/{realm}/clients"
|
|
557
|
+
header_data: dict[str, str] = {
|
|
558
|
+
"Authorization": f"Bearer {token}",
|
|
559
|
+
"Content-Type": "application/json"
|
|
560
|
+
}
|
|
561
|
+
params: dict[str, str] = {
|
|
562
|
+
"clientId": client_id
|
|
563
|
+
}
|
|
564
|
+
clients: list[dict[str, Any]] = __get_for_data(url=url,
|
|
565
|
+
header_data=header_data,
|
|
566
|
+
params=params,
|
|
567
|
+
errors=errors,
|
|
568
|
+
logger=logger)
|
|
569
|
+
if clients:
|
|
570
|
+
# obtain the client's secret password
|
|
571
|
+
client_uuid: str = clients[0]["id"]
|
|
572
|
+
if logger:
|
|
573
|
+
logger.debug(msg=f"Obtaining the secret for client UUID '{client_uuid}', "
|
|
574
|
+
f"in realm '{realm}' at IAM server '{iam_server}'")
|
|
575
|
+
url += f"/{client_uuid}/client-secret"
|
|
576
|
+
reply: dict[str, Any] = __get_for_data(url=url,
|
|
577
|
+
header_data=header_data,
|
|
578
|
+
params=None,
|
|
579
|
+
errors=errors,
|
|
580
|
+
logger=logger)
|
|
581
|
+
if reply:
|
|
582
|
+
# store the client's secret password and return it
|
|
583
|
+
result = reply["value"]
|
|
584
|
+
registry[IamParam.CLIENT_ID] = result
|
|
585
|
+
return result
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def __get_for_data(url: str,
|
|
589
|
+
header_data: dict[str, str],
|
|
590
|
+
params: dict[str, Any] | None,
|
|
591
|
+
errors: list[str] | None,
|
|
592
|
+
logger: Logger | None) -> Any:
|
|
593
|
+
"""
|
|
594
|
+
Send a *GET* request to *url* and return the data obtained.
|
|
595
|
+
|
|
596
|
+
:param url: the target URL
|
|
597
|
+
:param header_data: the data to send in the header of the request
|
|
598
|
+
:param params: the query parameters to send in the request
|
|
599
|
+
:param errors: incidental errors
|
|
600
|
+
:param logger: optional logger
|
|
601
|
+
:return: the data requested, or *None* if error
|
|
602
|
+
"""
|
|
603
|
+
# initialize the return variable
|
|
604
|
+
result: Any = None
|
|
605
|
+
|
|
606
|
+
# log the GET
|
|
607
|
+
if logger:
|
|
608
|
+
logger.debug(msg=f"GET {url}, {json.dumps(obj=params,
|
|
609
|
+
ensure_ascii=False)}")
|
|
610
|
+
try:
|
|
611
|
+
response: requests.Response = requests.get(url=url,
|
|
612
|
+
headers=header_data,
|
|
613
|
+
params=params)
|
|
614
|
+
if response.status_code == 200:
|
|
615
|
+
# request succeeded
|
|
616
|
+
result = response.json() or {}
|
|
617
|
+
if logger:
|
|
618
|
+
logger.debug(msg=f"GET success, {json.dumps(obj=result,
|
|
619
|
+
ensure_ascii=False)}")
|
|
620
|
+
else:
|
|
621
|
+
# request failed, report the problem
|
|
622
|
+
msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
|
|
623
|
+
if hasattr(response, "content") and response.content:
|
|
624
|
+
msg += f", content '{response.content}'"
|
|
625
|
+
if logger:
|
|
626
|
+
logger.error(msg=msg)
|
|
627
|
+
if isinstance(errors, list):
|
|
628
|
+
errors.append(msg)
|
|
629
|
+
except Exception as e:
|
|
630
|
+
# the operation raised an exception
|
|
631
|
+
msg: str = exc_format(exc=e,
|
|
632
|
+
exc_info=sys.exc_info())
|
|
633
|
+
if logger:
|
|
634
|
+
logger.error(msg=msg)
|
|
635
|
+
if isinstance(errors, list):
|
|
636
|
+
errors.append(msg)
|
|
637
|
+
|
|
638
|
+
return result
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def __post_data(url: str,
|
|
642
|
+
header_data: dict[str, str],
|
|
643
|
+
body_data: dict[str, Any],
|
|
644
|
+
errors: list[str] | None,
|
|
645
|
+
logger: Logger | None) -> None:
|
|
646
|
+
"""
|
|
647
|
+
Submit a *POST* request to *url*.
|
|
648
|
+
|
|
649
|
+
: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
|
|
651
|
+
:param errors: incidental errors
|
|
652
|
+
:param logger: optional logger
|
|
653
|
+
"""
|
|
654
|
+
# log the POST
|
|
655
|
+
if logger:
|
|
656
|
+
logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
|
|
657
|
+
ensure_ascii=False)}")
|
|
658
|
+
try:
|
|
659
|
+
response: requests.Response = requests.get(url=url,
|
|
660
|
+
headers=header_data,
|
|
661
|
+
data=body_data)
|
|
662
|
+
if response.status_code >= 400:
|
|
663
|
+
# request failed, report the problem
|
|
664
|
+
msg = f"POST failure, status {response.status_code}, reason {response.reason}"
|
|
665
|
+
if hasattr(response, "content") and response.content:
|
|
666
|
+
msg += f", content '{response.content}'"
|
|
667
|
+
if logger:
|
|
668
|
+
logger.error(msg=msg)
|
|
669
|
+
if isinstance(errors, list):
|
|
670
|
+
errors.append(msg)
|
|
671
|
+
elif logger:
|
|
672
|
+
logger.debug(msg=f"POST success")
|
|
673
|
+
except Exception as e:
|
|
674
|
+
# the operation raised an exception
|
|
675
|
+
msg = exc_format(exc=e,
|
|
676
|
+
exc_info=sys.exc_info())
|
|
677
|
+
if logger:
|
|
678
|
+
logger.error(msg=msg)
|
|
679
|
+
if isinstance(errors, list):
|
|
680
|
+
errors.append(msg)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def __post_for_token(iam_server: IamServer,
|
|
684
|
+
header_data: dict[str, str],
|
|
685
|
+
body_data: dict[str, Any],
|
|
686
|
+
errors: list[str] | None,
|
|
687
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
688
|
+
"""
|
|
689
|
+
Send a *POST* request to *iam_server* and return the authentication token data obtained.
|
|
690
|
+
|
|
691
|
+
For token acquisition, *body_data* will have the attributes:
|
|
692
|
+
- "grant_type": "authorization_code"
|
|
693
|
+
- "code": <16-character-random-code>
|
|
694
|
+
- "redirect_uri": <redirect-uri>
|
|
695
|
+
|
|
696
|
+
For token refresh, *body_data* will have the attributes:
|
|
697
|
+
- "grant_type": "refresh_token"
|
|
698
|
+
- "refresh_token": <current-refresh-token>
|
|
699
|
+
|
|
700
|
+
For token exchange, *body_data* will have the attributes:
|
|
701
|
+
- "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
702
|
+
- "subject_token": <token-to-be-exchanged>,
|
|
703
|
+
- "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
704
|
+
- "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
705
|
+
- "audience": <client-id>,
|
|
706
|
+
- "subject_issuer": "oidc"
|
|
707
|
+
|
|
708
|
+
For administrative token acquisition, *body_data* will have the attributes:
|
|
709
|
+
- "grant_type": "password"
|
|
710
|
+
- "username": <realm-administrator-identification>
|
|
711
|
+
- "password": <realm-administrator-secret>
|
|
712
|
+
|
|
713
|
+
These attributes are then added to *body_data*, except for acquiring administrative tokens:
|
|
714
|
+
- "client_id": <client-id>
|
|
715
|
+
- "client_secret": <client-secret>
|
|
716
|
+
|
|
717
|
+
If the operation is successful, the token data is stored in the *IAM* server's registry, and returned.
|
|
718
|
+
Otherwise, *errors* will contain the appropriate error message.
|
|
719
|
+
|
|
720
|
+
The typical data set returned contains the following attributes:
|
|
721
|
+
{
|
|
722
|
+
"token_type": "Bearer",
|
|
723
|
+
"access_token": <str>,
|
|
724
|
+
"expires_in": <number-of-seconds>,
|
|
725
|
+
"refresh_token": <str>,
|
|
726
|
+
"refesh_expires_in": <number-of-seconds>
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
:param iam_server: the reference registered *IAM* server
|
|
730
|
+
:param header_data: the data to send in the header of the request
|
|
731
|
+
:param body_data: the data to send in the body of the request
|
|
732
|
+
:param errors: incidental errors
|
|
733
|
+
:param logger: optional logger
|
|
734
|
+
:return: the token data, or *None* if error
|
|
735
|
+
"""
|
|
736
|
+
# initialize the return variable
|
|
737
|
+
result: dict[str, Any] | None = None
|
|
738
|
+
|
|
739
|
+
err_msg: str | None = None
|
|
740
|
+
with _iam_lock:
|
|
741
|
+
# retrieve the IAM server's registry
|
|
742
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
743
|
+
errors=errors,
|
|
744
|
+
logger=logger)
|
|
745
|
+
if registry:
|
|
746
|
+
# complete the data to send in body of request
|
|
747
|
+
if body_data["grant_type"] != "password":
|
|
748
|
+
body_data["client_id"] = registry[IamParam.CLIENT_ID]
|
|
749
|
+
|
|
750
|
+
# build the URL
|
|
751
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
752
|
+
url: str = f"{base_url}/protocol/openid-connect/token"
|
|
753
|
+
# 'client_secret' data must not be shown in log
|
|
754
|
+
msg: str = f"POST {url}, {json.dumps(obj=body_data,
|
|
755
|
+
ensure_ascii=False)}"
|
|
756
|
+
if body_data["grant_type"] != "password":
|
|
757
|
+
# 'client_secret' not required for requesting tokens from staging environments
|
|
758
|
+
client_secret: str = __get_client_secret(iam_server=iam_server,
|
|
759
|
+
errors=None,
|
|
760
|
+
logger=logger)
|
|
761
|
+
if client_secret:
|
|
762
|
+
body_data["client_secret"] = client_secret
|
|
763
|
+
# log the POST
|
|
764
|
+
if logger:
|
|
765
|
+
logger.debug(msg=msg)
|
|
766
|
+
|
|
767
|
+
# obtain the token
|
|
768
|
+
try:
|
|
769
|
+
# typical return on a token request:
|
|
770
|
+
# {
|
|
771
|
+
# "token_type": "Bearer",
|
|
772
|
+
# "access_token": <str>,
|
|
773
|
+
# "expires_in": <number-of-seconds>,
|
|
774
|
+
# "refresh_token": <str>,
|
|
775
|
+
# "refesh_expires_in": <number-of-seconds>
|
|
776
|
+
# }
|
|
777
|
+
response: requests.Response = requests.post(url=url,
|
|
778
|
+
headers=header_data,
|
|
779
|
+
data=body_data)
|
|
780
|
+
if response.status_code == 200:
|
|
781
|
+
# request succeeded
|
|
782
|
+
result = response.json()
|
|
783
|
+
if logger:
|
|
784
|
+
logger.debug(msg=f"POST success, {json.dumps(obj=result,
|
|
785
|
+
ensure_ascii=False)}")
|
|
786
|
+
else:
|
|
787
|
+
# request failed, report the problem
|
|
788
|
+
err_msg = f"POST failure, status {response.status_code}, reason {response.reason}"
|
|
789
|
+
if hasattr(response, "content") and response.content:
|
|
790
|
+
err_msg += f", content '{response.content}'"
|
|
791
|
+
if logger:
|
|
792
|
+
logger.error(msg=err_msg)
|
|
793
|
+
except Exception as e:
|
|
794
|
+
# the operation raised an exception
|
|
795
|
+
err_msg = exc_format(exc=e,
|
|
796
|
+
exc_info=sys.exc_info())
|
|
797
|
+
if logger:
|
|
798
|
+
logger.error(msg=err_msg)
|
|
799
|
+
|
|
800
|
+
if err_msg and isinstance(errors, list):
|
|
801
|
+
errors.append(err_msg)
|
|
802
|
+
|
|
803
|
+
return result
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def __validate_and_store(iam_server: IamServer,
|
|
807
|
+
user_data: dict[str, Any],
|
|
808
|
+
token_data: dict[str, Any],
|
|
809
|
+
now: int,
|
|
810
|
+
errors: list[str] | None,
|
|
811
|
+
logger: Logger) -> tuple[str, str] | None:
|
|
812
|
+
"""
|
|
813
|
+
Validate and store the token data.
|
|
814
|
+
|
|
815
|
+
The typical *token_data* contains the following attributes:
|
|
816
|
+
{
|
|
817
|
+
"token_type": "Bearer",
|
|
818
|
+
"access_token": <str>,
|
|
819
|
+
"expires_in": <number-of-seconds>,
|
|
820
|
+
"refresh_token": <str>,
|
|
821
|
+
"refesh_expires_in": <number-of-seconds>
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
:param iam_server: the reference registered *IAM* server
|
|
825
|
+
:param user_data: the aurthentication data kepth in *iam_server*'s registry
|
|
826
|
+
:param token_data: the token data
|
|
827
|
+
:param errors: incidental errors
|
|
828
|
+
:param logger: optional logger
|
|
829
|
+
:return: tuple containing the user identification and the validated and stored token, or *None* if error
|
|
830
|
+
"""
|
|
831
|
+
# initialize the return variable
|
|
832
|
+
result: tuple[str, str] | None = None
|
|
833
|
+
|
|
834
|
+
with _iam_lock:
|
|
835
|
+
# retrieve the IAM server's registry
|
|
836
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
837
|
+
errors=errors,
|
|
838
|
+
logger=logger)
|
|
839
|
+
if registry:
|
|
840
|
+
token: str = token_data.get("access_token")
|
|
841
|
+
user_data["access-token"] = token
|
|
842
|
+
# keep current refresh token if a new one is not provided
|
|
843
|
+
if token_data.get("refresh_token"):
|
|
844
|
+
user_data["refresh-token"] = token_data.get("refresh_token")
|
|
845
|
+
user_data["access-expiration"] = now + token_data.get("expires_in")
|
|
846
|
+
refresh_exp: int = user_data.get("refresh_expires_in")
|
|
847
|
+
user_data["refresh-expiration"] = (now + refresh_exp) if refresh_exp else sys.maxsize
|
|
848
|
+
public_key: str = _get_public_key(iam_server=iam_server,
|
|
849
|
+
errors=errors,
|
|
850
|
+
logger=logger)
|
|
851
|
+
recipient_attr = registry[IamParam.RECIPIENT_ATTR]
|
|
852
|
+
login_id = user_data.pop("login-id", None)
|
|
853
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
854
|
+
claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
855
|
+
issuer=base_url,
|
|
856
|
+
recipient_id=login_id,
|
|
857
|
+
recipient_attr=recipient_attr,
|
|
858
|
+
public_key=public_key,
|
|
859
|
+
errors=errors,
|
|
860
|
+
logger=logger)
|
|
861
|
+
if claims:
|
|
862
|
+
users: dict[str, dict[str, Any]] = _get_iam_users(iam_server=iam_server,
|
|
863
|
+
errors=errors,
|
|
864
|
+
logger=logger)
|
|
865
|
+
# must test with 'not errors'
|
|
866
|
+
if not errors:
|
|
867
|
+
user_id: str = login_id if login_id else claims["payload"][recipient_attr]
|
|
868
|
+
users[user_id] = user_data
|
|
869
|
+
result = (user_id, token)
|
|
870
|
+
return result
|