pypomes-iam 0.7.6__py3-none-any.whl → 0.8.5__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 +20 -12
- pypomes_iam/iam_actions.py +188 -66
- pypomes_iam/iam_common.py +71 -29
- pypomes_iam/iam_pomes.py +122 -99
- pypomes_iam/iam_services.py +326 -121
- pypomes_iam/provider_pomes.py +210 -39
- pypomes_iam/token_pomes.py +27 -0
- {pypomes_iam-0.7.6.dist-info → pypomes_iam-0.8.5.dist-info}/METADATA +2 -2
- pypomes_iam-0.8.5.dist-info/RECORD +11 -0
- pypomes_iam-0.7.6.dist-info/RECORD +0 -11
- {pypomes_iam-0.7.6.dist-info → pypomes_iam-0.8.5.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.7.6.dist-info → pypomes_iam-0.8.5.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/__init__.py
CHANGED
|
@@ -1,37 +1,45 @@
|
|
|
1
1
|
from .iam_actions import (
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
iam_callback, iam_exchange,
|
|
3
|
+
iam_login, iam_logout, iam_get_token, iam_userinfo
|
|
4
4
|
)
|
|
5
5
|
from .iam_common import (
|
|
6
6
|
IamServer, IamParam
|
|
7
7
|
)
|
|
8
8
|
from .iam_pomes import (
|
|
9
|
-
|
|
9
|
+
iam_setup_server, iam_setup_endpoints
|
|
10
10
|
)
|
|
11
11
|
from .iam_services import (
|
|
12
|
-
|
|
12
|
+
jwt_required, iam_setup_logger,
|
|
13
|
+
service_setup_server, service_login, service_logout,
|
|
14
|
+
service_get_token, service_userinfo, service_callback,
|
|
15
|
+
service_exchange, service_callback_exchange
|
|
13
16
|
)
|
|
14
17
|
from .provider_pomes import (
|
|
15
|
-
|
|
18
|
+
service_get_token, provider_get_token,
|
|
19
|
+
provider_setup_endpoint, provider_setup_logger, provider_setup_server
|
|
16
20
|
)
|
|
17
21
|
from .token_pomes import (
|
|
18
|
-
token_validate
|
|
22
|
+
token_get_claims, token_get_values, token_validate
|
|
19
23
|
)
|
|
20
24
|
|
|
21
25
|
__all__ = [
|
|
22
26
|
# iam_actions
|
|
23
|
-
"
|
|
24
|
-
"
|
|
27
|
+
"iam_callback", "iam_exchange",
|
|
28
|
+
"iam_login", "iam_logout", "iam_get_token", "iam_userinfo",
|
|
25
29
|
# iam_commons
|
|
26
30
|
"IamServer", "IamParam",
|
|
27
31
|
# iam_pomes
|
|
28
|
-
"
|
|
32
|
+
"iam_setup_server", "iam_setup_endpoints",
|
|
29
33
|
# iam_services
|
|
30
|
-
"jwt_required", "
|
|
34
|
+
"jwt_required", "iam_setup_logger",
|
|
35
|
+
"service_setup_server", "service_login", "service_logout",
|
|
36
|
+
"service_get_token", "service_userinfo", "service_callback",
|
|
37
|
+
"service_exchange", "service_callback_exchange",
|
|
31
38
|
# provider_pomes
|
|
32
|
-
"
|
|
39
|
+
"provider_setup_server", "provider_get_token",
|
|
40
|
+
"provider_setup_endpoint", "provider_setup_logger", "provider_setup_server",
|
|
33
41
|
# token_pomes
|
|
34
|
-
"token_validate"
|
|
42
|
+
"token_get_claims", "token_get_values", "token_validate"
|
|
35
43
|
]
|
|
36
44
|
|
|
37
45
|
from importlib.metadata import version
|
pypomes_iam/iam_actions.py
CHANGED
|
@@ -13,13 +13,13 @@ from .iam_common import (
|
|
|
13
13
|
_get_iam_users, _get_iam_registry, _get_public_key,
|
|
14
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
|
|
|
@@ -32,6 +32,11 @@ def action_login(iam_server: IamServer,
|
|
|
32
32
|
returned by *iam_server* upon login. On success, the appropriate URL for invoking
|
|
33
33
|
the IAM server's authentication page is returned.
|
|
34
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
|
+
|
|
35
40
|
:param iam_server: the reference registered *IAM* server
|
|
36
41
|
:param args: the arguments passed when requesting the service
|
|
37
42
|
:param errors: incidental error messages
|
|
@@ -51,7 +56,7 @@ def action_login(iam_server: IamServer,
|
|
|
51
56
|
# ('oauth_state' is a randomly-generated string, thus 'user_data' is always a new entry)
|
|
52
57
|
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
53
58
|
if target_idp:
|
|
54
|
-
oauth_state += f"idp={target_idp}"
|
|
59
|
+
oauth_state += f"#idp={target_idp}"
|
|
55
60
|
|
|
56
61
|
with _iam_lock:
|
|
57
62
|
# retrieve the user data from the IAM server's registry
|
|
@@ -88,16 +93,16 @@ def action_login(iam_server: IamServer,
|
|
|
88
93
|
return result
|
|
89
94
|
|
|
90
95
|
|
|
91
|
-
def
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
def iam_logout(iam_server: IamServer,
|
|
97
|
+
args: dict[str, Any],
|
|
98
|
+
errors: list[str] = None,
|
|
99
|
+
logger: Logger = None) -> None:
|
|
95
100
|
"""
|
|
96
101
|
Logout the user, by removing all data associating it from *iam_server*'s registry.
|
|
97
102
|
|
|
98
|
-
The user is identified by the attribute *user-id* or
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
101
106
|
|
|
102
107
|
:param iam_server: the reference registered *IAM* server
|
|
103
108
|
:param args: the arguments passed when requesting the service
|
|
@@ -109,33 +114,90 @@ def action_logout(iam_server: IamServer,
|
|
|
109
114
|
|
|
110
115
|
if user_id:
|
|
111
116
|
with _iam_lock:
|
|
112
|
-
# retrieve the data for all users
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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)
|
|
120
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)
|
|
121
176
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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]:
|
|
126
182
|
"""
|
|
127
183
|
Retrieve the authentication token for the user, from *iam_server*.
|
|
128
184
|
|
|
129
185
|
The user is identified by the attribute *user-id* or *login*, provided in *args*.
|
|
130
186
|
|
|
187
|
+
On success, the returned *dict* will contain the following JSON:
|
|
188
|
+
{
|
|
189
|
+
"access-token": <token>,
|
|
190
|
+
"user-id": <user-identification
|
|
191
|
+
}
|
|
192
|
+
|
|
131
193
|
:param iam_server: the reference registered *IAM* server
|
|
132
194
|
:param args: the arguments passed when requesting the service
|
|
133
195
|
:param errors: incidental error messages
|
|
134
196
|
:param logger: optional logger
|
|
135
|
-
:return: the
|
|
197
|
+
:return: the user identification and token issued, or *None* if error
|
|
136
198
|
"""
|
|
137
199
|
# initialize the return variable
|
|
138
|
-
result: str | None = None
|
|
200
|
+
result: dict[str, str] | None = None
|
|
139
201
|
|
|
140
202
|
# obtain the user's identification
|
|
141
203
|
user_id: str = args.get("user-id") or args.get("login")
|
|
@@ -154,7 +216,10 @@ def action_token(iam_server: IamServer,
|
|
|
154
216
|
access_expiration: int = user_data.get(UserParam.ACCESS_EXPIRATION)
|
|
155
217
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
156
218
|
if now < access_expiration:
|
|
157
|
-
result =
|
|
219
|
+
result = {
|
|
220
|
+
"access-token": token,
|
|
221
|
+
"user-id": user_id
|
|
222
|
+
}
|
|
158
223
|
else:
|
|
159
224
|
# access token has expired
|
|
160
225
|
refresh_token: str = user_data[UserParam.REFRESH_TOKEN]
|
|
@@ -162,7 +227,7 @@ def action_token(iam_server: IamServer,
|
|
|
162
227
|
refresh_expiration: int = user_data[UserParam.REFRESH_EXPIRATION]
|
|
163
228
|
if now < refresh_expiration:
|
|
164
229
|
header_data: dict[str, str] = {
|
|
165
|
-
"Content-Type": "application/
|
|
230
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
166
231
|
}
|
|
167
232
|
body_data: dict[str, str] = {
|
|
168
233
|
"grant_type": "refresh_token",
|
|
@@ -182,7 +247,10 @@ def action_token(iam_server: IamServer,
|
|
|
182
247
|
now=now,
|
|
183
248
|
errors=errors,
|
|
184
249
|
logger=logger)
|
|
185
|
-
result =
|
|
250
|
+
result = {
|
|
251
|
+
"access-token": token_info[1],
|
|
252
|
+
"user-id": user_id
|
|
253
|
+
}
|
|
186
254
|
else:
|
|
187
255
|
# refresh token is no longer valid
|
|
188
256
|
user_data[UserParam.REFRESH_TOKEN] = None
|
|
@@ -210,10 +278,10 @@ def action_token(iam_server: IamServer,
|
|
|
210
278
|
return result
|
|
211
279
|
|
|
212
280
|
|
|
213
|
-
def
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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:
|
|
217
285
|
"""
|
|
218
286
|
Entry point for the callback from *iam_server* via the front-end application, on authentication operations.
|
|
219
287
|
|
|
@@ -221,6 +289,10 @@ def action_callback(iam_server: IamServer,
|
|
|
221
289
|
- *state*: used to enhance security during the authorization process, typically to provide *CSRF* protection
|
|
222
290
|
- *code*: the temporary authorization code provided by *iam_server*, to be exchanged for the token
|
|
223
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
|
+
|
|
224
296
|
:param iam_server: the reference registered *IAM* server
|
|
225
297
|
:param args: the arguments passed when requesting the service
|
|
226
298
|
:param errors: incidental errors
|
|
@@ -250,7 +322,7 @@ def action_callback(iam_server: IamServer,
|
|
|
250
322
|
if int(datetime.now(tz=TZ_LOCAL).timestamp()) > expiration:
|
|
251
323
|
errors.append("Operation timeout")
|
|
252
324
|
else:
|
|
253
|
-
pos: int = oauth_state.rfind("idp=")
|
|
325
|
+
pos: int = oauth_state.rfind("#idp=")
|
|
254
326
|
target_idp: str = oauth_state[pos+4:] if pos > 0 else None
|
|
255
327
|
target_iam = IamServer(target_idp) if target_idp in IamServer else None
|
|
256
328
|
target_data: dict[str, Any] = user_data.copy() if target_iam else None
|
|
@@ -285,8 +357,8 @@ def action_callback(iam_server: IamServer,
|
|
|
285
357
|
registry: dict[str, Any] = _get_iam_registry(iam_server,
|
|
286
358
|
errors=errors,
|
|
287
359
|
logger=logger)
|
|
288
|
-
url: str = f"{registry[IamParam.URL_BASE]}/realms/
|
|
289
|
-
|
|
360
|
+
url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
|
|
361
|
+
f"{registry[IamParam.CLIENT_REALM]}/broker/{target_idp}/token")
|
|
290
362
|
header_data: dict[str, str] = {
|
|
291
363
|
"Authorization": f"Bearer {result[1]}",
|
|
292
364
|
"Content-Type": "application/json"
|
|
@@ -315,10 +387,10 @@ def action_callback(iam_server: IamServer,
|
|
|
315
387
|
return result
|
|
316
388
|
|
|
317
389
|
|
|
318
|
-
def
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
390
|
+
def iam_exchange(iam_server: IamServer,
|
|
391
|
+
args: dict[str, Any],
|
|
392
|
+
errors: list[str] = None,
|
|
393
|
+
logger: Logger = None) -> tuple[str, str]:
|
|
322
394
|
"""
|
|
323
395
|
Request *iam_server* to issue a token in exchange for the token obtained from another *IAM* server.
|
|
324
396
|
|
|
@@ -349,12 +421,10 @@ def action_exchange(iam_server: IamServer,
|
|
|
349
421
|
|
|
350
422
|
# obtain the token to be exchanged
|
|
351
423
|
token: str = args.get("access-token") if user_id else None
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
logger=logger) if token else None
|
|
355
|
-
token_issuer: str = _iam_server_from_issuer(issuer=token_claims["payload"]["iss"],
|
|
424
|
+
token_issuer: tuple[str] = token_get_values(token=token,
|
|
425
|
+
keys=("iss",),
|
|
356
426
|
errors=errors,
|
|
357
|
-
logger=logger)
|
|
427
|
+
logger=logger)
|
|
358
428
|
if not errors:
|
|
359
429
|
# HAZARD: only 'IAM_KEYCLOAK' is currently supported
|
|
360
430
|
with _iam_lock:
|
|
@@ -367,6 +437,7 @@ def action_exchange(iam_server: IamServer,
|
|
|
367
437
|
__assert_link(iam_server=iam_server,
|
|
368
438
|
user_id=user_id,
|
|
369
439
|
token=token,
|
|
440
|
+
token_issuer=token_issuer[0],
|
|
370
441
|
errors=errors,
|
|
371
442
|
logger=logger)
|
|
372
443
|
if not errors:
|
|
@@ -409,9 +480,64 @@ def action_exchange(iam_server: IamServer,
|
|
|
409
480
|
return result
|
|
410
481
|
|
|
411
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
|
+
|
|
412
537
|
def __assert_link(iam_server: IamServer,
|
|
413
538
|
user_id: str,
|
|
414
539
|
token: str,
|
|
540
|
+
token_issuer: str,
|
|
415
541
|
errors: list[str] | None,
|
|
416
542
|
logger: Logger | None) -> None:
|
|
417
543
|
"""
|
|
@@ -439,7 +565,7 @@ def __assert_link(iam_server: IamServer,
|
|
|
439
565
|
# obtain the internal user identification for 'user_id'
|
|
440
566
|
if logger:
|
|
441
567
|
logger.debug(msg="Obtaining internal identification "
|
|
442
|
-
f"for user {user_id} in IAM server {iam_server}")
|
|
568
|
+
f"for user '{user_id}' in IAM server '{iam_server}'")
|
|
443
569
|
url: str = f"{registry[IamParam.URL_BASE]}/admin/realms/{registry[IamParam.CLIENT_REALM]}/users"
|
|
444
570
|
header_data: dict[str, str] = {
|
|
445
571
|
"Authorization": f"Bearer {admin_token}",
|
|
@@ -455,12 +581,12 @@ def __assert_link(iam_server: IamServer,
|
|
|
455
581
|
errors=errors,
|
|
456
582
|
logger=logger)
|
|
457
583
|
if users:
|
|
458
|
-
# verify whether the
|
|
459
|
-
#
|
|
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
|
|
460
586
|
internal_id: str = users[0].get("id")
|
|
461
587
|
if logger:
|
|
462
|
-
logger.debug(msg="Obtaining the providers federated
|
|
463
|
-
f"
|
|
588
|
+
logger.debug(msg="Obtaining the providers federated in IAM server "
|
|
589
|
+
f"'{iam_server}', for internal identification '{internal_id}'")
|
|
464
590
|
url = (f"{registry[IamParam.URL_BASE]}/admin/realms/"
|
|
465
591
|
f"{registry[IamParam.CLIENT_REALM]}/users/{internal_id}/federated-identity")
|
|
466
592
|
providers: list[dict[str, Any]] = __get_for_data(url=url,
|
|
@@ -469,13 +595,9 @@ def __assert_link(iam_server: IamServer,
|
|
|
469
595
|
errors=errors,
|
|
470
596
|
logger=logger)
|
|
471
597
|
no_link: bool = True
|
|
472
|
-
|
|
473
|
-
errors=errors,
|
|
474
|
-
logger=logger)
|
|
475
|
-
issuer: str = claims["payload"]["iss"] if claims else None
|
|
476
|
-
provider_name: str = _iam_server_from_issuer(issuer=issuer,
|
|
598
|
+
provider_name: str = _iam_server_from_issuer(issuer=token_issuer,
|
|
477
599
|
errors=errors,
|
|
478
|
-
logger=logger)
|
|
600
|
+
logger=logger)
|
|
479
601
|
if provider_name:
|
|
480
602
|
for provider in providers:
|
|
481
603
|
if provider.get("identityProvider") == provider_name:
|
|
@@ -483,17 +605,17 @@ def __assert_link(iam_server: IamServer,
|
|
|
483
605
|
break
|
|
484
606
|
if no_link:
|
|
485
607
|
# link the identities
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
608
|
+
token_sub: tuple[str] = token_get_values(token=token,
|
|
609
|
+
keys=("sub",),
|
|
610
|
+
errors=errors,
|
|
611
|
+
logger=logger)
|
|
612
|
+
if token_sub:
|
|
491
613
|
if logger:
|
|
492
614
|
logger.debug(msg="Creating an association between identifications "
|
|
493
|
-
f"'{user_id}' and '{token_sub}' in IAM server {iam_server}")
|
|
615
|
+
f"'{user_id}' and '{token_sub}' in IAM server '{iam_server}'")
|
|
494
616
|
url += f"/{provider_name}"
|
|
495
617
|
json_data: dict[str, Any] = {
|
|
496
|
-
"userId": token_sub,
|
|
618
|
+
"userId": token_sub[0],
|
|
497
619
|
"userName": user_id
|
|
498
620
|
}
|
|
499
621
|
__post_json(url=url,
|
|
@@ -797,8 +919,8 @@ def __post_for_token(iam_server: IamServer,
|
|
|
797
919
|
body_data["client_id"] = registry[IamParam.CLIENT_ID]
|
|
798
920
|
|
|
799
921
|
# build the URL
|
|
800
|
-
|
|
801
|
-
|
|
922
|
+
url: str = (f"{registry[IamParam.URL_BASE]}/realms/"
|
|
923
|
+
f"{registry[IamParam.CLIENT_REALM]}/protocol/openid-connect/token")
|
|
802
924
|
# 'client_secret' data must not be shown in log
|
|
803
925
|
msg: str = f"POST {url}, {json.dumps(obj=body_data,
|
|
804
926
|
ensure_ascii=False)}"
|
pypomes_iam/iam_common.py
CHANGED
|
@@ -3,7 +3,10 @@ import sys
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from enum import StrEnum, auto
|
|
5
5
|
from logging import Logger
|
|
6
|
-
from pypomes_core import
|
|
6
|
+
from pypomes_core import (
|
|
7
|
+
APP_PREFIX, TZ_LOCAL, exc_format,
|
|
8
|
+
env_get_str, env_get_int, env_get_enums
|
|
9
|
+
)
|
|
7
10
|
from pypomes_crypto import crypto_jwk_convert
|
|
8
11
|
from threading import RLock
|
|
9
12
|
from typing import Any, Final
|
|
@@ -21,12 +24,14 @@ class IamParam(StrEnum):
|
|
|
21
24
|
"""
|
|
22
25
|
Parameters for configuring *IAM* servers.
|
|
23
26
|
"""
|
|
27
|
+
|
|
24
28
|
ADMIN_ID = "admin-id"
|
|
25
29
|
ADMIN_SECRET = "admin-secret"
|
|
26
30
|
CLIENT_ID = "client-id"
|
|
27
31
|
CLIENT_REALM = "client-realm"
|
|
28
32
|
CLIENT_SECRET = "client-secret"
|
|
29
33
|
ENDPOINT_CALLBACK = "endpoint-callback"
|
|
34
|
+
ENDPOINT_CALLBACK_EXCHANGE = "endpoint-callback-exchange"
|
|
30
35
|
ENDPOINT_LOGIN = "endpoint-login"
|
|
31
36
|
ENDPOINT_LOGOUT = "endpoint_logout"
|
|
32
37
|
ENDPOINT_TOKEN = "endpoint-token"
|
|
@@ -34,8 +39,9 @@ class IamParam(StrEnum):
|
|
|
34
39
|
LOGIN_TIMEOUT = "login-timeout"
|
|
35
40
|
PK_EXPIRATION = "pk-expiration"
|
|
36
41
|
PK_LIFETIME = "pk-lifetime"
|
|
37
|
-
PUBLIC_KEY = "public-key"
|
|
38
42
|
RECIPIENT_ATTR = "recipient-attr"
|
|
43
|
+
# dynamic attributes
|
|
44
|
+
PUBLIC_KEY = "public-key"
|
|
39
45
|
URL_BASE = "url-base"
|
|
40
46
|
USERS = "users"
|
|
41
47
|
|
|
@@ -54,31 +60,66 @@ class UserParam(StrEnum):
|
|
|
54
60
|
REDIRECT_URI = "redirect-uri"
|
|
55
61
|
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
63
|
+
def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
|
|
64
|
+
"""
|
|
65
|
+
Obtain the configuration data for select *IAM* servers.
|
|
66
|
+
|
|
67
|
+
The configuration parameters for the IAM servers are specified dynamically with environment variables,
|
|
68
|
+
or dynamically with calls to *iam_setup_server()*. Specifying configuration parameters with environment
|
|
69
|
+
variables can be done by following these steps:
|
|
70
|
+
|
|
71
|
+
1. Specify *<APP_PREFIX>_AUTH_SERVERS* with a list of names among the values found in *IamServer* class
|
|
72
|
+
(currently, *jusbr* and *keycloak* are supported), and the data set below for each server, where
|
|
73
|
+
*<IAM>* stands for the server's name as presented in *IamServer* class:
|
|
74
|
+
- *<APP_PREFIX>_<IAM>_ADMIN_ID* (optional, required if administrative duties are performed)
|
|
75
|
+
- *<APP_PREFIX>_<IAM>_ADMIN_PWD* (optional, required if administrative duties are performed)
|
|
76
|
+
- *<APP_PREFIX>_<IAM>_CLIENT_ID* (required)
|
|
77
|
+
- *<APP_PREFIX>_<IAM>_CLIENT_REALM* (required)
|
|
78
|
+
- *<APP_PREFIX>_<IAM>_CLIENT_SECRET* (required)
|
|
79
|
+
- *<APP_PREFIX>_<IAM>_LOGIN_TIMEOUT* (optional, defaults to no timeout)
|
|
80
|
+
- *<APP_PREFIX>_<IAM>_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
|
|
81
|
+
- *<APP_PREFIX>_<IAM>_RECIPIENT_ATTR* (required)
|
|
82
|
+
- *<APP_PREFIX>_<IAM>_URL_BASE* (required)
|
|
83
|
+
|
|
84
|
+
2. A group of special environment variables identifying endpoints for authentication services may be specified,
|
|
85
|
+
following the same scheme as presented in item *1* above. These are not part of the *IAM* server's setup,
|
|
86
|
+
but are meant to be used by function *iam_setup_endpoints()*, wherein the values in those variables
|
|
87
|
+
would represent default values for its parameters, respectively:
|
|
88
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_CALLBACK*
|
|
89
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_CALLBACK_EXCHANGE*
|
|
90
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_EXCHANGE*
|
|
91
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_LOGIN*
|
|
92
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_LOGOUT*
|
|
93
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_TOKEN*
|
|
94
|
+
- *<APP_PREFIX>_<IAM>_ENDPOINT_USERINFO*
|
|
95
|
+
|
|
96
|
+
:return: the configuration data for the select *IAM* servers.
|
|
97
|
+
"""
|
|
98
|
+
# initialize the return variable
|
|
99
|
+
result: dict[IamServer, dict[IamParam, Any]] = {}
|
|
100
|
+
|
|
101
|
+
servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_AUTH_SERVERS",
|
|
102
|
+
enum_class=IamServer) or []
|
|
103
|
+
for server in servers:
|
|
104
|
+
prefix = server.name
|
|
105
|
+
result[server] = {
|
|
106
|
+
IamParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
|
|
107
|
+
IamParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_SECRET"),
|
|
108
|
+
IamParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
|
|
109
|
+
IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
|
|
110
|
+
IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
|
|
111
|
+
IamParam.LOGIN_TIMEOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_LOGIN_TIMEOUT"),
|
|
112
|
+
IamParam.PK_LIFETIME: env_get_int(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME"),
|
|
113
|
+
IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
|
|
114
|
+
IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_AUTH_BASE"),
|
|
115
|
+
# dynamically set
|
|
116
|
+
IamParam.PK_EXPIRATION: 0,
|
|
117
|
+
IamParam.PUBLIC_KEY: None,
|
|
118
|
+
IamParam.USERS: {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result
|
|
122
|
+
|
|
82
123
|
|
|
83
124
|
# registry structure:
|
|
84
125
|
# { <IamServer>:
|
|
@@ -91,6 +132,7 @@ class UserParam(StrEnum):
|
|
|
91
132
|
# "client-realm": <str,
|
|
92
133
|
# "client-timeout": <int>,
|
|
93
134
|
# "recipient-attr": <str>,
|
|
135
|
+
# # dynamic attributes
|
|
94
136
|
# "public-key": <str>,
|
|
95
137
|
# "pk-lifetime": <int>,
|
|
96
138
|
# "pk-expiration": <int>,
|
|
@@ -112,10 +154,10 @@ class UserParam(StrEnum):
|
|
|
112
154
|
# },
|
|
113
155
|
# ...
|
|
114
156
|
# }
|
|
115
|
-
_IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] =
|
|
157
|
+
_IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
|
|
116
158
|
|
|
117
159
|
|
|
118
|
-
# the lock protecting the data in '
|
|
160
|
+
# the lock protecting the data in '_<IAM>_SERVERS'
|
|
119
161
|
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
120
162
|
_iam_lock: Final[RLock] = RLock()
|
|
121
163
|
|