diff --git a/promgen/actions.py b/promgen/actions.py index f3047ddc..e54ed5b9 100644 --- a/promgen/actions.py +++ b/promgen/actions.py @@ -1,6 +1,14 @@ from django.contrib import admin, messages +from django.db import transaction +from django.http import HttpResponseRedirect +from django.template.response import TemplateResponse +from django.utils.translation import gettext as _ +from guardian.models import UserObjectPermission +from guardian.shortcuts import assign_perm +from social_django.models import UserSocialAuth -from promgen import tasks +from promgen import models, tasks +from promgen.notification.user import NotificationUser @admin.action(description="Clear Tombstones") @@ -54,3 +62,102 @@ def shard_rules(modeladmin, request, queryset): def shard_urls(modeladmin, request, queryset): for shard in queryset: prometheus_urls(modeladmin, request, shard.prometheus_set.all()) + + +@admin.action(description="Merge selected users") +def merge_users_action(modeladmin, request, queryset): + if "new_user_id" not in request.POST: + return TemplateResponse( + request, + "promgen/user_merge.html", + context={ + "title": _("Merge Users"), + "users": queryset, + }, + ) + + new_user_id = int(request.POST["new_user_id"]) + new_user = queryset.get(id=new_user_id) + old_users = queryset.exclude(id=new_user_id) + count = old_users.count() + + merge_users(old_users, new_user) + messages.success(request, f"Merged {count} user(s) into {new_user.username}") + + return HttpResponseRedirect(request.get_full_path()) + + +@transaction.atomic +def merge_users(old_users, new_user): + # Merge social auth accounts + UserSocialAuth.objects.filter(user__in=old_users).update(user=new_user) + + # Update owner fields + models.Sender.objects.filter(owner__in=old_users).update(owner=new_user) + models.Project.objects.filter(owner__in=old_users).update(owner=new_user) + models.Service.objects.filter(owner__in=old_users).update(owner=new_user) + + # Update sender value fields for notification.user type + models.Sender.objects.filter( + sender=NotificationUser.__module__, value__in=old_users.values_list("id", flat=True) + ).update(value=str(new_user.id)) + + # Copy group memberships and user permissions + for old_user in old_users: + for group in old_user.groups.all(): + new_user.groups.add(group) + for perm in old_user.user_permissions.all(): + new_user.user_permissions.add(perm) + + # Update object permissions. If all users have many permissions on the same object, we want to + # keep the highest level of permission to the new user. See map_obj_perm_by_perm_rank function. + user_ids = list(old_users.values_list("id", flat=True)) + [new_user.id] + + service_perms = UserObjectPermission.objects.filter( + user_id__in=user_ids, content_type__app_label="promgen", content_type__model="service" + ).values("object_pk", "permission__codename") + service_perm_map = map_obj_perm_by_perm_rank( + service_perms, + {"service_admin": 3, "service_editor": 2, "service_viewer": 1}, + ) + for service_id, codename in service_perm_map.items(): + assign_perm(codename, new_user, models.Service.objects.get(pk=service_id)) + + project_perms = UserObjectPermission.objects.filter( + user_id__in=user_ids, content_type__app_label="promgen", content_type__model="project" + ).values("object_pk", "permission__codename") + project_perm_map = map_obj_perm_by_perm_rank( + project_perms, + {"project_admin": 3, "project_editor": 2, "project_viewer": 1}, + ) + for project_id, codename in project_perm_map.items(): + assign_perm(codename, new_user, models.Project.objects.get(pk=project_id)) + + group_perms = UserObjectPermission.objects.filter( + user_id__in=user_ids, content_type__app_label="auth", content_type__model="group" + ).values("object_pk", "permission__codename") + group_perm_map = map_obj_perm_by_perm_rank( + group_perms, + {"group_admin": 2, "group_member": 1}, + ) + for group_id, codename in group_perm_map.items(): + assign_perm(codename, new_user, models.Group.objects.get(pk=group_id)) + + # Delete the old users, related objects will be cascade deleted + old_users.delete() + + +def map_obj_perm_by_perm_rank(user_obj_perms, perm_rank): + permission_map = {} + for perm in user_obj_perms: + obj_id = perm["object_pk"] + codename = perm["permission__codename"] + if permission_map.get(obj_id, None): + current_rank = perm_rank[permission_map[obj_id]] + new_rank = perm_rank[codename] + if new_rank > current_rank: + permission_map[obj_id] = codename + else: + permission_map[obj_id] = codename + + return permission_map diff --git a/promgen/admin.py b/promgen/admin.py index 79bee7b5..5c4f98fb 100644 --- a/promgen/admin.py +++ b/promgen/admin.py @@ -5,6 +5,8 @@ import guardian.admin from django import forms from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import User from django.utils.html import format_html from promgen import actions, models, plugins @@ -173,3 +175,11 @@ def has_add_permission(self, request, obj=None): def has_change_permission(self, request, obj=None): return False + + +class PromgenUserAdmin(UserAdmin): + actions = [actions.merge_users_action] + + +admin.site.unregister(User) +admin.site.register(User, PromgenUserAdmin) diff --git a/promgen/templates/promgen/user_merge.html b/promgen/templates/promgen/user_merge.html new file mode 100644 index 00000000..16b48ec0 --- /dev/null +++ b/promgen/templates/promgen/user_merge.html @@ -0,0 +1,36 @@ +{% extends "admin/base_site.html" %} +{% load i18n static %} +{% load admin_urls %} +{% block breadcrumbs %} +
+{% endblock %} +{% block content %} +