transfer from private server/git server
This commit is contained in:
69
web/services/rgpd_service.py
Normal file
69
web/services/rgpd_service.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
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}
|
||||
Reference in New Issue
Block a user