cardo-python-utils 0.5.dev1__tar.gz → 0.5.dev4__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.
- {cardo_python_utils-0.5.dev1/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev4}/PKG-INFO +4 -1
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4/cardo_python_utils.egg-info}/PKG-INFO +4 -1
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/cardo_python_utils.egg-info/SOURCES.txt +4 -1
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/cardo_python_utils.egg-info/requires.txt +4 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/pyproject.toml +5 -1
- cardo_python_utils-0.5.dev4/python_utils/django/keycloak/admin.py +86 -0
- cardo_python_utils-0.5.dev4/python_utils/django/keycloak/models.py +19 -0
- cardo_python_utils-0.5.dev4/python_utils/django/keycloak/service.py +100 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/LICENSE +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/MANIFEST.in +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/README.rst +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/cardo_python_utils.egg-info/dependency_links.txt +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/cardo_python_utils.egg-info/top_level.txt +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/__init__.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/choices.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/data_structures.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/db.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/django/auth/__init__.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/django/auth/admin.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/django/auth/drf.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/django/auth/ninja.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/django/utils.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/esma_choices.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/exceptions.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/imports.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/math.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/text.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/time.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/types_hinting.py +0 -0
- {cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/setup.cfg +0 -0
{cardo_python_utils-0.5.dev1/cardo_python_utils.egg-info → cardo_python_utils-0.5.dev4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cardo-python-utils
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.dev4
|
|
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
|
{cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4/cardo_python_utils.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cardo-python-utils
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.dev4
|
|
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
|
{cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/cardo_python_utils.egg-info/SOURCES.txt
RENAMED
|
@@ -22,4 +22,7 @@ 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/ninja.py
|
|
25
|
+
python_utils/django/auth/ninja.py
|
|
26
|
+
python_utils/django/keycloak/admin.py
|
|
27
|
+
python_utils/django/keycloak/models.py
|
|
28
|
+
python_utils/django/keycloak/service.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.
|
|
7
|
+
version = "0.5.dev4"
|
|
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"
|
|
@@ -48,6 +48,10 @@ django-admin-auth = [
|
|
|
48
48
|
"Django",
|
|
49
49
|
"mozilla-django-oidc>=4.0.1",
|
|
50
50
|
]
|
|
51
|
+
django-keycloak-groups = [
|
|
52
|
+
"Django",
|
|
53
|
+
"python-keycloak>=5.8.1",
|
|
54
|
+
]
|
|
51
55
|
all = [
|
|
52
56
|
"Django",
|
|
53
57
|
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from django.contrib import admin, messages
|
|
2
|
+
from django.core.cache import cache
|
|
3
|
+
from django.urls import path
|
|
4
|
+
from django.shortcuts import redirect
|
|
5
|
+
|
|
6
|
+
from .service import KeycloakService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UserGroupAdmin(admin.ModelAdmin):
|
|
10
|
+
list_display = ("path",)
|
|
11
|
+
search_fields = ("id", "path")
|
|
12
|
+
readonly_fields = ("id", "path")
|
|
13
|
+
|
|
14
|
+
# To show ManyToMany fields with a horizontal filter widget
|
|
15
|
+
# filter_horizontal = ("allowed_entities",)
|
|
16
|
+
|
|
17
|
+
change_list_template = "user_groups_changelist.html"
|
|
18
|
+
|
|
19
|
+
# To show fields in this order in the detail view
|
|
20
|
+
# fieldsets = (
|
|
21
|
+
# (
|
|
22
|
+
# None,
|
|
23
|
+
# {
|
|
24
|
+
# "fields": (
|
|
25
|
+
# "id",
|
|
26
|
+
# "path",
|
|
27
|
+
# "allow_all_entities",
|
|
28
|
+
# "allowed_entities",
|
|
29
|
+
# )
|
|
30
|
+
# },
|
|
31
|
+
# ),
|
|
32
|
+
# )
|
|
33
|
+
|
|
34
|
+
def has_add_permission(self, request):
|
|
35
|
+
# Manual addition of user groups is not allowed
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def has_delete_permission(self, request, obj=None):
|
|
39
|
+
# Manual deletion of user groups is not allowed
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
# To show annotated fields in the list view
|
|
43
|
+
# def get_queryset(self, request):
|
|
44
|
+
# return (
|
|
45
|
+
# super()
|
|
46
|
+
# .get_queryset(request)
|
|
47
|
+
# .annotate(allowed_job_configs_count=Count("allowed_job_configs"))
|
|
48
|
+
# )
|
|
49
|
+
|
|
50
|
+
def get_urls(self):
|
|
51
|
+
return [
|
|
52
|
+
path("sync-with-keycloak/", self.sync_groups_with_keycloak),
|
|
53
|
+
*super().get_urls(),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def sync_groups_with_keycloak(request): # noqa
|
|
58
|
+
"""Syncs user groups with Keycloak"""
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
KeycloakService().sync_user_groups(raise_exceptions=True)
|
|
62
|
+
messages.success(request, "User groups synced successfully.")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
messages.error(request, f"Error syncing user groups: {e}")
|
|
65
|
+
|
|
66
|
+
return redirect("..")
|
|
67
|
+
|
|
68
|
+
# To allow sorting by annotated fields
|
|
69
|
+
# @admin.display(
|
|
70
|
+
# description="Allowed Job Configs", ordering="allowed_job_configs_count"
|
|
71
|
+
# )
|
|
72
|
+
# def allowed_job_configs_count(self, obj):
|
|
73
|
+
# return obj.allowed_job_configs_count
|
|
74
|
+
|
|
75
|
+
def changelist_view(self, request, extra_context=None):
|
|
76
|
+
"""
|
|
77
|
+
When the list view is accessed, sync the user groups from Keycloak.
|
|
78
|
+
Cache the sync for 10 minutes to avoid excessive requests.
|
|
79
|
+
"""
|
|
80
|
+
cache_key = "keycloak_group_sync_lock"
|
|
81
|
+
|
|
82
|
+
if cache.get(cache_key) is None:
|
|
83
|
+
KeycloakService().sync_user_groups()
|
|
84
|
+
cache.set(cache_key, "true", 60 * 10)
|
|
85
|
+
|
|
86
|
+
return super().changelist_view(request, extra_context)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UserGroupBase(models.Model):
|
|
5
|
+
"""
|
|
6
|
+
Abstract base model for Keycloak user groups.
|
|
7
|
+
"""
|
|
8
|
+
id = models.UUIDField(
|
|
9
|
+
primary_key=True,
|
|
10
|
+
help_text="The ID of the group, as coming from Keycloak.",
|
|
11
|
+
)
|
|
12
|
+
path = models.CharField(
|
|
13
|
+
max_length=255,
|
|
14
|
+
help_text="The full path of the group, as coming from Keycloak.",
|
|
15
|
+
db_index=True,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
class Meta:
|
|
19
|
+
abstract = True
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from keycloak import KeycloakAdmin
|
|
4
|
+
from keycloak import KeycloakOpenIDConnection
|
|
5
|
+
from keycloak.exceptions import KeycloakGetError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class KeycloakService:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._user_group_model = self._get_user_group_model()
|
|
11
|
+
self._keycloak_admin = self._get_keycloak_admin()
|
|
12
|
+
|
|
13
|
+
def sync_user_groups(self, raise_exceptions: bool = False):
|
|
14
|
+
print("Syncing user groups from Keycloak...")
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
groups = self._keycloak_admin.get_groups(full_hierarchy=True)
|
|
18
|
+
except KeycloakGetError as e:
|
|
19
|
+
print(f"Failed to fetch groups from Keycloak: {str(e)}")
|
|
20
|
+
if raise_exceptions:
|
|
21
|
+
raise e
|
|
22
|
+
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# Process existing and new groups
|
|
26
|
+
existing_groups = self._user_group_model.objects.all()
|
|
27
|
+
existing_groups_by_id = {str(group.id): group for group in existing_groups}
|
|
28
|
+
|
|
29
|
+
reported_group_ids = set()
|
|
30
|
+
for group in groups:
|
|
31
|
+
self._process_group_recursively(
|
|
32
|
+
group, existing_groups_by_id, reported_group_ids
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Identify deleted groups
|
|
36
|
+
deleted_groups = self._user_group_model.objects.exclude(
|
|
37
|
+
id__in=reported_group_ids
|
|
38
|
+
)
|
|
39
|
+
if deleted_groups.exists():
|
|
40
|
+
print(
|
|
41
|
+
f"Deleting groups no longer present in Keycloak: {list(deleted_groups.values_list('path', flat=True))}"
|
|
42
|
+
)
|
|
43
|
+
deleted_groups.delete()
|
|
44
|
+
|
|
45
|
+
def _get_keycloak_admin(self):
|
|
46
|
+
keycloak_connection = KeycloakOpenIDConnection(
|
|
47
|
+
server_url=settings.KEYCLOAK_SERVER_URL,
|
|
48
|
+
realm_name=settings.KEYCLOAK_REALM,
|
|
49
|
+
user_realm_name=settings.KEYCLOAK_REALM,
|
|
50
|
+
client_id=settings.KEYCLOAK_ADMIN_CLIENT_ID,
|
|
51
|
+
client_secret_key=settings.KEYCLOAK_ADMIN_CLIENT_SECRET,
|
|
52
|
+
verify=True,
|
|
53
|
+
)
|
|
54
|
+
return KeycloakAdmin(connection=keycloak_connection)
|
|
55
|
+
|
|
56
|
+
def _get_user_group_model(self):
|
|
57
|
+
"""
|
|
58
|
+
Dynamically get the UserGroup model.
|
|
59
|
+
|
|
60
|
+
Attempts to use the model specified in settings.KEYCLOAK_USER_GROUP_MODEL.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The UserGroup model class.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
LookupError: If the model cannot be found.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
model_string = getattr(settings, "KEYCLOAK_USER_GROUP_MODEL")
|
|
70
|
+
except AttributeError:
|
|
71
|
+
raise LookupError(
|
|
72
|
+
"Please set KEYCLOAK_USER_GROUP_MODEL in your Django settings "
|
|
73
|
+
"(e.g., 'myapp.UserGroup')."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return apps.get_model(model_string)
|
|
77
|
+
|
|
78
|
+
def _process_group_recursively(
|
|
79
|
+
self, group, existing_groups_by_id, reported_group_ids
|
|
80
|
+
):
|
|
81
|
+
group_id = str(group["id"])
|
|
82
|
+
reported_group_ids.add(group_id)
|
|
83
|
+
|
|
84
|
+
if group_id in existing_groups_by_id:
|
|
85
|
+
existing_group = existing_groups_by_id[group_id]
|
|
86
|
+
if existing_group.path != group["path"]:
|
|
87
|
+
print(
|
|
88
|
+
f"Updating group path from {existing_group.path} to {group['path']}..."
|
|
89
|
+
)
|
|
90
|
+
existing_group.path = group["path"]
|
|
91
|
+
existing_group.save()
|
|
92
|
+
else:
|
|
93
|
+
print(f"Creating new group with path {group['path']}...")
|
|
94
|
+
self._user_group_model.objects.create(id=group_id, path=group["path"])
|
|
95
|
+
|
|
96
|
+
if subgroups := group.get("subGroups"):
|
|
97
|
+
for subgroup in subgroups:
|
|
98
|
+
self._process_group_recursively(
|
|
99
|
+
subgroup, existing_groups_by_id, reported_group_ids
|
|
100
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/django/auth/__init__.py
RENAMED
|
File without changes
|
{cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/django/auth/admin.py
RENAMED
|
File without changes
|
|
File without changes
|
{cardo_python_utils-0.5.dev1 → cardo_python_utils-0.5.dev4}/python_utils/django/auth/ninja.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|