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.

@@ -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, Response, redirect, request, jsonify
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
- # "cache-obj": <FIFOCache>,
47
+ # "access-token": <str>
48
+ # "refresh-token": <str>
49
49
  # "access-expiration": <timestamp>,
50
- # "login-expiration": <int>, <-- transient
51
- # "login-id": <str>, <-- transient
52
- # data in <FIFOCache>:
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
- _logger: Logger | None = None
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
- global _keycloak_registry
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
- global _logger
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
- "users": []
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
- # @flask_app.route(rule=<login_endpoint>, # KEYCLOAK_LOGIN_ENDPOINT: /iam/keycloak:login
138
- # methods=["GET"])
139
- def service_login() -> Response:
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
- Entry point for the callback from Keycloak on authentication operation.
139
+ Retrieve a Keycloak authentication token for *user_id*.
227
140
 
228
- :return: the response containing the token, or *NOT AUTHORIZED*
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
- result: Response
291
- if errors:
292
- result = jsonify({"errors": "; ".join(errors)})
293
- result.status_code = 400
294
- else:
295
- result = jsonify({
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
- return result
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
- Retrieve the authentication token for user *user_id*.
160
+ Set the OAuth2 scope of *user_id* to *scope*.
330
161
 
331
162
  :param user_id: the user's identification
332
- :param errors: incidental error messages
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
- # initialize the return variable
339
- result: str | None = None
340
- return result
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.1.8
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.0
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,,