Vai al contenuto
DedicatedPHP Contatto

Isolamento dei dati multi-tenant in PHP senza fughe

Progetta un SaaS PHP che impedisca accessi incrociati tra organizzazioni con contesto esplicito, controlli su dati, code, cache e test.

Diagramma editoriale di un SaaS PHP con organizzazioni isolate in database, cache, file e code

L'isolamento dei dati multi-tenant in PHP non si risolve aggiungendo una condizione WHERE organization_id = ? nella schermata principale. Una fuga può originarsi in un'API, un'esportazione, una cache, un allegato, un consumer di coda o un processo pianificato. Può verificarsi anche quando un amministratore legittimo cambia organizzazione e il sistema conserva un contesto precedente.

L'obiettivo architetturale deve essere chiaro: nessuna operazione che legga, modifichi, elabori o consegni informazioni del cliente deve poter agire senza un ambito organizzativo verificabile. Tale ambito deve essere propagato in modo esplicito e convalidato a ogni confine rilevante dell'applicazione.

Cosa deve isolare un'applicazione SaaS

Cosa deve isolare un'applicazione SaaS — guía visual de DedicatedPHP

Il modello transazionale è soltanto una parte della superficie di rischio. Censite le risorse che hanno un proprietario organizzativo e definite per ciascuna come viene identificata, archiviata, recuperata, eliminata e sottoposta ad audit la sua appartenenza.

  • Dati transazionali: utenti, progetti, ordini, fatture, configurazioni e relazioni tra entità.
  • File e allegati: oggetti nello storage esterno, miniature, documenti generati e relativi metadati.
  • Cache: risultati delle query, autorizzazioni calcolate, sessioni, risposte API e dati di configurazione.
  • Indici di ricerca: documenti indicizzati, suggerimenti e filtri aggregati in precedenza.
  • Elaborazione asincrona: job in coda, retry, batch di importazione e notifiche.
  • Operatività e osservabilità: log, trace, metriche, esportazioni di supporto e strumenti interni.

Non tutte le risorse richiedono la stessa strategia. Un catalogo pubblico può essere condiviso, mentre una fattura, il suo PDF e i log di download devono mantenere il collegamento inequivocabile con l'organizzazione. La decisione deve essere documentata per evitare che una nuova entità nasca senza regole di proprietà.

Scegliere il modello di isolamento dei dati

Esistono tre modelli comuni. Nessuno è universalmente superiore: la scelta dipende da requisiti normativi, volume, operatività, modello commerciale e capacità del team di mantenere la piattaforma.

Database condiviso con chiave di organizzazione

Tutte le organizzazioni condividono le tabelle e ogni record soggetto a isolamento contiene una chiave come organization_id. È l'approccio più diretto per evolvere il prodotto ed eseguire query aggregate globali. In cambio, richiede disciplina estrema: ogni query, relazione, indice, cache e attività deve rispettare l'ambito.

Come minimo, usate chiavi esterne quando opportuno, indici composti che inizino con organization_id e vincoli univoci anch'essi composti. Per esempio, un codice d'ordine univoco all'interno di un'organizzazione non deve essere dichiarato univoco globalmente se questa non è la regola di business.

Schema separato per organizzazione

Ogni cliente opera in uno schema logico distinto all'interno dello stesso server di database. Ciò riduce il rischio di omettere un filtro in tabelle separate, ma complica migrazioni, connessioni, strumenti di analisi e query globali. È appropriato soltanto se il motore, il framework e l'operatività quotidiana supportano in modo coerente questo pattern.

Database per organizzazione

Separare i database offre un confine più forte e può facilitare ripristini o spostamenti di singoli clienti. Aumenta anche l'inventario di connessioni, migrazioni, backup, monitoraggio e deploy delle modifiche alla struttura. È opportuno valutare in particolare come verranno eseguiti report globali, modifiche massive e ripristino in caso di errori.

La separazione fisica riduce alcune classi di errore, ma non sostituisce l'autorizzazione, il controllo dei file, la gestione dei segreti né la convalida del contesto nei servizi condivisi.

Architettura di riferimento: contesto esplicito ai confini

Il contesto organizzativo non deve essere dedotto da parametri arbitrari inviati dal browser. Deve essere risolto a partire da una fonte autenticata e autorizzata: un sottodominio convalidato, un token con audience appropriata, un'appartenenza dell'utente o una credenziale di integrazione associata a una sola organizzazione.

In un'applicazione PHP, un layer di ingresso può costruire un oggetto di contesto immutabile con l'identificatore dell'organizzazione, l'attore, le sue autorizzazioni e un identificatore di richiesta. I controller, i comandi della console e i consumer di coda ricevono tale contesto o lo ricostruiscono mediante dati verificati. Evitate variabili globali mutabili che potrebbero persistere impropriamente nei processi a lunga durata.

final class OrganizationContext {
    public function __construct(
        public readonly string $organizationId,
        public readonly string $actorId
    ) {}
}

I repository devono richiedere il contesto per interrogare o modificare entità isolate. È preferibile un'interfaccia che renda difficile ometterlo a una convenzione implicita che dipenda dalla memoria di ogni sviluppatore. Quando possibile, applicate inoltre policy di accesso nel layer di dominio: appartenere a un'organizzazione non autorizza automaticamente qualsiasi azione al suo interno.

Evitare filtri dimenticati in query e relazioni

Una query isolata deve filtrare per organizzazione prima di cercare in base agli identificatori di business. Recuperare prima un record tramite id e verificarne successivamente il proprietario può produrre esposizioni se il risultato viene serializzato, registrato o utilizzato prima di rifiutarlo.

  • Centralizzate le query in repository o servizi di lettura con metodi che ricevano il contesto.
  • Vietate gli accessi diretti a modelli isolati da controller, template e consumer di eventi.
  • Rivedete le relazioni: una relazione caricata in modo lazy può eludere il filtro applicato all'entità principale.
  • Usate vincoli di database per impedire relazioni tra righe di organizzazioni diverse quando il modello lo consente.
  • Definite convenzioni per migrazioni, seed di test e query analitiche.

Nei motori che offrono policy di sicurezza a livello di riga, queste possono fornire una difesa aggiuntiva. Tuttavia, la loro adozione deve includere test di connessione, gestione dei ruoli e revisione dei processi amministrativi. Non è opportuno presumere che una policy di database protegga automaticamente file, cache o indici esterni.

Rischi al di fuori del flusso web principale

Gli identificatori opachi riducono l'enumerazione, ma non autorizzano l'accesso. Un UUID o un identificatore casuale deve comunque essere risolto all'interno dell'organizzazione attiva. Analogamente, un URL di download firmato richiede un oggetto appartenente all'ambito corretto, una scadenza adeguata e regole di revoca quando cambiano le autorizzazioni.

Le chiavi di cache devono includere l'identificatore dell'organizzazione e, quando il contenuto dipende dalle autorizzazioni, una dimensione aggiuntiva di ruolo o versione dell'autorizzazione. Una chiave come dashboard:summary non è sicura in un ambiente multi-tenant; una chiave con ambito esplicito consente inoltre invalidazioni più precise.

Le esportazioni sono particolarmente sensibili perché solitamente vengono eseguite al di fuori della richiesta originale. Memorizzate chi l'ha richiesta, per quale organizzazione, quali filtri sono stati approvati e dove verrà consegnato il risultato. Non inviate allegati o link a destinatari calcolati da dati non convalidati.

Propagare il contesto in API, webhook e code

Un'API deve derivare l'organizzazione dalla credenziale o verificare che la risorsa richiesta appartenga all'organizzazione associata a tale credenziale. Consentire un'intestazione X-Organization-Id può essere valido per operatori con delega esplicita, ma richiede autorizzazione specifica, audit e un'interfaccia che renda visibile il cambio di ambito.

I webhook in entrata non devono fidarsi di un identificatore di organizzazione incluso nel body senza verificare firma, mittente e associazione precedente dell'integrazione. Per i webhook in uscita, generate eventi da dati già delimitati ed evitate di riutilizzare payload da una coda condivisa senza convalidare il destinatario.

Ogni job asincrono deve trasportare un identificatore dell'organizzazione insieme all'identificatore della risorsa e ricostruire il contesto prima di interrogare. Il consumer deve verificare entrambi i valori, anche se il job è stato creato internamente. I retry, i job differiti e le attività pianificate richiedono la stessa regola: non esiste un contesto di richiesta implicito disponibile in modo sicuro.

Test e segnali diagnostici verificabili

Il test più importante non è che un'organizzazione veda i propri dati, ma che non possa leggere né modificare quelli di un'altra. Create due organizzazioni con dati deliberatamente simili ed eseguite test di integrazione su ogni punto di ingresso: interfaccia web, API, comandi, esportazioni, download e consumer di coda.

  • Richiedete una risorsa dell'organizzazione B usando una sessione o credenziale dell'organizzazione A e attendete una risposta che non riveli informazioni.
  • Tentate di aggiornare, eliminare, scaricare ed esportare risorse incrociate, non solo di interrogarle.
  • Verificate che le chiavi di cache di A e B generino risultati indipendenti.
  • Eseguite un job di coda con una risorsa di un'altra organizzazione e verificate che fallisca in modo controllato.
  • Testate ripristini, importazioni e attività notturne con dati di più di un'organizzazione.
  • Registrate le azioni sensibili con attore, organizzazione, risorsa e risultato, senza introdurre dati personali non necessari nei log.

I test basati sulle proprietà possono integrare i casi manuali: per qualsiasi risorsa creata sotto un'organizzazione, nessun attore privo di appartenenza valida dovrebbe poterla osservare o alterare tramite un percorso esposto. Questa proprietà deve applicarsi a modifiche future di endpoint e repository.

Piano di adozione per un'applicazione esistente

Se i dati sono già mescolati, non iniziate riscrivendo l'intera applicazione. Prima censite entità, flussi, integrazioni e accessi amministrativi. Poi definite la proprietà di ogni record e risolvete i casi ambigui con regole di business riesaminabili.

  1. Aggiungete l'entità organizzazione e la chiave di appartenenza alle tabelle di destinazione.
  2. Popolate tale chiave mediante una migrazione controllata e conservate evidenza dei casi senza assegnazione affidabile.
  3. Introducete repository delimitati e test di accesso incrociato nei percorsi più sensibili.
  4. Includete l'ambito in cache, file, ricerche e nuovi job.
  5. Effettuate progressivamente la migrazione dei flussi legacy e bloccate nuove query senza contesto durante la code review.
  6. Attivate controlli più rigorosi quando metriche e test dimostrano una copertura sufficiente.

Decisioni da documentare prima di crescere

Decisioni da documentare prima di crescere — guía visual de DedicatedPHP

Prima di integrare la prossima organizzazione, documentate il modello scelto, la fonte di verità del contesto, le eccezioni di accesso amministrativo, la strategia degli identificatori, i confini della cache, la proprietà dei file, il recupero dei dati, la conservazione dei log e la procedura in caso di sospetto accesso incrociato.

Stabilite inoltre chi può agire per conto di un'altra organizzazione, come tale delega viene approvata e come viene revocata. L'isolamento dei dati multi-tenant in PHP viene mantenuto con decisioni esplicite, vincoli tecnici ripetibili e test che trasformano una promessa architetturale in un comportamento verificabile.

Vuoi applicare queste idee al tuo progetto?Parliamo della tua piattaforma PHP.
Visualizza il servizio correlato