pypomes-iam 0.5.1__tar.gz → 0.5.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.5.1 → pypomes_iam-0.5.2}/PKG-INFO +1 -1
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/pyproject.toml +1 -1
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/src/pypomes_iam/iam_common.py +58 -29
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/src/pypomes_iam/iam_pomes.py +78 -3
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/src/pypomes_iam/iam_services.py +24 -24
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/src/pypomes_iam/jusbr_pomes.py +4 -1
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/src/pypomes_iam/token_pomes.py +39 -0
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/.gitignore +0 -0
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/LICENSE +0 -0
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/README.md +0 -0
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/src/pypomes_iam/__init__.py +0 -0
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.2}/src/pypomes_iam/keycloak_pomes.py +0 -0
- {pypomes_iam-0.5.1 → pypomes_iam-0.5.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.5.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -55,6 +55,64 @@ _IAM_SERVERS: Final[dict[IamServer, dict[str, Any]]] = {}
|
|
|
55
55
|
_iam_lock: Final[RLock] = RLock()
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
def _iam_server_from_endpoint(endpoint: str,
|
|
59
|
+
errors: list[str] | None,
|
|
60
|
+
logger: Logger | None) -> IamServer | None:
|
|
61
|
+
"""
|
|
62
|
+
Retrieve the registered *IAM* server associated with the service's invocation *endpoint*.
|
|
63
|
+
|
|
64
|
+
:param endpoint: the service's invocation endpoint
|
|
65
|
+
:param errors: incidental error messages
|
|
66
|
+
:param logger: optional logger
|
|
67
|
+
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
68
|
+
"""
|
|
69
|
+
# declare the return variable
|
|
70
|
+
result: IamServer | None
|
|
71
|
+
|
|
72
|
+
if endpoint.startswith("jusbr"):
|
|
73
|
+
result = IamServer.IAM_JUSRBR
|
|
74
|
+
elif endpoint.startswith("keycloak"):
|
|
75
|
+
result = IamServer.IAM_KEYCLOAK
|
|
76
|
+
else:
|
|
77
|
+
result = None
|
|
78
|
+
msg: str = f"Unable to find a IAM server to service endpoint '{endpoint}'"
|
|
79
|
+
if logger:
|
|
80
|
+
logger.error(msg=msg)
|
|
81
|
+
if isinstance(errors, list):
|
|
82
|
+
errors.append(msg)
|
|
83
|
+
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _iam_server_from_issuer(issuer: str,
|
|
88
|
+
errors: list[str] | None,
|
|
89
|
+
logger: Logger | None) -> IamServer | None:
|
|
90
|
+
"""
|
|
91
|
+
Retrieve the registered *IAM* server associated with the token's *issuer*.
|
|
92
|
+
|
|
93
|
+
:param issuer: the token's issuer
|
|
94
|
+
:param errors: incidental error messages
|
|
95
|
+
:param logger: optional logger
|
|
96
|
+
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
97
|
+
"""
|
|
98
|
+
# initialize the return variable
|
|
99
|
+
result: IamServer | None = None
|
|
100
|
+
|
|
101
|
+
for iam_server, server_data in _IAM_SERVERS.items():
|
|
102
|
+
if server_data["base-url"] == issuer:
|
|
103
|
+
result = IamServer(iam_server)
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if not result:
|
|
107
|
+
msg: str = f"Unable to find a IAM server associated with token issuer '{issuer}'"
|
|
108
|
+
if logger:
|
|
109
|
+
logger.error(msg=msg)
|
|
110
|
+
if isinstance(errors, list):
|
|
111
|
+
errors.append(msg)
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
58
116
|
def _get_public_key(iam_server: IamServer,
|
|
59
117
|
errors: list[str] | None,
|
|
60
118
|
logger: Logger | None) -> str:
|
|
@@ -178,35 +236,6 @@ def _get_user_data(iam_server: IamServer,
|
|
|
178
236
|
return result
|
|
179
237
|
|
|
180
238
|
|
|
181
|
-
def _get_iam_server(endpoint: str,
|
|
182
|
-
errors: list[str] | None,
|
|
183
|
-
logger: Logger | None) -> IamServer | None:
|
|
184
|
-
"""
|
|
185
|
-
Retrieve the registered *IAM* server associated with the service's invocation *endpoint*.
|
|
186
|
-
|
|
187
|
-
:param endpoint: the service's invocation endpoint
|
|
188
|
-
:param errors: incidental error messages
|
|
189
|
-
:param logger: optional logger
|
|
190
|
-
:return: the corresponding *IAM* server, or *None* if one could not be obtained
|
|
191
|
-
"""
|
|
192
|
-
# declare the return variable
|
|
193
|
-
result: IamServer | None
|
|
194
|
-
|
|
195
|
-
if endpoint.startswith("jusbr"):
|
|
196
|
-
result = IamServer.IAM_JUSRBR
|
|
197
|
-
elif endpoint.startswith("keycloak"):
|
|
198
|
-
result = IamServer.IAM_KEYCLOAK
|
|
199
|
-
else:
|
|
200
|
-
result = None
|
|
201
|
-
msg: str = f"Unable to find a IAM server to service endpoint '{endpoint}'"
|
|
202
|
-
if logger:
|
|
203
|
-
logger.error(msg=msg)
|
|
204
|
-
if isinstance(errors, list):
|
|
205
|
-
errors.append(msg)
|
|
206
|
-
|
|
207
|
-
return result
|
|
208
|
-
|
|
209
|
-
|
|
210
239
|
def _get_iam_registry(iam_server: IamServer,
|
|
211
240
|
errors: list[str] | None,
|
|
212
241
|
logger: Logger | None) -> dict[str, Any]:
|
|
@@ -4,16 +4,34 @@ import secrets
|
|
|
4
4
|
import string
|
|
5
5
|
import sys
|
|
6
6
|
from datetime import datetime
|
|
7
|
+
from flask import Request, Response, request
|
|
7
8
|
from logging import Logger
|
|
8
9
|
from pypomes_core import TZ_LOCAL, exc_format
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from .iam_common import (
|
|
12
13
|
IamServer, _iam_lock,
|
|
13
|
-
_get_iam_users, _get_iam_registry,
|
|
14
|
-
_get_login_timeout, _get_user_data,
|
|
14
|
+
_get_iam_users, _get_iam_registry, # _get_public_key,
|
|
15
|
+
_get_login_timeout, _get_user_data, _iam_server_from_issuer
|
|
15
16
|
)
|
|
16
|
-
from .token_pomes import token_validate
|
|
17
|
+
from .token_pomes import token_get_claims, token_validate
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def jwt_required(func: callable) -> callable:
|
|
21
|
+
"""
|
|
22
|
+
Create a decorator to authenticate service endpoints with JWT tokens.
|
|
23
|
+
|
|
24
|
+
:param func: the function being decorated
|
|
25
|
+
"""
|
|
26
|
+
# ruff: noqa: ANN003 - Missing type annotation for *{name}
|
|
27
|
+
def wrapper(*args, **kwargs) -> Response:
|
|
28
|
+
response: Response = __request_validate(request=request)
|
|
29
|
+
return response if response else func(*args, **kwargs)
|
|
30
|
+
|
|
31
|
+
# prevent a rogue error ("View function mapping is overwriting an existing endpoint function")
|
|
32
|
+
wrapper.__name__ = func.__name__
|
|
33
|
+
|
|
34
|
+
return wrapper
|
|
17
35
|
|
|
18
36
|
|
|
19
37
|
def user_login(iam_server: IamServer,
|
|
@@ -339,6 +357,63 @@ def token_exchange(iam_server: IamServer,
|
|
|
339
357
|
return result
|
|
340
358
|
|
|
341
359
|
|
|
360
|
+
def __request_validate(request: Request) -> Response:
|
|
361
|
+
"""
|
|
362
|
+
Verify whether the HTTP *request* has the proper authorization, as per the JWT standard.
|
|
363
|
+
|
|
364
|
+
This implementation assumes that HTTP requests are handled with the *Flask* framework.
|
|
365
|
+
|
|
366
|
+
:param request: the *request* to be verified
|
|
367
|
+
:return: *None* if the *request* is valid, otherwise a *Response* reporting the error
|
|
368
|
+
"""
|
|
369
|
+
# initialize the return variable
|
|
370
|
+
result: Response | None = None
|
|
371
|
+
|
|
372
|
+
# retrieve the authorization from the request header
|
|
373
|
+
auth_header: str = request.headers.get("Authorization")
|
|
374
|
+
|
|
375
|
+
# validate the authorization token
|
|
376
|
+
bad_token: bool = True
|
|
377
|
+
if auth_header and auth_header.startswith("Bearer "):
|
|
378
|
+
# extract and validate the JWT access token
|
|
379
|
+
token: str = auth_header.split(" ")[1]
|
|
380
|
+
claims: dict[str, Any] = token_get_claims(token=token)
|
|
381
|
+
if claims:
|
|
382
|
+
issuer: str = claims["payload"].get("iss")
|
|
383
|
+
recipient_attr: str | None = None
|
|
384
|
+
recipient_id: str = request.values.get("user-id") or request.values.get("login")
|
|
385
|
+
with _iam_lock:
|
|
386
|
+
iam_server: IamServer = _iam_server_from_issuer(issuer=issuer,
|
|
387
|
+
errors=None,
|
|
388
|
+
logger=None)
|
|
389
|
+
# public_key: str = _get_public_key(iam_server=iam_server,
|
|
390
|
+
# errors=errors,
|
|
391
|
+
# logger=logger)
|
|
392
|
+
public_key = None
|
|
393
|
+
|
|
394
|
+
# validate the token's recipient only if a user identification is provided
|
|
395
|
+
if recipient_id:
|
|
396
|
+
registry: dict[str, Any] = _get_iam_registry(iam_server=iam_server,
|
|
397
|
+
errors=None,
|
|
398
|
+
logger=None)
|
|
399
|
+
recipient_attr = registry["recipient-attr"]
|
|
400
|
+
|
|
401
|
+
# validate the token
|
|
402
|
+
if token_validate(token=token,
|
|
403
|
+
issuer=issuer,
|
|
404
|
+
recipient_id=recipient_id,
|
|
405
|
+
recipient_attr=recipient_attr,
|
|
406
|
+
public_key=public_key):
|
|
407
|
+
# token is valid
|
|
408
|
+
bad_token = False
|
|
409
|
+
|
|
410
|
+
# deny the authorization
|
|
411
|
+
if bad_token:
|
|
412
|
+
result = Response(response="Authorization failed",
|
|
413
|
+
status=401)
|
|
414
|
+
return result
|
|
415
|
+
|
|
416
|
+
|
|
342
417
|
def __post_for_token(iam_server: IamServer,
|
|
343
418
|
body_data: dict[str, Any],
|
|
344
419
|
errors: list[str] | None,
|
|
@@ -3,7 +3,7 @@ from flask import Request, Response, request, jsonify
|
|
|
3
3
|
from logging import Logger
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from .iam_common import IamServer, _iam_lock,
|
|
6
|
+
from .iam_common import IamServer, _iam_lock, _iam_server_from_endpoint
|
|
7
7
|
from .iam_pomes import (
|
|
8
8
|
user_login, user_logout,
|
|
9
9
|
user_token, token_exchange, login_callback
|
|
@@ -55,9 +55,9 @@ def service_login() -> Response:
|
|
|
55
55
|
errors: list[str] = []
|
|
56
56
|
with _iam_lock:
|
|
57
57
|
# retrieve the IAM server
|
|
58
|
-
iam_server: IamServer =
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
59
|
+
errors=errors,
|
|
60
|
+
logger=__IAM_LOGGER)
|
|
61
61
|
if iam_server:
|
|
62
62
|
# obtain the login URL
|
|
63
63
|
login_url: str = user_login(iam_server=iam_server,
|
|
@@ -67,8 +67,8 @@ def service_login() -> Response:
|
|
|
67
67
|
if login_url:
|
|
68
68
|
result = jsonify({"login-url": login_url})
|
|
69
69
|
if errors:
|
|
70
|
-
result = Response("; ".join(errors)
|
|
71
|
-
|
|
70
|
+
result = Response(response="; ".join(errors),
|
|
71
|
+
status=400)
|
|
72
72
|
|
|
73
73
|
# log the response
|
|
74
74
|
if __IAM_LOGGER:
|
|
@@ -101,9 +101,9 @@ def service_logout() -> Response:
|
|
|
101
101
|
errors: list[str] = []
|
|
102
102
|
with _iam_lock:
|
|
103
103
|
# retrieve the IAM server
|
|
104
|
-
iam_server: IamServer =
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
105
|
+
errors=errors,
|
|
106
|
+
logger=__IAM_LOGGER)
|
|
107
107
|
if iam_server:
|
|
108
108
|
# logout the user
|
|
109
109
|
user_logout(iam_server=iam_server,
|
|
@@ -111,8 +111,8 @@ def service_logout() -> Response:
|
|
|
111
111
|
errors=errors,
|
|
112
112
|
logger=__IAM_LOGGER)
|
|
113
113
|
if errors:
|
|
114
|
-
result = Response("; ".join(errors)
|
|
115
|
-
|
|
114
|
+
result = Response(response="; ".join(errors),
|
|
115
|
+
status=400)
|
|
116
116
|
else:
|
|
117
117
|
result = Response(status=204)
|
|
118
118
|
|
|
@@ -155,9 +155,9 @@ def service_callback() -> Response:
|
|
|
155
155
|
token_data: tuple[str, str] | None = None
|
|
156
156
|
with _iam_lock:
|
|
157
157
|
# retrieve the IAM server
|
|
158
|
-
iam_server: IamServer =
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
159
|
+
errors=errors,
|
|
160
|
+
logger=__IAM_LOGGER)
|
|
161
161
|
if iam_server:
|
|
162
162
|
# process the callback operation
|
|
163
163
|
token_data = login_callback(iam_server=iam_server,
|
|
@@ -209,9 +209,9 @@ def service_token() -> Response:
|
|
|
209
209
|
if user_id:
|
|
210
210
|
with _iam_lock:
|
|
211
211
|
# retrieve the IAM server
|
|
212
|
-
iam_server: IamServer =
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
213
|
+
errors=errors,
|
|
214
|
+
logger=__IAM_LOGGER)
|
|
215
215
|
if iam_server:
|
|
216
216
|
# retrieve the token
|
|
217
217
|
errors: list[str] = []
|
|
@@ -227,8 +227,8 @@ def service_token() -> Response:
|
|
|
227
227
|
|
|
228
228
|
result: Response
|
|
229
229
|
if errors:
|
|
230
|
-
result = Response("; ".join(errors)
|
|
231
|
-
|
|
230
|
+
result = Response(response="; ".join(errors),
|
|
231
|
+
status=400)
|
|
232
232
|
else:
|
|
233
233
|
result = jsonify({"user-id": user_id,
|
|
234
234
|
"access-token": token})
|
|
@@ -271,9 +271,9 @@ def service_exchange() -> Response:
|
|
|
271
271
|
errors: list[str] = []
|
|
272
272
|
with _iam_lock:
|
|
273
273
|
# retrieve the IAM server (currently, only 'IAM_KEYCLOAK' is supported)
|
|
274
|
-
iam_server: IamServer =
|
|
275
|
-
|
|
276
|
-
|
|
274
|
+
iam_server: IamServer = _iam_server_from_endpoint(endpoint=request.endpoint,
|
|
275
|
+
errors=errors,
|
|
276
|
+
logger=__IAM_LOGGER)
|
|
277
277
|
# exchange the token
|
|
278
278
|
token_data: dict[str, Any] | None = None
|
|
279
279
|
if iam_server:
|
|
@@ -284,8 +284,8 @@ def service_exchange() -> Response:
|
|
|
284
284
|
logger=__IAM_LOGGER)
|
|
285
285
|
result: Response
|
|
286
286
|
if errors:
|
|
287
|
-
result = Response("; ".join(errors)
|
|
288
|
-
|
|
287
|
+
result = Response(response="; ".join(errors),
|
|
288
|
+
status=400)
|
|
289
289
|
else:
|
|
290
290
|
result = jsonify(token_data)
|
|
291
291
|
|
|
@@ -24,6 +24,7 @@ JUSBR_ENDPOINT_TOKEN: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_ENDPOINT
|
|
|
24
24
|
|
|
25
25
|
JUSBR_PUBLIC_KEY_LIFETIME: Final[int] = env_get_int(key=f"{APP_PREFIX}_JUSBR_PUBLIC_KEY_LIFETIME",
|
|
26
26
|
def_value=86400) # 24 hours
|
|
27
|
+
JUSBR_REALM: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_REALM")
|
|
27
28
|
JUSBR_RECIPIENT_ATTR: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_RECIPIENT_ATTR",
|
|
28
29
|
def_value="preferred_username")
|
|
29
30
|
JUSBR_URL_AUTH_BASE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_AUTH_BASE")
|
|
@@ -31,6 +32,7 @@ JUSBR_URL_AUTH_BASE: Final[str] = env_get_str(key=f"{APP_PREFIX}_JUSBR_URL_AUTH_
|
|
|
31
32
|
|
|
32
33
|
def jusbr_setup(flask_app: Flask,
|
|
33
34
|
base_url: str = JUSBR_URL_AUTH_BASE,
|
|
35
|
+
realm: str = JUSBR_REALM,
|
|
34
36
|
client_id: str = JUSBR_CLIENT_ID,
|
|
35
37
|
client_secret: str = JUSBR_CLIENT_SECRET,
|
|
36
38
|
client_timeout: int = JUSBR_CLIENT_TIMEOUT,
|
|
@@ -47,6 +49,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
47
49
|
|
|
48
50
|
:param flask_app: the Flask application
|
|
49
51
|
:param base_url: base URL to request JusBR services
|
|
52
|
+
:param realm: the JusBR realm
|
|
50
53
|
:param client_id: the client's identification with JusBR
|
|
51
54
|
:param client_secret: the client's password with JusBR
|
|
52
55
|
:param client_timeout: timeout for login authentication (in seconds,defaults to no timeout)
|
|
@@ -64,7 +67,7 @@ def jusbr_setup(flask_app: Flask,
|
|
|
64
67
|
cache["users"] = {}
|
|
65
68
|
with _iam_lock:
|
|
66
69
|
_IAM_SERVERS[IamServer.IAM_JUSRBR] = {
|
|
67
|
-
"base-url": base_url,
|
|
70
|
+
"base-url": f"{base_url}/realms/{realm}",
|
|
68
71
|
"client-id": client_id,
|
|
69
72
|
"client-secret": client_secret,
|
|
70
73
|
"client-timeout": client_timeout,
|
|
@@ -7,6 +7,45 @@ from pypomes_core import exc_format
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
def token_get_claims(token: str,
|
|
11
|
+
errors: list[str] = None,
|
|
12
|
+
logger: Logger = None) -> dict[str, dict[str, Any]] | None:
|
|
13
|
+
"""
|
|
14
|
+
Retrieve the claims set of a JWT *token*.
|
|
15
|
+
|
|
16
|
+
Any well-constructed JWT token may be provided in *token*.
|
|
17
|
+
Note that neither the token's signature nor its expiration is verified.
|
|
18
|
+
|
|
19
|
+
:param token: the refrence token
|
|
20
|
+
:param errors: incidental error messages
|
|
21
|
+
:param logger: optional logger
|
|
22
|
+
:return: the token's claimset, or *None* if error
|
|
23
|
+
"""
|
|
24
|
+
# initialize the return variable
|
|
25
|
+
result: dict[str, dict[str, Any]] | None = None
|
|
26
|
+
|
|
27
|
+
if logger:
|
|
28
|
+
logger.debug(msg="Retrieve claims for token")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
header: dict[str, Any] = jwt.get_unverified_header(jwt=token)
|
|
32
|
+
payload: dict[str, Any] = jwt.decode(jwt=token,
|
|
33
|
+
options={"verify_signature": False})
|
|
34
|
+
result = {
|
|
35
|
+
"header": header,
|
|
36
|
+
"payload": payload
|
|
37
|
+
}
|
|
38
|
+
except Exception as e:
|
|
39
|
+
exc_err: str = exc_format(exc=e,
|
|
40
|
+
exc_info=sys.exc_info())
|
|
41
|
+
if logger:
|
|
42
|
+
logger.error(msg=f"Error retrieving the token's claims: {exc_err}")
|
|
43
|
+
if isinstance(errors, list):
|
|
44
|
+
errors.append(exc_err)
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
10
49
|
def token_validate(token: str,
|
|
11
50
|
issuer: str = None,
|
|
12
51
|
recipient_id: str = None,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|