pypomes-iam 0.3.0__tar.gz → 0.3.2__tar.gz
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-0.3.0 → pypomes_iam-0.3.2}/PKG-INFO +1 -1
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/pyproject.toml +1 -1
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/src/pypomes_iam/__init__.py +5 -0
- pypomes_iam-0.3.2/src/pypomes_iam/iam_common.py +366 -0
- pypomes_iam-0.3.2/src/pypomes_iam/iam_pomes.py +326 -0
- pypomes_iam-0.3.2/src/pypomes_iam/iam_services.py +243 -0
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/src/pypomes_iam/jusbr_pomes.py +20 -14
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/src/pypomes_iam/keycloak_pomes.py +38 -21
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/src/pypomes_iam/token_pomes.py +19 -4
- pypomes_iam-0.3.0/src/pypomes_iam/iam_common.py +0 -427
- pypomes_iam-0.3.0/src/pypomes_iam/iam_pomes.py +0 -216
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/.gitignore +0 -0
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/LICENSE +0 -0
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/README.md +0 -0
- {pypomes_iam-0.3.0 → pypomes_iam-0.3.2}/src/pypomes_iam/provider_pomes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: A collection of Python pomes, penyeach (IAM modules)
|
|
5
5
|
Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-IAM
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-IAM/issues
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
from .iam_pomes import (
|
|
2
|
+
register_logger, user_login, user_logout, user_token, login_callback
|
|
3
|
+
)
|
|
1
4
|
from .jusbr_pomes import (
|
|
2
5
|
jusbr_setup, jusbr_get_token
|
|
3
6
|
)
|
|
@@ -12,6 +15,8 @@ from .token_pomes import (
|
|
|
12
15
|
)
|
|
13
16
|
|
|
14
17
|
__all__ = [
|
|
18
|
+
# iam_pomes
|
|
19
|
+
"register_logger", "user_login", "user_logout", "user_token", "login_callback",
|
|
15
20
|
# jusbr_pomes
|
|
16
21
|
"jusbr_setup", "jusbr_get_token",
|
|
17
22
|
# keycloak_pomes
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
import sys
|
|
4
|
+
from cachetools import Cache
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from logging import Logger
|
|
8
|
+
from pypomes_core import TZ_LOCAL, exc_format
|
|
9
|
+
from pypomes_crypto import crypto_jwk_convert
|
|
10
|
+
from typing import Any, Final
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IamServer(StrEnum):
|
|
14
|
+
"""
|
|
15
|
+
Supported IAM servers.
|
|
16
|
+
"""
|
|
17
|
+
IAM_JUSRBR = "iam-jusbr",
|
|
18
|
+
IAM_KEYCLOAK = "iam-keycloak"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# the logger for IAM operations
|
|
22
|
+
__IAM_LOGGER: Logger | None = None
|
|
23
|
+
|
|
24
|
+
# registry structure:
|
|
25
|
+
# { <IamServer>:
|
|
26
|
+
# {
|
|
27
|
+
# "client-id": <str>,
|
|
28
|
+
# "client-secret": <str>,
|
|
29
|
+
# "client-timeout": <int>,
|
|
30
|
+
# "recipient-attr": <str>,
|
|
31
|
+
# "public_key": <str>,
|
|
32
|
+
# "pk-lifetime": <int>,
|
|
33
|
+
# "pk-expiration": <int>,
|
|
34
|
+
# "base-url": <str>,
|
|
35
|
+
# "cache": <FIFOCache>,
|
|
36
|
+
# "redirect-uri": <str> <-- transient
|
|
37
|
+
# },
|
|
38
|
+
# ...
|
|
39
|
+
# }
|
|
40
|
+
# data in "cache":
|
|
41
|
+
# {
|
|
42
|
+
# "users": {
|
|
43
|
+
# "<user-id>": {
|
|
44
|
+
# "access-token": <str>
|
|
45
|
+
# "refresh-token": <str>
|
|
46
|
+
# "access-expiration": <timestamp>,
|
|
47
|
+
# "refresh-expiration": <timestamp>,
|
|
48
|
+
# "login-expiration": <timestamp>, <-- transient
|
|
49
|
+
# "login-id": <str>, <-- transient
|
|
50
|
+
# }
|
|
51
|
+
# },
|
|
52
|
+
# ...
|
|
53
|
+
# }
|
|
54
|
+
_IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_logger() -> Logger | None:
|
|
58
|
+
"""
|
|
59
|
+
Retrieve the registered logger for *IAM* operations.
|
|
60
|
+
|
|
61
|
+
:return: the registered logger for *IAM* operations.
|
|
62
|
+
"""
|
|
63
|
+
return __IAM_LOGGER
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _register_logger(logger: Logger) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Register the logger for *IAM* operations
|
|
69
|
+
|
|
70
|
+
:param logger: the logger to be rergistered
|
|
71
|
+
"""
|
|
72
|
+
global __IAM_LOGGER
|
|
73
|
+
__IAM_LOGGER = logger
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_public_key(iam_server: IamServer,
|
|
77
|
+
errors: list[str] | None,
|
|
78
|
+
logger: Logger | None) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Obtain the public key used by *iam_server* to sign the authentication tokens.
|
|
81
|
+
|
|
82
|
+
The public key is saved in *iam_server*'s registry.
|
|
83
|
+
|
|
84
|
+
:param iam_server: the reference registered *IAM* server
|
|
85
|
+
:param errors: incidental error messages
|
|
86
|
+
:param logger: optional logger
|
|
87
|
+
:return: the public key in *PEM* format, or *None* if the server is unknown
|
|
88
|
+
"""
|
|
89
|
+
# initialize the return variable
|
|
90
|
+
result: str | None = None
|
|
91
|
+
|
|
92
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
93
|
+
errors=errors,
|
|
94
|
+
logger=logger)
|
|
95
|
+
if registry:
|
|
96
|
+
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
97
|
+
if now > registry["pk-expiration"]:
|
|
98
|
+
# obtain a new public key
|
|
99
|
+
url: str = f"{registry["base-url"]}/protocol/openid-connect/certs"
|
|
100
|
+
if logger:
|
|
101
|
+
logger.debug(msg=f"GET '{url}'")
|
|
102
|
+
try:
|
|
103
|
+
response: requests.Response = requests.get(url=url)
|
|
104
|
+
if response.status_code == 200:
|
|
105
|
+
# request succeeded
|
|
106
|
+
if logger:
|
|
107
|
+
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
108
|
+
reply: dict[str, Any] = response.json()
|
|
109
|
+
result = crypto_jwk_convert(jwk=reply["keys"][0],
|
|
110
|
+
fmt="PEM")
|
|
111
|
+
registry["public-key"] = result
|
|
112
|
+
lifetime: int = registry["pk-lifetime"] or 0
|
|
113
|
+
registry["pk-expiration"] = now + lifetime
|
|
114
|
+
elif logger:
|
|
115
|
+
msg: str = f"GET failure, status {response.status_code}, reason '{response.reason}'"
|
|
116
|
+
if hasattr(response, "content") and response.content:
|
|
117
|
+
msg += f", content '{response.content}'"
|
|
118
|
+
logger.error(msg=msg)
|
|
119
|
+
if isinstance(errors, list):
|
|
120
|
+
errors.append(msg)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
# the operation raised an exception
|
|
123
|
+
msg = exc_format(exc=e,
|
|
124
|
+
exc_info=sys.exc_info())
|
|
125
|
+
if logger:
|
|
126
|
+
logger.error(msg=msg)
|
|
127
|
+
if isinstance(errors, list):
|
|
128
|
+
errors.append(msg)
|
|
129
|
+
else:
|
|
130
|
+
result = registry["public-key"]
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_login_timeout(iam_server: IamServer,
|
|
136
|
+
errors: list[str] | None,
|
|
137
|
+
logger: Logger) -> int | None:
|
|
138
|
+
"""
|
|
139
|
+
Retrieve the timeout currently applicable for the login operation.
|
|
140
|
+
|
|
141
|
+
:param iam_server: the reference registered *IAM* server
|
|
142
|
+
:param errors: incidental error messages
|
|
143
|
+
:param logger: optional logger
|
|
144
|
+
:return: the current login timeout, or *None* if the server is unknown or none has been set.
|
|
145
|
+
"""
|
|
146
|
+
# initialize the return variable
|
|
147
|
+
result: int | None = None
|
|
148
|
+
|
|
149
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
150
|
+
errors=errors,
|
|
151
|
+
logger=logger)
|
|
152
|
+
if registry:
|
|
153
|
+
timeout: int = registry.get("client-timeout")
|
|
154
|
+
if isinstance(timeout, int) and timeout > 0:
|
|
155
|
+
result = timeout
|
|
156
|
+
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _get_user_data(iam_server: IamServer,
|
|
161
|
+
user_id: str,
|
|
162
|
+
errors: list[str] | None,
|
|
163
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
164
|
+
"""
|
|
165
|
+
Retrieve the data for *user_id* from *iam_server*'s registry.
|
|
166
|
+
|
|
167
|
+
If an entry is not found for *user_id* in the registry, it is created.
|
|
168
|
+
It will remain there until the user is logged out.
|
|
169
|
+
|
|
170
|
+
:param iam_server: the reference registered *IAM* server
|
|
171
|
+
:param errors: incidental error messages
|
|
172
|
+
:param logger: optional logger
|
|
173
|
+
:return: the data for *user_id* in *iam_server*'s registry, or *None* if the server is unknown
|
|
174
|
+
"""
|
|
175
|
+
# initialize the return variable
|
|
176
|
+
result: dict[str, Any] | None = None
|
|
177
|
+
|
|
178
|
+
cache: Cache = _get_iam_cache(iam_server=iam_server,
|
|
179
|
+
errors=errors,
|
|
180
|
+
logger=logger)
|
|
181
|
+
if cache:
|
|
182
|
+
users: dict[str, dict[str, Any]] = cache.get("users")
|
|
183
|
+
result = users.get(user_id)
|
|
184
|
+
if not result:
|
|
185
|
+
result = {
|
|
186
|
+
"access-token": None,
|
|
187
|
+
"refresh-token": None,
|
|
188
|
+
"access-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
189
|
+
"refresh-expiration": sys.maxsize
|
|
190
|
+
}
|
|
191
|
+
users[user_id] = result
|
|
192
|
+
if logger:
|
|
193
|
+
logger.debug(msg=f"Entry for '{user_id}' added to {iam_server}'s registry")
|
|
194
|
+
elif logger:
|
|
195
|
+
logger.debug(msg=f"Entry for '{user_id}' obtained from {iam_server}'s registry")
|
|
196
|
+
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _get_iam_server(endpoint: str,
|
|
201
|
+
errors: list[str] | None,
|
|
202
|
+
logger: Logger | None) -> IamServer | None:
|
|
203
|
+
"""
|
|
204
|
+
Retrieve the registered *IAM* server associated with the service's invocation *endpoint*.
|
|
205
|
+
|
|
206
|
+
:param endpoint: the service's invocation endpoint
|
|
207
|
+
:param errors: incidental error messages
|
|
208
|
+
:param logger: optional logger
|
|
209
|
+
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
210
|
+
"""
|
|
211
|
+
# declare the return variable
|
|
212
|
+
result: IamServer | None
|
|
213
|
+
|
|
214
|
+
if endpoint.startswith("jusbr"):
|
|
215
|
+
result = IamServer.IAM_JUSRBR
|
|
216
|
+
elif endpoint.startswith("keycloak"):
|
|
217
|
+
result = IamServer.IAM_KEYCLOAK
|
|
218
|
+
else:
|
|
219
|
+
result = None
|
|
220
|
+
msg: str = f"Unknown endpoind {endpoint}"
|
|
221
|
+
if logger:
|
|
222
|
+
logger.error(msg=msg)
|
|
223
|
+
if isinstance(errors, list):
|
|
224
|
+
errors.append(msg)
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _get_iam_registry(iam_server: IamServer,
|
|
230
|
+
errors: list[str] | None,
|
|
231
|
+
logger: Logger | None) -> dict[str, Any]:
|
|
232
|
+
"""
|
|
233
|
+
Retrieve the registry associated with *iam_server*.
|
|
234
|
+
|
|
235
|
+
:param iam_server: the reference registered *IAM* server
|
|
236
|
+
:param errors: incidental error messages
|
|
237
|
+
:param logger: optional logger
|
|
238
|
+
:return: the registry associated with *iam_server*, or *None* if the server is unknown
|
|
239
|
+
"""
|
|
240
|
+
# declare the return variable
|
|
241
|
+
result: dict[str, Any] | None
|
|
242
|
+
|
|
243
|
+
match iam_server:
|
|
244
|
+
case IamServer.IAM_JUSRBR:
|
|
245
|
+
result = _IAM_SERVERS[IamServer.IAM_JUSRBR]
|
|
246
|
+
case IamServer.IAM_KEYCLOAK:
|
|
247
|
+
result = _IAM_SERVERS[IamServer.IAM_KEYCLOAK]
|
|
248
|
+
case _:
|
|
249
|
+
result = None
|
|
250
|
+
msg = f"Unknown IAM server '{iam_server}'"
|
|
251
|
+
if logger:
|
|
252
|
+
logger.error(msg=msg)
|
|
253
|
+
if isinstance(errors, list):
|
|
254
|
+
errors.append(msg)
|
|
255
|
+
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _get_iam_cache(iam_server: IamServer,
|
|
260
|
+
errors: list[str] | None,
|
|
261
|
+
logger: Logger | None) -> Cache:
|
|
262
|
+
"""
|
|
263
|
+
Retrieve the cache storage in *iam_server*'s registry.
|
|
264
|
+
|
|
265
|
+
:param iam_server: the reference registered *IAM* server
|
|
266
|
+
:param errors: incidental error messages
|
|
267
|
+
:param logger: optional logger
|
|
268
|
+
:return: the cache storage in *iam_server*'s registry, or *None* if the server is unknown
|
|
269
|
+
"""
|
|
270
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
271
|
+
errors=errors,
|
|
272
|
+
logger=logger)
|
|
273
|
+
return registry["cache"] if registry else None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _post_for_token(iam_server: IamServer,
|
|
277
|
+
body_data: dict[str, Any],
|
|
278
|
+
errors: list[str] | None,
|
|
279
|
+
logger: Logger | None) -> dict[str, Any] | None:
|
|
280
|
+
"""
|
|
281
|
+
Send a POST request to obtain the authentication token data, and return the data received.
|
|
282
|
+
|
|
283
|
+
For token acquisition, *body_data* will have the attributes:
|
|
284
|
+
- "grant_type": "authorization_code"
|
|
285
|
+
- "code": <16-character-random-code>
|
|
286
|
+
- "redirect_uri": <redirect-uri>
|
|
287
|
+
|
|
288
|
+
For token refresh, *body_data* will have the attributes:
|
|
289
|
+
- "grant_type": "refresh_token"
|
|
290
|
+
- "refresh_token": <current-refresh-token>
|
|
291
|
+
|
|
292
|
+
For token exchange, *body_data* will have the attributes:
|
|
293
|
+
- "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
294
|
+
- "subject_token": <token-to-be-exchanged>,
|
|
295
|
+
- "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
296
|
+
- "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
|
|
297
|
+
- "audience": <client-id>,
|
|
298
|
+
- "subject_issuer": "oidc"
|
|
299
|
+
|
|
300
|
+
These attributes are then added to *body_data*:
|
|
301
|
+
- "client_id": <client-id>,
|
|
302
|
+
- "client_secret": <client-secret>,
|
|
303
|
+
|
|
304
|
+
If the operation is successful, the token data is stored in the registry.
|
|
305
|
+
Otherwise, *errors* will contain the appropriate error message.
|
|
306
|
+
|
|
307
|
+
:param iam_server: the reference registered *IAM* server
|
|
308
|
+
:param body_data: the data to send in the body of the request
|
|
309
|
+
:param errors: incidental errors
|
|
310
|
+
:param logger: optional logger
|
|
311
|
+
:return: the access token obtained, or *None* if error
|
|
312
|
+
"""
|
|
313
|
+
# initialize the return variable
|
|
314
|
+
result: dict[str, Any] | None = None
|
|
315
|
+
|
|
316
|
+
# PBTAIN THE iam SERVER'S REGISTRY
|
|
317
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
318
|
+
errors=errors,
|
|
319
|
+
logger=logger)
|
|
320
|
+
err_msg: str | None = None
|
|
321
|
+
if registry:
|
|
322
|
+
# complete the data to send in body of request
|
|
323
|
+
body_data["client_id"] = registry["client-id"]
|
|
324
|
+
client_secret: str = registry["client-secret"]
|
|
325
|
+
if client_secret:
|
|
326
|
+
body_data["client_secret"] = client_secret
|
|
327
|
+
|
|
328
|
+
# obtain the token
|
|
329
|
+
url: str = registry["base-url"] + "/protocol/openid-connect/token"
|
|
330
|
+
if logger:
|
|
331
|
+
logger.debug(msg=f"POST '{url}', data {json.dumps(obj=body_data,
|
|
332
|
+
ensure_ascii=False)}")
|
|
333
|
+
try:
|
|
334
|
+
# typical return on a token request:
|
|
335
|
+
# {
|
|
336
|
+
# "token_type": "Bearer",
|
|
337
|
+
# "access_token": <str>,
|
|
338
|
+
# "expires_in": <number-of-seconds>,
|
|
339
|
+
# "refresh_token": <str>,
|
|
340
|
+
# "refesh_expires_in": <number-of-seconds>
|
|
341
|
+
# }
|
|
342
|
+
response: requests.Response = requests.post(url=url,
|
|
343
|
+
data=body_data)
|
|
344
|
+
if response.status_code == 200:
|
|
345
|
+
# request succeeded
|
|
346
|
+
if logger:
|
|
347
|
+
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
348
|
+
result = response.json()
|
|
349
|
+
else:
|
|
350
|
+
# request resulted in error
|
|
351
|
+
err_msg = f"POST failure, status {response.status_code}, reason '{response.reason}'"
|
|
352
|
+
if hasattr(response, "content") and response.content:
|
|
353
|
+
err_msg += f", content '{response.content}'"
|
|
354
|
+
if logger:
|
|
355
|
+
logger.error(msg=err_msg)
|
|
356
|
+
except Exception as e:
|
|
357
|
+
# the operation raised an exception
|
|
358
|
+
err_msg = exc_format(exc=e,
|
|
359
|
+
exc_info=sys.exc_info())
|
|
360
|
+
if logger:
|
|
361
|
+
logger.error(msg=err_msg)
|
|
362
|
+
|
|
363
|
+
if err_msg and isinstance(errors, list):
|
|
364
|
+
errors.append(err_msg)
|
|
365
|
+
|
|
366
|
+
return result
|