cardo-python-utils 0.5.dev10__tar.gz → 0.5.dev12__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.
Files changed (35) hide show
  1. {cardo_python_utils-0.5.dev10/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev12}/PKG-INFO +1 -1
  2. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12/cardo_python_utils.egg-info}/PKG-INFO +1 -1
  3. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/cardo_python_utils.egg-info/SOURCES.txt +1 -0
  4. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/pyproject.toml +1 -1
  5. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/api/drf.py +3 -49
  6. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/api/ninja.py +7 -47
  7. cardo_python_utils-0.5.dev12/python_utils/django/keycloak/api/utils.py +77 -0
  8. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/LICENSE +0 -0
  9. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/MANIFEST.in +0 -0
  10. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/README.rst +0 -0
  11. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
  12. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/cardo_python_utils.egg-info/requires.txt +0 -0
  13. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/cardo_python_utils.egg-info/top_level.txt +0 -0
  14. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/__init__.py +0 -0
  15. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/choices.py +0 -0
  16. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/data_structures.py +0 -0
  17. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/db.py +0 -0
  18. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/__init__.py +0 -0
  19. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/admin/__init__.py +0 -0
  20. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/admin/auth.py +0 -0
  21. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/admin/user_group.py +0 -0
  22. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/admin/user_groups_changelist.html +0 -0
  23. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/api/__init__.py +0 -0
  24. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/models/__init__.py +0 -0
  25. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/models/user_group.py +0 -0
  26. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/keycloak/service.py +0 -0
  27. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/django/utils.py +0 -0
  28. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/esma_choices.py +0 -0
  29. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/exceptions.py +0 -0
  30. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/imports.py +0 -0
  31. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/math.py +0 -0
  32. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/text.py +0 -0
  33. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/time.py +0 -0
  34. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/python_utils/types_hinting.py +0 -0
  35. {cardo_python_utils-0.5.dev10 → cardo_python_utils-0.5.dev12}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev10
3
+ Version: 0.5.dev12
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
5
  Author-email: CardoAI <hello@cardoai.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev10
3
+ Version: 0.5.dev12
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
5
  Author-email: CardoAI <hello@cardoai.com>
6
6
  License: MIT
@@ -28,5 +28,6 @@ python_utils/django/keycloak/admin/user_groups_changelist.html
28
28
  python_utils/django/keycloak/api/__init__.py
29
29
  python_utils/django/keycloak/api/drf.py
30
30
  python_utils/django/keycloak/api/ninja.py
31
+ python_utils/django/keycloak/api/utils.py
31
32
  python_utils/django/keycloak/models/__init__.py
32
33
  python_utils/django/keycloak/models/user_group.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardo-python-utils"
7
- version = "0.5.dev10"
7
+ version = "0.5.dev12"
8
8
  description = "Python library enhanced with a wide range of functions for different scenarios."
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.8"
@@ -1,31 +1,19 @@
1
- import jwt
2
-
3
1
  from django.conf import settings
4
- from django.contrib.auth import get_user_model
5
- from jwt import PyJWKClient
6
2
  from jwt.exceptions import InvalidTokenError
7
3
 
8
4
  from rest_framework import authentication
9
5
  from rest_framework.exceptions import AuthenticationFailed
10
6
  from rest_framework.permissions import BasePermission
11
7
 
12
-
13
- jwks_client = PyJWKClient(getattr(settings, "JWKS_URL", ""))
8
+ from .utils import create_or_update_user, decode_jwt
14
9
 
15
10
 
16
11
  class AuthenticationBackend(authentication.TokenAuthentication):
17
12
  keyword = "Bearer"
18
13
 
19
14
  def authenticate_credentials(self, token: str):
20
- signing_key = jwks_client.get_signing_key_from_jwt(token)
21
-
22
15
  try:
23
- payload = jwt.decode(
24
- token,
25
- signing_key.key,
26
- algorithms=["RS256"],
27
- audience=getattr(settings, "JWT_AUDIENCE", None),
28
- )
16
+ payload = decode_jwt(token)
29
17
  except InvalidTokenError as e:
30
18
  raise AuthenticationFailed(f"Invalid token: {str(e)}") from e
31
19
 
@@ -36,43 +24,9 @@ class AuthenticationBackend(authentication.TokenAuthentication):
36
24
  "Invalid token: preferred_username not present."
37
25
  ) from e
38
26
 
39
- user = self._get_user(username, payload)
27
+ user = create_or_update_user(username, payload)
40
28
  return user, payload
41
29
 
42
- def _get_user(self, username: str, payload: dict):
43
- """
44
- Get or create a user based on the JWT payload.
45
- If the user exists, update their details.
46
- """
47
- user_model = get_user_model()
48
- user_data = {
49
- "first_name": payload.get("given_name") or "",
50
- "last_name": payload.get("family_name") or "",
51
- "email": payload.get("email") or "",
52
- "is_staff": payload.get("is_staff", False),
53
- }
54
- if hasattr(user_model, "is_demo"):
55
- user_data["is_demo"] = payload.get("is_demo", False)
56
-
57
- user = user_model.objects.filter(username=username).first()
58
- if user:
59
- update_needed = False
60
-
61
- for field, value in user_data.items():
62
- if getattr(user, field) != value:
63
- setattr(user, field, value)
64
- update_needed = True
65
-
66
- if update_needed:
67
- user.save(update_fields=list(user_data.keys()))
68
-
69
- return user
70
- else:
71
- return user_model.objects.create(
72
- username=username,
73
- **user_data,
74
- )
75
-
76
30
 
77
31
  class HasScope(BasePermission):
78
32
  """
@@ -1,27 +1,18 @@
1
1
  from typing import Literal
2
2
 
3
- from jwt import PyJWKClient, decode as jwt_decode
4
3
  from jwt.exceptions import InvalidTokenError
5
4
 
6
5
  from django.conf import settings
7
- from django.contrib.auth import get_user_model
8
6
  from ninja.security import HttpBearer
9
7
  from ninja.errors import AuthenticationError, HttpError
10
8
 
11
- jwks_client = PyJWKClient(getattr(settings, "JWKS_URL", ""))
9
+ from .utils import create_or_update_user, decode_jwt
12
10
 
13
11
 
14
12
  class AuthBearer(HttpBearer):
15
13
  def authenticate(self, request, token):
16
- signing_key = jwks_client.get_signing_key_from_jwt(token)
17
-
18
14
  try:
19
- payload = jwt_decode(
20
- token,
21
- signing_key.key,
22
- algorithms=["RS256"],
23
- audience=getattr(settings, "JWT_AUDIENCE", None),
24
- )
15
+ payload = decode_jwt(token)
25
16
  except InvalidTokenError as e:
26
17
  raise AuthenticationError(f"Invalid token: {str(e)}") from e
27
18
 
@@ -32,45 +23,14 @@ class AuthBearer(HttpBearer):
32
23
  "Invalid token: preferred_username not present."
33
24
  ) from e
34
25
 
35
- user = self._get_user(username, payload)
26
+ user = create_or_update_user(username, payload)
36
27
 
37
28
  self._verify_scopes(request, payload)
38
29
 
39
- return user
40
-
41
- def _get_user(self, username: str, payload: dict):
42
- """
43
- Get or create a user based on the JWT payload.
44
- If the user exists, update their details.
45
- """
46
- user_model = get_user_model()
47
- user_data = {
48
- "first_name": payload.get("given_name") or "",
49
- "last_name": payload.get("family_name") or "",
50
- "email": payload.get("email") or "",
51
- "is_staff": payload.get("is_staff", False),
52
- }
53
- if hasattr(user_model, "is_demo"):
54
- user_data["is_demo"] = payload.get("is_demo", False)
55
-
56
- user = user_model.objects.filter(username=username).first()
57
- if user:
58
- update_needed = False
59
-
60
- for field, value in user_data.items():
61
- if getattr(user, field) != value:
62
- setattr(user, field, value)
63
- update_needed = True
64
-
65
- if update_needed:
66
- user.save(update_fields=list(user_data.keys()))
67
-
68
- return user
69
- else:
70
- return user_model.objects.create(
71
- username=username,
72
- **user_data,
73
- )
30
+ request.user = user
31
+
32
+ # The return value is stored in request.auth
33
+ return payload
74
34
 
75
35
  def _verify_scopes(self, request, token_payload):
76
36
  allowed_scopes = self._get_view_allowed_scopes(request)
@@ -0,0 +1,77 @@
1
+ from typing import TypedDict
2
+
3
+ from django.conf import settings
4
+ from django.contrib.auth import get_user_model
5
+ from jwt import decode, PyJWKClient
6
+
7
+ jwks_client = PyJWKClient(getattr(settings, "JWKS_URL", ""))
8
+
9
+
10
+ class TokenPayload(TypedDict, total=False):
11
+ exp: int
12
+ iat: int
13
+ jti: str
14
+ iss: str
15
+ aud: str | list[str]
16
+ typ: str
17
+ azp: str
18
+ sid: str
19
+ scope: str
20
+ preferred_username: str
21
+ given_name: str
22
+ family_name: str
23
+ email: str
24
+ is_staff: bool
25
+ is_demo: bool
26
+ groups: list[str] # Full path of the user group, e.g. "/group1/subgroup1"
27
+
28
+
29
+ def decode_jwt(token: str) -> TokenPayload:
30
+ """
31
+ Decode a JWT token using the public certificate of the Auth Server.
32
+
33
+ Raises:
34
+ jwt.exceptions.InvalidTokenError: If the token is invalid or cannot be decoded.
35
+ """
36
+ signing_key = jwks_client.get_signing_key_from_jwt(token)
37
+
38
+ return decode(
39
+ token,
40
+ signing_key.key,
41
+ algorithms=["RS256"],
42
+ audience=getattr(settings, "JWT_AUDIENCE", None),
43
+ )
44
+
45
+
46
+ def create_or_update_user(username: str, payload: TokenPayload):
47
+ """
48
+ Create or update a user based on the JWT payload.
49
+ """
50
+ user_model = get_user_model()
51
+ user_data = {
52
+ "first_name": payload.get("given_name") or "",
53
+ "last_name": payload.get("family_name") or "",
54
+ "email": payload.get("email") or "",
55
+ "is_staff": payload.get("is_staff", False),
56
+ }
57
+ if hasattr(user_model, "is_demo"):
58
+ user_data["is_demo"] = payload.get("is_demo", False)
59
+
60
+ user = user_model.objects.filter(username=username).first()
61
+ if user:
62
+ update_needed = False
63
+
64
+ for field, value in user_data.items():
65
+ if getattr(user, field) != value:
66
+ setattr(user, field, value)
67
+ update_needed = True
68
+
69
+ if update_needed:
70
+ user.save(update_fields=list(user_data.keys()))
71
+
72
+ return user
73
+ else:
74
+ return user_model.objects.create(
75
+ username=username,
76
+ **user_data,
77
+ )