-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Suppression des données personnelles (#1607)
- Loading branch information
Showing
13 changed files
with
478 additions
and
576 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
lemarche/users/management/commands/anonymize_old_users.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
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 | ||
|
||
from lemarche.conversations.models import TemplateTransactional | ||
from lemarche.siaes.models import SiaeUser | ||
from lemarche.users.models import User | ||
|
||
|
||
class DryRunException(Exception): | ||
"""To be raised in a dry run mode to abort current transaction""" | ||
|
||
pass | ||
|
||
|
||
class Command(BaseCommand): | ||
"""Update and anonymize inactive users past a defined inactivity period""" | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument( | ||
"--month_timeout", | ||
type=int, | ||
default=settings.INACTIVE_USER_TIMEOUT_IN_MONTHS, | ||
help="Délai en mois à partir duquel les utilisateurs sont considérés inactifs", | ||
) | ||
parser.add_argument( | ||
"--warning_delay", | ||
type=int, | ||
default=settings.INACTIVE_USER_WARNING_DELAY_IN_DAYS, | ||
help="Délai en jours avant la date de suppression pour prevenir les utilisateurs", | ||
) | ||
parser.add_argument( | ||
"--dry_run", | ||
type=bool, | ||
default=False, | ||
help="La commande est exécutée sans que les modifications soient transmises à la base", | ||
) | ||
|
||
def handle(self, *args, **options): | ||
expiry_date = timezone.now() - relativedelta(months=options["month_timeout"]) | ||
warning_date = expiry_date + relativedelta(days=options["warning_delay"]) | ||
|
||
try: | ||
self.anonymize_old_users(expiry_date=expiry_date, dry_run=options["dry_run"]) | ||
except DryRunException: | ||
self.stdout.write("Fin du dry_run d'anonymisation") | ||
|
||
self.warn_users_by_email(expiry_date=expiry_date, warning_date=warning_date, dry_run=options["dry_run"]) | ||
|
||
@transaction.atomic | ||
def anonymize_old_users(self, expiry_date: timezone.datetime, dry_run: bool): | ||
"""Update inactive users since x months and strip them from their personal data. | ||
email is unique and not nullable, therefore it's replaced with the object id.""" | ||
|
||
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()), | ||
) | ||
# remove anonymized users in Siaes | ||
SiaeUser.objects.filter(user__is_anonymized=True).delete() | ||
|
||
self.stdout.write(f"Utilisateurs anonymisés avec succès ({users_to_update_count} traités)") | ||
|
||
if dry_run: # cancel transaction | ||
raise DryRunException | ||
|
||
@transaction.atomic | ||
def warn_users_by_email(self, warning_date: timezone.datetime, expiry_date: timezone.datetime, dry_run: bool): | ||
email_template = TemplateTransactional.objects.get(code="USER_ANONYMIZATION_WARNING") | ||
|
||
# Users that have already received the mail are excluded | ||
users_to_warn = User.objects.filter(last_login__lte=warning_date, is_active=True, is_anonymized=False).exclude( | ||
recipient_transactional_send_logs__template_transactional__code=email_template.code, | ||
recipient_transactional_send_logs__extra_data__contains={"sent": True}, | ||
) | ||
|
||
if dry_run: | ||
self.stdout.write( | ||
f"Fin du dry_run d'avertissement des utilisateurs, {users_to_warn.count()} auraient été avertis" | ||
) | ||
return # exit before sending emails | ||
|
||
for user in users_to_warn: | ||
email_template.send_transactional_email( | ||
recipient_email=user.email, | ||
recipient_name=user.full_name, | ||
variables={ | ||
"user_full_name": user.full_name, | ||
"anonymization_date": defaulttags.date(expiry_date), # natural date | ||
}, | ||
recipient_content_object=user, | ||
) | ||
|
||
self.stdout.write(f"Un email d'avertissement a été envoyé à {users_to_warn.count()} utilisateurs") |
48 changes: 48 additions & 0 deletions
48
lemarche/users/migrations/0040_remove_user_c4_email_verified_remove_user_c4_id_and_more.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# Generated by Django 4.2.15 on 2024-12-23 17:02 | ||
|
||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("users", "0039_remove_user_user_email_ci_uniqueness_and_more"), | ||
] | ||
|
||
operations = [ | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_email_verified", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_id", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_id_card_verified", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_naf", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_phone_prefix", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_phone_verified", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_siret", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_time_zone", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="c4_website", | ||
), | ||
] |
20 changes: 20 additions & 0 deletions
20
lemarche/users/migrations/0041_remove_user_image_name_remove_user_image_url.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Generated by Django 4.2.15 on 2024-12-24 09:04 | ||
|
||
from django.db import migrations | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("users", "0040_remove_user_c4_email_verified_remove_user_c4_id_and_more"), | ||
] | ||
|
||
operations = [ | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="image_name", | ||
), | ||
migrations.RemoveField( | ||
model_name="user", | ||
name="image_url", | ||
), | ||
] |
30 changes: 30 additions & 0 deletions
30
lemarche/users/migrations/0042_email_template_anonymise_user.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from django.db import migrations | ||
|
||
|
||
def create_template(apps, schema_editor): | ||
TemplateTransactional = apps.get_model("conversations", "TemplateTransactional") | ||
TemplateTransactional.objects.create( | ||
name="Avertissement d'anonymisation de compte utilisateur", | ||
code="USER_ANONYMIZATION_WARNING", | ||
description=""" | ||
Bonjour {{ params.user_full_name }}, votre compte va être supprimé le {{ params.anonymization_date }} | ||
si vous ne vous connectez pas avant. | ||
Bonne journée, | ||
L'équipe du marché de l'inclusion | ||
""", | ||
) | ||
|
||
|
||
def delete_template(apps, schema_editor): | ||
TemplateTransactional = apps.get_model("conversations", "TemplateTransactional") | ||
TemplateTransactional.objects.get(code="USER_ANONYMIZATION_WARNING").delete() | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("users", "0041_remove_user_image_name_remove_user_image_url"), | ||
] | ||
|
||
operations = [ | ||
migrations.RunPython(create_template, reverse_code=delete_template), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Generated by Django 4.2.15 on 2024-12-26 09:23 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
dependencies = [ | ||
("users", "0042_email_template_anonymise_user"), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name="user", | ||
name="is_anonymized", | ||
field=models.BooleanField(default=False, verbose_name="L'utilisateur à été anonymisé"), | ||
), | ||
] |
Oops, something went wrong.