• saas
  • multi-tenancy
  • postgres
  • row-level security
  • rls
  • trigger function
  • multi-tenant architecture
  • single-tenant architecture

Implementazione del multi-tenancy con PostgreSQL: Impara attraverso un semplice esempio reale

Scopri come implementare un'architettura multi-tenant con la sicurezza a livello di riga (RLS) di PostgreSQL e ruoli di database attraverso un esempio reale per un isolamento sicuro dei dati tra i tenant.

Yijun
Yijun
Developer

In alcuni dei nostri articoli precedenti, ci siamo addentrati nel concetto di multi-tenancy e nelle sue applicazioni in prodotti e scenari di business reali.

In questo articolo, esploreremo come implementare un'architettura multi-tenant per la tua applicazione utilizzando PostgreSQL da un punto di vista tecnico.

Cos'è l'architettura single-tenant?

L'architettura single-tenant si riferisce a un'architettura software in cui ogni cliente ha la propria istanza dedicata dell'applicazione e del database.

In questa architettura, i dati e le risorse di ogni tenant sono completamente isolati da altri tenant.

Single-tenancy

Cos'è l'architettura multi-tenant?

L'architettura multi-tenant è un'architettura software in cui più clienti (tenant) condividono la stessa istanza dell'applicazione e infrastruttura mantenendo l'isolamento dei dati. In questa architettura, un'unica istanza del software serve più tenant, con i dati di ciascun tenant mantenuti separati dagli altri attraverso vari meccanismi di isolamento.

Multi-tenancy

Architettura single-tenant vs architettura multi-tenant

L'architettura single-tenant e l'architettura multi-tenant differiscono in aspetti come l'isolamento dei dati, l'utilizzo delle risorse, la scalabilità, la gestione e la manutenzione, e la sicurezza.

Nell'architettura single-tenant, ogni cliente ha uno spazio dati indipendente, portando a un utilizzo delle risorse inferiore ma relativamente più semplice per la personalizzazione. Tipicamente, i software single-tenant sono adattati a esigenze specifiche dei clienti, come sistemi di inventario per un determinato fornitore di tessuti o un'app web per blog personali. La comunanza tra loro è che ogni cliente occupa un'istanza separata del servizio applicativo, facilitando la personalizzazione per soddisfare esigenze specifiche.

In un'architettura multi-tenant, più tenant condividono le stesse risorse sottostanti, risultando in un utilizzo delle risorse più elevato. Tuttavia, è cruciale assicurare l'isolamento dei dati e la sicurezza.

L'architettura multi-tenant è spesso l'architettura software preferita quando i fornitori di servizi offrono servizi standardizzati a diversi clienti. Questi servizi tipicamente hanno livelli bassi di personalizzazione e tutti i clienti condividono la stessa istanza dell'applicazione. Quando un'applicazione richiede un aggiornamento, aggiornare un'istanza dell'applicazione equivale ad aggiornare l'applicazione per tutti i clienti. Ad esempio, il CRM (Customer Relationship Management) è un requisito standardizzato. Questi sistemi tipicamente utilizzano un'architettura multi-tenant per fornire lo stesso servizio a tutti i tenant.

Strategie di isolamento dei dati dei tenant nell'architettura multi-tenant

In un'architettura multi-tenant, tutti i tenant condividono le stesse risorse sottostanti, rendendo cruciale l'isolamento delle risorse tra i tenant. Questo isolamento non deve necessariamente essere fisico; si richiede semplicemente di assicurare che le risorse tra i tenant non siano visibili l'una all'altra.

Nel design dell'architettura, si possono ottenere vari gradi di isolamento delle risorse tra i tenant:

Isolato a condiviso

In generale, più risorse sono condivise tra i tenant, minore è il costo di iterazione e manutenzione del sistema. Al contrario, meno risorse condivise, maggiore è il costo.

Inizio dell'implementazione multi-tenant con un esempio reale

In questo articolo, utilizzeremo un sistema CRM come esempio per introdurre un'architettura multi-tenant semplice ma pratica.

Riconosciamo che tutti i tenant utilizzano gli stessi servizi standard, quindi abbiamo deciso di far condividere a tutti i tenant le stesse risorse di base e implementeremo l'isolamento dei dati tra diversi tenant a livello di database utilizzando la Row-Level Security di PostgreSQL.

Inoltre, creeremo una connessione dati separata per ogni tenant per facilitare una migliore gestione delle autorizzazioni dei tenant.

Successivamente, introdurremo come implementare questa architettura multi-tenant.

Come implementare l'architettura multi-tenant con PostgreSQL

Aggiungi identificatore di tenant per tutte le risorse

In un sistema CRM, avremo molte risorse e saranno memorizzate in diverse tabelle. Ad esempio, le informazioni sui clienti sono memorizzate nella tabella customers.

Prima di implementare il multi-tenancy, queste risorse non sono associate a nessun tenant:

Per differenziare i tenant che possiedono diverse risorse, introduciamo una tabella tenants per memorizzare le informazioni sul tenant (dove db_user e db_user_password sono utilizzati per memorizzare le informazioni sulla connessione al database per ogni tenant, saranno dettagliate sotto). Inoltre, aggiungiamo un campo tenant_id a ogni risorsa per identificare a quale tenant appartiene:

Ora, ogni risorsa è associata a un tenant_id, teoricamente permettendoci di aggiungere una clausola where a tutte le query per limitare l'accesso alle risorse per ciascun tenant:

A prima vista, questo sembra semplice e fattibile. Tuttavia, avrà i seguenti problemi:

  • Quasi ogni query includerà questa clausola where, ingombrando il codice e rendendolo più difficile da mantenere, specialmente quando si scrivono istruzioni join complesse.
  • I nuovi membri del team di sviluppo potrebbero facilmente dimenticare di aggiungere questa clausola where.
  • I dati tra diversi tenant non sono veramente isolati, poiché ogni tenant ha ancora i permessi per accedere ai dati appartenenti ad altri tenant.

Pertanto, non adotteremo questo approccio. Invece, utilizzeremo la Row Level Security di PostgreSQL per affrontare queste preoccupazioni. Tuttavia, prima di procedere, creeremo un account di database dedicato per ogni tenant per accedere a questo database condiviso.

Configura i ruoli DB per i tenant

È una buona pratica assegnare un ruolo di database a ciascun utente che può connettersi al database. Questo consente un migliore controllo sull'accesso al database di ciascun utente, facilitando l'isolamento delle operazioni tra diversi utenti e migliorando la stabilità e la sicurezza del sistema.

Poiché tutti i tenant hanno le stesse autorizzazioni per le operazioni di database, possiamo creare un ruolo di base per gestire queste autorizzazioni:

Quindi, per differenziare ogni ruolo di tenant, un ruolo ereditato dal ruolo base viene assegnato a ciascun tenant al momento della creazione:

Successivamente, le informazioni sulla connessione al database per ogni tenant saranno memorizzate nella tabella tenants:

iddb_userdb_user_password
x2euiccrm_tenant_x2euicpa55w0rd

Questo meccanismo fornisce a ciascun tenant il proprio ruolo di database e questi ruoli condividono le autorizzazioni concesse al ruolo crm_tenant.

Possiamo quindi definire l'ambito di autorizzazione per i tenant utilizzando il ruolo crm_tenant:

  • I tenant dovrebbero avere accesso CRUD a tutte le tabelle delle risorse del sistema CRM.
  • Le tabelle non correlate alle risorse del sistema CRM dovrebbero essere invisibili ai tenant (assumendo solo la tabella systems).
  • I tenant non dovrebbero essere in grado di modificare la tabella tenants, e solo i campi id e db_user dovrebbero essere visibili per interrogarli quando eseguono operazioni di database.

Una volta configurati i ruoli per i tenant, quando un tenant richiede l'accesso al servizio, possiamo interagire con il database utilizzando il ruolo di database che rappresenta quel tenant:

Proteggi i dati dei tenant utilizzando la Row-Level Security di PostgreSQL

Finora, abbiamo stabilito ruoli di database corrispondenti per i tenant, ma questo non limita l'accesso ai dati tra i tenant. Successivamente, sfrutteremo la caratteristica Row-Level Security di PostgreSQL per limitare l'accesso di ciascun tenant ai propri dati.

In PostgreSQL, le tabelle possono avere policy di sicurezza a livello di riga che controllano quali righe possono essere accedute dalle query o modificate dai comandi di manipolazione dei dati. Questa caratteristica è conosciuta anche come RLS (Row-Level Security).

Di default, le tabelle non hanno policy di sicurezza a livello di riga. Per utilizzare l'RLS, devi abilitarlo per la tabella e creare policy di sicurezza che vengono eseguite ogni volta che la tabella viene acceduta.

Prendendo come esempio la tabella customers nel sistema CRM, abiliteremo l'RLS e creeremo una policy di sicurezza per limitare ogni tenant all'accesso ai soli dati dei propri clienti:

Nella dichiarazione della creazione della policy di sicurezza:

  • for all (opzionale) indica che questa policy di accesso sarà usata per le operazioni select, insert, update, e delete sulla tabella. Puoi specificare una policy di accesso per operazioni specifiche utilizzando for seguito dalla parola chiave del comando.
  • to crm_tenant indica che questa policy si applica agli utenti con il ruolo di database crm_tenant, cioè a tutti i tenant.
  • as restrictive specifica la modalità di applicazione della policy, indicando che l'accesso dovrebbe essere strettamente limitato. Di default, una tabella può avere più policy, più policy permissive saranno combinate con una relazione OR. In questo scenario, dichiariamo questa policy come restrictive perché vogliamo che questo controllo della policy sia obbligatorio per gli utenti appartenenti ai tenant del sistema CRM.
  • L'espressione using definisce le condizioni per l'accesso reale, limitando l'utente del database attualmente in query a vedere solo i dati appartenenti al proprio tenant. Questo vincolo si applica alle righe selezionate da un comando (select, update, o delete).
  • L'espressione with check definisce il vincolo necessario quando si modificano le righe di dati (insert o update), garantendo che i tenant possano solo aggiungere o aggiornare record per se stessi.

Utilizzare l'RLS per vincolare l'accesso dei tenant alle nostre tabelle delle risorse offre diversi benefici:

  • Questa policy effettivamente aggiunge where tenant_id = (select id from tenants where db_user = current_user) a tutte le operazioni di query (select, update, o delete). Ad esempio, quando esegui select * from customers, è equivalente a eseguire select * from customers where tenant_id = (select id from tenants where db_user = current_user). Questo elimina la necessità di aggiungere esplicitamente condizioni where nel codice dell'applicazione, semplificandolo e riducendo la probabilità di errori.
  • Controlla centralmente le autorizzazioni di accesso ai dati tra diversi tenant a livello di database, mitigando il rischio di vulnerabilità o incoerenze nell'applicazione, migliorando così la sicurezza del sistema.

Tuttavia, ci sono alcuni punti da notare:

  • Le policy di RLS vengono eseguite per ogni riga di dati. Se le condizioni di query all'interno della policy di RLS sono troppo complesse, potrebbero influire significativamente sulle prestazioni del sistema. Fortunatamente, la nostra query di verifica dei dati dei tenant è abbastanza semplice e non influirà sulle prestazioni. Se prevedi di implementare altre funzionalità usando l'RLS in seguito, puoi seguire le raccomandazioni sulle prestazioni della sicurezza a livello di riga di Supabase per ottimizzare le prestazioni dell'RLS.
  • Le policy di RLS non popolano automaticamente tenant_id durante le operazioni di insert. Restriggono solo i tenant all'inserimento dei propri dati. Ciò significa che quando si inseriscono dati, dobbiamo ancora fornire l'ID del tenant, il che è incoerente con il processo di interrogazione e può portare a confusione durante lo sviluppo, aumentando la probabilità di errori (questo sarà affrontato nei passaggi successivi).

Oltre alla tabella customers, dobbiamo applicare le stesse operazioni a tutte le tabelle delle risorse del sistema CRM (questo processo può essere un po' noioso, ma possiamo scrivere un programma per configurarlo durante l'inizializzazione delle tabelle), isolando così i dati da diversi tenant.

Crea funzione trigger per l'inserimento dei dati

Come menzionato in precedenza, l'RLS (Row-Level Security) ci consente di eseguire query senza preoccuparci dell'esistenza di tenant_id, poiché il database lo gestisce automaticamente. Tuttavia, per le operazioni di insert, dobbiamo ancora specificare manualmente il tenant_id corrispondente.

Per ottenere una convenienza simile a quella dell'RLS per l'inserimento dei dati, dobbiamo fare in modo che il database gestisca automaticamente il tenant_id durante l'inserimento dei dati.

Questo ha un chiaro vantaggio: a livello di sviluppo dell'applicazione, non dobbiamo più considerare a quale tenant appartengano i dati, riducendo la probabilità di errori e alleggerendo il nostro carico mentale quando sviluppiamo applicazioni multi-tenant.

Fortunatamente, PostgreSQL fornisce una potente funzionalità di trigger.

I trigger sono funzioni speciali associate alle tabelle che eseguono automaticamente azioni specifiche (come inserire, aggiornare o eliminare) quando vengono eseguite sulla tabella. Queste azioni possono essere attivate a livello di riga (per ogni riga) o a livello di dichiarazione (per l'intera dichiarazione). Con i trigger, possiamo eseguire logiche personalizzate prima o dopo operazioni specifiche del database, permettendoci di raggiungere facilmente il nostro obiettivo.

Prima di tutto, creiamo una funzione trigger set_tenant_id da eseguire prima di ogni inserimento di dati:

Successivamente, associamo questa funzione trigger alla tabella customers per le operazioni di insert (simile all'abilitazione dell'RLS per una tabella, questa funzione trigger deve essere associata a tutte le tabelle pertinenti):

Questo trigger garantisce che i dati inseriti contengano il tenant_id corretto. Se i nuovi dati includono già un tenant_id, la funzione trigger non fa nulla. Altrimenti, popola automaticamente il campo tenant_id in base alle informazioni dell'utente corrente nella tabella tenants.

In questo modo, otteniamo la gestione automatica del tenant_id a livello di database durante l'inserimento di dati da parte dei tenant.

Sommario

In questo articolo, ci siamo addentrati nell'applicazione pratica dell'architettura multi-tenant, usando un sistema CRM come esempio per dimostrare una soluzione pratica utilizzando il database PostgreSQL.

Abbiamo discusso la gestione dei ruoli di database, il controllo degli accessi e la funzione Row-Level Security di PostgreSQL per garantire l'isolamento dei dati tra i tenant. Inoltre, abbiamo utilizzato funzioni trigger per ridurre il carico cognitivo degli sviluppatori nella gestione dei diversi tenant.

Questo è tutto per questo articolo. Se desideri migliorare ulteriormente la tua applicazione multi-tenant con la gestione degli accessi degli utenti, puoi fare riferimento a Una guida facile per iniziare con le organizzazioni di Logto - per costruire un'app multi-tenant per ulteriori approfondimenti.