From 8488023710d08e0c86128b151273cccf870df6dd Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Sat, 18 Aug 2018 21:25:45 +0200 Subject: [PATCH] Implement anonimising deletion of users --- marietje/marietje/admin.py | 9 ++- .../management/commands/importhistory.py | 1 + .../management/commands/importsongs.py | 1 + .../management/commands/importusers.py | 1 + .../marietje/management/commands/old_users.py | 79 +++++++++++++++++++ marietje/marietje/models.py | 19 +++++ 6 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 marietje/marietje/management/commands/old_users.py diff --git a/marietje/marietje/admin.py b/marietje/marietje/admin.py index 1d7d50c..f9d4bb4 100644 --- a/marietje/marietje/admin.py +++ b/marietje/marietje/admin.py @@ -14,5 +14,12 @@ class UserAdmin(BaseUserAdmin): (_('Important dates'), {'fields': ('last_login', 'date_joined')}), (_('Activation'), {'fields': ('activation_token', 'reset_token')}), ) - list_display = ('username', 'email', 'name', 'date_joined', 'queue', 'is_staff') + list_display = ('username', 'email', 'name', 'date_joined', 'last_login', 'queue', 'is_staff') search_fields = ('username', 'name', 'email') + + def delete_model(self, request, user): + user.delete() + + def delete_queryset(self, request, users): + for user in users.all(): + self.delete_model(request, user) diff --git a/marietje/marietje/management/commands/importhistory.py b/marietje/marietje/management/commands/importhistory.py index a7d2321..4f499b9 100644 --- a/marietje/marietje/management/commands/importhistory.py +++ b/marietje/marietje/management/commands/importhistory.py @@ -1,4 +1,5 @@ import json + from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from songs.models import Song diff --git a/marietje/marietje/management/commands/importsongs.py b/marietje/marietje/management/commands/importsongs.py index c3f8844..b798072 100644 --- a/marietje/marietje/management/commands/importsongs.py +++ b/marietje/marietje/management/commands/importsongs.py @@ -1,4 +1,5 @@ import json + from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from django.db.utils import OperationalError diff --git a/marietje/marietje/management/commands/importusers.py b/marietje/marietje/management/commands/importusers.py index ce3a0d2..9b3b615 100644 --- a/marietje/marietje/management/commands/importusers.py +++ b/marietje/marietje/management/commands/importusers.py @@ -1,4 +1,5 @@ import json + from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from ...utils import get_first_queue diff --git a/marietje/marietje/management/commands/old_users.py b/marietje/marietje/management/commands/old_users.py new file mode 100644 index 0000000..ad3fa8d --- /dev/null +++ b/marietje/marietje/management/commands/old_users.py @@ -0,0 +1,79 @@ +from datetime import datetime, timedelta + +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from django.conf import settings +from django.db.models import Q +from django.utils.timezone import get_default_timezone + +_IMPORTANT_USERNAMES = ['root', 'admin', 'postmaster', 'dsprenkels', 'gmulder', 'bwesterb'] + +class Command(BaseCommand): + help = 'Get the list of users that has not logged in for quite a while' + + def add_arguments(self, parser): + parser.add_argument( + '--purge', + action='store_const', + default=False, + const=True, + help='interactively purge the old users') + parser.add_argument( + 'days_old', + nargs='?', + type=float, + default=365.25, + help='amount of days after which a user is considered old') + + def handle(self, purge, *args, **kwargs): + if purge: + return self.handle_purge_users(*args, **kwargs) + else: + return self.handle_get_users(*args, **kwargs) + + def handle_purge_users(self, days_old, *args, **kwargs): + users = get_old_users(days_old).all() + if not users: + self.stdout.write(self.style.NOTICE("No users to be deleted")) + return + + self.stdout.write("{} {} {}\n".format('username'.ljust(19), 'name'.ljust(29), 'last_login')) + for user in users: + self.stdout.write("{} {} {}\n".format(user.username.ljust(19), user.name.ljust(29), user.last_login)) + self.stdout.write("\n") + self.stdout.write(self.style.WARNING("I will be removing {} users, please check".format(len(users)))) + confirmation = input("Type 'YES' to confirm: ") + + for i in range(1, 3): + if confirmation == 'YES': + break + confirmation = input("Please type 'YES' to confirm (or ^C to abort): ") + else: + self.stdout.write( + self.style.ERROR( + 'Aborting purge operation after {} prompts'.format(i + 1))) + return + + deleted = 0 + for i, user in enumerate(users): + print("[{: 4.0f}% ] {}".format(100 * i / len(users), user)) + if user.username in _IMPORTANT_USERNAMES: + self.stdout.write(self.style.WARNING("Not deleting user '{}': username looks important".format(user))) + continue + + user.delete() + deleted += 1 + self.stdout.write(self.style.SUCCESS("Deleted {} users".format(deleted))) + + def handle_get_users(self, days_old, *args, **kwargs): + for user in get_old_users(days_old): + self.stdout.write("{}\n".format(user)) + + +def get_old_users(days_old): + """Return a queryset with all users older than an amount of days""" + User = get_user_model() + tz = get_default_timezone() + return User.objects.filter(is_staff=False, is_superuser=False, is_active=True).filter( + Q(last_login__lt=datetime.now(tz) - timedelta(days_old)) + | Q(last_login=None)).order_by('username') diff --git a/marietje/marietje/models.py b/marietje/marietje/models.py index ea34748..6906b9d 100644 --- a/marietje/marietje/models.py +++ b/marietje/marietje/models.py @@ -1,11 +1,13 @@ from django.db import models from queues.models import Queue from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager +from django.contrib.auth.hashers import make_password from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.validators import ASCIIUsernameValidator, UnicodeUsernameValidator from django.utils import six, timezone from django.utils.translation import ugettext_lazy as _ from django.core.mail import send_mail + from marietje.utils import get_first_queue @@ -105,6 +107,23 @@ class User(AbstractBaseUser, PermissionsMixin): verbose_name = _('user') verbose_name_plural = _('users') + def delete(self, *args, **kwargs): + """ + We want to override the deletion behaviour for users, because we want to keep the stats + valid. Instead of deleting the user, we will completely anonimise the user's personal + data. The user will still have their uploads and queues associated with them, but they are + not personal data. + + See also: UserAdmin.delete_queryset(request, queryset) + """ + self.username = '_deleteduser{:d}'.format(self.id) + self.password = make_password(None) + self.name = '[deleted]' + self.email = '' + self.is_active = False + self.study = '' + self.save(*args, **kwargs) + def get_full_name(self): return self.name