pypomes-iam 0.1.8__py3-none-any.whl → 0.2.0__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 +5 -0
- pypomes_iam/common_pomes.py +331 -19
- pypomes_iam/iam_pomes.py +176 -0
- pypomes_iam/jusbr_pomes.py +34 -358
- pypomes_iam/keycloak_pomes.py +43 -208
- {pypomes_iam-0.1.8.dist-info → pypomes_iam-0.2.0.dist-info}/METADATA +2 -2
- pypomes_iam-0.2.0.dist-info/RECORD +11 -0
- pypomes_iam-0.1.8.dist-info/RECORD +0 -10
- {pypomes_iam-0.1.8.dist-info → pypomes_iam-0.2.0.dist-info}/WHEEL +0 -0
- {pypomes_iam-0.1.8.dist-info → pypomes_iam-0.2.0.dist-info}/licenses/LICENSE +0 -0
pypomes_iam/keycloak_pomes.py
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import secrets
|
|
2
|
-
import string
|
|
3
|
-
# import sys
|
|
4
1
|
from cachetools import FIFOCache
|
|
5
2
|
from datetime import datetime
|
|
6
|
-
from flask import Flask
|
|
3
|
+
from flask import Flask
|
|
7
4
|
from logging import Logger
|
|
8
5
|
from pypomes_core import (
|
|
9
6
|
APP_PREFIX, TZ_LOCAL, env_get_int, env_get_str
|
|
10
7
|
)
|
|
11
8
|
from typing import Any, Final
|
|
12
9
|
|
|
13
|
-
from .common_pomes import
|
|
14
|
-
_get_login_timeout, _get_public_key, _get_user_data, _user_logout, _log_init
|
|
15
|
-
)
|
|
10
|
+
from .common_pomes import _service_token, _get_user_data
|
|
16
11
|
|
|
17
12
|
KEYCLOAK_CLIENT_ID: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_ID")
|
|
18
13
|
KEYCLOAK_CLIENT_SECRET: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK_CLIENT_SECRET")
|
|
@@ -43,22 +38,25 @@ KEYCLOAK_URL_AUTH_CALLBACK: Final[str] = env_get_str(key=f"{APP_PREFIX}_KEYCLOAK
|
|
|
43
38
|
# "key-expiration": <int>,
|
|
44
39
|
# "base-url": <str>,
|
|
45
40
|
# "callback-url": <str>,
|
|
41
|
+
# "safe-cache": <FIFOCache>
|
|
42
|
+
# }
|
|
43
|
+
# data in "safe-cache":
|
|
44
|
+
# {
|
|
46
45
|
# "users": {
|
|
47
46
|
# "<user-id>": {
|
|
48
|
-
# "
|
|
47
|
+
# "access-token": <str>
|
|
48
|
+
# "refresh-token": <str>
|
|
49
49
|
# "access-expiration": <timestamp>,
|
|
50
|
-
# "login-expiration": <
|
|
51
|
-
# "login-id": <str>,
|
|
52
|
-
#
|
|
53
|
-
# "access-token": <str>
|
|
54
|
-
# "refresh-token": <str>
|
|
50
|
+
# "login-expiration": <timestamp>, <-- transient
|
|
51
|
+
# "login-id": <str>, <-- transient
|
|
52
|
+
# "oauth-scope": <str> <-- optional
|
|
55
53
|
# }
|
|
56
54
|
# }
|
|
57
55
|
# }
|
|
58
56
|
_keycloak_registry: dict[str, Any] = {}
|
|
59
57
|
|
|
60
58
|
# dafault logger
|
|
61
|
-
|
|
59
|
+
_keycloak_logger: Logger | None = None
|
|
62
60
|
|
|
63
61
|
|
|
64
62
|
def keycloak_setup(flask_app: Flask,
|
|
@@ -93,11 +91,11 @@ def keycloak_setup(flask_app: Flask,
|
|
|
93
91
|
:param callback_url: URL for Keycloak to callback on login
|
|
94
92
|
:param logger: optional logger
|
|
95
93
|
"""
|
|
96
|
-
|
|
94
|
+
from .iam_pomes import service_login, service_logout, service_callback, service_token
|
|
95
|
+
global _keycloak_logger, _keycloak_registry
|
|
97
96
|
|
|
98
97
|
# establish the logger
|
|
99
|
-
|
|
100
|
-
_logger = logger
|
|
98
|
+
_keycloak_logger = logger
|
|
101
99
|
|
|
102
100
|
# configure the JusBR registry
|
|
103
101
|
_keycloak_registry = {
|
|
@@ -108,7 +106,7 @@ def keycloak_setup(flask_app: Flask,
|
|
|
108
106
|
"callback-url": callback_url,
|
|
109
107
|
"key-expiration": int(datetime.now(tz=TZ_LOCAL).timestamp()),
|
|
110
108
|
"key-lifetime": public_key_lifetime,
|
|
111
|
-
"
|
|
109
|
+
"safe-cache": FIFOCache(maxsize=1048576)
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
# establish the endpoints
|
|
@@ -134,207 +132,44 @@ def keycloak_setup(flask_app: Flask,
|
|
|
134
132
|
methods=["POST"])
|
|
135
133
|
|
|
136
134
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"""
|
|
141
|
-
Entry point for the Keycloak login service.
|
|
142
|
-
|
|
143
|
-
Redirect the request to the Keycloak authentication page, with the appropriate parameters.
|
|
144
|
-
|
|
145
|
-
:return: the response from the redirect operation
|
|
146
|
-
"""
|
|
147
|
-
global _keycloak_registry
|
|
148
|
-
|
|
149
|
-
# log the request
|
|
150
|
-
if _logger:
|
|
151
|
-
msg: str = _log_init(request=request)
|
|
152
|
-
_logger.debug(msg=msg)
|
|
153
|
-
|
|
154
|
-
# build the OAuth2 state, and temporarily use it as 'user_id'
|
|
155
|
-
oauth_state: str = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16))
|
|
156
|
-
# obtain the user data
|
|
157
|
-
user_data: dict[str, Any] = _get_user_data(registry=_keycloak_registry,
|
|
158
|
-
user_id=oauth_state,
|
|
159
|
-
logger=_logger)
|
|
160
|
-
# build the redirect url
|
|
161
|
-
timeout: int = _get_login_timeout(registry=_keycloak_registry)
|
|
162
|
-
safe_cache: FIFOCache
|
|
163
|
-
if timeout:
|
|
164
|
-
safe_cache = FIFOCache(maxsize=16)
|
|
165
|
-
else:
|
|
166
|
-
safe_cache = FIFOCache(maxsize=16)
|
|
167
|
-
safe_cache["valid"] = True
|
|
168
|
-
user_data["cache-obj"] = safe_cache
|
|
169
|
-
auth_url: str = (
|
|
170
|
-
f"{_keycloak_registry["base-url"]}/protocol/openid-connect/auth"
|
|
171
|
-
f"?client_id={_keycloak_registry["client-id"]}"
|
|
172
|
-
f"&response_type=code"
|
|
173
|
-
f"&scope=openid"
|
|
174
|
-
f"&redirect_uri={_keycloak_registry["callback-url"]}"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
# redirect the request
|
|
178
|
-
result: Response = redirect(location=auth_url)
|
|
179
|
-
|
|
180
|
-
# log the response
|
|
181
|
-
if _logger:
|
|
182
|
-
_logger.debug(msg=f"Response {result}")
|
|
183
|
-
|
|
184
|
-
return result
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
# @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:logout
|
|
188
|
-
# methods=["GET"])
|
|
189
|
-
def service_logout() -> Response:
|
|
190
|
-
"""
|
|
191
|
-
Entry point for the Keycloak logout service.
|
|
192
|
-
|
|
193
|
-
Remove all data associating the user with Keycloak from the registry.
|
|
194
|
-
|
|
195
|
-
:return: response *OK*
|
|
196
|
-
"""
|
|
197
|
-
global _keycloak_registry
|
|
198
|
-
|
|
199
|
-
# log the request
|
|
200
|
-
if _logger:
|
|
201
|
-
msg: str = _log_init(request=request)
|
|
202
|
-
_logger.debug(msg=msg)
|
|
203
|
-
|
|
204
|
-
# retrieve the user id
|
|
205
|
-
input_params: dict[str, Any] = request.args
|
|
206
|
-
user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
207
|
-
|
|
208
|
-
# logout the user
|
|
209
|
-
_user_logout(registry=_keycloak_registry,
|
|
210
|
-
user_id=user_id,
|
|
211
|
-
logger=_logger)
|
|
212
|
-
|
|
213
|
-
result: Response = Response(status=200)
|
|
214
|
-
|
|
215
|
-
# log the response
|
|
216
|
-
if _logger:
|
|
217
|
-
_logger.debug(msg=f"Response {result}")
|
|
218
|
-
|
|
219
|
-
return result
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
# @flask_app.route(rule=<callback_endpoint>, # KEYCLOAK_CALLBACK_ENDPOINT: /iam/keycloak:callback
|
|
223
|
-
# methods=["POST"])
|
|
224
|
-
def service_callback() -> Response:
|
|
135
|
+
def keycloak_get_token(user_id: str,
|
|
136
|
+
errors: list[str] = None,
|
|
137
|
+
logger: Logger = None) -> str:
|
|
225
138
|
"""
|
|
226
|
-
|
|
139
|
+
Retrieve a Keycloak authentication token for *user_id*.
|
|
227
140
|
|
|
228
|
-
:
|
|
141
|
+
:param user_id: the user's identification
|
|
142
|
+
:param errors: incidental errors
|
|
143
|
+
:param logger: optional logger
|
|
144
|
+
:return: the uthentication tokem
|
|
229
145
|
"""
|
|
230
146
|
global _keycloak_registry
|
|
231
|
-
from .token_pomes import token_validate
|
|
232
|
-
|
|
233
|
-
# log the request
|
|
234
|
-
if _logger:
|
|
235
|
-
msg: str = _log_init(request=request)
|
|
236
|
-
_logger.debug(msg=msg)
|
|
237
|
-
|
|
238
|
-
# validate the OAuth2 state
|
|
239
|
-
oauth_state: str = request.args.get("state")
|
|
240
|
-
user_id: str | None = None
|
|
241
|
-
user_data: dict[str, Any] | None = None
|
|
242
|
-
if oauth_state:
|
|
243
|
-
for user, data in _keycloak_registry.get("users").items():
|
|
244
|
-
safe_cache: FIFOCache = data.get("cache-obj")
|
|
245
|
-
if user == oauth_state:
|
|
246
|
-
if data.get("valid"):
|
|
247
|
-
user_id = user
|
|
248
|
-
user_data = data
|
|
249
|
-
else:
|
|
250
|
-
msg = "Operation timeout"
|
|
251
|
-
break
|
|
252
|
-
|
|
253
|
-
# exchange 'code' for the token
|
|
254
|
-
token: str | None = None
|
|
255
|
-
errors: list[str] = []
|
|
256
|
-
if user_data:
|
|
257
|
-
code: str = request.args.get("code")
|
|
258
|
-
body_data: dict[str, Any] = {
|
|
259
|
-
"grant_type": "authorization_code",
|
|
260
|
-
"code": code,
|
|
261
|
-
"redirec_url": _keycloak_registry.get("callback-url"),
|
|
262
|
-
}
|
|
263
|
-
# token = __post_jusbr(user_data=user_data,
|
|
264
|
-
# body_data=body_data,
|
|
265
|
-
# errors=errors,
|
|
266
|
-
# logger=_logger)
|
|
267
|
-
# retrieve the token's claims
|
|
268
|
-
if not errors:
|
|
269
|
-
public_key: bytes = _get_public_key(registry=_keycloak_registry,
|
|
270
|
-
url=_keycloak_registry["base-url"],
|
|
271
|
-
logger=_logger)
|
|
272
|
-
token_claims: dict[str, dict[str, Any]] = token_validate(token=token,
|
|
273
|
-
issuer=_keycloak_registry["base-url"],
|
|
274
|
-
public_key=public_key,
|
|
275
|
-
errors=errors,
|
|
276
|
-
logger=_logger)
|
|
277
|
-
if not errors:
|
|
278
|
-
token_user: str = token_claims["payload"].get("preferred_username")
|
|
279
|
-
if user_id == oauth_state:
|
|
280
|
-
user_id = token_user
|
|
281
|
-
_keycloak_registry["users"][user_id] = _keycloak_registry["users"].pop(oauth_state)
|
|
282
|
-
elif token_user != user_id:
|
|
283
|
-
errors.append(f"Token was issued to user '{token_user}'")
|
|
284
|
-
else:
|
|
285
|
-
msg: str = "Unknown OAuth2 code received"
|
|
286
|
-
if _get_login_timeout(registry=_keycloak_registry):
|
|
287
|
-
msg += " - possible operation timeout"
|
|
288
|
-
errors.append(msg)
|
|
289
147
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
"user_id": user_id,
|
|
297
|
-
"access_token": token})
|
|
148
|
+
# retrieve the token
|
|
149
|
+
args: dict[str, Any] = {"user-id": user_id}
|
|
150
|
+
return _service_token(registry=_keycloak_registry,
|
|
151
|
+
args=args,
|
|
152
|
+
errors=errors,
|
|
153
|
+
logger=logger)
|
|
298
154
|
|
|
299
|
-
# log the response
|
|
300
|
-
if _logger:
|
|
301
|
-
_logger.debug(msg=f"Response {result}")
|
|
302
155
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
# @flask_app.route(rule=<token_endpoint>, # JUSBR_TOKEN_ENDPOINT: /iam/jusbr:get-token
|
|
307
|
-
# methods=["GET"])
|
|
308
|
-
def service_token() -> Response:
|
|
309
|
-
"""
|
|
310
|
-
Entry point for retrieving the Keycloak token.
|
|
311
|
-
|
|
312
|
-
:return: the response containing the token, or *UNAUTHORIZED*
|
|
313
|
-
"""
|
|
314
|
-
# log the request
|
|
315
|
-
if _logger:
|
|
316
|
-
msg: str = _log_init(request=request)
|
|
317
|
-
_logger.debug(msg=msg)
|
|
318
|
-
|
|
319
|
-
# retrieve the user id
|
|
320
|
-
input_params: dict[str, Any] = request.args
|
|
321
|
-
_user_id: str = input_params.get("user-id") or input_params.get("login")
|
|
322
|
-
return Response()
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def keycloak_get_token(user_id: str,
|
|
326
|
-
errors: list[str] = None,
|
|
327
|
-
logger: Logger = None) -> str:
|
|
156
|
+
def keycloak_set_scope(user_id: str,
|
|
157
|
+
scope: str,
|
|
158
|
+
logger: Logger | None) -> None:
|
|
328
159
|
"""
|
|
329
|
-
|
|
160
|
+
Set the OAuth2 scope of *user_id* to *scope*.
|
|
330
161
|
|
|
331
162
|
:param user_id: the user's identification
|
|
332
|
-
:param
|
|
163
|
+
:param scope: the OAuth2 scope to set to the user
|
|
333
164
|
:param logger: optional logger
|
|
334
|
-
:return: the token for *user_id*, or *None* if error
|
|
335
165
|
"""
|
|
336
166
|
global _keycloak_registry
|
|
337
167
|
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
|
|
168
|
+
# retrieve user data
|
|
169
|
+
user_data: dict[str, Any] = _get_user_data(registry=_keycloak_registry,
|
|
170
|
+
user_id=user_id,
|
|
171
|
+
logger=logger)
|
|
172
|
+
# set the OAuth2 scope
|
|
173
|
+
user_data["oauth-scope"] = scope
|
|
174
|
+
if logger:
|
|
175
|
+
logger.debug(msg=f"Scope for user '{user_id}' set to '{scope}'")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pypomes_iam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -13,6 +13,6 @@ Requires-Python: >=3.12
|
|
|
13
13
|
Requires-Dist: cachetools>=6.2.1
|
|
14
14
|
Requires-Dist: flask>=3.1.2
|
|
15
15
|
Requires-Dist: pyjwt>=2.10.1
|
|
16
|
-
Requires-Dist: pypomes-core>=2.8.
|
|
16
|
+
Requires-Dist: pypomes-core>=2.8.1
|
|
17
17
|
Requires-Dist: pypomes-crypto>=0.4.8
|
|
18
18
|
Requires-Dist: requests>=2.32.5
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pypomes_iam/__init__.py,sha256=ieysDaKOQc3B50PvChh8DLDG5R3XgbTzX3bU0ekGoUk,760
|
|
2
|
+
pypomes_iam/common_pomes.py,sha256=bLDaoWM5KLccxsNSyiK5UbXRNBgqsQ7TB0Q4Nc72QoI,16415
|
|
3
|
+
pypomes_iam/iam_pomes.py,sha256=THztlEWObDY4_L8GHQem2uX5J8_44XEP-mUg2Fi_Gx0,5527
|
|
4
|
+
pypomes_iam/jusbr_pomes.py,sha256=R-i0FatmlvTp3UszUrrz2L3BQRkZue8F9Nfy0i4cKHw,7084
|
|
5
|
+
pypomes_iam/keycloak_pomes.py,sha256=TCye3E4xijyisgG-vKoJOhXywNgdyzTuuVzFjNbaJ3I,7490
|
|
6
|
+
pypomes_iam/provider_pomes.py,sha256=eP8XzjTUEpwejTkO0wmDiqKjqbIEOzRNCR2ju5E15og,5856
|
|
7
|
+
pypomes_iam/token_pomes.py,sha256=McjKB8omCjuicenwvDVPiWYu3-7gQeLg1AzgAVKK32M,4309
|
|
8
|
+
pypomes_iam-0.2.0.dist-info/METADATA,sha256=nnSivBbIIMIyu5rSSXr5aQq8S1HhcF9xgb3WFeIx-jA,694
|
|
9
|
+
pypomes_iam-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
pypomes_iam-0.2.0.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
11
|
+
pypomes_iam-0.2.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
pypomes_iam/__init__.py,sha256=lHnqNqW1stQjcM6cr9wf3GGnw5_zGf1HN3zyHGb8PCA,577
|
|
2
|
-
pypomes_iam/common_pomes.py,sha256=kdzyEJX275SmMa_zi6AJaC9gVxlXcOailyantPvNOyQ,3908
|
|
3
|
-
pypomes_iam/jusbr_pomes.py,sha256=kNgAgQAMDdODoNO4XKrSggFwQ7R2ID-LLz7tmT3PXH4,19510
|
|
4
|
-
pypomes_iam/keycloak_pomes.py,sha256=m4jM_4c_McVg74T7JG7j3tbMo9Yxp6IKgt8TuauIp7o,13204
|
|
5
|
-
pypomes_iam/provider_pomes.py,sha256=eP8XzjTUEpwejTkO0wmDiqKjqbIEOzRNCR2ju5E15og,5856
|
|
6
|
-
pypomes_iam/token_pomes.py,sha256=McjKB8omCjuicenwvDVPiWYu3-7gQeLg1AzgAVKK32M,4309
|
|
7
|
-
pypomes_iam-0.1.8.dist-info/METADATA,sha256=fjisTEC7XbvWn37I-2Jez6vtk7Uo83Q1WCjq172hOok,694
|
|
8
|
-
pypomes_iam-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
-
pypomes_iam-0.1.8.dist-info/licenses/LICENSE,sha256=YvUELgV8qvXlaYsy9hXG5EW3Bmsrkw-OJmmILZnonAc,1086
|
|
10
|
-
pypomes_iam-0.1.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|