pypomes-iam 0.5.7__py3-none-any.whl → 0.5.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.
Potentially problematic release.
This version of pypomes-iam might be problematic. Click here for more details.
- pypomes_iam/__init__.py +7 -12
- pypomes_iam/iam_actions.py +357 -47
- pypomes_iam/iam_common.py +115 -49
- pypomes_iam/iam_pomes.py +156 -0
- pypomes_iam/iam_services.py +7 -7
- pypomes_iam/provider_pomes.py +46 -26
- pypomes_iam/token_pomes.py +0 -2
- {pypomes_iam-0.5.7.dist-info → pypomes_iam-0.5.9.dist-info}/METADATA +1 -1
- pypomes_iam-0.5.9.dist-info/RECORD +11 -0
- pypomes_iam/jusbr_pomes.py +0 -125
- pypomes_iam/keycloak_pomes.py +0 -136
- pypomes_iam-0.5.7.dist-info/RECORD +0 -12
- {pypomes_iam-0.5.7.dist-info → pypomes_iam-0.5.9.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.7.dist-info → pypomes_iam-0.5.9.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,42 +13,107 @@ class IamServer(StrEnum):
|
|
|
13
13
|
"""
|
|
14
14
|
Supported IAM servers.
|
|
15
15
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
JUSRBR = 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_SERVER* (required, one of *jusbr*, *keycloak*)
|
|
62
|
+
# - *<APP_PREFIX>_IAM_ADMIN_ID* (optional, needed only if administrative duties are performed)
|
|
63
|
+
# - *<APP_PREFIX>_IAM_ADMIN_PWD* (optional, needed only if administrative duties are performed)
|
|
64
|
+
# - *<APP_PREFIX>_IAM_CLIENT_ID* (required)
|
|
65
|
+
# - *<APP_PREFIX>_IAM_CLIENT_REALM* (required)
|
|
66
|
+
# - *<APP_PREFIX>_IAM_CLIENT_SECRET* (required)
|
|
67
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_CALLBACK* (optional)
|
|
68
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_LOGIN* (optional)
|
|
69
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_LOGOUT* (optional)
|
|
70
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_TOKEN* (optional)
|
|
71
|
+
# - *<APP_PREFIX>_IAM_ENDPOINT_EXCHANGE* (optional)
|
|
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
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)
|
|
@@ -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
|
|
|
@@ -161,9 +227,10 @@ def _get_public_key(iam_server: IamServer,
|
|
|
161
227
|
logger=logger)
|
|
162
228
|
if registry:
|
|
163
229
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
164
|
-
if now > registry[
|
|
230
|
+
if now > registry[IamParam.PK_EXPIRATION]:
|
|
165
231
|
# obtain the JWKS (JSON Web Key Set) from the token issuer
|
|
166
|
-
|
|
232
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
233
|
+
url: str = f"{base_url}/protocol/openid-connect/certs"
|
|
167
234
|
if logger:
|
|
168
235
|
logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
|
|
169
236
|
logger.debug(msg=f"GET {url}")
|
|
@@ -174,9 +241,8 @@ def _get_public_key(iam_server: IamServer,
|
|
|
174
241
|
if logger:
|
|
175
242
|
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
176
243
|
# select the appropriate JWK
|
|
177
|
-
reply: dict[str,
|
|
244
|
+
reply: dict[str, list[dict[str, str]]] = response.json()
|
|
178
245
|
jwk: dict[str, str] | None = None
|
|
179
|
-
# replay["keys"]: list[dict[str, str]]
|
|
180
246
|
for key in reply["keys"]:
|
|
181
247
|
if key.get("use") == "sig":
|
|
182
248
|
jwk = key
|
|
@@ -185,11 +251,11 @@ def _get_public_key(iam_server: IamServer,
|
|
|
185
251
|
# convert from 'JWK' to 'PEM' and save it for further use
|
|
186
252
|
result = crypto_jwk_convert(jwk=jwk,
|
|
187
253
|
fmt="PEM")
|
|
188
|
-
registry[
|
|
189
|
-
lifetime: int = registry[
|
|
190
|
-
registry[
|
|
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
|
|
191
257
|
if logger:
|
|
192
|
-
logger.debug(
|
|
258
|
+
logger.debug("Public key obtained and saved")
|
|
193
259
|
else:
|
|
194
260
|
msg = "Signature public key missing from the token issuer's JWKS"
|
|
195
261
|
if logger:
|
|
@@ -212,7 +278,7 @@ def _get_public_key(iam_server: IamServer,
|
|
|
212
278
|
if isinstance(errors, list):
|
|
213
279
|
errors.append(msg)
|
|
214
280
|
else:
|
|
215
|
-
result = registry[
|
|
281
|
+
result = registry[IamParam.PUBLIC_KEY]
|
|
216
282
|
|
|
217
283
|
return result
|
|
218
284
|
|
|
@@ -267,10 +333,10 @@ def _get_user_data(iam_server: IamServer,
|
|
|
267
333
|
result = users.get(user_id)
|
|
268
334
|
if not result:
|
|
269
335
|
result = {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
274
340
|
}
|
|
275
341
|
users[user_id] = result
|
|
276
342
|
if logger:
|
|
@@ -296,10 +362,10 @@ def _get_iam_registry(iam_server: IamServer,
|
|
|
296
362
|
result: dict[str, Any] | None
|
|
297
363
|
|
|
298
364
|
match iam_server:
|
|
299
|
-
case IamServer.
|
|
300
|
-
result = _IAM_SERVERS[IamServer.
|
|
301
|
-
case IamServer.
|
|
302
|
-
result = _IAM_SERVERS[IamServer.
|
|
365
|
+
case IamServer.JUSRBR:
|
|
366
|
+
result = _IAM_SERVERS[IamServer.JUSRBR]
|
|
367
|
+
case IamServer.KEYCLOAK:
|
|
368
|
+
result = _IAM_SERVERS[IamServer.KEYCLOAK]
|
|
303
369
|
case _:
|
|
304
370
|
result = None
|
|
305
371
|
msg = f"Unknown IAM server '{iam_server}'"
|
|
@@ -315,14 +381,14 @@ def _get_iam_users(iam_server: IamServer,
|
|
|
315
381
|
errors: list[str] | None,
|
|
316
382
|
logger: Logger | None) -> dict[str, dict[str, Any]]:
|
|
317
383
|
"""
|
|
318
|
-
Retrieve the
|
|
384
|
+
Retrieve the users data storage in *iam_server*'s registry.
|
|
319
385
|
|
|
320
386
|
:param iam_server: the reference registered *IAM* server
|
|
321
387
|
:param errors: incidental error messages
|
|
322
388
|
:param logger: optional logger
|
|
323
|
-
:return: the
|
|
389
|
+
:return: the users data storage in *iam_server*'s registry, or *None* if the server is unknown
|
|
324
390
|
"""
|
|
325
391
|
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
326
392
|
errors=errors,
|
|
327
393
|
logger=logger)
|
|
328
|
-
return registry[
|
|
394
|
+
return registry[IamParam.USERS] if registry else None
|
pypomes_iam/iam_pomes.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
from logging import Logger
|
|
3
|
+
from pypomes_core import APP_PREFIX, env_get_int, env_get_str
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .iam_common import (
|
|
7
|
+
_IAM_SERVERS, IamServer, IamParam, _iam_lock
|
|
8
|
+
)
|
|
9
|
+
from .iam_actions import action_token
|
|
10
|
+
from .iam_services import (
|
|
11
|
+
service_login, service_logout, service_callback, service_exchange, service_token
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def iam_setup(flask_app: Flask,
|
|
16
|
+
iam_server: IamServer,
|
|
17
|
+
base_url: str,
|
|
18
|
+
client_id: str,
|
|
19
|
+
client_realm: str,
|
|
20
|
+
client_secret: str | None,
|
|
21
|
+
recipient_attribute: str,
|
|
22
|
+
admin_id: str = None,
|
|
23
|
+
admin_secret: str = None,
|
|
24
|
+
login_timeout: int = None,
|
|
25
|
+
public_key_lifetime: int = None,
|
|
26
|
+
callback_endpoint: str = None,
|
|
27
|
+
exchange_endpoint: str = None,
|
|
28
|
+
login_endpoint: str = None,
|
|
29
|
+
logout_endpoint: str = None,
|
|
30
|
+
token_endpoint: str = None) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Establish the provided parameters for configuring the *IAM* server *iam_server*.
|
|
33
|
+
|
|
34
|
+
The parameters *admin_id* and *admin_* are required only if administrative are task are planned.
|
|
35
|
+
The optional parameter *client_timeout* refers to the maximum time in seconds allowed for the
|
|
36
|
+
user to login at the *IAM* server's login page, and defaults to no time limit.
|
|
37
|
+
|
|
38
|
+
The parameter *client_secret* is required in most requests to the *IAM* server. In the case
|
|
39
|
+
it is not provided, but *admin_id* and *admin_secret* are, it is obtained from the *IAM* server itself
|
|
40
|
+
the first time it is needed.
|
|
41
|
+
|
|
42
|
+
:param flask_app: the Flask application
|
|
43
|
+
:param iam_server: identifies the supported *IAM* server (*jusbr* or *keycloak*)
|
|
44
|
+
:param base_url: base URL to request services
|
|
45
|
+
:param client_id: the client's identification with the *IAM* server
|
|
46
|
+
:param client_realm: the client realm
|
|
47
|
+
:param client_secret: the client's password with the *IAM* server
|
|
48
|
+
:param recipient_attribute: attribute in the token's payload holding the token's subject
|
|
49
|
+
:param admin_id: identifies the realm administrator
|
|
50
|
+
:param admin_secret: password for the realm administrator
|
|
51
|
+
:param login_timeout: timeout for login authentication (in seconds,defaults to no timeout)
|
|
52
|
+
:param public_key_lifetime: how long to use *IAM* server's public key, before refreshing it (in seconds)
|
|
53
|
+
:param callback_endpoint: endpoint for the callback from the front end
|
|
54
|
+
:param exchange_endpoint: endpoint for requesting token exchange
|
|
55
|
+
:param login_endpoint: endpoint for redirecting user to the *IAM* server's login page
|
|
56
|
+
:param logout_endpoint: endpoint for terminating user access
|
|
57
|
+
:param token_endpoint: endpoint for retrieving authentication token
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# configure the Keycloak registry
|
|
61
|
+
with _iam_lock:
|
|
62
|
+
_IAM_SERVERS[iam_server] = {
|
|
63
|
+
IamParam.URL_BASE: base_url,
|
|
64
|
+
IamParam.CLIENT_ID: client_id,
|
|
65
|
+
IamParam.CLIENT_REALM: client_realm,
|
|
66
|
+
IamParam.CLIENT_SECRET: client_secret,
|
|
67
|
+
IamParam.RECIPIENT_ATTR: recipient_attribute,
|
|
68
|
+
IamParam.ADMIN_ID: admin_id,
|
|
69
|
+
IamParam.ADMIN_SECRET: admin_secret,
|
|
70
|
+
IamParam.LOGIN_TIMEOUT: login_timeout,
|
|
71
|
+
IamParam.PK_LIFETIME: public_key_lifetime,
|
|
72
|
+
IamParam.PK_EXPIRATION: 0,
|
|
73
|
+
IamParam.PUBLIC_KEY: None,
|
|
74
|
+
IamParam.USERS: {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# establish the endpoints
|
|
78
|
+
if callback_endpoint:
|
|
79
|
+
flask_app.add_url_rule(rule=callback_endpoint,
|
|
80
|
+
endpoint=f"{iam_server}-callback",
|
|
81
|
+
view_func=service_callback,
|
|
82
|
+
methods=["GET"])
|
|
83
|
+
if login_endpoint:
|
|
84
|
+
flask_app.add_url_rule(rule=login_endpoint,
|
|
85
|
+
endpoint=f"{iam_server}-login",
|
|
86
|
+
view_func=service_login,
|
|
87
|
+
methods=["GET"])
|
|
88
|
+
if logout_endpoint:
|
|
89
|
+
flask_app.add_url_rule(rule=logout_endpoint,
|
|
90
|
+
endpoint=f"{iam_server}-logout",
|
|
91
|
+
view_func=service_logout,
|
|
92
|
+
methods=["GET"])
|
|
93
|
+
if token_endpoint:
|
|
94
|
+
flask_app.add_url_rule(rule=token_endpoint,
|
|
95
|
+
endpoint=f"{iam_server}-token",
|
|
96
|
+
view_func=service_token,
|
|
97
|
+
methods=["GET"])
|
|
98
|
+
if exchange_endpoint:
|
|
99
|
+
flask_app.add_url_rule(rule=exchange_endpoint,
|
|
100
|
+
endpoint=f"{iam_server}-exchange",
|
|
101
|
+
view_func=service_exchange,
|
|
102
|
+
methods=["POST"])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def iam_get_env_parameters(iam_prefix: str = None) -> dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Retrieve the set parameters for a *IAM* server from the environment.
|
|
108
|
+
|
|
109
|
+
the parameters are returned ready to be used as a '**kwargs' parameter set in a call to *iam_setup()*,
|
|
110
|
+
and sorted in the order appropriate to use them instead with a '*args' parameter set.
|
|
111
|
+
|
|
112
|
+
:param iam_prefix: the prefix classifying the parameters
|
|
113
|
+
:return: the sorted parameters classified by *prefix*
|
|
114
|
+
"""
|
|
115
|
+
return {
|
|
116
|
+
"url_base": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_URL_BASE"),
|
|
117
|
+
"client_id": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_ID"),
|
|
118
|
+
"client_realm": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_REALM"),
|
|
119
|
+
"client_secret": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_CLIENT_SECRET"),
|
|
120
|
+
"recipient_attr": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_RECIPIENT_ATTR"),
|
|
121
|
+
"admin_id": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ADMIN_ID"),
|
|
122
|
+
"admin_secret": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ADMIN_SECRET"),
|
|
123
|
+
"login_timeout": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_LOGIN_TIMEOUT"),
|
|
124
|
+
"public_key_lifetime": env_get_int(key=f"{APP_PREFIX}_{iam_prefix}_PUBLIC_KEY_LIFETIME"),
|
|
125
|
+
"callback_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_CALLBACK"),
|
|
126
|
+
"exchange_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_EXCHANGE"),
|
|
127
|
+
"login_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_LOGIN"),
|
|
128
|
+
"logout_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}__ENDPOINT_LOGOUT"),
|
|
129
|
+
"token_endpoint": env_get_str(key=f"{APP_PREFIX}_{iam_prefix}_ENDPOINT_TOKEN")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def iam_get_token(iam_server: IamServer,
|
|
134
|
+
user_id: str,
|
|
135
|
+
errors: list[str] = None,
|
|
136
|
+
logger: Logger = None) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Retrieve an authentication token for *user_id*.
|
|
139
|
+
|
|
140
|
+
:param iam_server: identifies the *IAM* server
|
|
141
|
+
:param user_id: identifies the user
|
|
142
|
+
:param errors: incidental errors
|
|
143
|
+
:param logger: optional logger
|
|
144
|
+
:return: the uthentication tokem
|
|
145
|
+
"""
|
|
146
|
+
# declare the return variable
|
|
147
|
+
result: str
|
|
148
|
+
|
|
149
|
+
# retrieve the token
|
|
150
|
+
args: dict[str, Any] = {"user-id": user_id}
|
|
151
|
+
with _iam_lock:
|
|
152
|
+
result = action_token(iam_server=iam_server,
|
|
153
|
+
args=args,
|
|
154
|
+
errors=errors,
|
|
155
|
+
logger=logger)
|
|
156
|
+
return result
|
pypomes_iam/iam_services.py
CHANGED
|
@@ -4,7 +4,7 @@ from logging import Logger
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from .iam_common import (
|
|
7
|
-
IamServer, _iam_lock,
|
|
7
|
+
IamServer, IamParam, _iam_lock,
|
|
8
8
|
_get_iam_registry, _get_public_key,
|
|
9
9
|
_iam_server_from_endpoint, _iam_server_from_issuer
|
|
10
10
|
)
|
|
@@ -73,7 +73,7 @@ def __request_validate(request: Request) -> Response:
|
|
|
73
73
|
errors=None,
|
|
74
74
|
logger=__IAM_LOGGER)
|
|
75
75
|
if registry:
|
|
76
|
-
recipient_attr = registry[
|
|
76
|
+
recipient_attr = registry[IamParam.RECIPIENT_ATTR]
|
|
77
77
|
public_key: str = _get_public_key(iam_server=iam_server,
|
|
78
78
|
errors=None,
|
|
79
79
|
logger=__IAM_LOGGER)
|
|
@@ -90,7 +90,7 @@ def __request_validate(request: Request) -> Response:
|
|
|
90
90
|
elif __IAM_LOGGER:
|
|
91
91
|
__IAM_LOGGER.error("; ".join(errors))
|
|
92
92
|
if bad_token and __IAM_LOGGER:
|
|
93
|
-
__IAM_LOGGER.error(f"
|
|
93
|
+
__IAM_LOGGER.error(f"Authorization refused for token {token}")
|
|
94
94
|
|
|
95
95
|
# deny the authorization
|
|
96
96
|
if bad_token:
|
|
@@ -256,9 +256,9 @@ def service_callback() -> Response:
|
|
|
256
256
|
else:
|
|
257
257
|
result = jsonify({"user-id": token_data[0],
|
|
258
258
|
"access-token": token_data[1]})
|
|
259
|
-
# log the response
|
|
260
259
|
if __IAM_LOGGER:
|
|
261
|
-
|
|
260
|
+
# log the response (the returned data is not logged, as it contains the token)
|
|
261
|
+
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
262
262
|
|
|
263
263
|
return result
|
|
264
264
|
|
|
@@ -317,9 +317,9 @@ def service_token() -> Response:
|
|
|
317
317
|
else:
|
|
318
318
|
result = jsonify({"user-id": user_id,
|
|
319
319
|
"access-token": token})
|
|
320
|
-
# log the response
|
|
321
320
|
if __IAM_LOGGER:
|
|
322
|
-
|
|
321
|
+
# log the response (the returned data is not logged, as it contains the token)
|
|
322
|
+
__IAM_LOGGER.debug(msg=f"Response {result}")
|
|
323
323
|
|
|
324
324
|
return result
|
|
325
325
|
|
pypomes_iam/provider_pomes.py
CHANGED
|
@@ -3,22 +3,42 @@ import requests
|
|
|
3
3
|
import sys
|
|
4
4
|
from base64 import b64encode
|
|
5
5
|
from datetime import datetime
|
|
6
|
+
from enum import StrEnum
|
|
6
7
|
from logging import Logger
|
|
7
8
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
8
9
|
from threading import Lock
|
|
9
10
|
from typing import Any, Final
|
|
10
11
|
|
|
12
|
+
|
|
13
|
+
class ProviderParam(StrEnum):
|
|
14
|
+
"""
|
|
15
|
+
Parameters for configuring a *JWT* token provider.
|
|
16
|
+
"""
|
|
17
|
+
URL = "url"
|
|
18
|
+
USER = "user"
|
|
19
|
+
PWD = "pwd"
|
|
20
|
+
CUSTOM_AUTH = "custom-auth"
|
|
21
|
+
HEADER_DATA = "headers-data"
|
|
22
|
+
BODY_DATA = "body-data"
|
|
23
|
+
ACCESS_TOKEN = "access-token"
|
|
24
|
+
ACCESS_EXPIRATION = "access-expiration"
|
|
25
|
+
REFRESH_TOKEN = "refresh-token"
|
|
26
|
+
REFRESH_EXPIRATION = "refresh-expiration"
|
|
27
|
+
|
|
28
|
+
|
|
11
29
|
# structure:
|
|
12
30
|
# {
|
|
13
31
|
# <provider-id>: {
|
|
14
32
|
# "url": <strl>,
|
|
15
33
|
# "user": <str>,
|
|
16
34
|
# "pwd": <str>,
|
|
17
|
-
# "
|
|
35
|
+
# "custom-auth": <bool>,
|
|
18
36
|
# "headers-data": <dict[str, str]>,
|
|
19
37
|
# "body-data": <dict[str, str],
|
|
20
38
|
# "access-token": <str>,
|
|
21
|
-
# "access-expiration": <timestamp
|
|
39
|
+
# "access-expiration": <timestamp>,
|
|
40
|
+
# "refresh-token": <str>,
|
|
41
|
+
# "refresh-expiration": <timestamp>
|
|
22
42
|
# }
|
|
23
43
|
# }
|
|
24
44
|
_provider_registry: Final[dict[str, dict[str, Any]]] = {}
|
|
@@ -58,16 +78,16 @@ def provider_register(provider_id: str,
|
|
|
58
78
|
|
|
59
79
|
with _provider_lock:
|
|
60
80
|
_provider_registry[provider_id] = {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
ProviderParam.URL: auth_url,
|
|
82
|
+
ProviderParam.USER: auth_user,
|
|
83
|
+
ProviderParam.PWD: auth_pwd,
|
|
84
|
+
ProviderParam.CUSTOM_AUTH: custom_auth,
|
|
85
|
+
ProviderParam.HEADER_DATA: headers_data,
|
|
86
|
+
ProviderParam.BODY_DATA: body_data,
|
|
87
|
+
ProviderParam.ACCESS_TOKEN: None,
|
|
88
|
+
ProviderParam.ACCESS_EXPIRATION: 0,
|
|
89
|
+
ProviderParam.REFRESH_TOKEN: None,
|
|
90
|
+
ProviderParam.REFRESH_EXPIRATION: 0
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
|
|
@@ -91,19 +111,19 @@ def provider_get_token(provider_id: str,
|
|
|
91
111
|
provider: dict[str, Any] = _provider_registry.get(provider_id)
|
|
92
112
|
if provider:
|
|
93
113
|
now: float = datetime.now(tz=TZ_LOCAL).timestamp()
|
|
94
|
-
if now > provider.get(
|
|
95
|
-
user: str = provider.get(
|
|
96
|
-
pwd: str = provider.get(
|
|
97
|
-
headers_data: dict[str, str] = provider.get(
|
|
98
|
-
body_data: dict[str, str] = provider.get(
|
|
99
|
-
custom_auth: tuple[str, str] = provider.get(
|
|
114
|
+
if now > provider.get(ProviderParam.ACCESS_EXPIRATION):
|
|
115
|
+
user: str = provider.get(ProviderParam.USER)
|
|
116
|
+
pwd: str = provider.get(ProviderParam.PWD)
|
|
117
|
+
headers_data: dict[str, str] = provider.get(ProviderParam.HEADER_DATA) or {}
|
|
118
|
+
body_data: dict[str, str] = provider.get(ProviderParam.BODY_DATA) or {}
|
|
119
|
+
custom_auth: tuple[str, str] = provider.get(ProviderParam.CUSTOM_AUTH)
|
|
100
120
|
if custom_auth:
|
|
101
121
|
body_data[custom_auth[0]] = user
|
|
102
122
|
body_data[custom_auth[1]] = pwd
|
|
103
123
|
else:
|
|
104
124
|
enc_bytes: bytes = b64encode(f"{user}:{pwd}".encode())
|
|
105
125
|
headers_data["Authorization"] = f"Basic {enc_bytes.decode()}"
|
|
106
|
-
url: str = provider.get(
|
|
126
|
+
url: str = provider.get(ProviderParam.URL)
|
|
107
127
|
if logger:
|
|
108
128
|
logger.debug(msg=f"POST {url}, {json.dumps(obj=body_data,
|
|
109
129
|
ensure_ascii=False)}")
|
|
@@ -130,14 +150,14 @@ def provider_get_token(provider_id: str,
|
|
|
130
150
|
if logger:
|
|
131
151
|
logger.debug(msg=f"POST success, status {response.status_code}")
|
|
132
152
|
reply: dict[str, Any] = response.json()
|
|
133
|
-
provider[
|
|
134
|
-
provider[
|
|
135
|
-
if reply.get(
|
|
136
|
-
provider[
|
|
153
|
+
provider[ProviderParam.ACCESS_TOKEN] = reply.get("access_token")
|
|
154
|
+
provider[ProviderParam.ACCESS_EXPIRATION] = now + int(reply.get("expires_in"))
|
|
155
|
+
if reply.get(ProviderParam.REFRESH_TOKEN):
|
|
156
|
+
provider[ProviderParam.REFRESH_TOKEN] = reply["refresh_token"]
|
|
137
157
|
if reply.get("refresh_expires_in"):
|
|
138
|
-
provider[
|
|
158
|
+
provider[ProviderParam.REFRESH_EXPIRATION] = now + int(reply.get("refresh_expires_in"))
|
|
139
159
|
else:
|
|
140
|
-
provider[
|
|
160
|
+
provider[ProviderParam.REFRESH_EXPIRATION] = sys.maxsize
|
|
141
161
|
if logger:
|
|
142
162
|
logger.debug(msg=f"POST {url}: status {response.status_code}")
|
|
143
163
|
except Exception as e:
|
|
@@ -154,7 +174,7 @@ def provider_get_token(provider_id: str,
|
|
|
154
174
|
if logger:
|
|
155
175
|
logger.error(msg=err_msg)
|
|
156
176
|
else:
|
|
157
|
-
result = provider.get(
|
|
177
|
+
result = provider.get(ProviderParam.ACCESS_TOKEN)
|
|
158
178
|
|
|
159
179
|
return result
|
|
160
180
|
|
pypomes_iam/token_pomes.py
CHANGED
|
@@ -117,8 +117,6 @@ def token_validate(token: str,
|
|
|
117
117
|
"verify_nbf": False,
|
|
118
118
|
"verify_signature": token_alg in ["RS256", "RS512"] and public_key is not None
|
|
119
119
|
}
|
|
120
|
-
if issuer:
|
|
121
|
-
options["require"].append("iss")
|
|
122
120
|
try:
|
|
123
121
|
# raises:
|
|
124
122
|
# InvalidTokenError: token is invalid
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.9
|
|
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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pypomes_iam/__init__.py,sha256=_6tSFfjuU-5p6TAMqNLHSL6IQmaJMSYuEW-TG3ybhTI,1044
|
|
2
|
+
pypomes_iam/iam_actions.py,sha256=Bmd8rBg3948Afsg10B6B1ZrFY4wYtbxi55rX4Rlqiyk,39167
|
|
3
|
+
pypomes_iam/iam_common.py,sha256=asgool1T1ja1RKtQP1h71EeG3SJf8UX59NEDisHqLb8,15672
|
|
4
|
+
pypomes_iam/iam_pomes.py,sha256=fSB4KnCVM9HKZ6_LBfud9vtMM4psNQPrP98QDal3l9Y,7342
|
|
5
|
+
pypomes_iam/iam_services.py,sha256=IkCjrKDX1Ix7BiHh-BL3VKz5xogcNC8prXkHyJzQoZ8,15862
|
|
6
|
+
pypomes_iam/provider_pomes.py,sha256=N0nL9_hgHmAjG9JKFoXC33zk8b1ckPG1veu1jTp-2JE,8045
|
|
7
|
+
pypomes_iam/token_pomes.py,sha256=K4nSAotKUoHIE2s3ltc_nVimlNeKS9tnD-IlslkAvkk,6626
|
|
8
|
+
pypomes_iam-0.5.9.dist-info/METADATA,sha256=iTKxygsO09k_vx4DEmbTc7IIkwt1_8Fu9z-YpybSM24,694
|
|
9
|
+
pypomes_iam-0.5.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
pypomes_iam-0.5.9.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
11
|
+
pypomes_iam-0.5.9.dist-info/RECORD,,
|