Presque tous les systèmes ont une table audit_log avec une colonne JSONB où tout rentre : login, erreur, changement de configuration, concession de rôle, ouverture de document. Ça marche jusqu'à ce que quelqu'un pose une vraie question de gouvernance, et là, interroger la table devient de l'archéologie.
Je sépare la trace en trois plans, parce qu'ils répondent à des questions différentes :
| Plan | Question | Exemples |
|---|---|---|
| Opérationnel | Que s'est-il passé dans le système ? | login, erreur, configuration, traitement |
| Concession | Qui a donné quel pouvoir à qui, et quand ? | rôles, grants directs, shares, révocations |
| Accès | Qui a vu ou consulté quel contenu ? | ouverture, téléchargement, impression, consultation de microdonnées |
Chaque plan a son propre volume, sa rétention, ses index et sa criticité. La concession, c'est peu de volume et beaucoup de valeur probante. L'accès, c'est un gros volume qui demande un partitionnement par date. L'opérationnel, c'est du bruit utile. Tout mélanger fait dépendre une trace importante d'un writer pensé pour autre chose.
L'état n'est pas l'événement
Une table user_role dit qui a le rôle aujourd'hui. Elle ne dit pas qui l'avait il y a trois mois. Pour ça, il existe une table append-only d'événements. Les deux sont nécessaires, et l'une ne remplace pas l'autre.
Une table d'état dit qui a accès. Seule la trace des événements dit comment il en est arrivé là.
Asymétrie d'échec
Tout échec d'audit ne doit pas avoir la même conséquence :
| Nature | Si la trace échoue | Pourquoi |
|---|---|---|
| opérationnel | best-effort | le fait a peut-être déjà eu lieu |
| concession | échec fermé | un pouvoir invisible est inacceptable |
| accès sensible | échec fermé | un accès sans preuve casse l'accountability |
C'est une décision réglementaire et opérationnelle, et elle a un coût : le fail-closed met le stockage de la trace sur le chemin critique. Avant de l'adopter, mesurez le p50 et le p99 de l'insert, le taux d'erreur causé par la trace, la croissance quotidienne, le bloat et le comportement du pool. La politique ne peut pas exiger une disponibilité que l'infrastructure n'a pas été dimensionnée pour tenir.
Concession et événement dans la même transaction
BEGIN;
INSERT INTO grant_event (...);
INSERT INTO user_role (...);
UPDATE app_user
SET permission_version = permission_version + 1
WHERE id = ...;
COMMIT;Si une étape échoue, rollback de tout. Un événement qui affirme une concession qui n'existe pas est aussi grave qu'une concession sans événement. Sans Unit of Work global, un helper étroit fait l'affaire :
security_transaction(operation):
begin
operation(shared_session)
commit_or_rollbackEt la règle qui donne des dents au helper : tout call site de concession en dehors de lui est un bug structurel, et la CI le cherche.
Enregistrer avant de servir
Quand l'accès sensible est fail-closed, l'ordre est : autoriser, enregistrer l'événement et seulement ensuite sérialiser et renvoyer le contenu. L'erreur résiduelle, c'est d'enregistrer un accès qui n'est pas arrivé à l'utilisateur parce que quelque chose a échoué après. Cette erreur « par excès » est auditable. L'inverse, du contenu livré sans événement, c'est exactement ce que la politique veut empêcher.
Enregistrez la voie
Quand quelqu'un ouvre une ressource, ça vaut la peine d'enregistrer par quel chemin l'autorisation a été satisfaite :
access_via = own_partition | share | all_partitions | platform_bypass | legacySi le share est révoqué demain, la trace continue d'expliquer l'accès d'aujourd'hui sans avoir à reconstruire l'état passé. Quand plusieurs voies sont vraies en même temps, définissez une précédence et tenez-vous-y, ou enregistrez-les toutes. L'important, c'est de ne pas laisser d'ambiguïté historique.
Ce qu'il ne faut pas enregistrer
Le polling de l'interface n'est pas un accès. Lister des métadonnées et ouvrir un contenu sont des événements différents, et enregistrer chaque rafraîchissement automatique génère un volume énorme pour peu de valeur probante. Définissez ce qui compte comme accès avant d'activer le writer.
Et n'oubliez pas que la trace elle-même est une donnée personnelle : identité, IP, horaire, objet consulté, habitudes d'usage. Elle a besoin d'un contrôle d'accès, d'une rétention explicite et, dans les environnements les plus exigeants, d'un utilisateur applicatif sans UPDATE ni DELETE sur elle.