pypomes-iam 0.5.7__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 +7 -12
- pypomes_iam/iam_actions.py +357 -47
- pypomes_iam/iam_common.py +165 -49
- pypomes_iam/iam_pomes.py +130 -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.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.7.dist-info/RECORD +0 -12
- {pypomes_iam-0.5.7.dist-info → pypomes_iam-0.5.8.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.5.7.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
|
|
|
@@ -161,9 +277,10 @@ def _get_public_key(iam_server: IamServer,
|
|
|
161
277
|
logger=logger)
|
|
162
278
|
if registry:
|
|
163
279
|
now: int = int(datetime.now(tz=TZ_LOCAL).timestamp())
|
|
164
|
-
if now > registry[
|
|
280
|
+
if now > registry[IamParam.PK_EXPIRATION]:
|
|
165
281
|
# obtain the JWKS (JSON Web Key Set) from the token issuer
|
|
166
|
-
|
|
282
|
+
base_url: str = f"{registry[IamParam.URL_BASE]}/realms/{registry[IamParam.CLIENT_REALM]}"
|
|
283
|
+
url: str = f"{base_url}/protocol/openid-connect/certs"
|
|
167
284
|
if logger:
|
|
168
285
|
logger.debug(msg=f"Obtaining signature public key used by IAM server '{iam_server}'")
|
|
169
286
|
logger.debug(msg=f"GET {url}")
|
|
@@ -174,9 +291,8 @@ def _get_public_key(iam_server: IamServer,
|
|
|
174
291
|
if logger:
|
|
175
292
|
logger.debug(msg=f"GET success, status {response.status_code}")
|
|
176
293
|
# select the appropriate JWK
|
|
177
|
-
reply: dict[str,
|
|
294
|
+
reply: dict[str, list[dict[str, str]]] = response.json()
|
|
178
295
|
jwk: dict[str, str] | None = None
|
|
179
|
-
# replay["keys"]: list[dict[str, str]]
|
|
180
296
|
for key in reply["keys"]:
|
|
181
297
|
if key.get("use") == "sig":
|
|
182
298
|
jwk = key
|
|
@@ -185,11 +301,11 @@ def _get_public_key(iam_server: IamServer,
|
|
|
185
301
|
# convert from 'JWK' to 'PEM' and save it for further use
|
|
186
302
|
result = crypto_jwk_convert(jwk=jwk,
|
|
187
303
|
fmt="PEM")
|
|
188
|
-
registry[
|
|
189
|
-
lifetime: int = registry[
|
|
190
|
-
registry[
|
|
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
|
|
191
307
|
if logger:
|
|
192
|
-
logger.debug(
|
|
308
|
+
logger.debug("Public key obtained and saved")
|
|
193
309
|
else:
|
|
194
310
|
msg = "Signature public key missing from the token issuer's JWKS"
|
|
195
311
|
if logger:
|
|
@@ -212,7 +328,7 @@ def _get_public_key(iam_server: IamServer,
|
|
|
212
328
|
if isinstance(errors, list):
|
|
213
329
|
errors.append(msg)
|
|
214
330
|
else:
|
|
215
|
-
result = registry[
|
|
331
|
+
result = registry[IamParam.PUBLIC_KEY]
|
|
216
332
|
|
|
217
333
|
return result
|
|
218
334
|
|
|
@@ -267,10 +383,10 @@ def _get_user_data(iam_server: IamServer,
|
|
|
267
383
|
result = users.get(user_id)
|
|
268
384
|
if not result:
|
|
269
385
|
result = {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
274
390
|
}
|
|
275
391
|
users[user_id] = result
|
|
276
392
|
if logger:
|
|
@@ -296,10 +412,10 @@ def _get_iam_registry(iam_server: IamServer,
|
|
|
296
412
|
result: dict[str, Any] | None
|
|
297
413
|
|
|
298
414
|
match iam_server:
|
|
299
|
-
case IamServer.
|
|
300
|
-
result = _IAM_SERVERS[IamServer.
|
|
301
|
-
case IamServer.
|
|
302
|
-
result = _IAM_SERVERS[IamServer.
|
|
415
|
+
case IamServer.JUSRBR:
|
|
416
|
+
result = _IAM_SERVERS[IamServer.JUSRBR]
|
|
417
|
+
case IamServer.KEYCLOAK:
|
|
418
|
+
result = _IAM_SERVERS[IamServer.KEYCLOAK]
|
|
303
419
|
case _:
|
|
304
420
|
result = None
|
|
305
421
|
msg = f"Unknown IAM server '{iam_server}'"
|
|
@@ -315,14 +431,14 @@ def _get_iam_users(iam_server: IamServer,
|
|
|
315
431
|
errors: list[str] | None,
|
|
316
432
|
logger: Logger | None) -> dict[str, dict[str, Any]]:
|
|
317
433
|
"""
|
|
318
|
-
Retrieve the
|
|
434
|
+
Retrieve the users data storage in *iam_server*'s registry.
|
|
319
435
|
|
|
320
436
|
:param iam_server: the reference registered *IAM* server
|
|
321
437
|
:param errors: incidental error messages
|
|
322
438
|
:param logger: optional logger
|
|
323
|
-
:return: the
|
|
439
|
+
:return: the users data storage in *iam_server*'s registry, or *None* if the server is unknown
|
|
324
440
|
"""
|
|
325
441
|
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
326
442
|
errors=errors,
|
|
327
443
|
logger=logger)
|
|
328
|
-
return registry[
|
|
444
|
+
return registry[IamParam.USERS] if registry else None
|
pypomes_iam/iam_pomes.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from flask import Flask
|
|
2
|
+
from logging import Logger
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .iam_common import (
|
|
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
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
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:
|
|
30
|
+
"""
|
|
31
|
+
Establish the provided parameters for configuring the *IAM* server *iam_server*.
|
|
32
|
+
|
|
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.
|
|
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.
|
|
40
|
+
|
|
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
|
|
57
|
+
"""
|
|
58
|
+
|
|
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
|
|
75
|
+
|
|
76
|
+
if public_key_lifetime:
|
|
77
|
+
IamParam.PK_LIFETIME = public_key_lifetime
|
|
78
|
+
|
|
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"])
|
|
105
|
+
|
|
106
|
+
|
|
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*.
|
|
113
|
+
|
|
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
|
|
122
|
+
|
|
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)
|
|
130
|
+
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.8
|
|
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=f-2W_zrCmXExubJPExQrhAwGpiQCmybEC_wguRYFHsw,994
|
|
2
|
+
pypomes_iam/iam_actions.py,sha256=Bmd8rBg3948Afsg10B6B1ZrFY4wYtbxi55rX4Rlqiyk,39167
|
|
3
|
+
pypomes_iam/iam_common.py,sha256=I-HtwpvrhByTbOoSQrMktjpbYgeIPlYM1YC6wkFUhI4,18251
|
|
4
|
+
pypomes_iam/iam_pomes.py,sha256=BetEVGv41wkcP9E1wRvYiQgmJElDXH4Iz8qgf7iH6X0,5617
|
|
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.8.dist-info/METADATA,sha256=Q60cQU69Gbay_IjFewESe9P4O4Z6mQ5tz_tYvw7yIMM,694
|
|
9
|
+
pypomes_iam-0.5.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
pypomes_iam-0.5.8.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
11
|
+
pypomes_iam-0.5.8.dist-info/RECORD,,
|