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 problema
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.jsJavaScript
// 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.jsJavaScript
// 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.

sessione.jsJavaScript
// 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.

accesso.jsJavaScript
// 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à#

registro.sqlSQL
-- 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_id

Le 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#

revoca.jsJavaScript
// 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#

contesto.jsJavaScript
// 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#

interfaccia.jsJavaScript
// 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.

Credenziale condivisaMappaturaDoppia identità
Chi ha fatto l'azioneno
Su quali dati
Permessi granularinono
Delega a terminenodipende
Revoca del singolo delegatonosì, immediata
Robustezza ai percorsi dimenticatinono
Costo di implementazioneoregiorniuna settimana
Quando ha sensoprototipo usa e gettasistema interno, pochi utentiqualunque 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.