pypomes-iam 0.5.6__py3-none-any.whl → 0.5.8__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 +6 -16
- pypomes_iam/iam_actions.py +357 -47
- pypomes_iam/iam_common.py +213 -52
- pypomes_iam/iam_pomes.py +111 -63
- pypomes_iam/iam_services.py +90 -5
- pypomes_iam/provider_pomes.py +46 -26
- pypomes_iam/token_pomes.py +0 -2
- {pypomes_iam-0.5.6.dist-info → pypomes_iam-0.5.8.dist-info}/METADATA +1 -1
- pypomes_iam-0.5.8.dist-info/RECORD +11 -0
- pypomes_iam/jusbr_pomes.py +0 -125
- pypomes_iam/keycloak_pomes.py +0 -136
- pypomes_iam-0.5.6.dist-info/RECORD +0 -13
- {pypomes_iam-0.5.6.dist-info → pypomes_iam-0.5.8.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.6.dist-info → pypomes_iam-0.5.8.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_common.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
import sys
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from enum import StrEnum
|
|
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,
|
|
8
|
+
env_get_int, env_get_str, env_get_enum, env_get_enums, exc_format
|
|
9
|
+
)
|
|
7
10
|
from pypomes_crypto import crypto_jwk_convert
|
|
8
11
|
from threading import RLock
|
|
9
12
|
from typing import Any, Final
|
|
@@ -13,8 +16,120 @@ class IamServer(StrEnum):
|
|
|
13
16
|
"""
|
|
14
17
|
Supported IAM servers.
|
|
15
18
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
JUSRBR = auto()
|
|
20
|
+
KEYCLOAK = auto()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IamParam(StrEnum):
|
|
24
|
+
"""
|
|
25
|
+
Parameters for configuring *IAM* servers.
|
|
26
|
+
"""
|
|
27
|
+
ADMIN_ID = "admin-id"
|
|
28
|
+
ADMIN_SECRET = "admin-secret"
|
|
29
|
+
CLIENT_ID = "client-id"
|
|
30
|
+
CLIENT_REALM = "client-realm"
|
|
31
|
+
CLIENT_SECRET = "client-secret"
|
|
32
|
+
ENDPOINT_CALLBACK = "endpoint-callback"
|
|
33
|
+
ENDPOINT_LOGIN = "endpoint-login"
|
|
34
|
+
ENDPOINT_LOGOUT = "endpoint_logout"
|
|
35
|
+
ENDPOINT_TOKEN = "endpoint-token"
|
|
36
|
+
ENDPOINT_EXCHANGE = "endpoint-exchange"
|
|
37
|
+
LOGIN_TIMEOUT = "login-timeout"
|
|
38
|
+
PK_EXPIRATION = "pk-expiration"
|
|
39
|
+
PK_LIFETIME = "pk-lifetime"
|
|
40
|
+
PUBLIC_KEY = "public-key"
|
|
41
|
+
RECIPIENT_ATTR = "recipient-attr"
|
|
42
|
+
URL_BASE = "url-base"
|
|
43
|
+
USERS = "users"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class UserParam(StrEnum):
|
|
47
|
+
"""
|
|
48
|
+
Parameters for handling *IAM* users.
|
|
49
|
+
"""
|
|
50
|
+
ACCESS_TOKEN = "access-token"
|
|
51
|
+
REFRESH_TOKEN = "refresh-token"
|
|
52
|
+
ACCESS_EXPIRATION = "access-expiration"
|
|
53
|
+
REFRESH_EXPIRATION = "refresh-expiration"
|
|
54
|
+
# transient attributes
|
|
55
|
+
LOGIN_EXPIRATION = "login-expiration"
|
|
56
|
+
LOGIN_ID = "login-id"
|
|
57
|
+
REDIRECT_URI = "redirect-uri"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __get_iam_data() -> dict[IamServer, dict[IamParam, Any]]:
|
|
61
|
+
"""
|
|
62
|
+
Establish the configuration data for select *IAM* servers, from environment variables.
|
|
63
|
+
|
|
64
|
+
The preferred way to specify configuration parameters is dynamically with *iam_setup()*;.
|
|
65
|
+
Specifying configuration parameters with environment variables can be done in two ways:
|
|
66
|
+
|
|
67
|
+
1. for a single *IAM* server, specify the data set
|
|
68
|
+
- *<APP_PREFIX>_IAM_SERVER* (required, one of *jusbr*, *keycloak*)
|
|
69
|
+
- *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed only if administrative duties are performed)
|
|
70
|
+
- *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed only if administrative duties are performed)
|
|
71
|
+
- *<APP_PREFIX>_IAM_CLIENT_ID* (required)
|
|
72
|
+
- *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
|
|
73
|
+
- *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
|
|
74
|
+
- *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (optional)
|
|
75
|
+
- *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (optional)
|
|
76
|
+
- *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (optional)
|
|
77
|
+
- *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (optional)
|
|
78
|
+
- *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (optional)
|
|
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. the parameters *PUBLIC_KEY*, *PK_EXPIRATION*, and *USERS* cannot be assigned values,
|
|
85
|
+
as they are reserved for internal use
|
|
86
|
+
|
|
87
|
+
3. for multiple *IAM* servers, specify a comma-separated list of servers in
|
|
88
|
+
*<APP_PREFIX>_IAM_SERVERS*, and for each server, specify the data set above,
|
|
89
|
+
respectively replacing *_IAM_* with *_JUSBR_* or *_KEYCLOAK_*, for the servers listed above
|
|
90
|
+
|
|
91
|
+
:return: the configuration data for the selected *IAM* servers
|
|
92
|
+
"""
|
|
93
|
+
# initialize the return valiable
|
|
94
|
+
result: dict[IamServer, dict[IamParam, Any]] = {}
|
|
95
|
+
|
|
96
|
+
servers: list[IamServer] = []
|
|
97
|
+
single_server: IamServer = env_get_enum(key=f"{APP_PREFIX}_IAM_SERVER",
|
|
98
|
+
enum_class=IamServer)
|
|
99
|
+
if single_server:
|
|
100
|
+
default_setup: bool = True
|
|
101
|
+
servers.append(single_server)
|
|
102
|
+
else:
|
|
103
|
+
default_setup: bool = False
|
|
104
|
+
multi_servers: list[IamServer] = env_get_enums(key=f"{APP_PREFIX}_IAM_SERVERS",
|
|
105
|
+
enum_class=IamServer)
|
|
106
|
+
if multi_servers:
|
|
107
|
+
servers.extend(multi_servers)
|
|
108
|
+
|
|
109
|
+
for server in servers:
|
|
110
|
+
if default_setup:
|
|
111
|
+
prefix: str = "IAM"
|
|
112
|
+
default_setup = False
|
|
113
|
+
else:
|
|
114
|
+
prefix: str = server
|
|
115
|
+
result[server] = {
|
|
116
|
+
IamParam.ADMIN_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_ID"),
|
|
117
|
+
IamParam.ADMIN_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_ADMIN_PWD"),
|
|
118
|
+
IamParam.CLIENT_ID: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_ID"),
|
|
119
|
+
IamParam.CLIENT_REALM: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_REALM"),
|
|
120
|
+
IamParam.CLIENT_SECRET: env_get_str(key=f"{APP_PREFIX}_{prefix}_CLIENT_SECRET"),
|
|
121
|
+
IamParam.LOGIN_TIMEOUT: env_get_int(key=f"{APP_PREFIX}_{prefix}_CLIENT_TIMEOUT"),
|
|
122
|
+
IamParam.ENDPOINT_CALLBACK: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_CALLBACK"),
|
|
123
|
+
IamParam.ENDPOINT_LOGIN: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGIN"),
|
|
124
|
+
IamParam.ENDPOINT_LOGOUT: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_LOGOUT"),
|
|
125
|
+
IamParam.ENDPOINT_TOKEN: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_TOKEN"),
|
|
126
|
+
IamParam.ENDPOINT_EXCHANGE: env_get_str(key=f"{APP_PREFIX}_{prefix}_ENDPOINT_EXCHANGE"),
|
|
127
|
+
IamParam.PK_LIFETIME: env_get_str(key=f"{APP_PREFIX}_{prefix}_PK_LIFETIME"),
|
|
128
|
+
IamParam.RECIPIENT_ATTR: env_get_str(key=f"{APP_PREFIX}_{prefix}_RECIPIENT_ATTR"),
|
|
129
|
+
IamParam.URL_BASE: env_get_str(key=f"{APP_PREFIX}_{prefix}_URL_BASE")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result
|
|
18
133
|
|
|
19
134
|
|
|
20
135
|
# registry structure:
|
|
@@ -23,32 +138,32 @@ class IamServer(StrEnum):
|
|
|
23
138
|
# "base-url": <str>,
|
|
24
139
|
# "client-id": <str>,
|
|
25
140
|
# "client-secret": <str>,
|
|
141
|
+
# "client-realm": <str,
|
|
26
142
|
# "client-timeout": <int>,
|
|
27
143
|
# "recipient-attr": <str>,
|
|
28
144
|
# "public-key": <str>,
|
|
29
145
|
# "pk-lifetime": <int>,
|
|
30
146
|
# "pk-expiration": <int>,
|
|
31
|
-
# "
|
|
147
|
+
# "users": {}
|
|
32
148
|
# },
|
|
33
149
|
# ...
|
|
34
150
|
# }
|
|
35
|
-
# data in "
|
|
151
|
+
# data in "users":
|
|
36
152
|
# {
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# }
|
|
48
|
-
# },
|
|
153
|
+
# "<user-id>": {
|
|
154
|
+
# "access-token": <str>
|
|
155
|
+
# "refresh-token": <str>
|
|
156
|
+
# "access-expiration": <timestamp>,
|
|
157
|
+
# "refresh-expiration": <timestamp>,
|
|
158
|
+
# # transient attributes
|
|
159
|
+
# "login-expiration": <timestamp>,
|
|
160
|
+
# "login-id": <str>,
|
|
161
|
+
# "redirect-uri": <str>
|
|
162
|
+
# },
|
|
49
163
|
# ...
|
|
50
164
|
# }
|
|
51
|
-
_IAM_SERVERS: Final[dict[IamServer, dict[
|
|
165
|
+
_IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = __get_iam_data()
|
|
166
|
+
|
|
52
167
|
|
|
53
168
|
# the lock protecting the data in '_IAM_SERVERS'
|
|
54
169
|
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
@@ -66,15 +181,15 @@ def _iam_server_from_endpoint(endpoint: str,
|
|
|
66
181
|
:param logger: optional logger
|
|
67
182
|
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
68
183
|
"""
|
|
69
|
-
#
|
|
70
|
-
result: IamServer | None
|
|
184
|
+
# initialize the return variable
|
|
185
|
+
result: IamServer | None = None
|
|
71
186
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
187
|
+
for iam_server in _IAM_SERVERS:
|
|
188
|
+
if endpoint.startswith(iam_server):
|
|
189
|
+
result = IamServer.JUSRBR
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
if not result:
|
|
78
193
|
msg: str = f"Unable to find a IAM server to service endpoint '{endpoint}'"
|
|
79
194
|
if logger:
|
|
80
195
|
logger.error(msg=msg)
|
|
@@ -98,8 +213,9 @@ def _iam_server_from_issuer(issuer: str,
|
|
|
98
213
|
# initialize the return variable
|
|
99
214
|
result: IamServer | None = None
|
|
100
215
|
|
|
101
|
-
for iam_server,
|
|
102
|
-
|
|
216
|
+
for iam_server, registry in _IAM_SERVERS.items():
|
|
217
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
218
|
+
if base_url == issuer:
|
|
103
219
|
result = IamServer(iam_server)
|
|
104
220
|
break
|
|
105
221
|
|
|
@@ -119,12 +235,39 @@ def _get_public_key(iam_server: IamServer,
|
|
|
119
235
|
"""
|
|
120
236
|
Obtain the public key used by *iam_server* to sign the authentication tokens.
|
|
121
237
|
|
|
122
|
-
|
|
238
|
+
This is accomplished by requesting the token issuer for its *JWKS* (JSON Web Key Set),
|
|
239
|
+
containing the public keys used for various purposes, as indicated in the attribute *use*:
|
|
240
|
+
- *enc*: the key is intended for encryption
|
|
241
|
+
- *sig*: the key is intended for digital signature
|
|
242
|
+
- *wrap*: the key is intended for key wrapping
|
|
243
|
+
|
|
244
|
+
A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
|
|
245
|
+
{
|
|
246
|
+
"keys": [
|
|
247
|
+
{
|
|
248
|
+
"kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
|
|
249
|
+
"kty": "RSA",
|
|
250
|
+
"alg": "RSA256",
|
|
251
|
+
"use": "sig",
|
|
252
|
+
"n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
|
|
253
|
+
"e": "AQAB",
|
|
254
|
+
"x5c": [
|
|
255
|
+
"MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
|
|
256
|
+
],
|
|
257
|
+
"x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
|
|
258
|
+
"x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
|
|
259
|
+
},
|
|
260
|
+
...
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Once the signature key is obtained, it is converted from its original *JWK* (JSON Web Key) format
|
|
265
|
+
to *PEM* (Privacy-Enhanced Mail) format. The public key is saved in *iam_server*'s registry.
|
|
123
266
|
|
|
124
267
|
:param iam_server: the reference registered *IAM* server
|
|
125
268
|
:param errors: incidental error messages
|
|
126
269
|
:param logger: optional logger
|
|
127
|
-
:return: the public key in *PEM* format, or *None* if
|
|
270
|
+
:return: the public key in *PEM* format, or *None* if error
|
|
128
271
|
"""
|
|
129
272
|
# initialize the return variable
|
|
130
273
|
result: str | None = None
|
|
@@ -134,10 +277,12 @@ def _get_public_key(iam_server: IamServer,
|
|
|
134
277
|
logger=logger)
|
|
135
278
|
if registry:
|
|
136
279
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
137
|
-
if now > registry[
|
|
138
|
-
# obtain
|
|
139
|
-
|
|
280
|
+
if now > registry[IamParam.PK_EXPIRATION]:
|
|
281
|
+
# obtain the JWKS (JSON Web Key Set) from the token issuer
|
|
282
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
283
|
+
url: str = f"{base_url}/protocol/openid-connect/certs"
|
|
140
284
|
if logger:
|
|
285
|
+
logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
|
|
141
286
|
logger.debug(msg=f"GET {url}")
|
|
142
287
|
try:
|
|
143
288
|
response: requests.Response = requests.get(url=url)
|
|
@@ -145,12 +290,28 @@ def _get_public_key(iam_server: IamServer,
|
|
|
145
290
|
# request succeeded
|
|
146
291
|
if logger:
|
|
147
292
|
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
293
|
+
# select the appropriate JWK
|
|
294
|
+
reply: dict[str, list[dict[str, str]]] = response.json()
|
|
295
|
+
jwk: dict[str, str] | None = None
|
|
296
|
+
for key in reply["keys"]:
|
|
297
|
+
if key.get("use") == "sig":
|
|
298
|
+
jwk = key
|
|
299
|
+
break
|
|
300
|
+
if jwk:
|
|
301
|
+
# convert from 'JWK' to 'PEM' and save it for further use
|
|
302
|
+
result = crypto_jwk_convert(jwk=jwk,
|
|
303
|
+
fmt="PEM")
|
|
304
|
+
registry[IamParam.PUBLIC_KEY] = result
|
|
305
|
+
lifetime: int = registry[IamParam.PK_LIFETIME] or 0
|
|
306
|
+
registry[IamParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
|
|
307
|
+
if logger:
|
|
308
|
+
logger.debug("Public key obtained and saved")
|
|
309
|
+
else:
|
|
310
|
+
msg = "Signature public key missing from the token issuer's JWKS"
|
|
311
|
+
if logger:
|
|
312
|
+
logger.error(msg=msg)
|
|
313
|
+
if isinstance(errors, list):
|
|
314
|
+
errors.append(msg)
|
|
154
315
|
elif logger:
|
|
155
316
|
msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
|
|
156
317
|
if hasattr(response, "content") and response.content:
|
|
@@ -167,7 +328,7 @@ def _get_public_key(iam_server: IamServer,
|
|
|
167
328
|
if isinstance(errors, list):
|
|
168
329
|
errors.append(msg)
|
|
169
330
|
else:
|
|
170
|
-
result = registry[
|
|
331
|
+
result = registry[IamParam.PUBLIC_KEY]
|
|
171
332
|
|
|
172
333
|
return result
|
|
173
334
|
|
|
@@ -222,10 +383,10 @@ def _get_user_data(iam_server: IamServer,
|
|
|
222
383
|
result = users.get(user_id)
|
|
223
384
|
if not result:
|
|
224
385
|
result = {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
386
|
+
UserParam.ACCESS_TOKEN: None,
|
|
387
|
+
UserParam.REFRESH_TOKEN: None,
|
|
388
|
+
UserParam.ACCESS_EXPIRATION: int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
389
|
+
UserParam.REFRESH_EXPIRATION: sys.maxsize
|
|
229
390
|
}
|
|
230
391
|
users[user_id] = result
|
|
231
392
|
if logger:
|
|
@@ -251,10 +412,10 @@ def _get_iam_registry(iam_server: IamServer,
|
|
|
251
412
|
result: dict[str, Any] | None
|
|
252
413
|
|
|
253
414
|
match iam_server:
|
|
254
|
-
case IamServer.
|
|
255
|
-
result = _IAM_SERVERS[IamServer.
|
|
256
|
-
case IamServer.
|
|
257
|
-
result = _IAM_SERVERS[IamServer.
|
|
415
|
+
case IamServer.JUSRBR:
|
|
416
|
+
result = _IAM_SERVERS[IamServer.JUSRBR]
|
|
417
|
+
case IamServer.KEYCLOAK:
|
|
418
|
+
result = _IAM_SERVERS[IamServer.KEYCLOAK]
|
|
258
419
|
case _:
|
|
259
420
|
result = None
|
|
260
421
|
msg = f"Unknown IAM server '{iam_server}'"
|
|
@@ -270,14 +431,14 @@ def _get_iam_users(iam_server: IamServer,
|
|
|
270
431
|
errors: list[str] | None,
|
|
271
432
|
logger: Logger | None) -> dict[str, dict[str, Any]]:
|
|
272
433
|
"""
|
|
273
|
-
Retrieve the
|
|
434
|
+
Retrieve the users data storage in *iam_server*'s registry.
|
|
274
435
|
|
|
275
436
|
:param iam_server: the reference registered *IAM* server
|
|
276
437
|
:param errors: incidental error messages
|
|
277
438
|
:param logger: optional logger
|
|
278
|
-
:return: the
|
|
439
|
+
:return: the users data storage in *iam_server*'s registry, or *None* if the server is unknown
|
|
279
440
|
"""
|
|
280
441
|
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
281
442
|
errors=errors,
|
|
282
443
|
logger=logger)
|
|
283
|
-
return registry[
|
|
444
|
+
return registry[IamParam.USERS] if registry else None
|
pypomes_iam/iam_pomes.py
CHANGED
|
@@ -1,82 +1,130 @@
|
|
|
1
|
-
from flask import
|
|
1
|
+
from flask import Flask
|
|
2
|
+
from logging import Logger
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
5
|
from .iam_common import (
|
|
5
|
-
IamServer,
|
|
6
|
-
|
|
6
|
+
_IAM_SERVERS, IamServer, IamParam, _iam_lock
|
|
7
|
+
)
|
|
8
|
+
from .iam_actions import action_token
|
|
9
|
+
from .iam_services import (
|
|
10
|
+
service_login, service_logout, service_callback, service_exchange, service_token
|
|
7
11
|
)
|
|
8
|
-
from .token_pomes import token_get_claims, token_validate
|
|
9
|
-
|
|
10
12
|
|
|
11
|
-
def jwt_required(func: callable) -> callable:
|
|
12
|
-
"""
|
|
13
|
-
Create a decorator to authenticate service endpoints with JWT tokens.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
def iam_setup(flask_app: Flask,
|
|
15
|
+
iam_server: IamServer,
|
|
16
|
+
base_url: str,
|
|
17
|
+
client_id: str,
|
|
18
|
+
client_realm: str,
|
|
19
|
+
recipient_attribute: str,
|
|
20
|
+
client_secret: str = None,
|
|
21
|
+
login_timeout: int = None,
|
|
22
|
+
admin_id: str = None,
|
|
23
|
+
admin_secret: str = None,
|
|
24
|
+
public_key_lifetime: int = None,
|
|
25
|
+
callback_endpoint: str = None,
|
|
26
|
+
login_endpoint: str = None,
|
|
27
|
+
logout_endpoint: str = None,
|
|
28
|
+
token_endpoint: str = None,
|
|
29
|
+
exchange_endpoint: str = None) -> None:
|
|
16
30
|
"""
|
|
17
|
-
|
|
18
|
-
def wrapper(*args, **kwargs) -> Response:
|
|
19
|
-
response: Response = __request_validate(request=request)
|
|
20
|
-
return response if response else func(*args, **kwargs)
|
|
21
|
-
|
|
22
|
-
# prevent a rogue error ("View function mapping is overwriting an existing endpoint function")
|
|
23
|
-
wrapper.__name__ = func.__name__
|
|
31
|
+
Establish the provided parameters for configuring the *IAM* server *iam_server*.
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
The parameters *admin_id* and *admin_* are required only if administrative are task are planned.
|
|
34
|
+
The optional parameter *client_timeout* refers to the maximum time in seconds allowed for the
|
|
35
|
+
user to login at the *IAM* server's login page, and defaults to no time limit.
|
|
26
36
|
|
|
37
|
+
The parameter *client_secret* is required in most requests to the *IAM* server. In the case
|
|
38
|
+
it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
|
|
39
|
+
the first time it is needed.
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
:param flask_app: the Flask application
|
|
42
|
+
:param iam_server: identifies the supported *IAM* server (*jusbr* or *keycloak*)
|
|
43
|
+
:param base_url: base URL to request services
|
|
44
|
+
:param client_id: the client's identification with the *IAM* server
|
|
45
|
+
:param client_realm: the client realm
|
|
46
|
+
:param recipient_attribute: attribute in the token's payload holding the token's subject
|
|
47
|
+
:param client_secret: the client's password with the *IAM* server
|
|
48
|
+
:param login_timeout: timeout for login authentication (in seconds,defaults to no timeout)
|
|
49
|
+
:param admin_id: identifies the realm administrator
|
|
50
|
+
:param admin_secret: password for the realm administrator
|
|
51
|
+
:param public_key_lifetime: how long to use *IAM* server's public key, before refreshing it (in seconds)
|
|
52
|
+
:param callback_endpoint: endpoint for the callback from the front end
|
|
53
|
+
:param login_endpoint: endpoint for redirecting user to the *IAM* server's login page
|
|
54
|
+
:param logout_endpoint: endpoint for terminating user access
|
|
55
|
+
:param token_endpoint: endpoint for retrieving authentication token
|
|
56
|
+
:param exchange_endpoint: endpoint for requesting token exchange
|
|
29
57
|
"""
|
|
30
|
-
Verify whether the HTTP *request* has the proper authorization, as per the JWT standard.
|
|
31
58
|
|
|
32
|
-
|
|
59
|
+
# configure the Keycloak registry
|
|
60
|
+
with _iam_lock:
|
|
61
|
+
_IAM_SERVERS[iam_server] = {
|
|
62
|
+
IamParam.URL_BASE: base_url,
|
|
63
|
+
IamParam.CLIENT_ID: client_id,
|
|
64
|
+
IamParam.CLIENT_REALM: client_realm,
|
|
65
|
+
IamParam.CLIENT_SECRET: client_secret,
|
|
66
|
+
IamParam.LOGIN_TIMEOUT: login_timeout,
|
|
67
|
+
IamParam.RECIPIENT_ATTR: recipient_attribute,
|
|
68
|
+
IamParam.PK_EXPIRATION: 0,
|
|
69
|
+
IamParam.PUBLIC_KEY: None,
|
|
70
|
+
IamParam.USERS: {}
|
|
71
|
+
}
|
|
72
|
+
if admin_id and admin_secret:
|
|
73
|
+
IamParam.ADMIN_ID = admin_id
|
|
74
|
+
IamParam.ADMIN_SECRET = admin_secret
|
|
33
75
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"""
|
|
37
|
-
# initialize the return variable
|
|
38
|
-
result: Response | None = None
|
|
76
|
+
if public_key_lifetime:
|
|
77
|
+
IamParam.PK_LIFETIME = public_key_lifetime
|
|
39
78
|
|
|
40
|
-
#
|
|
41
|
-
|
|
79
|
+
# establish the endpoints
|
|
80
|
+
if callback_endpoint:
|
|
81
|
+
flask_app.add_url_rule(rule=callback_endpoint,
|
|
82
|
+
endpoint=f"{iam_server}-callback",
|
|
83
|
+
view_func=service_callback,
|
|
84
|
+
methods=["GET"])
|
|
85
|
+
if login_endpoint:
|
|
86
|
+
flask_app.add_url_rule(rule=login_endpoint,
|
|
87
|
+
endpoint=f"{iam_server}-login",
|
|
88
|
+
view_func=service_login,
|
|
89
|
+
methods=["GET"])
|
|
90
|
+
if logout_endpoint:
|
|
91
|
+
flask_app.add_url_rule(rule=logout_endpoint,
|
|
92
|
+
endpoint=f"{iam_server}-logout",
|
|
93
|
+
view_func=service_logout,
|
|
94
|
+
methods=["GET"])
|
|
95
|
+
if token_endpoint:
|
|
96
|
+
flask_app.add_url_rule(rule=token_endpoint,
|
|
97
|
+
endpoint=f"{iam_server}-token",
|
|
98
|
+
view_func=service_token,
|
|
99
|
+
methods=["GET"])
|
|
100
|
+
if exchange_endpoint:
|
|
101
|
+
flask_app.add_url_rule(rule=exchange_endpoint,
|
|
102
|
+
endpoint=f"{iam_server}-exchange",
|
|
103
|
+
view_func=service_exchange,
|
|
104
|
+
methods=["POST"])
|
|
42
105
|
|
|
43
|
-
# validate the authorization token
|
|
44
|
-
bad_token: bool = True
|
|
45
|
-
if auth_header and auth_header.startswith("Bearer "):
|
|
46
|
-
# extract and validate the JWT access token
|
|
47
|
-
token: str = auth_header.split(" ")[1]
|
|
48
|
-
claims: dict[str, Any] = token_get_claims(token=token)
|
|
49
|
-
if claims:
|
|
50
|
-
issuer: str = claims["payload"].get("iss")
|
|
51
|
-
recipient_attr: str | None = None
|
|
52
|
-
recipient_id: str = request.values.get("user-id") or request.values.get("login")
|
|
53
|
-
with _iam_lock:
|
|
54
|
-
iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
|
|
55
|
-
errors=None,
|
|
56
|
-
logger=None)
|
|
57
|
-
# public_key: str = _get_public_key(iam_server=iam_server,
|
|
58
|
-
# errors=errors,
|
|
59
|
-
# logger=logger)
|
|
60
|
-
public_key = None
|
|
61
106
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
107
|
+
def iam_get_token(iam_server: IamServer,
|
|
108
|
+
user_id: str,
|
|
109
|
+
errors: list[str] = None,
|
|
110
|
+
logger: Logger = None) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Retrieve an authentication token for *user_id*.
|
|
68
113
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
114
|
+
:param iam_server: identifies the *IAM* server
|
|
115
|
+
:param user_id: identifies the user
|
|
116
|
+
:param errors: incidental errors
|
|
117
|
+
:param logger: optional logger
|
|
118
|
+
:return: the uthentication tokem
|
|
119
|
+
"""
|
|
120
|
+
# declare the return variable
|
|
121
|
+
result: str
|
|
77
122
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
123
|
+
# retrieve the token
|
|
124
|
+
args: dict[str, Any] = {"user-id": user_id}
|
|
125
|
+
with _iam_lock:
|
|
126
|
+
result = action_token(iam_server=iam_server,
|
|
127
|
+
args=args,
|
|
128
|
+
errors=errors,
|
|
129
|
+
logger=logger)
|
|
82
130
|
return result
|