cardo-python-utils 0.5.dev1__py3-none-any.whl → 0.5.dev2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cardo-python-utils
3
- Version: 0.5.dev1
3
+ Version: 0.5.dev2
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
@@ -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,4 +1,4 @@
1
- cardo_python_utils-0.5.dev1.dist-info/licenses/LICENSE,sha256=N-YtxDy8n5A1Mo7JKKItNIlboiK_pMOZ48ojx76jo3g,1046
1
+ cardo_python_utils-0.5.dev2.dist-info/licenses/LICENSE,sha256=N-YtxDy8n5A1Mo7JKKItNIlboiK_pMOZ48ojx76jo3g,1046
2
2
  python_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  python_utils/choices.py,sha256=pu1fUBlem-Uz2J36FmDv-tzr0UqOSF00xIaF6cVOa4g,2505
4
4
  python_utils/data_structures.py,sha256=ZqkZYPy20zyGYOVhwb9qst4vF_P7X2A9z5E36rMUC6I,16820
@@ -14,8 +14,9 @@ python_utils/django/utils.py,sha256=lgLZLsUfdDJJmBYD2lqQDfiC9nDnT4oSIxVt3pKYIwI,
14
14
  python_utils/django/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  python_utils/django/auth/admin.py,sha256=1zuqfvAXSKEEPvrdv7tYOG1x0namOIlJwPeHaF2f1EA,1517
16
16
  python_utils/django/auth/drf.py,sha256=k7KISe1FZzwaPOpQU6geoTvnTnaVHa5sBALClb_n4-4,3735
17
+ python_utils/django/auth/keycloak.py,sha256=lUznzC_O4ar9R7iodwtSO9r9EjuJRM4NijN0JNq5AC0,4049
17
18
  python_utils/django/auth/ninja.py,sha256=h8O0otdOi6aUUDR4WXeh6OBfBMdeKl0_wF7CKrOtS_I,4237
18
- cardo_python_utils-0.5.dev1.dist-info/METADATA,sha256=KjJsod4NI_Sui9swL-3UQEJCLMVoP4BiMZaJBzxNIcE,3067
19
- cardo_python_utils-0.5.dev1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- cardo_python_utils-0.5.dev1.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
21
- cardo_python_utils-0.5.dev1.dist-info/RECORD,,
19
+ cardo_python_utils-0.5.dev2.dist-info/METADATA,sha256=l520dlJtZHjCDQJrRn4C1uSLFvZT0hcQmDwUc_QK41c,3236
20
+ cardo_python_utils-0.5.dev2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ cardo_python_utils-0.5.dev2.dist-info/top_level.txt,sha256=zAx6OfEsjJs8BEW3okSiG_j9gpkI69xWShzum6oBgKI,13
22
+ cardo_python_utils-0.5.dev2.dist-info/RECORD,,
@@ -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
+ )