Files
gesthub/web/services/rgpd_service.py
2026-08-21 09:12:02 +02:00

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}