""" 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}