misc: add a DeletedUser model to keep metadata about deleted users (#41933)

This commit is contained in:
Benjamin Dauvergne 2020-04-21 23:16:55 +02:00
parent 5c6fd54baa
commit bda672d59e
5 changed files with 125 additions and 20 deletions

View File

@ -319,6 +319,12 @@ default_settings = dict(
A2_EMAILS_ADDRESS_RATELIMIT=Setting(
default='3/d',
definition='Maximum rate of emails sent to the same email address.'),
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)

View File

@ -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
@ -54,6 +55,32 @@ class UserQuerySet(models.QuerySet):
self = self.distinct()
return self
@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)
loaded = list(qs)
def log():
logger = logging.getLogger('authentic2')
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.rsplit('#', 1)[0]
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()
class UserManager(BaseUserManager):
@ -84,18 +111,3 @@ class UserManager(BaseUserManager):
def get_by_natural_key(self, uuid):
return self.get(uuid=uuid)
@transaction.atomic
def cleanup(self, threshold=600, timestamp=None):
'''Delete all deleted users for more than 10 minutes.'''
not_after = (timestamp or timezone.now()) - datetime.timedelta(seconds=threshold)
qs = self.filter(deleted__lt=not_after)
loaded = list(qs)
def log():
logger = logging.getLogger('authentic2')
for user in loaded:
logger.info(u'deleted account %s', user)
transaction.on_commit(log)
qs.delete()

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.12 on 2020-05-05 14:16
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('custom_user', '0019_add_user_deleted'),
]
operations = [
migrations.CreateModel(
name='DeletedUser',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('deleted', models.DateTimeField(verbose_name='Deletion date')),
('old_uuid', models.TextField(blank=True, null=True, verbose_name='Old UUID')),
('old_user_id', models.PositiveIntegerField(blank=True, null=True, verbose_name='Old user id')),
('old_email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Old email adress')),
('old_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='Old data')),
],
options={
'verbose_name': 'deleted user',
'verbose_name_plural': 'deleted users',
'ordering': ('deleted', 'id'),
},
),
]

View File

@ -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 <http://www.gnu.org/licenses/>.
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
@ -232,7 +237,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 '<User: %r>' % six.text_type(self)
@ -339,3 +344,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')

View File

@ -16,14 +16,28 @@
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.rsplit('#', 1)[0]
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'