From 8ef98877501face9d6458e072fea3c2f9123d28c Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Tue, 21 Apr 2020 22:32:54 +0200 Subject: [PATCH 2/3] misc: replace DeletedUser model by attribute deleted on User --- src/authentic2/admin.py | 5 --- src/authentic2/custom_user/managers.py | 20 +++++++++- .../migrations/0019_user_deleted.py | 37 +++++++++++++++++ src/authentic2/custom_user/models.py | 20 +++++++++- .../commands/clean-unused-accounts.py | 3 +- .../authentic2/manager/user_detail.html | 7 ---- src/authentic2/manager/user_views.py | 13 ++++-- src/authentic2/managers.py | 16 -------- .../migrations/0027_auto_20200421_1609.py | 40 +++++++++++++++++++ src/authentic2/models.py | 16 -------- src/authentic2/views.py | 12 +++--- tests/test_all.py | 1 + tests/test_api.py | 4 +- tests/test_cleanup.py | 5 +-- tests/test_commands.py | 27 ++++++++----- tests/test_user_manager.py | 17 ++++---- tests/test_views.py | 18 +++++---- 17 files changed, 174 insertions(+), 87 deletions(-) create mode 100644 src/authentic2/custom_user/migrations/0019_user_deleted.py create mode 100644 src/authentic2/migrations/0027_auto_20200421_1609.py diff --git src/authentic2/admin.py src/authentic2/admin.py index 8e28f728..f7058184 100644 --- src/authentic2/admin.py +++ src/authentic2/admin.py @@ -84,11 +84,6 @@ class UserExternalIdAdmin(admin.ModelAdmin): admin.site.register(models.UserExternalId, UserExternalIdAdmin) -class DeletedUserAdmin(admin.ModelAdmin): - list_display = ('user', 'creation') - date_hierarchy = 'creation' -admin.site.register(models.DeletedUser, DeletedUserAdmin) - DB_SESSION_ENGINES = ( 'django.contrib.sessions.backends.db', 'django.contrib.sessions.backends.cached_db', diff --git src/authentic2/custom_user/managers.py src/authentic2/custom_user/managers.py index 339e5a9a..d344e604 100644 --- src/authentic2/custom_user/managers.py +++ src/authentic2/custom_user/managers.py @@ -14,7 +14,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.db import models +import datetime +import logging + +from django.db import models, transaction from django.utils import six from django.utils import timezone from django.contrib.auth.models import BaseUserManager @@ -81,3 +84,18 @@ 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() diff --git src/authentic2/custom_user/migrations/0019_user_deleted.py src/authentic2/custom_user/migrations/0019_user_deleted.py new file mode 100644 index 00000000..6d434adb --- /dev/null +++ src/authentic2/custom_user/migrations/0019_user_deleted.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.29 on 2020-04-21 13:38 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.contrib.postgres.fields.jsonb + + +class Migration(migrations.Migration): + + dependencies = [ + ('custom_user', '0018_user_last_account_deletion_alert'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='deleted', + field=models.DateTimeField(blank=True, null=True, verbose_name='Deletion date'), + ), + 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'), + }, + ), + ] diff --git src/authentic2/custom_user/models.py src/authentic2/custom_user/models.py index b7e094f9..fed4b060 100644 --- src/authentic2/custom_user/models.py +++ src/authentic2/custom_user/models.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import random + from django.db import models, transaction from django.utils import timezone from django.core.mail import send_mail @@ -155,13 +157,15 @@ class User(AbstractBaseUser, PermissionMixin): default=True, help_text=_('Designates whether this user should be treated as ' 'active. Unselect this instead of deleting accounts.')) - date_joined = models.DateTimeField(_('date joined'), default=timezone.now) ou = models.ForeignKey( verbose_name=_('organizational unit'), to='a2_rbac.OrganizationalUnit', blank=True, null=True, swappable=False) + + # events dates + date_joined = models.DateTimeField(_('date joined'), default=timezone.now) modified = models.DateTimeField( verbose_name=_('Last modification time'), db_index=True, @@ -170,6 +174,10 @@ class User(AbstractBaseUser, PermissionMixin): verbose_name=_('Last account deletion alert'), null=True, blank=True) + deleted = models.DateTimeField( + verbose_name=_('Deletion date'), + null=True, + blank=True) objects = UserManager.from_queryset(UserQuerySet)() attributes = AttributesDescriptor() @@ -323,3 +331,13 @@ class User(AbstractBaseUser, PermissionMixin): if hasattr(self, '_a2_attributes_cache'): del self._a2_attributes_cache return super(User, self).refresh_from_db(*args, **kwargs) + + def mark_as_deleted(self, timestamp=None, force=True, save=True): + self.is_active = False + self.email_verified = False + if self.email and '#' not in self.email: + self.email += '#%d' % random.randint(1, 10000000) + if force or not self.deleted: + self.deleted = timestamp or timezone.now() + if save: + self.save(update_fields=['email', 'email_verified', 'is_active', 'deleted']) diff --git src/authentic2/management/commands/clean-unused-accounts.py src/authentic2/management/commands/clean-unused-accounts.py index 36d9bdbf..95aca5bf 100644 --- src/authentic2/management/commands/clean-unused-accounts.py +++ src/authentic2/management/commands/clean-unused-accounts.py @@ -27,7 +27,6 @@ from django.utils import timezone from django.utils.six.moves.urllib import parse as urlparse from django_rbac.utils import get_ou_model -from authentic2.models import DeletedUser from authentic2.utils import send_templated_mail from django.conf import settings @@ -120,5 +119,5 @@ class Command(BaseCommand): ctx = {'user': user} with atomic(): if not self.fake: - DeletedUser.objects.delete_user(user) + user.mark_as_deleted(timestamp=self.now) self.send_mail('authentic2/unused_account_delete', user, ctx) diff --git src/authentic2/manager/templates/authentic2/manager/user_detail.html src/authentic2/manager/templates/authentic2/manager/user_detail.html index 1df94dcd..63c052a5 100644 --- src/authentic2/manager/templates/authentic2/manager/user_detail.html +++ src/authentic2/manager/templates/authentic2/manager/user_detail.html @@ -41,13 +41,6 @@ {% endblock %} {% block other_actions %} - - {% if object.deletion %} -

- {% blocktrans with date=object.deletion.creation %}Prepared for deletion since {{ date }}{% endblocktrans %} -

- {% endif %} -