Skip to content

Commit

Permalink
Custom action for admin to anonymize users
Browse files Browse the repository at this point in the history
  • Loading branch information
Guilouf committed Jan 3, 2025
1 parent ec4fbbf commit fb18b9f
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 20 deletions.
7 changes: 7 additions & 0 deletions lemarche/users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ class UserAdmin(FieldsetsInlineMixin, UserAdmin):
search_fields = ["id", "email", "first_name", "last_name"]
search_help_text = "Cherche sur les champs : ID, E-mail, Prénom, Nom"
ordering = ["-created_at"]
actions = ["anonymize_users"]

autocomplete_fields = ["company", "partner_network"]
readonly_fields = (
Expand Down Expand Up @@ -402,3 +403,9 @@ def extra_data_display(self, instance: User = None):
return "-"

extra_data_display.short_description = User._meta.get_field("extra_data").verbose_name

@admin.action(description="Anonymiser les utilisateurs sélectionnés")
def anonymize_users(self, request, queryset):
"""Wipe personnal data of all selected users"""
queryset.anonymize_update()
self.message_user(request, "L'anonymisation s'est déroulée avec succès")
20 changes: 2 additions & 18 deletions lemarche/users/management/commands/anonymize_old_users.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX
from django.contrib.postgres.functions import RandomUUID
from django.core.management.base import BaseCommand
from django.db import transaction
from django.db.models import F, Value
from django.db.models.functions import Concat
from django.template import defaulttags
from django.utils import timezone

Expand Down Expand Up @@ -62,20 +58,8 @@ def anonymize_old_users(self, expiry_date: timezone.datetime, dry_run: bool):
qs = User.objects.filter(last_login__lte=expiry_date, is_anonymized=False)
users_to_update_count = qs.count()

qs.update(
is_active=False, # inactive users are allowed to log in standard login views
is_anonymized=True,
email=Concat(F("id"), Value("@domain.invalid")),
first_name="",
last_name="",
phone="",
api_key=None,
api_key_last_updated=None,
# https://docs.djangoproject.com/en/5.1/ref/contrib/auth/#django.contrib.auth.models.User.set_unusable_password
# Imitating the method but in sql. Prevent password reset attempt
# Random string is to avoid chances of impersonation by admins https://code.djangoproject.com/ticket/20079
password=Concat(Value(UNUSABLE_PASSWORD_PREFIX), RandomUUID()),
)
qs.anonymize_update()

# remove anonymized users in Siaes
SiaeUser.objects.filter(user__is_anonymized=True).delete()

Expand Down
23 changes: 21 additions & 2 deletions lemarche/users/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from django.conf import settings
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX
from django.contrib.auth.models import AbstractUser
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.functions import RandomUUID
from django.db import models
from django.db.models import Count
from django.db.models.functions import Greatest, Lower
from django.db.models import Count, F, Value
from django.db.models.functions import Concat, Greatest, Lower
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.forms.models import model_to_dict
Expand Down Expand Up @@ -61,6 +63,23 @@ def with_latest_activities(self):
)
)

def anonymize_update(self):
"""Wipe or replace personal data stored on User model only"""
return self.update(
is_active=False, # inactive users are allowed to log in standard login views
is_anonymized=True,
email=Concat(F("id"), Value("@domain.invalid")),
first_name="",
last_name="",
phone="",
api_key=None,
api_key_last_updated=None,
# https://docs.djangoproject.com/en/5.1/ref/contrib/auth/#django.contrib.auth.models.User.set_unusable_password
# Imitating the method but in sql. Prevent password reset attempt
# Random string is to avoid chances of impersonation by admins https://code.djangoproject.com/ticket/20079
password=Concat(Value(UNUSABLE_PASSWORD_PREFIX), RandomUUID()),
)


class UserManager(BaseUserManager):
"""
Expand Down
28 changes: 28 additions & 0 deletions lemarche/users/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from unittest.mock import patch

from dateutil.relativedelta import relativedelta
from django.contrib.messages import get_messages
from django.core.management import call_command
from django.core.validators import validate_email
from django.db.models import F
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone

from lemarche.companies.factories import CompanyFactory
Expand Down Expand Up @@ -310,3 +312,29 @@ def test_dryrun_warn_command(self):
call_command("anonymize_old_users", dry_run=True, stdout=self.std_out)

self.assertFalse(TemplateTransactionalSendLog.objects.all())


class UserAdminTestCase(TestCase):
def setUp(self):
UserFactory(is_staff=False, is_anonymized=False)
super_user = UserFactory(is_staff=True, is_superuser=True)
self.client.force_login(super_user)

def test_anonymize_action(self):
"""Test the anonymize_users action from the admin"""

users_ids = User.objects.values_list("id", flat=True)
data = {
"action": "anonymize_users",
"_selected_action": users_ids,
}
# https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#reversing-admin-urls
change_url = reverse("admin:users_user_changelist")
response = self.client.post(path=change_url, data=data)

self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, change_url)
self.assertTrue(User.objects.filter(is_staff=False).first().is_anonymized)

messages_strings = [str(message) for message in get_messages(response.wsgi_request)]
self.assertIn("L'anonymisation s'est déroulée avec succès", messages_strings)

0 comments on commit fb18b9f

Please sign in to comment.