From d71090fc02e430e94f3aed966194306b238c6f28 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 21 Apr 2020 23:16:55 +0200 Subject: [PATCH 1/2] misc: add a DeletedUser model to keep metadata about deleted users (#41933) --- src/authentic2/app_settings.py | 6 ++++ src/authentic2/custom_user/managers.py | 12 +++++++ src/authentic2/custom_user/models.py | 45 +++++++++++++++++++++++++- tests/test_cleanup.py | 23 ++++++++++--- 4 files changed, 81 insertions(+), 5 deletions(-) diff --git src/authentic2/app_settings.py src/authentic2/app_settings.py index 6f5a7111..365b4021 100644 --- src/authentic2/app_settings.py +++ src/authentic2/app_settings.py @@ -314,6 +314,12 @@ default_settings = dict( A2_ACCEPT_EMAIL_AUTHENTICATION=Setting( default=True, definition='Enable authentication by email'), + A2_USER_DELETED_KEEP_DATA=Setting( + default=['email', 'uuid'], + definition='User data to keep after deletion'), + A2_USER_DELETED_KEEP_DATA_DAYS=Setting( + default=365, + definition='Number of days to keep data on deleted users'), ) app_settings = AppSettings(default_settings) diff --git src/authentic2/custom_user/managers.py src/authentic2/custom_user/managers.py index d344e604..f92bed7b 100644 --- src/authentic2/custom_user/managers.py +++ src/authentic2/custom_user/managers.py @@ -22,6 +22,7 @@ from django.utils import six from django.utils import timezone from django.contrib.auth.models import BaseUserManager +from authentic2 import app_settings from authentic2.models import Attribute @@ -88,6 +89,8 @@ class UserManager(BaseUserManager): @transaction.atomic def cleanup(self, threshold=600, timestamp=None): '''Delete all deleted users for more than 10 minutes.''' + from .models import DeletedUser + not_after = (timestamp or timezone.now()) - datetime.timedelta(seconds=threshold) qs = self.filter(deleted__lt=not_after) @@ -98,4 +101,13 @@ class UserManager(BaseUserManager): for user in loaded: logger.info(u'deleted account %s', user) transaction.on_commit(log) + deleted_users = [] + for user in qs: + deleted_user = DeletedUser(deleted=user.deleted, old_user_id=user.id) + if 'email' in app_settings.A2_USER_DELETED_KEEP_DATA: + deleted_user.old_email = user.email + if 'uuid' in app_settings.A2_USER_DELETED_KEEP_DATA: + deleted_user.old_uuid = user.uuid + deleted_users.append(deleted_user) + DeletedUser.objects.bulk_create(deleted_users) qs.delete() diff --git src/authentic2/custom_user/models.py src/authentic2/custom_user/models.py index fed4b060..2cda6009 100644 --- src/authentic2/custom_user/models.py +++ src/authentic2/custom_user/models.py @@ -1,3 +1,4 @@ +# coding: utf-8 # authentic2 - versatile identity manager # Copyright (C) 2010-2019 Entr'ouvert # @@ -14,6 +15,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from __future__ import unicode_literals + +import datetime import random from django.db import models, transaction @@ -27,6 +31,7 @@ try: except ImportError: from django.contrib.contenttypes.generic import GenericRelation from django.contrib.auth.models import AbstractBaseUser +from django.contrib.postgres.fields import JSONField from django_rbac.models import PermissionMixin from django_rbac.utils import get_role_parenting_model @@ -234,7 +239,7 @@ class User(AbstractBaseUser, PermissionMixin): def __str__(self): human_name = self.username or self.email or self.get_full_name() short_id = self.uuid[:6] - return u'%s (%s)' % (human_name, short_id) + return '%s (%s)' % (human_name, short_id) def __repr__(self): return '' % six.text_type(self) @@ -341,3 +346,41 @@ class User(AbstractBaseUser, PermissionMixin): self.deleted = timestamp or timezone.now() if save: self.save(update_fields=['email', 'email_verified', 'is_active', 'deleted']) + + +@six.python_2_unicode_compatible +class DeletedUser(models.Model): + deleted = models.DateTimeField( + verbose_name=_('Deletion date')) + old_uuid = models.TextField( + verbose_name=_('Old UUID'), + null=True, + blank=True) + old_user_id = models.PositiveIntegerField( + verbose_name=_('Old user id'), + null=True, + blank=True) + old_email = models.EmailField( + verbose_name=_('Old email adress'), + null=True, + blank=True) + old_data = JSONField( + verbose_name=_('Old data'), + null=True, + blank=True) + + @classmethod + def cleanup(cls, threshold=None, timestamp=None): + threshold = threshold or (timezone.now() - datetime.timedelta(days=app_settings.A2_USER_DELETED_KEEP_DATA_DAYS)) + cls.objects.filter(deleted__lt=threshold).delete() + + def __str__(self): + return 'DeletedUser(old_id=%s, old_uuid=%s…, old_email=%s)' % ( + self.old_user_id or '-', + (self.old_uuid or '')[:6], + self.old_email or '-') + + class Meta: + verbose_name = _('deleted user') + verbose_name_plural = _('deleted users') + ordering = ('deleted', 'id') diff --git tests/test_cleanup.py tests/test_cleanup.py index c83f57b9..2dbd7141 100644 --- tests/test_cleanup.py +++ tests/test_cleanup.py @@ -16,14 +16,29 @@ import datetime -from django.contrib.auth import get_user_model from django.utils.timezone import now +from authentic2.custom_user.models import User, DeletedUser -def test_deleted_user_cleanup(db): - User = get_user_model() - u = User.objects.create(username='john.doe') + +def test_deleted_user_cleanup(db, freezer): + freezer.move_to('2020-01-01') + u = User.objects.create(username='john.doe', email='john@example.com') assert User.objects.count() == 1 + assert DeletedUser.objects.count() == 0 u.mark_as_deleted() User.objects.cleanup(timestamp=now() + datetime.timedelta(seconds=700)) assert User.objects.count() == 0 + assert DeletedUser.objects.count() == 1 + deleted_user = DeletedUser.objects.get(old_user_id=u.id) + assert deleted_user.deleted == u.deleted + assert deleted_user.old_email == u.email + assert deleted_user.old_uuid == u.uuid + assert deleted_user.old_data is None + freezer.move_to(datetime.timedelta(days=365)) + DeletedUser.cleanup() + assert DeletedUser.objects.count() == 1, 'DeletedUser are deleted after 365 days' + freezer.move_to(datetime.timedelta(seconds=1)) + DeletedUser.cleanup() + assert DeletedUser.objects.count() == 0, 'DeletedUser are deleted after 365 days' + -- 2.26.0