diff --git a/management/commands/force_delete_user.py b/management/commands/force_delete_user.py new file mode 100644 index 0000000..2a829ab --- /dev/null +++ b/management/commands/force_delete_user.py @@ -0,0 +1,176 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import getpass +from time import sleep + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.mail import mail_admins +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Q +from django.test import override_settings + +from note.models import Alias, Transaction + + +class Command(BaseCommand): + """ + This script is used to force delete a user. + THIS IS ONLY ATTENDED TO BE USED TO DELETE FAKE ACCOUNTS THAT + WERE VALIDATED BY ERRORS. Please use data anonymization if you + want to block the account of a user. + """ + + def add_arguments(self, parser): + parser.add_argument('user', type=str, nargs='+', help="User id to delete.") + parser.add_argument('--force', '-f', action='store_true', + help="Force the script to have low verbosity.") + parser.add_argument('--doit', '-d', action='store_true', + help="Don't ask for a final confirmation and commit modification. " + "This option should really be used carefully.") + + def handle(self, *args, **kwargs): + force = kwargs['force'] + + if not force: + self.stdout.write(self.style.WARNING("This is a dangerous script. " + "Please use --force to indicate that you known what you are doing. " + "Nothing will be deleted yet.")) + sleep(5) + + # We need to know who to blame. + qs = User.objects.filter(note__alias__normalized_name=Alias.normalize(getpass.getuser())) + if not qs.exists(): + self.stderr.write(self.style.ERROR("I don't know who you are. Please add your linux id as an alias of " + "your own account.")) + exit(2) + executor = qs.get() + + deleted_users = [] + deleted = [] + + # Don't send mails during the process + with override_settings(EMAIL_BACKEND='django.core.mail.backends.dummy.EmailBackend'): + for user_id in kwargs['user']: + if user_id.isnumeric(): + qs = User.objects.filter(pk=int(user_id)) + if not qs.exists(): + self.stderr.write(self.style.WARNING(f"User {user_id} was not found. Ignoring...")) + continue + user = qs.get() + else: + qs = Alias.objects.filter(normalized_name=Alias.normalize(user_id), note__noteuser__isnull=False) + if not qs.exists(): + self.stderr.write(self.style.WARNING(f"User {user_id} was not found. Ignoring...")) + continue + user = qs.get().note.user + + with transaction.atomic(): + local_deleted = [] + + # Unlock note to enable modifications + if force and not user.note.is_active: + user.note.is_active = True + user.note.save() + + # Deleting transactions + transactions = Transaction.objects.filter(Q(source=user.note) | Q(destination=user.note)).all() + local_deleted += list(transactions) + if kwargs['verbosity'] >= 1: + for tr in transactions: + self.stdout.write(f"Removing {tr}...") + if force: + transactions.delete() + + # Deleting memberships + memberships = user.memberships.all() + local_deleted += list(memberships) + if kwargs['verbosity'] >= 1: + for membership in memberships: + self.stdout.write(f"Removing {membership}...") + if force: + memberships.delete() + + # Deleting aliases + alias_set = user.note.alias.all() + local_deleted += list(alias_set) + if kwargs['verbosity'] >= 1: + for alias in alias_set: + self.stdout.write(f"Removing alias {alias}...") + if force: + alias_set.delete() + + if 'activity' in settings.INSTALLED_APPS: + from activity.models import Guest, Entry + + # Deleting activity entries + entries = Entry.objects.filter(Q(note=user.note) | Q(guest__inviter=user.note)).all() + local_deleted += list(entries) + if kwargs['verbosity'] >= 1: + for entry in entries: + self.stdout.write(f"Removing {entry}...") + if force: + entries.delete() + + # Deleting invited guests + guests = Guest.objects.filter(inviter=user.note).all() + local_deleted += list(guests) + if kwargs['verbosity'] >= 1: + for guest in guests: + self.stdout.write(f"Removing guest {guest}...") + if force: + guests.delete() + + if 'treasury' in settings.INSTALLED_APPS: + from treasury.models import SogeCredit + + # Deleting soge credit + credits = SogeCredit.objects.filter(user=user).all() + local_deleted += list(credits) + if kwargs['verbosity'] >= 1: + for credit in credits: + self.stdout.write(f"Removing {credit}...") + if force: + credits.delete() + + # Deleting note + local_deleted.append(user.note) + if force: + user.note.delete() + + if 'logs' in settings.INSTALLED_APPS: + from logs.models import Changelog + + # Replace log executors by the runner of the script + Changelog.objects.filter(user=user).update(user=executor) + + # Deleting profile + local_deleted.append(user.profile) + if force: + user.profile.delete() + + # Finally deleting user + if force: + user.delete() + local_deleted.append(user) + + # This script should really not be used. + if not kwargs['doit'] and not input('You are about to delete real user data. ' + 'Are you really sure that it is what you want? [y/N] ')\ + .lower().startswith('y'): + self.stdout.write(self.style.ERROR("Aborted.")) + exit(1) + + if kwargs['verbosity'] >= 1: + self.stdout.write(self.style.SUCCESS(f"User {user} deleted.")) + deleted_users.append(user) + deleted += local_deleted + + if deleted_users: + message = f"Les utilisateurs {deleted_users} ont été supprimés par {executor}.\n\n" + message += "Ont été supprimés en conséquence les objets suivants :\n\n" + for obj in deleted: + message += f"{repr(obj)} (pk: {obj.pk})\n" + mail_admins("Utilisateurs supprimés", message)