70 lines
2.7 KiB
Python
70 lines
2.7 KiB
Python
"""
|
|
rgpd_service.py — Droit à l'effacement / anonymisation RGPD (art. 17).
|
|
|
|
GestHub ne stocke pas de compte utilisateur : l'unique donnée personnelle
|
|
conservée est le `sub` OIDC Keycloak, référencé dans annonces, fichiers,
|
|
evenements et audit_log (cf. rgpd_model.py). "Supprimer un utilisateur" au
|
|
sens RGPD consiste donc à remplacer ce `sub` partout où il apparaît par un
|
|
pseudonyme irréversible, sans toucher au contenu produit (titres, fichiers,
|
|
événements) qui reste une donnée d'activité du service, pas une donnée
|
|
personnelle une fois l'auteur dé-identifié.
|
|
|
|
Pseudonyme = HMAC-SHA256(pepper, sub), tronqué et préfixé "anon:" pour tenir
|
|
dans la colonne VARCHAR(64) et rester reconnaissable dans les journaux/DB.
|
|
Le pepper (RGPD_ANON_PEPPER, ou SECRET_KEY à défaut) n'est jamais stocké
|
|
dans la base : sans lui, remonter du pseudonyme au `sub` d'origine est
|
|
calculatoirement infaisable (irréversibilité, cf. RGPD art. 17 §1).
|
|
Le hash reste déterministe pour un même `sub` : les quatre tables reçoivent
|
|
donc le même pseudonyme, ce qui permet de vérifier a posteriori que la purge
|
|
a bien été appliquée partout, sans jamais réexposer l'identité d'origine.
|
|
"""
|
|
|
|
import hashlib
|
|
import hmac
|
|
|
|
from models import rgpd_model
|
|
from services import audit_service
|
|
|
|
ANON_PREFIX = "anon:"
|
|
_HASH_HEX_LEN = 64 - len(ANON_PREFIX) # tient dans VARCHAR(64)
|
|
|
|
|
|
class InvalidSubError(Exception):
|
|
"""Levée quand le `sub` cible est vide ou manifestement invalide."""
|
|
|
|
|
|
def _anon_value(pepper, target_sub):
|
|
digest = hmac.new(pepper.encode("utf-8"), target_sub.encode("utf-8"), hashlib.sha256)
|
|
return ANON_PREFIX + digest.hexdigest()[:_HASH_HEX_LEN]
|
|
|
|
|
|
def anonymize_user(db, target_sub, pepper, actor_sub=None, ip=None):
|
|
"""Anonymise toutes les données rattachées à `target_sub`.
|
|
|
|
Retourne {"anonymized": bool, "pseudonym": str, "rows": {...}}.
|
|
`anonymized` est False si aucune ligne ne référençait ce `sub` (rien à
|
|
faire — permet à la route de répondre 404 plutôt que de journaliser une
|
|
action fictive).
|
|
"""
|
|
target_sub = (target_sub or "").strip()
|
|
if not target_sub:
|
|
raise InvalidSubError("Le sub cible est obligatoire.")
|
|
if target_sub.startswith(ANON_PREFIX):
|
|
raise InvalidSubError("Cet utilisateur est déjà anonymisé.")
|
|
|
|
pseudonym = _anon_value(pepper, target_sub)
|
|
rows = rgpd_model.anonymize(db, target_sub, pseudonym)
|
|
total = sum(rows.values())
|
|
|
|
if total > 0:
|
|
audit_service.log(
|
|
db,
|
|
"RGPD_ANONYMIZE_USER",
|
|
f"user:{pseudonym}",
|
|
"SUCCESS",
|
|
actor_sub,
|
|
ip,
|
|
)
|
|
|
|
return {"anonymized": total > 0, "pseudonym": pseudonym, "rows": rows}
|