diff --git a/server/vbv_lernwelt/core/admin.py b/server/vbv_lernwelt/core/admin.py index 64234013..8c44c349 100644 --- a/server/vbv_lernwelt/core/admin.py +++ b/server/vbv_lernwelt/core/admin.py @@ -3,24 +3,11 @@ from django.contrib.auth import admin as auth_admin, get_user_model from django.utils.translation import gettext_lazy as _ from vbv_lernwelt.core.models import Country, JobLog, Organisation -from vbv_lernwelt.core.signals import create_sso_user_signal, sync_sso_roles_signal from vbv_lernwelt.core.utils import pretty_print_json User = get_user_model() -@admin.action(description="KEYCLOAK: Sync SSO Roles") -def sync_sso_roles(modeladmin, request, queryset): - for user in queryset: - sync_sso_roles_signal.send(sender="core.admin", user=user) - - -@admin.action(description="KEYCLOAK: Create User") -def create_sso_user(modeladmin, request, queryset): - for user in queryset: - create_sso_user_signal.send(sender="core.admin", user=user) - - class LogAdmin(admin.ModelAdmin): def has_add_permission(self, request): return False @@ -96,7 +83,6 @@ class UserAdmin(auth_admin.UserAdmin): "sso_id", ] search_fields = ["first_name", "last_name", "email", "username", "sso_id"] - actions = [sync_sso_roles, create_sso_user] @admin.register(JobLog) diff --git a/server/vbv_lernwelt/core/signals.py b/server/vbv_lernwelt/core/signals.py deleted file mode 100644 index 27106448..00000000 --- a/server/vbv_lernwelt/core/signals.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.dispatch import Signal - -sync_sso_roles_signal = Signal( - providing_args=[ - "user", - ] -) - -create_sso_user_signal = Signal( - providing_args=[ - "user", - ] -) diff --git a/server/vbv_lernwelt/sso/admin.py b/server/vbv_lernwelt/sso/admin.py index 8c38f3f3..274c49f2 100644 --- a/server/vbv_lernwelt/sso/admin.py +++ b/server/vbv_lernwelt/sso/admin.py @@ -1,3 +1,138 @@ -from django.contrib import admin +from django.contrib import admin, messages +from django.contrib.auth import admin as auth_admin, get_user_model +from django.utils.translation import gettext_lazy as _ -# Register your models here. +from vbv_lernwelt.course.models import CourseSessionUser +from vbv_lernwelt.importer.services import update_user_json_data +from vbv_lernwelt.sso.exceptions import MyVbvKeycloakDeleteError, MyVbvKeycloakPostError +from vbv_lernwelt.sso.models import SsoSyncError, SsoUser +from vbv_lernwelt.sso.role_sync.services import create_user, sync_roles_for_user + +User = get_user_model() + + +def create_sso_user_from_admin(user: User, request): + try: + sso_data = {"intermediate_sso_id": create_user(user)} + update_user_json_data(user, sso_data) + user.save() + messages.add_message( + request, messages.SUCCESS, f"Der Bentuzer wurde in Keycloak erstellt." + ) + except MyVbvKeycloakPostError as e: + additional_data = getattr(e, "additional_data", {}) + SsoSyncError.objects.create( + user=user, action=SsoSyncError.Action.CREATE, data=additional_data + ) + messages.add_message( + request, + messages.WARNING, + f"Der Benutzer ({e}) konnte nicht in Keycloak erstellt werden.", + ) + + +def sync_sso_roles_from_admin(user: User, request): + course_roles = [ + (csu.course_session.course.slug, csu.role) + for csu in CourseSessionUser.objects.filter(user=user) + ] + try: + sync_roles_for_user(user, course_roles) + messages.add_message( + request, messages.SUCCESS, f"Die Daten wurden mit Keycloak synchronisiert." + ) + except MyVbvKeycloakDeleteError as e: + additional_data = getattr(e, "additional_data", {}) + SsoSyncError.objects.create( + user=user, action=SsoSyncError.Action.REMOVE, data=additional_data + ) + messages.add_message( + request, + messages.WARNING, + f"Die bestehenden Rollen für Benutzer ({e}) konnten in Keycloak nicht gelöscht werden.", + ) + except MyVbvKeycloakPostError as e: + additional_data = getattr(e, "additional_data", {}) + SsoSyncError.objects.create( + user=user, action=SsoSyncError.Action.ADD, data=additional_data + ) + messages.add_message( + request, + messages.WARNING, + f"Die neuen Rollen für Benutzer ({e}) konnten in Keycloak nicht erstellt werden.", + ) + + +@admin.action(description="KEYCLOAK: Sync SSO Roles") +def sync_sso_roles(modeladmin, request, queryset): + for user in queryset: + sync_sso_roles_from_admin(user, request) + + +@admin.action(description="KEYCLOAK: Create User") +def create_sso_user(modeladmin, request, queryset): + for user in queryset: + create_sso_user_from_admin(user, request) + + +@admin.register(SsoUser) +class SsoUserAdmin(auth_admin.UserAdmin): + fieldsets = ( + ( + _("Personal info"), + {"fields": ("first_name", "last_name", "email", "sso_id")}, + ), + (_("Additional data"), {"fields": ("additional_json_data",)}), + ) + list_display = [ + "username", + "first_name", + "last_name", + "sso_id", + "intermedia_sso_id", + ] + search_fields = ["first_name", "last_name", "email", "username", "sso_id"] + actions = [sync_sso_roles, create_sso_user] + + # Make fields read-only + readonly_fields = ( + "username", + "password", + "first_name", + "last_name", + "email", + "additional_json_data", + ) + + # Disable delete action + def has_delete_permission(self, request, obj=None): + return False + + def get_actions(self, request): + actions = super().get_actions(request) + if "delete_selected" in actions: + del actions["delete_selected"] + return actions + + def intermedia_sso_id(self, obj): + return obj.additional_json_data.get("intermediate_sso_id", "") + + intermedia_sso_id.short_description = "Keycloak SSO ID" + + +@admin.register(SsoSyncError) +class SsoSyncErrorAdmin(admin.ModelAdmin): + list_display = [ + "created_at", + "user", + "action", + "data", + ] + raw_id_fields = [ + "user", + ] + search_fields = [ + "user.email", + "user.username", + ] + list_filter = ("action",) diff --git a/server/vbv_lernwelt/sso/migrations/0001_initial.py b/server/vbv_lernwelt/sso/migrations/0001_initial.py new file mode 100644 index 00000000..687c6a17 --- /dev/null +++ b/server/vbv_lernwelt/sso/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.25 on 2024-06-20 05:24 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="SsoSyncError", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "action", + models.CharField( + choices=[ + ("ADD", "Add"), + ("REMOVE", "Remove"), + ("CREATE", "Create"), + ], + default="ADD", + max_length=255, + ), + ), + ("data", models.JSONField(blank=True, default=dict)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/server/vbv_lernwelt/sso/models.py b/server/vbv_lernwelt/sso/models.py index 4b40cfae..c69a647e 100644 --- a/server/vbv_lernwelt/sso/models.py +++ b/server/vbv_lernwelt/sso/models.py @@ -3,6 +3,11 @@ from django.db import models from vbv_lernwelt.core.models import User +class SsoUser(User): + class Meta: + proxy = True + + class SsoSyncError(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/server/vbv_lernwelt/sso/signals.py b/server/vbv_lernwelt/sso/signals.py index 342b7554..2d3a6aa8 100644 --- a/server/vbv_lernwelt/sso/signals.py +++ b/server/vbv_lernwelt/sso/signals.py @@ -1,15 +1,12 @@ from django.db.models.signals import post_delete, pre_save from django.dispatch import receiver -from vbv_lernwelt.core.signals import create_sso_user_signal, sync_sso_roles_signal from vbv_lernwelt.course.models import CourseSessionUser from vbv_lernwelt.sso.exceptions import MyVbvKeycloakDeleteError, MyVbvKeycloakPostError from vbv_lernwelt.sso.models import SsoSyncError from vbv_lernwelt.sso.role_sync.services import ( add_roles_to_user, - create_user, remove_roles_from_user, - sync_roles_for_user, update_roles_for_user, ) @@ -56,34 +53,3 @@ def update_sso_roles(sender, instance: CourseSessionUser, **kwargs): SsoSyncError.objects.create( user=instance.user, action=SsoSyncError.Action.ADD, data=additional_data ) - - -@receiver(sync_sso_roles_signal, dispatch_uid="sync_sso_roles") -def sync_sso_roles(sender, user, **kwargs): - course_roles = [ - (csu.course_session.course.slug, csu.role) - for csu in CourseSessionUser.objects.filter(user=user) - ] - try: - sync_roles_for_user(user, course_roles) - except MyVbvKeycloakDeleteError as e: - additional_data = getattr(e, "additional_data", {}) - SsoSyncError.objects.create( - user=user, action=SsoSyncError.Action.REMOVE, data=additional_data - ) - except MyVbvKeycloakPostError as e: - additional_data = getattr(e, "additional_data", {}) - SsoSyncError.objects.create( - user=user, action=SsoSyncError.Action.ADD, data=additional_data - ) - - -@receiver(create_sso_user_signal, dispatch_uid="create_sso_user") -def create_sso_user(sender, user, **kwargs): - try: - create_user(user) - except MyVbvKeycloakPostError as e: - additional_data = getattr(e, "additional_data", {}) - SsoSyncError.objects.create( - user=user, action=SsoSyncError.Action.CREATE, data=additional_data - )