misc: add a DeletedUser model to keep metadata about deleted users (#41933)
This commit is contained in:
parent
5c6fd54baa
commit
bda672d59e
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue