pypomes-iam 0.5.2__py3-none-any.whl → 0.6.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 +16 -18
- pypomes_iam/iam_actions.py +822 -0
- pypomes_iam/iam_common.py +170 -65
- pypomes_iam/iam_pomes.py +140 -570
- pypomes_iam/iam_services.py +113 -28
- pypomes_iam/provider_pomes.py +160 -79
- pypomes_iam/token_pomes.py +0 -2
- {pypomes_iam-0.5.2.dist-info → pypomes_iam-0.6.2.dist-info}/METADATA +1 -2
- pypomes_iam-0.6.2.dist-info/RECORD +11 -0
- pypomes_iam/jusbr_pomes.py +0 -125
- pypomes_iam/keycloak_pomes.py +0 -136
- pypomes_iam-0.5.2.dist-info/RECORD +0 -12
- {pypomes_iam-0.5.2.dist-info → pypomes_iam-0.6.2.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.2.dist-info → pypomes_iam-0.6.2.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/iam_common.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
6
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
7
7
|
from pypomes_crypto import crypto_jwk_convert
|
|
@@ -13,44 +13,109 @@ class IamServer(StrEnum):
|
|
|
13
13
|
"""
|
|
14
14
|
Supported IAM servers.
|
|
15
15
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
JUSBR = auto()
|
|
17
|
+
KEYCLOAK = auto()
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class IamParam(StrEnum):
|
|
21
|
+
"""
|
|
22
|
+
Parameters for configuring *IAM* servers.
|
|
23
|
+
"""
|
|
24
|
+
ADMIN_ID = "admin-id"
|
|
25
|
+
ADMIN_SECRET = "admin-secret"
|
|
26
|
+
CLIENT_ID = "client-id"
|
|
27
|
+
CLIENT_REALM = "client-realm"
|
|
28
|
+
CLIENT_SECRET = "client-secret"
|
|
29
|
+
ENDPOINT_CALLBACK = "endpoint-callback"
|
|
30
|
+
ENDPOINT_LOGIN = "endpoint-login"
|
|
31
|
+
ENDPOINT_LOGOUT = "endpoint_logout"
|
|
32
|
+
ENDPOINT_TOKEN = "endpoint-token"
|
|
33
|
+
ENDPOINT_EXCHANGE = "endpoint-exchange"
|
|
34
|
+
LOGIN_TIMEOUT = "login-timeout"
|
|
35
|
+
PK_EXPIRATION = "pk-expiration"
|
|
36
|
+
PK_LIFETIME = "pk-lifetime"
|
|
37
|
+
PUBLIC_KEY = "public-key"
|
|
38
|
+
RECIPIENT_ATTR = "recipient-attr"
|
|
39
|
+
URL_BASE = "url-base"
|
|
40
|
+
USERS = "users"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UserParam(StrEnum):
|
|
44
|
+
"""
|
|
45
|
+
Parameters for handling *IAM* users.
|
|
46
|
+
"""
|
|
47
|
+
ACCESS_TOKEN = "access-token"
|
|
48
|
+
REFRESH_TOKEN = "refresh-token"
|
|
49
|
+
ACCESS_EXPIRATION = "access-expiration"
|
|
50
|
+
REFRESH_EXPIRATION = "refresh-expiration"
|
|
51
|
+
# transient attributes
|
|
52
|
+
LOGIN_EXPIRATION = "login-expiration"
|
|
53
|
+
LOGIN_ID = "login-id"
|
|
54
|
+
REDIRECT_URI = "redirect-uri"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# The configuration parameters for the IAM servers are specified dynamically dynamically with *iam_setup()*
|
|
58
|
+
# Specifying configuration parameters with environment variables can be done in two ways:
|
|
59
|
+
#
|
|
60
|
+
# 1. for a single *IAM* server, specify the data set
|
|
61
|
+
# - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed if administrative duties are performed)
|
|
62
|
+
# - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed if administrative duties are performed)
|
|
63
|
+
# - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
|
|
64
|
+
# - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
|
|
65
|
+
# - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
|
|
66
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (required)
|
|
67
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (required)
|
|
68
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (required)
|
|
69
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (required)
|
|
70
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_PROVIDER* (optional, needed if requesting tokens to providers)
|
|
71
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (required)
|
|
72
|
+
# - *<APP_PREFIX>_IAM_LOGIN_TIMEOUT* (optional, defaults to no timeout)
|
|
73
|
+
# - *<APP_PREFIX>_IAM_PK_LIFETIME* (optional, defaults to non-terminating lifetime)
|
|
74
|
+
# - *<APP_PREFIX>_IAM_RECIPIENT_ATTR* (required)
|
|
75
|
+
# - *<APP_PREFIX>_IAM_URL_BASE* (required)
|
|
76
|
+
#
|
|
77
|
+
# 2. for multiple *IAM* servers, specify the data set above for each server,
|
|
78
|
+
# respectively replacing *IAM* with a name in *IamServer* (currently, *JUSBR* and *KEYCLOAK* are supported).
|
|
79
|
+
#
|
|
80
|
+
# 3. the parameters *PUBLIC_KEY*, *PK_EXPIRATION*, and *USERS* cannot be assigned values,
|
|
81
|
+
# as they are reserved for internal use
|
|
82
|
+
|
|
20
83
|
# registry structure:
|
|
21
84
|
# { <IamServer>:
|
|
22
85
|
# {
|
|
23
86
|
# "base-url": <str>,
|
|
87
|
+
# "admin-id": <str>,
|
|
88
|
+
# "admin-secret": <str>,
|
|
24
89
|
# "client-id": <str>,
|
|
25
90
|
# "client-secret": <str>,
|
|
91
|
+
# "client-realm": <str,
|
|
26
92
|
# "client-timeout": <int>,
|
|
27
93
|
# "recipient-attr": <str>,
|
|
28
94
|
# "public-key": <str>,
|
|
29
95
|
# "pk-lifetime": <int>,
|
|
30
96
|
# "pk-expiration": <int>,
|
|
31
|
-
# "
|
|
97
|
+
# "users": {}
|
|
32
98
|
# },
|
|
33
99
|
# ...
|
|
34
100
|
# }
|
|
35
|
-
# data in "
|
|
101
|
+
# data in "users":
|
|
36
102
|
# {
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# }
|
|
48
|
-
# },
|
|
103
|
+
# "<user-id>": {
|
|
104
|
+
# "access-token": <str>
|
|
105
|
+
# "refresh-token": <str>
|
|
106
|
+
# "access-expiration": <timestamp>,
|
|
107
|
+
# "refresh-expiration": <timestamp>,
|
|
108
|
+
# # transient attributes
|
|
109
|
+
# "login-expiration": <timestamp>,
|
|
110
|
+
# "login-id": <str>,
|
|
111
|
+
# "redirect-uri": <str>
|
|
112
|
+
# },
|
|
49
113
|
# ...
|
|
50
114
|
# }
|
|
51
|
-
_IAM_SERVERS: Final[dict[IamServer, dict[
|
|
115
|
+
_IAM_SERVERS: Final[dict[IamServer, dict[IamParam, Any]]] = {}
|
|
116
|
+
|
|
52
117
|
|
|
53
|
-
# the lock protecting the data in '
|
|
118
|
+
# the lock protecting the data in '_IAM_SERVERS'
|
|
54
119
|
# (because it is 'Final' and set at declaration time, it can be accessed through simple imports)
|
|
55
120
|
_iam_lock: Final[RLock] = RLock()
|
|
56
121
|
|
|
@@ -66,15 +131,15 @@ def _iam_server_from_endpoint(endpoint: str,
|
|
|
66
131
|
:param logger: optional logger
|
|
67
132
|
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
68
133
|
"""
|
|
69
|
-
#
|
|
70
|
-
result: IamServer | None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
134
|
+
# initialize the return variable
|
|
135
|
+
result: IamServer | None = None
|
|
136
|
+
|
|
137
|
+
for iam_server in _IAM_SERVERS:
|
|
138
|
+
if endpoint.startswith(iam_server):
|
|
139
|
+
result = iam_server
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
if not result:
|
|
78
143
|
msg: str = f"Unable to find a IAM server to service endpoint '{endpoint}'"
|
|
79
144
|
if logger:
|
|
80
145
|
logger.error(msg=msg)
|
|
@@ -98,8 +163,9 @@ def _iam_server_from_issuer(issuer: str,
|
|
|
98
163
|
# initialize the return variable
|
|
99
164
|
result: IamServer | None = None
|
|
100
165
|
|
|
101
|
-
for iam_server,
|
|
102
|
-
|
|
166
|
+
for iam_server, registry in _IAM_SERVERS.items():
|
|
167
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
168
|
+
if base_url == issuer:
|
|
103
169
|
result = IamServer(iam_server)
|
|
104
170
|
break
|
|
105
171
|
|
|
@@ -119,12 +185,39 @@ def _get_public_key(iam_server: IamServer,
|
|
|
119
185
|
"""
|
|
120
186
|
Obtain the public key used by *iam_server* to sign the authentication tokens.
|
|
121
187
|
|
|
122
|
-
|
|
188
|
+
This is accomplished by requesting the token issuer for its *JWKS* (JSON Web Key Set),
|
|
189
|
+
containing the public keys used for various purposes, as indicated in the attribute *use*:
|
|
190
|
+
- *enc*: the key is intended for encryption
|
|
191
|
+
- *sig*: the key is intended for digital signature
|
|
192
|
+
- *wrap*: the key is intended for key wrapping
|
|
193
|
+
|
|
194
|
+
A typical JWKS set has the following format (for simplicity, 'n' and 'x5c' are truncated):
|
|
195
|
+
{
|
|
196
|
+
"keys": [
|
|
197
|
+
{
|
|
198
|
+
"kid": "X2QEcSQ4Tg2M2EK6s2nhRHZH_GwD_zxZtiWVwP4S0tg",
|
|
199
|
+
"kty": "RSA",
|
|
200
|
+
"alg": "RSA256",
|
|
201
|
+
"use": "sig",
|
|
202
|
+
"n": "tQmDmyM3tMFt5FMVMbqbQYpaDPf6A5l4e_kTVDBiHrK_bRlGfkk8hYm5SNzNzCZ...",
|
|
203
|
+
"e": "AQAB",
|
|
204
|
+
"x5c": [
|
|
205
|
+
"MIIClzCCAX8CBgGZY0bqrTANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARpanVk..."
|
|
206
|
+
],
|
|
207
|
+
"x5t": "MHfVp4kBjEZuYOtiaaGsfLCL15Q",
|
|
208
|
+
"x5t#S256": "QADezSLgD8emuonBz8hn8ghTnxo7AHX4NVNkr4luEhk"
|
|
209
|
+
},
|
|
210
|
+
...
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
Once the signature key is obtained, it is converted from its original *JWK* (JSON Web Key) format
|
|
215
|
+
to *PEM* (Privacy-Enhanced Mail) format. The public key is saved in *iam_server*'s registry.
|
|
123
216
|
|
|
124
217
|
:param iam_server: the reference registered *IAM* server
|
|
125
218
|
:param errors: incidental error messages
|
|
126
219
|
:param logger: optional logger
|
|
127
|
-
:return: the public key in *PEM* format, or *None* if
|
|
220
|
+
:return: the public key in *PEM* format, or *None* if error
|
|
128
221
|
"""
|
|
129
222
|
# initialize the return variable
|
|
130
223
|
result: str | None = None
|
|
@@ -134,10 +227,12 @@ def _get_public_key(iam_server: IamServer,
|
|
|
134
227
|
logger=logger)
|
|
135
228
|
if registry:
|
|
136
229
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
137
|
-
if now > registry[
|
|
138
|
-
# obtain
|
|
139
|
-
|
|
230
|
+
if now > registry[IamParam.PK_EXPIRATION]:
|
|
231
|
+
# obtain the JWKS (JSON Web Key Set) from the token issuer
|
|
232
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
233
|
+
url: str = f"{base_url}/protocol/openid-connect/certs"
|
|
140
234
|
if logger:
|
|
235
|
+
logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
|
|
141
236
|
logger.debug(msg=f"GET {url}")
|
|
142
237
|
try:
|
|
143
238
|
response: requests.Response = requests.get(url=url)
|
|
@@ -145,14 +240,30 @@ def _get_public_key(iam_server: IamServer,
|
|
|
145
240
|
# request succeeded
|
|
146
241
|
if logger:
|
|
147
242
|
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
243
|
+
# select the appropriate JWK
|
|
244
|
+
reply: dict[str, list[dict[str, str]]] = response.json()
|
|
245
|
+
jwk: dict[str, str] | None = None
|
|
246
|
+
for key in reply["keys"]:
|
|
247
|
+
if key.get("use") == "sig":
|
|
248
|
+
jwk = key
|
|
249
|
+
break
|
|
250
|
+
if jwk:
|
|
251
|
+
# convert from 'JWK' to 'PEM' and save it for further use
|
|
252
|
+
result = crypto_jwk_convert(jwk=jwk,
|
|
253
|
+
fmt="PEM")
|
|
254
|
+
registry[IamParam.PUBLIC_KEY] = result
|
|
255
|
+
lifetime: int = registry[IamParam.PK_LIFETIME] or 0
|
|
256
|
+
registry[IamParam.PK_EXPIRATION] = now + lifetime if lifetime else sys.maxsize
|
|
257
|
+
if logger:
|
|
258
|
+
logger.debug("Public key obtained and saved")
|
|
259
|
+
else:
|
|
260
|
+
msg = "Signature public key missing from the token issuer's JWKS"
|
|
261
|
+
if logger:
|
|
262
|
+
logger.error(msg=msg)
|
|
263
|
+
if isinstance(errors, list):
|
|
264
|
+
errors.append(msg)
|
|
154
265
|
elif logger:
|
|
155
|
-
msg: str = f"GET failure, status {response.status_code}, reason
|
|
266
|
+
msg: str = f"GET failure, status {response.status_code}, reason {response.reason}"
|
|
156
267
|
if hasattr(response, "content") and response.content:
|
|
157
268
|
msg += f", content {response.content}"
|
|
158
269
|
logger.error(msg=msg)
|
|
@@ -167,7 +278,7 @@ def _get_public_key(iam_server: IamServer,
|
|
|
167
278
|
if isinstance(errors, list):
|
|
168
279
|
errors.append(msg)
|
|
169
280
|
else:
|
|
170
|
-
result = registry[
|
|
281
|
+
result = registry[IamParam.PUBLIC_KEY]
|
|
171
282
|
|
|
172
283
|
return result
|
|
173
284
|
|
|
@@ -222,10 +333,10 @@ def _get_user_data(iam_server: IamServer,
|
|
|
222
333
|
result = users.get(user_id)
|
|
223
334
|
if not result:
|
|
224
335
|
result = {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
336
|
+
UserParam.ACCESS_TOKEN: None,
|
|
337
|
+
UserParam.REFRESH_TOKEN: None,
|
|
338
|
+
UserParam.ACCESS_EXPIRATION: int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
339
|
+
UserParam.REFRESH_EXPIRATION: sys.maxsize
|
|
229
340
|
}
|
|
230
341
|
users[user_id] = result
|
|
231
342
|
if logger:
|
|
@@ -247,21 +358,15 @@ def _get_iam_registry(iam_server: IamServer,
|
|
|
247
358
|
:param logger: optional logger
|
|
248
359
|
:return: the registry associated with *iam_server*, or *None* if the server is unknown
|
|
249
360
|
"""
|
|
250
|
-
#
|
|
251
|
-
result: dict[str, Any]
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
result = None
|
|
260
|
-
msg = f"Unknown IAM server '{iam_server}'"
|
|
261
|
-
if logger:
|
|
262
|
-
logger.error(msg=msg)
|
|
263
|
-
if isinstance(errors, list):
|
|
264
|
-
errors.append(msg)
|
|
361
|
+
# assign the return variable
|
|
362
|
+
result: dict[str, Any] = _IAM_SERVERS.get(iam_server)
|
|
363
|
+
|
|
364
|
+
if not result:
|
|
365
|
+
msg = f"Unknown IAM server '{iam_server}'"
|
|
366
|
+
if logger:
|
|
367
|
+
logger.error(msg=msg)
|
|
368
|
+
if isinstance(errors, list):
|
|
369
|
+
errors.append(msg)
|
|
265
370
|
|
|
266
371
|
return result
|
|
267
372
|
|
|
@@ -270,14 +375,14 @@ def _get_iam_users(iam_server: IamServer,
|
|
|
270
375
|
errors: list[str] | None,
|
|
271
376
|
logger: Logger | None) -> dict[str, dict[str, Any]]:
|
|
272
377
|
"""
|
|
273
|
-
Retrieve the
|
|
378
|
+
Retrieve the users data storage in *iam_server*'s registry.
|
|
274
379
|
|
|
275
380
|
:param iam_server: the reference registered *IAM* server
|
|
276
381
|
:param errors: incidental error messages
|
|
277
382
|
:param logger: optional logger
|
|
278
|
-
:return: the
|
|
383
|
+
:return: the users data storage in *iam_server*'s registry, or *None* if the server is unknown
|
|
279
384
|
"""
|
|
280
385
|
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
281
386
|
errors=errors,
|
|
282
387
|
logger=logger)
|
|
283
|
-
return registry[
|
|
388
|
+
return registry[IamParam.USERS] if registry else None
|