Quasi ogni sistema ha una tabella audit_log con una colonna JSONB in cui ci sta tutto: login, errori, modifiche di configurazione, concessioni di role, aperture di documenti. Funziona finché qualcuno non fa una vera domanda di governance, e a quel punto interrogarla diventa archeologia.
Divido la traccia in tre piani, perché rispondono a domande diverse:
| Piano | Domanda | Esempi |
|---|---|---|
| Operativo | Che cosa è successo nel sistema? | login, errori, configurazione, elaborazione |
| Concessione | Chi ha dato quale potere a chi, e quando? | role, grant diretti, share, revoche |
| Accesso | Chi ha visto o consultato quale contenuto? | apertura, download, stampa, consultazione di microdati |
Ogni piano ha volume, retention, indici e criticità propri. La concessione ha poco volume e molto valore probatorio. L'accesso ha un volume alto e richiede il partizionamento per tempo. L'operativo è rumore utile. Mescolare tutto fa dipendere una traccia importante da un writer pensato per un altro scopo.
Stato non è evento
Una tabella user_role dice chi ha il role oggi. Non dice chi lo aveva tre mesi fa. Per questo esiste una tabella append-only di eventi. Servono entrambe, e una non sostituisce l'altra.
Una tabella di stato dice chi ha accesso. Solo la traccia degli eventi dice come ci è arrivato.
Asimmetria dei guasti
Non tutti i guasti dell'audit devono avere la stessa conseguenza:
| Natura | Se la traccia fallisce | Perché |
|---|---|---|
| operativa | best-effort | il fatto può essere già avvenuto |
| concessione | fail-closed | un potere invisibile è inaccettabile |
| accesso sensibile | fail-closed | un accesso senza evidenza rompe l'accountability |
Questa è una decisione regolatoria e operativa, e ha un costo: il fail-closed mette lo storage della traccia nel percorso critico. Prima di adottarlo, misura p50 e p99 dell'insert, il tasso di errore causato dalla traccia, la crescita giornaliera, il bloat e il comportamento del pool. La policy non può esigere una disponibilità che l'infrastruttura non è stata dimensionata per sostenere.
Concessione ed evento nella stessa transazione
BEGIN;
INSERT INTO grant_event (...);
INSERT INTO user_role (...);
UPDATE app_user
SET permission_version = permission_version + 1
WHERE id = ...;
COMMIT;Se una qualsiasi fase fallisce, rollback di tutto. Un evento che afferma una concessione inesistente è grave quanto una concessione senza evento. Senza una Unit of Work globale, basta un helper ristretto:
security_transaction(operation):
begin
operation(shared_session)
commit_or_rollbackE la regola che dà i denti all'helper: qualsiasi call site di concessione fuori da esso è un bug strutturale, e la CI lo cerca.
Registrare prima di servire
Quando l'accesso sensibile è fail-closed, l'ordine è autorizzare, registrare l'evento e solo allora serializzare e restituire il contenuto. L'errore residuo è registrare un accesso che non è arrivato all'utente perché qualcosa è fallito dopo. Questo errore “per eccesso” è verificabile. L'inverso, contenuto consegnato senza evento, è esattamente ciò che la policy vuole impedire.
Registra la via
Quando qualcuno apre una risorsa, conviene salvare per quale strada l'autorizzazione è stata soddisfatta:
access_via = own_partition | share | all_partitions | platform_bypass | legacySe domani lo share viene revocato, la traccia continua a spiegare l'accesso di oggi senza dover ricostruire lo stato passato. Quando più di una via è vera nello stesso momento, definisci una precedenza e mantienila, oppure registrale tutte. L'importante è non lasciare ambiguità storiche.
Cosa non registrare
Il polling dell'interfaccia non è accesso. Elencare metadati e aprire contenuti sono eventi diversi, e registrare ogni refresh automatico genera un volume enorme con poco valore probatorio. Definisci che cosa conta come accesso prima di accendere il writer.
E ricorda che la traccia stessa è un dato personale: identità, IP, orario, oggetto consultato, pattern di utilizzo. Ha bisogno di controllo degli accessi, di una retention esplicita e, negli ambienti più esigenti, di un utente applicativo senza UPDATE né DELETE su di essa.