Prima o poi ogni gestionale incontra questa richiesta: un collaboratore deve poter lavorare sui dati di un cliente, una filiale su quelli della sede, un commercialista su quelli di venti aziende diverse. Sembra un problema di credenziali. È un problema di identità, e la differenza si paga il giorno in cui qualcuno chiede conto di un'operazione.
Le domande a cui rispondere#
Il caso: un account "titolare" possiede i dati. Altri account — collaboratori,filiali, dispositivi, un commercialista, un'agenzia — devono operarci sopra.Le domande a cui l'architettura deve rispondere, e che vengono poste tutteinsieme il giorno dell'incidente: 1. CHI ha fatto questa azione? → identità reale 2. SU QUALI dati l'ha fatta? → identità effettiva 3. Poteva farla? → permessi, non identità 4. Da quando, e fino a quando? → durata della delega 5. Come si revoca, subito? → punto di controllo unicoIl pattern che risponde solo alla 2 e alla 3 sembra funzionare per un anno.Vale la pena tenerle a portata di mano, perché sono il metro con cui misurare le tre soluzioni che seguono. Un'architettura che risponde a due su cinque non è «una versione più semplice»: è una che vi lascerà senza risposta esattamente quando la risposta serve.
Pattern 1 — Credenziale condivisa#
// PATTERN 1 — Credenziale condivisa// Gli account delegati usano direttamente il token del titolare.function risolviToken(utente) { return utente.delegato ? token(utente.titolareId) // ← da qui in poi, "è" il titolare : token(utente.id);}// Nei log a valle esiste UN solo soggetto. La domanda "chi ha cancellato// la fattura 1042?" non ha risposta: risulta averlo fatto il titolare.// La revoca di un singolo delegato non esiste: si può solo ruotare la// credenziale del titolare, che scollega tutti gli altri insieme.È l'implementazione che si scrive in un pomeriggio, ed è tentante perché non richiede di toccare nient'altro: a valle del punto di traduzione, tutto il sistema continua a vedere un solo utente. Proprio per questo, però, a valle non esiste più la distinzione. Il registro degli accessi, i log applicativi, le notifiche, la cronologia delle modifiche: ovunque risulta che abbia agito il titolare.
Pattern 2 — Mappatura dell'identità#
// PATTERN 2 — Mappatura dell'identità// Ogni delegato mantiene la propria identità; il sistema la traduce// nelle credenziali del titolare al momento dell'accesso ai dati.async function contesto(utente) { const delega = await deleghe.attivaPer(utente.id); if (!delega) return { chi: utente.id, dati: utente.id }; return { chi: utente.id, // resta tracciabile dati: delega.titolareId // ma opera sui dati del titolare };}// Meglio: il registro degli accessi distingue i soggetti.// Ancora insufficiente: la traduzione avviene in un punto, e ogni percorso// che dimentica di passare da lì torna al problema precedente. Il permesso// è ancora binario — delegato o no — senza granularità.Un passo avanti reale: le due identità esistono entrambe, e chi ha fatto cosa torna a essere una domanda con risposta. Restano però due debolezze.
La prima è che la traduzione avviene in un punto, e ogni percorso che non ci passa — un job in background, un webhook, uno script di manutenzione, un endpoint nuovo — ricade nel comportamento del Pattern 1 senza che nessuno se ne accorga. La seconda è che il permesso resta binario: si è delegati o non lo si è. Non c'è modo di dire «può leggere le fatture ma non emetterle», che è la richiesta che arriva il mese dopo.
Pattern 3 — Doppia identità nella sessione#
La differenza rispetto al secondo pattern è dove vivono le due identità: non in una funzione che va chiamata, ma nella sessione, per tutta la sua durata. Non c'è un punto da attraversare, quindi non c'è un punto da dimenticare.
// PATTERN 3 — Doppia identità nella sessione// La sessione porta ENTRAMBE le identità, per tutta la sua durata.// È l'unica forma in cui la distinzione non può essere persa per strada.const sessione = { // chi si è autenticato: non cambia mai, nemmeno cambiando contesto attore: { id: 'u_8812', email: 'anna@studio.it', mfa: true }, // su quali dati sta operando adesso soggetto: { id: 'org_204', ragione: 'Rossi Srl' }, // che cosa può farci: derivato dalla delega, non dal ruolo dell'attore permessi: ['fatture:leggi', 'fatture:crea', 'report:leggi'], delega: { id: 'dl_77', concessaDa: 'u_1102', dal: '2026-01-01', al: '2026-12-31', // le deleghe scadono: è la proprietà chiave motivo: 'contabilità 2026' }};Tre elementi meritano attenzione. L'attore non cambia mai: chi si è autenticato resta quello, anche cambiando dieci volte contesto nella stessa sessione. I permessi vengono dalla delega e non dal ruolo dell'attore, il che permette alla stessa persona di avere poteri diversi su clienti diversi. E la delega ha una scadenza: è la proprietà che trasforma un accesso permanente in un accesso con un termine, e che rende possibile la revisione periodica.
// L'accesso ai dati usa SEMPRE il soggetto, mai l'attore.// Come per l'autorizzazione sull'oggetto, il perimetro sta nella query.class Fatture { constructor(db, sessione) { this.db = db; this.s = sessione; if (!sessione.permessi.includes('fatture:leggi')) { throw new PermessoNegato('fatture:leggi'); } } perId(id) { return this.db.get( 'SELECT * FROM fatture WHERE id = ? AND org_id = ?', [id, this.s.soggetto.id] // ← soggetto, non attore ); }}Il perimetro finisce dentro la query, per la stessa ragione discussa nell'articolo su IDOR e autorizzazione: un controllo separato dalla lettura è un controllo che qualcuno, prima o poi, salterà.
Il registro a doppia identità#
-- Il registro con due colonne di identità. È la differenza fra-- "sappiamo cosa è successo" e "sappiamo chi lo ha fatto".CREATE TABLE registro_azioni ( id INTEGER PRIMARY KEY, quando TEXT NOT NULL, attore_id TEXT NOT NULL, -- chi era autenticato soggetto_id TEXT NOT NULL, -- su quali dati delega_id TEXT, -- NULL se agiva per sé azione TEXT NOT NULL, -- 'fatture:elimina' risorsa TEXT NOT NULL, -- 'fattura:1042' esito TEXT NOT NULL, -- 'ok' | 'negato' ip TEXT, agente TEXT);CREATE INDEX idx_registro_attore ON registro_azioni(attore_id, quando);CREATE INDEX idx_registro_soggetto ON registro_azioni(soggetto_id, quando);-- Le due domande dell'incidente, ciascuna una query:-- "cosa ha toccato Anna in tutti i clienti?" → attore_id-- "chi ha toccato i dati di Rossi Srl?" → soggetto_idLe due colonne separate, ciascuna con il proprio indice, esistono per due domande che arrivano in momenti diversi. «Cosa ha toccato Anna, in tutti i clienti?» è la domanda che si pone quando un account viene compromesso. «Chi ha toccato i dati di Rossi Srl?» è quella che pone il cliente. Con una sola colonna se ne può rispondere una.
La revoca, che è la prova del nove#
// La revoca è la prova del nove dell'architettura: quanto tempo passa// fra "ho tolto la delega" e "la delega non funziona più"?//// Con i token autofirmati (JWT) la risposta è: fino alla scadenza del// token. Un JWT valido 24 ore resta valido 24 ore, anche dopo il// licenziamento. È il difetto strutturale della verifica senza stato.// ✓ Sessioni con stato: revoca immediata, un giro in più sul database.async function verifica(idSessione) { const s = await sessioni.leggi(idSessione); // Redis: ~0,2 ms if (!s) return null; if (s.delega && !await deleghe.ancoraValida(s.delega.id)) { await sessioni.elimina(idSessione); return null; } return s;}// ✓ Compromesso praticabile: JWT a vita breve (5-10 min) + refresh token// con stato. La revoca ha effetto entro una finestra dichiarata, e il// percorso caldo resta senza database.La domanda da porsi in fase di progetto: quanto tempo passa fra il clic su «revoca» e il momento in cui l'accesso smette davvero di funzionare? Con token autofirmati verificati senza consultare alcuno stato, la risposta onesta è: fino alla scadenza del token. Se dura ventiquattro ore, la delega revocata funziona ancora per ventiquattro ore.
Per un accesso delegato — che per definizione è temporaneo, concesso da qualcuno che può cambiare idea — è un comportamento inaccettabile. Le due vie d'uscita sono sessioni con stato (una lettura in più su un archivio veloce, ed è la scelta che consiglio per questo caso) oppure token a vita molto breve con rinnovo verificato, che riducono la finestra a qualcosa che si può dichiarare al cliente.
Il cambio di contesto è un'azione#
// Il cambio di contesto è un'AZIONE, non un parametro della richiesta.// ✗ Se il soggetto arriva dal client a ogni chiamata, il controllo va// rifatto ogni volta — e prima o poi un percorso lo salta.GET /api/fatture?org=org_204 // ← chi verifica che possa?// ✓ Il contesto si assume una volta, e resta nella sessione lato server.app.post('/api/contesto', autenticato, async (req, res) => { const delega = await deleghe.attiva(req.sessione.attore.id, req.body.orgId); if (!delega) return res.status(403).json({ errore: 'delega non attiva' }); await sessioni.aggiorna(req.sessione.id, { soggetto: await organizzazioni.perId(delega.titolareId), permessi: delega.permessi, delega: { id: delega.id, ... } }); await registro.scrivi({ attore: req.sessione.attore.id, soggetto: delega.titolareId, azione: 'contesto:assumi', esito: 'ok' }); res.json({ ok: true });});Passare l'organizzazione come parametro di ogni richiesta sposta la responsabilità del controllo su ogni singolo endpoint. Assumerla una volta, con un'azione esplicita che verifica la delega e la registra, la concentra in un punto solo — che è anche l'unico posto in cui ha senso registrare l'evento «Anna ha cominciato a operare su Rossi Srl».
Il pezzo che manca sempre#
// L'ultimo pezzo, e quello che si dimentica sempre: chi sta operando// deve VEDERE per conto di chi lo sta facendo. Un'interfaccia identica// a quella normale produce errori onesti — un'azione fatta sul cliente// sbagliato, con la piena convinzione di essere nel proprio account.//// Una barra fissa, non un badge nell'angolo://// ┌──────────────────────────────────────────────────────────────┐// │ ⚠ Stai operando come ROSSI SRL (delega fino al 31/12) │// │ Anna Bianchi · Studio Verdi [ Torna al mio account ]│// └──────────────────────────────────────────────────────────────┘//// E una email al titolare quando qualcuno assume il suo contesto per la// prima volta in una sessione: è l'unico canale attraverso cui si accorge// di una delega che non ricorda di aver concesso.È la parte meno tecnica e quella che previene più danni reali. Quando l'interfaccia in modalità delegata è identica a quella normale, gli errori che ne derivano non sono attacchi: sono persone competenti che emettono una fattura sul cliente sbagliato perché avevano dimenticato di aver cambiato contesto due ore prima.
Riepilogo#
| Credenziale condivisa | Mappatura | Doppia identità | |
|---|---|---|---|
| Chi ha fatto l'azione | no | sì | sì |
| Su quali dati | sì | sì | sì |
| Permessi granulari | no | no | sì |
| Delega a termine | no | dipende | sì |
| Revoca del singolo delegato | no | sì | sì, immediata |
| Robustezza ai percorsi dimenticati | no | no | sì |
| Costo di implementazione | ore | giorni | una settimana |
| Quando ha senso | prototipo usa e getta | sistema interno, pochi utenti | qualunque cosa con clienti veri |
La riga da leggere per prima è l'ultima. Il Pattern 1 non è «la versione veloce del Pattern 3»: è una scelta che diventa costosa da correggere, perché quando ci si accorge del problema i dati storici sono già stati scritti senza la distinzione — e quella informazione non si recupera.