cardo-python-utils 0.5.dev0__tar.gz → 0.5.dev2__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 (28) hide show
  1. {cardo_python_utils-0.5.dev0/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev2}/PKG-INFO +5 -2
  2. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2/cardo_python_utils.egg-info}/PKG-INFO +5 -2
  3. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/cardo_python_utils.egg-info/SOURCES.txt +1 -0
  4. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/cardo_python_utils.egg-info/requires.txt +4 -0
  5. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/pyproject.toml +6 -3
  6. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/django/auth/admin.py +1 -3
  7. cardo_python_utils-0.5.dev2/python_utils/django/auth/keycloak.py +119 -0
  8. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/LICENSE +0 -0
  9. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/MANIFEST.in +0 -0
  10. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/README.rst +0 -0
  11. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
  12. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/cardo_python_utils.egg-info/top_level.txt +0 -0
  13. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/__init__.py +0 -0
  14. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/choices.py +0 -0
  15. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/data_structures.py +0 -0
  16. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/db.py +0 -0
  17. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/django/auth/__init__.py +0 -0
  18. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/django/auth/drf.py +0 -0
  19. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/django/auth/ninja.py +0 -0
  20. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/django/utils.py +0 -0
  21. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/esma_choices.py +0 -0
  22. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/exceptions.py +0 -0
  23. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/imports.py +0 -0
  24. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/math.py +0 -0
  25. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/text.py +0 -0
  26. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/time.py +0 -0
  27. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/python_utils/types_hinting.py +0 -0
  28. {cardo_python_utils-0.5.dev0 → cardo_python_utils-0.5.dev2}/setup.cfg +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev0
3
+ Version: 0.5.dev2
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
- Author-email: Kristi Kotini <hello@cardoai.com>, Klajdi Çaushi <hello@cardoai.com>
5
+ Author-email: CardoAI <hello@cardoai.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/CardoAI/cardo-python-utils
8
8
  Project-URL: Repository, https://github.com/CardoAI/cardo-python-utils.git
@@ -37,6 +37,9 @@ Requires-Dist: PyJWT; extra == "drf"
37
37
  Provides-Extra: django-admin-auth
38
38
  Requires-Dist: Django; extra == "django-admin-auth"
39
39
  Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "django-admin-auth"
40
+ Provides-Extra: django-keycloak-groups
41
+ Requires-Dist: Django; extra == "django-keycloak-groups"
42
+ Requires-Dist: python-keycloak>=5.8.1; extra == "django-keycloak-groups"
40
43
  Provides-Extra: all
41
44
  Requires-Dist: Django; extra == "all"
42
45
  Provides-Extra: dev
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev0
3
+ Version: 0.5.dev2
4
4
  Summary: Python library enhanced with a wide range of functions for different scenarios.
5
- Author-email: Kristi Kotini <hello@cardoai.com>, Klajdi Çaushi <hello@cardoai.com>
5
+ Author-email: CardoAI <hello@cardoai.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/CardoAI/cardo-python-utils
8
8
  Project-URL: Repository, https://github.com/CardoAI/cardo-python-utils.git
@@ -37,6 +37,9 @@ Requires-Dist: PyJWT; extra == "drf"
37
37
  Provides-Extra: django-admin-auth
38
38
  Requires-Dist: Django; extra == "django-admin-auth"
39
39
  Requires-Dist: mozilla-django-oidc>=4.0.1; extra == "django-admin-auth"
40
+ Provides-Extra: django-keycloak-groups
41
+ Requires-Dist: Django; extra == "django-keycloak-groups"
42
+ Requires-Dist: python-keycloak>=5.8.1; extra == "django-keycloak-groups"
40
43
  Provides-Extra: all
41
44
  Requires-Dist: Django; extra == "all"
42
45
  Provides-Extra: dev
@@ -22,4 +22,5 @@ python_utils/django/utils.py
22
22
  python_utils/django/auth/__init__.py
23
23
  python_utils/django/auth/admin.py
24
24
  python_utils/django/auth/drf.py
25
+ python_utils/django/auth/keycloak.py
25
26
  python_utils/django/auth/ninja.py
@@ -15,6 +15,10 @@ Django
15
15
  Django
16
16
  mozilla-django-oidc>=4.0.1
17
17
 
18
+ [django-keycloak-groups]
19
+ Django
20
+ python-keycloak>=5.8.1
21
+
18
22
  [django-ninja]
19
23
  Django
20
24
  django-ninja
@@ -4,14 +4,13 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cardo-python-utils"
7
- version = "0.5.dev0"
7
+ version = "0.5.dev2"
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"
11
11
  license = {text = "MIT"}
12
12
  authors = [
13
- {name = "Kristi Kotini", email = "hello@cardoai.com"},
14
- {name = "Klajdi Çaushi", email = "hello@cardoai.com"}
13
+ {name = "CardoAI", email = "hello@cardoai.com"},
15
14
  ]
16
15
  keywords = ["utilities", "helpers", "django"]
17
16
  classifiers = [
@@ -49,6 +48,10 @@ django-admin-auth = [
49
48
  "Django",
50
49
  "mozilla-django-oidc>=4.0.1",
51
50
  ]
51
+ django-keycloak-groups = [
52
+ "Django",
53
+ "python-keycloak>=5.8.1",
54
+ ]
52
55
  all = [
53
56
  "Django",
54
57
  ]
@@ -44,6 +44,4 @@ class OIDCCustomAuthenticationBackend(OIDCAuthenticationBackend):
44
44
 
45
45
 
46
46
  def has_permission(request):
47
- # The user does not need to be staff to access the admin site
48
- # Only superusers will have access to do anything in the admin site
49
- return request.user.is_active and request.user.is_superuser
47
+ return request.user.is_active
@@ -0,0 +1,119 @@
1
+ from django.apps import apps
2
+ from django.conf import settings
3
+ from django.db import models
4
+ from keycloak import KeycloakAdmin
5
+ from keycloak import KeycloakOpenIDConnection
6
+ from keycloak.exceptions import KeycloakGetError
7
+
8
+
9
+ class UserGroupBase(models.Model):
10
+ """
11
+ Abstract base model for Keycloak user groups.
12
+ """
13
+ id = models.UUIDField(
14
+ primary_key=True,
15
+ help_text="The ID of the group, as coming from Keycloak.",
16
+ )
17
+ path = models.CharField(
18
+ max_length=255,
19
+ help_text="The full path of the group, as coming from Keycloak.",
20
+ db_index=True,
21
+ )
22
+
23
+ class Meta:
24
+ abstract = True
25
+
26
+
27
+ class KeycloakService:
28
+ def __init__(self):
29
+ self._user_group_model = self._get_user_group_model()
30
+ self._keycloak_admin = self._get_keycloak_admin()
31
+
32
+ def sync_user_groups(self, raise_exceptions: bool = False):
33
+ print("Syncing user groups from Keycloak...")
34
+
35
+ try:
36
+ groups = self._keycloak_admin.get_groups(full_hierarchy=True)
37
+ except KeycloakGetError as e:
38
+ print(f"Failed to fetch groups from Keycloak: {str(e)}")
39
+ if raise_exceptions:
40
+ raise e
41
+
42
+ return
43
+
44
+ # Process existing and new groups
45
+ existing_groups = self._user_group_model.objects.all()
46
+ existing_groups_by_id = {str(group.id): group for group in existing_groups}
47
+
48
+ reported_group_ids = set()
49
+ for group in groups:
50
+ self._process_group_recursively(
51
+ group, existing_groups_by_id, reported_group_ids
52
+ )
53
+
54
+ # Identify deleted groups
55
+ deleted_groups = self._user_group_model.objects.exclude(
56
+ id__in=reported_group_ids
57
+ )
58
+ if deleted_groups.exists():
59
+ print(
60
+ f"Deleting groups no longer present in Keycloak: {list(deleted_groups.values_list('path', flat=True))}"
61
+ )
62
+ deleted_groups.delete()
63
+
64
+ def _get_keycloak_admin(self):
65
+ keycloak_connection = KeycloakOpenIDConnection(
66
+ server_url=settings.KEYCLOAK_SERVER_URL,
67
+ realm_name=settings.KEYCLOAK_REALM,
68
+ user_realm_name=settings.KEYCLOAK_REALM,
69
+ client_id=settings.KEYCLOAK_ADMIN_CLIENT_ID,
70
+ client_secret_key=settings.KEYCLOAK_ADMIN_CLIENT_SECRET,
71
+ verify=True,
72
+ )
73
+ return KeycloakAdmin(connection=keycloak_connection)
74
+
75
+ def _get_user_group_model(self):
76
+ """
77
+ Dynamically get the UserGroup model.
78
+
79
+ Attempts to use the model specified in settings.KEYCLOAK_USER_GROUP_MODEL.
80
+
81
+ Returns:
82
+ The UserGroup model class.
83
+
84
+ Raises:
85
+ LookupError: If the model cannot be found.
86
+ """
87
+ try:
88
+ model_string = getattr(settings, "KEYCLOAK_USER_GROUP_MODEL")
89
+ except AttributeError:
90
+ raise LookupError(
91
+ "Please set KEYCLOAK_USER_GROUP_MODEL in your Django settings "
92
+ "(e.g., 'myapp.models.UserGroup')."
93
+ )
94
+
95
+ return apps.get_model(model_string)
96
+
97
+ def _process_group_recursively(
98
+ self, group, existing_groups_by_id, reported_group_ids
99
+ ):
100
+ group_id = str(group["id"])
101
+ reported_group_ids.add(group_id)
102
+
103
+ if group_id in existing_groups_by_id:
104
+ existing_group = existing_groups_by_id[group_id]
105
+ if existing_group.path != group["path"]:
106
+ print(
107
+ f"Updating group path from {existing_group.path} to {group['path']}..."
108
+ )
109
+ existing_group.path = group["path"]
110
+ existing_group.save()
111
+ else:
112
+ print(f"Creating new group with path {group['path']}...")
113
+ self._user_group_model.objects.create(id=group_id, path=group["path"])
114
+
115
+ if subgroups := group.get("subGroups"):
116
+ for subgroup in subgroups:
117
+ self._process_group_recursively(
118
+ subgroup, existing_groups_by_id, reported_group_ids
119
+ )