Multi-tenancy rigorosa
L'isolamento tra organizzazioni è l'invariante fondante di betool. Ecco come viene garantito, strato per strato.
A livello di database
Le tabelle di business contengono una colonna org_id che collega ogni riga alla propria organizzazione (il più delle volte vincolata da una foreign key a organisation(id)).
Tutte le query lato server passano attraverso metodi store che filtrano sistematicamente in base all'org_id della sessione corrente. Nessun percorso di codice espone una query arbitraria; nessun client può forzare un org_id diverso.
Per le rare risorse condivisibili (tipicamente gli account LLM condivisi a livello di piattaforma), una colonna is_shared BOOLEAN NOT NULL DEFAULT FALSE abilita un opt-in esplicito. Regole:
- Qualsiasi lettura è autorizzata con
WHERE org_id = %s OR is_shared = TRUE. - Qualsiasi mutazione di una riga
is_shared = TRUErichiede il ruolo hyperadmin (operatore di piattaforma). - Qualsiasi mutazione invalida la cache globalmente — nessuna divergenza per singola org.
A livello di storage dei file
I file sono archiviati in uno storage a oggetti (disco locale o bucket cloud a seconda del deployment), in uno spazio proprio di ciascuna organizzazione:
orgs/
└── <org_id>/
├── files/<canale>/<exchange_id>/<file> ← file ricevuti dai canali
└── fixtures/ ← fixture di benchmark
Le chiavi sono costruite lato server (nome di file sanificato, anti path-traversal). Qualsiasi lettura di un file per conto di un utente, di un tool o di un agent passa per una validazione che rifiuta una chiave al di fuori dello spazio orgs/<org_id>/files/ dell'organizzazione corrente.
A livello di cache
Le cache applicative (ad esempio la risoluzione dei provider LLM) sono indicizzate per organizzazione. Quando una risorsa is_shared = TRUE viene mutata, la cache è invalidata globalmente — non esiste una finestra durante la quale un'org potrebbe vedere un valore obsoleto.
A livello di segreti
Le credenziali (chiavi LLM, token di autenticazione degli operatori, segreti dei webhook) sono archiviate lato server, associate a ciascuna org. Non vengono mai restituite in chiaro dall'API: le route GET indicano soltanto la loro presenza (ad esempio has_api_key: bool).
A livello di esecuzione
Quando una pipeline viene eseguita:
- Gli operatori hanno accesso soltanto agli account esterni dichiarati nell'org.
- I modelli LLM sono risolti nell'ordine
own > shared (fallback automatico)— sempre. - I file allegati sono leggibili soltanto nello spazio di storage dell'org corrente.
Se un operatore tenta di accedere a una risorsa fuori perimetro, l'accesso viene rifiutato con un errore esplicito — non un timeout silenzioso.
Test pratico
A ogni aggiunta di una nuova risorsa condivisibile, il team risponde alle 4 domande:
- Se compare una seconda org di backoffice, il mio codice si comporta correttamente?
- Se muto una riga condivisa, tutte le cache per singola org vengono invalidate?
- I miei segreti restano strettamente lato server?
- La mia GET consente ai picker di altre org di elencare la risorsa senza esporre il segreto?
Se una qualsiasi risposta è no, la risorsa non viene rilasciata.