cardo-python-utils 0.5.dev24__tar.gz → 0.5.dev26__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 (39) hide show
  1. {cardo_python_utils-0.5.dev24/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev26}/PKG-INFO +1 -1
  2. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26/cardo_python_utils.egg-info}/PKG-INFO +1 -1
  3. cardo_python_utils-0.5.dev26/cardo_python_utils.egg-info/SOURCES.txt +35 -0
  4. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/pyproject.toml +1 -1
  5. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/admin/auth.py +11 -3
  6. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/service.py +24 -42
  7. cardo_python_utils-0.5.dev26/python_utils/keycloak/utils.py +51 -0
  8. cardo_python_utils-0.5.dev24/cardo_python_utils.egg-info/SOURCES.txt +0 -35
  9. cardo_python_utils-0.5.dev24/python_utils/django/keycloak/admin/user_groups_changelist.html +0 -7
  10. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/LICENSE +0 -0
  11. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/MANIFEST.in +0 -0
  12. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/README.rst +0 -0
  13. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
  14. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/cardo_python_utils.egg-info/requires.txt +0 -0
  15. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/cardo_python_utils.egg-info/top_level.txt +0 -0
  16. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/__init__.py +0 -0
  17. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/choices.py +0 -0
  18. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/data_structures.py +0 -0
  19. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/db.py +0 -0
  20. /cardo_python_utils-0.5.dev24/python_utils/django/utils.py → /cardo_python_utils-0.5.dev26/python_utils/django_utils.py +0 -0
  21. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/esma_choices.py +0 -0
  22. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/exceptions.py +0 -0
  23. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/imports.py +0 -0
  24. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/__init__.py +0 -0
  25. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/admin/__init__.py +0 -0
  26. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/admin/user_group.py +0 -0
  27. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/api/__init__.py +0 -0
  28. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/api/drf.py +0 -0
  29. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/api/ninja.py +0 -0
  30. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/api/utils.py +0 -0
  31. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/models/__init__.py +0 -0
  32. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/models/user_group.py +0 -0
  33. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/tests/__init__.py +0 -0
  34. {cardo_python_utils-0.5.dev24/python_utils/django/keycloak → cardo_python_utils-0.5.dev26/python_utils/keycloak/django}/tests/conftest.py +0 -0
  35. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/math.py +0 -0
  36. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/text.py +0 -0
  37. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/time.py +0 -0
  38. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/python_utils/types_hinting.py +0 -0
  39. {cardo_python_utils-0.5.dev24 → cardo_python_utils-0.5.dev26}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev24
3
+ Version: 0.5.dev26
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.dev24
3
+ Version: 0.5.dev26
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
@@ -0,0 +1,35 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.rst
4
+ pyproject.toml
5
+ cardo_python_utils.egg-info/PKG-INFO
6
+ cardo_python_utils.egg-info/SOURCES.txt
7
+ cardo_python_utils.egg-info/dependency_links.txt
8
+ cardo_python_utils.egg-info/requires.txt
9
+ cardo_python_utils.egg-info/top_level.txt
10
+ python_utils/__init__.py
11
+ python_utils/choices.py
12
+ python_utils/data_structures.py
13
+ python_utils/db.py
14
+ python_utils/django_utils.py
15
+ python_utils/esma_choices.py
16
+ python_utils/exceptions.py
17
+ python_utils/imports.py
18
+ python_utils/math.py
19
+ python_utils/text.py
20
+ python_utils/time.py
21
+ python_utils/types_hinting.py
22
+ python_utils/keycloak/utils.py
23
+ python_utils/keycloak/django/__init__.py
24
+ python_utils/keycloak/django/service.py
25
+ python_utils/keycloak/django/admin/__init__.py
26
+ python_utils/keycloak/django/admin/auth.py
27
+ python_utils/keycloak/django/admin/user_group.py
28
+ python_utils/keycloak/django/api/__init__.py
29
+ python_utils/keycloak/django/api/drf.py
30
+ python_utils/keycloak/django/api/ninja.py
31
+ python_utils/keycloak/django/api/utils.py
32
+ python_utils/keycloak/django/models/__init__.py
33
+ python_utils/keycloak/django/models/user_group.py
34
+ python_utils/keycloak/django/tests/__init__.py
35
+ python_utils/keycloak/django/tests/conftest.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.dev24"
7
+ version = "0.5.dev26"
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,13 +1,21 @@
1
1
  from django.conf import settings
2
2
  from mozilla_django_oidc.auth import OIDCAuthenticationBackend
3
3
 
4
+ from ...utils import get_keycloak_confidential_client_token
5
+
4
6
 
5
7
  class AdminAuthenticationBackend(OIDCAuthenticationBackend):
8
+ def get_token(self, payload):
9
+ # Instead of passing client_id and client_secret,
10
+ # client_assertion with service account token will be used
11
+ payload.pop("client_id", None)
12
+ payload.pop("client_secret", None)
13
+
14
+ return get_keycloak_confidential_client_token(**payload)
15
+
6
16
  def _get_user_data(self, claims) -> dict:
7
17
  client_roles = (
8
- claims.get("resource_access", {})
9
- .get(getattr(settings, "OIDC_RP_CLIENT_ID", ""), {})
10
- .get("roles", [])
18
+ claims.get("resource_access", {}).get(getattr(settings, "OIDC_RP_CLIENT_ID", ""), {}).get("roles", [])
11
19
  )
12
20
  is_superuser = "Admin" in client_roles
13
21
 
@@ -5,6 +5,13 @@ from keycloak import KeycloakAdmin
5
5
  from keycloak import KeycloakOpenIDConnection
6
6
  from keycloak.exceptions import KeycloakGetError
7
7
 
8
+ from ..utils import (
9
+ KEYCLOAK_SERVER_URL,
10
+ KEYCLOAK_REALM,
11
+ KEYCLOAK_CONFIDENTIAL_CLIENT_ID,
12
+ get_keycloak_confidential_client_token,
13
+ )
14
+
8
15
 
9
16
  def get_user_group_model():
10
17
  """
@@ -21,10 +28,7 @@ def get_user_group_model():
21
28
  try:
22
29
  model_string = getattr(settings, "KEYCLOAK_USER_GROUP_MODEL")
23
30
  except AttributeError:
24
- raise LookupError(
25
- "Please set KEYCLOAK_USER_GROUP_MODEL in your Django settings "
26
- "(e.g., 'myapp.UserGroup')."
27
- )
31
+ raise LookupError("Please set KEYCLOAK_USER_GROUP_MODEL in your Django settings (e.g., 'myapp.UserGroup').")
28
32
 
29
33
  return apps.get_model(model_string)
30
34
 
@@ -52,14 +56,10 @@ class KeycloakService:
52
56
 
53
57
  reported_group_ids = set()
54
58
  for group in groups:
55
- self._process_group_recursively(
56
- group, existing_groups_by_id, reported_group_ids
57
- )
59
+ self._process_group_recursively(group, existing_groups_by_id, reported_group_ids)
58
60
 
59
61
  # Identify deleted groups
60
- deleted_groups = self._user_group_model.objects.exclude(
61
- id__in=reported_group_ids
62
- )
62
+ deleted_groups = self._user_group_model.objects.exclude(id__in=reported_group_ids)
63
63
  if deleted_groups.exists():
64
64
  print(
65
65
  f"Deleting groups no longer present in Keycloak: {list(deleted_groups.values_list('path', flat=True))}"
@@ -68,11 +68,11 @@ class KeycloakService:
68
68
 
69
69
  def _get_keycloak_admin(self):
70
70
  keycloak_connection = KeycloakOpenIDConnection(
71
- server_url=settings.KEYCLOAK_SERVER_URL,
72
- realm_name=settings.KEYCLOAK_REALM,
73
- user_realm_name=settings.KEYCLOAK_REALM,
74
- client_id=settings.KEYCLOAK_ADMIN_CLIENT_ID,
75
- client_secret_key=settings.KEYCLOAK_ADMIN_CLIENT_SECRET,
71
+ server_url=KEYCLOAK_SERVER_URL,
72
+ realm_name=KEYCLOAK_REALM,
73
+ user_realm_name=KEYCLOAK_REALM,
74
+ client_id=KEYCLOAK_CONFIDENTIAL_CLIENT_ID,
75
+ token=get_keycloak_confidential_client_token(),
76
76
  verify=True,
77
77
  )
78
78
  return KeycloakAdmin(connection=keycloak_connection)
@@ -86,9 +86,7 @@ class KeycloakService:
86
86
  if group_id in existing_groups_by_id:
87
87
  existing_group = existing_groups_by_id[group_id]
88
88
  if existing_group.path != group["path"]:
89
- print(
90
- f"Updating group path from {existing_group.path} to {group['path']}..."
91
- )
89
+ print(f"Updating group path from {existing_group.path} to {group['path']}...")
92
90
  existing_group.path = group["path"]
93
91
  existing_group.save()
94
92
  else:
@@ -97,9 +95,7 @@ class KeycloakService:
97
95
 
98
96
  if subgroups := group.get("subGroups"):
99
97
  for subgroup in subgroups:
100
- self._process_group_recursively(
101
- subgroup, existing_groups_by_id, reported_group_ids
102
- )
98
+ self._process_group_recursively(subgroup, existing_groups_by_id, reported_group_ids)
103
99
 
104
100
 
105
101
  class KeycloakServiceAsync(KeycloakService):
@@ -121,24 +117,16 @@ class KeycloakServiceAsync(KeycloakService):
121
117
 
122
118
  # Process existing and new groups
123
119
  existing_groups = self._user_group_model.objects.all()
124
- existing_groups_by_id = {
125
- str(group.id): group async for group in existing_groups
126
- }
120
+ existing_groups_by_id = {str(group.id): group async for group in existing_groups}
127
121
 
128
122
  reported_group_ids = set()
129
123
  for group in groups:
130
- await self._process_group_recursively(
131
- group, existing_groups_by_id, reported_group_ids
132
- )
124
+ await self._process_group_recursively(group, existing_groups_by_id, reported_group_ids)
133
125
 
134
126
  # Identify deleted groups
135
- deleted_groups = self._user_group_model.objects.exclude(
136
- id__in=reported_group_ids
137
- )
127
+ deleted_groups = self._user_group_model.objects.exclude(id__in=reported_group_ids)
138
128
  if await deleted_groups.aexists():
139
- paths = [
140
- path async for path in deleted_groups.values_list("path", flat=True)
141
- ]
129
+ paths = [path async for path in deleted_groups.values_list("path", flat=True)]
142
130
  print(f"Deleting groups no longer present in Keycloak: {paths}")
143
131
 
144
132
  await deleted_groups.adelete()
@@ -152,22 +140,16 @@ class KeycloakServiceAsync(KeycloakService):
152
140
  if group_id in existing_groups_by_id:
153
141
  existing_group = existing_groups_by_id[group_id]
154
142
  if existing_group.path != group["path"]:
155
- print(
156
- f"Updating group path from {existing_group.path} to {group['path']}..."
157
- )
143
+ print(f"Updating group path from {existing_group.path} to {group['path']}...")
158
144
  existing_group.path = group["path"]
159
145
  await existing_group.asave()
160
146
  else:
161
147
  print(f"Creating new group with path {group['path']}...")
162
- await self._user_group_model.objects.acreate(
163
- id=group_id, path=group["path"]
164
- )
148
+ await self._user_group_model.objects.acreate(id=group_id, path=group["path"])
165
149
 
166
150
  if subgroups := group.get("subGroups"):
167
151
  for subgroup in subgroups:
168
- await self._process_group_recursively(
169
- subgroup, existing_groups_by_id, reported_group_ids
170
- )
152
+ await self._process_group_recursively(subgroup, existing_groups_by_id, reported_group_ids)
171
153
 
172
154
 
173
155
  class AuthServiceBase:
@@ -0,0 +1,51 @@
1
+ import os
2
+ import requests
3
+
4
+ KEYCLOAK_SERVER_URL = os.getenv("KEYCLOAK_SERVER_URL")
5
+ KEYCLOAK_REALM = os.getenv("KEYCLOAK_REALM")
6
+ KEYCLOAK_REALM_URL = f"{KEYCLOAK_SERVER_URL}/realms/{KEYCLOAK_REALM}"
7
+ KEYCLOAK_TOKEN_ENDPOINT = f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/token"
8
+
9
+ KEYCLOAK_CONFIDENTIAL_CLIENT_ID = os.getenv("KEYCLOAK_CONFIDENTIAL_CLIENT_ID")
10
+ KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH = os.getenv(
11
+ "KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH"
12
+ )
13
+ KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials"
14
+ KEYCLOAK_CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
15
+
16
+
17
+ def get_confidential_client_service_account_token() -> str:
18
+ """
19
+ Reads the Keycloak confidential client service account token from a file.
20
+ """
21
+ token_file_path = KEYCLOAK_CONFIDENTIAL_CLIENT_SERVICE_ACCOUNT_TOKEN_FILE_PATH
22
+ if not token_file_path or not os.path.isfile(token_file_path):
23
+ raise FileNotFoundError(f"Keycloak service account token file not found: {token_file_path}")
24
+
25
+ with open(token_file_path, "r") as f:
26
+ token = f.read().strip()
27
+
28
+ if not token:
29
+ raise ValueError("Keycloak service account token is empty.")
30
+
31
+ return token
32
+
33
+
34
+ def get_keycloak_confidential_client_token(**kwargs) -> dict:
35
+ """
36
+ Obtains token for a Keycloak confidential client with the client credentials grant,
37
+ using a service account token for authentication.
38
+ """
39
+
40
+ response = requests.post(
41
+ KEYCLOAK_TOKEN_ENDPOINT,
42
+ data={
43
+ "grant_type": KEYCLOAK_CLIENT_CREDENTIALS_GRANT_TYPE,
44
+ "client_assertion_type": KEYCLOAK_CLIENT_ASSERTION_TYPE,
45
+ "client_assertion": get_confidential_client_service_account_token(),
46
+ **kwargs,
47
+ },
48
+ )
49
+ response.raise_for_status()
50
+
51
+ return response.json()
@@ -1,35 +0,0 @@
1
- LICENSE
2
- MANIFEST.in
3
- README.rst
4
- pyproject.toml
5
- cardo_python_utils.egg-info/PKG-INFO
6
- cardo_python_utils.egg-info/SOURCES.txt
7
- cardo_python_utils.egg-info/dependency_links.txt
8
- cardo_python_utils.egg-info/requires.txt
9
- cardo_python_utils.egg-info/top_level.txt
10
- python_utils/__init__.py
11
- python_utils/choices.py
12
- python_utils/data_structures.py
13
- python_utils/db.py
14
- python_utils/esma_choices.py
15
- python_utils/exceptions.py
16
- python_utils/imports.py
17
- python_utils/math.py
18
- python_utils/text.py
19
- python_utils/time.py
20
- python_utils/types_hinting.py
21
- python_utils/django/utils.py
22
- python_utils/django/keycloak/__init__.py
23
- python_utils/django/keycloak/service.py
24
- python_utils/django/keycloak/admin/__init__.py
25
- python_utils/django/keycloak/admin/auth.py
26
- python_utils/django/keycloak/admin/user_group.py
27
- python_utils/django/keycloak/admin/user_groups_changelist.html
28
- python_utils/django/keycloak/api/__init__.py
29
- python_utils/django/keycloak/api/drf.py
30
- python_utils/django/keycloak/api/ninja.py
31
- python_utils/django/keycloak/api/utils.py
32
- python_utils/django/keycloak/models/__init__.py
33
- python_utils/django/keycloak/models/user_group.py
34
- python_utils/django/keycloak/tests/__init__.py
35
- python_utils/django/keycloak/tests/conftest.py
@@ -1,7 +0,0 @@
1
- {% extends 'admin/change_list.html' %}
2
-
3
- {% block object-tools-items %}
4
- {{ block.super }}
5
- <li><a href="sync-with-keycloak/">Sync groups with Keycloak 🔄</a></li>
6
-
7
- {% endblock %}