La sicurezza a livello di riga è un meccanismo del database in cui delle policy filtrano, a ogni query, le righe che ciascuno può vedere o modificare. Il modello mentale è quello di una clausola WHERE invisibile e obbligatoria: la condizione che altrimenti andrebbe ricordata in ogni query diventa una proprietà della tabella stessa – applicata dal motore, su ogni percorso di accesso, compresi quelli che il codice applicativo non vede mai.
Punti chiave
| Domanda | Risposta |
|---|---|
| Che cos’è | Controllo degli accessi riga per riga, applicato dal motore del database stesso |
| Il modello mentale | Una clausola WHERE impossibile da dimenticare |
| Il caso d’uso principale | L’isolamento multi-tenant e i dati per utente, nel livello che una violazione non può saltare |
| Le clausole in piccolo | Ruoli di aggiramento, rifiuto predefinito, contesto di pooling – le insidie sono operative |
| Il guasto che previene | Un filtro dimenticato nel codice applicativo = violazione fra tenant |
RLS in dieci righe di SQL
CREATE TABLE invoices (
tenant_id uuid NOT NULL,
total numeric(10,2)
);
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; -- da qui: rifiuto predefinito
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant')::uuid) -- ciò che si può leggere
WITH CHECK (tenant_id = current_setting('app.tenant')::uuid); -- ciò che si può scrivere
-- Ogni query ora si comporta come se finisse con WHERE tenant_id = <il proprio>.
-- Una query che dimentica il filtro non restituisce nulla – non tutto.
La stessa garanzia esiste nei database documentali come controllo degli accessi portato dalla riga stessa – ogni oggetto porta la propria ACL, e la piattaforma la fa rispettare a ogni richiesta:
// JavaScript / Node.js — Back4app JS SDK
// Row-level security as data: this row is readable by its owner, period
const note = new Parse.Object('Note');
note.set('text', 'Q3 salary planning');
note.setACL(new Parse.ACL(Parse.User.current())); // owner-only, on the row itself
await note.save();
// Later, any query by any user — the row exists only for its owner
const notes = await new Parse.Query('Note').find(); // others never see it // Flutter / Dart — Back4app Flutter SDK
// Row-level security as data: this row is readable by its owner, period
final user = await ParseUser.currentUser() as ParseUser;
final acl = ParseACL(owner: user); // owner-only, on the row itself
final note = ParseObject('Note')
..set('text', 'Q3 salary planning')
..setACL(acl);
await note.save(); // other users' queries never return this row // iOS / Swift — Back4app Swift SDK
// Row-level security as data: this row is readable by its owner, period
var note = Note()
note.text = "Q3 salary planning"
note.ACL = try ParseACL.defaultACL() // owner-only, on the row itself
note.save { result in
if case .success = result {
print("saved — other users' queries never return this row")
}
} // Android / Kotlin — Back4app Android SDK
// Row-level security as data: this row is readable by its owner, period
val note = ParseObject("Note").apply {
put("text", "Q3 salary planning")
acl = ParseACL(ParseUser.getCurrentUser()) // owner-only, on the row itself
}
note.saveInBackground { e ->
if (e == null) Log.d("Notes", "saved — invisible to every other user")
} Che cosa fa il motore con una query
USING vs. WITH CHECK, policy permissive vs. restrittive
| Concetto | Che cosa regola | Regola pratica |
|---|---|---|
USING | Ciò che esiste: le letture e le righe eleggibili per update/delete | ”Lo posso vedere?” |
WITH CHECK | Ciò che si può scrivere: gli insert e i valori dopo un update | ”Lo posso creare così?” |
| Policy permissive (predefinito) | Combinate in OR – basta una corrispondenza per concedere | Concedere l’accesso |
| Policy restrittive | Aggiunte in AND – devono passare tutte | Imporre limiti obbligatori |
Due regole di composizione da ricordare dalla documentazione di PostgreSQL: omettere WITH CHECK riusa USING per le scritture, e almeno una policy permissiva deve passare prima che le restrittive vengano perfino consultate – solo policy restrittive significa che non entra nessuno.
Dove RLS è supportata
| Famiglia di motori | Meccanismo |
|---|---|
| PostgreSQL (9.5+) e derivati | CREATE POLICY – l’implementazione di riferimento |
| SQL Server (2016+) | Policy di sicurezza su funzioni di predicato inline (filter + block) |
| SQL distribuito (CockroachDB, YugabyteDB) | Policy compatibili con PostgreSQL |
| Data warehouse cloud | Policy di accesso per riga, con la variante di ogni fornitore |
| Database documentali | Non basate su policy – ACL per oggetto applicate dal livello di piattaforma |
L’ultima riga è quella che interessa a questo glossario: sugli archivi documentali il confine a livello di riga è tipicamente un dato portato dalla riga (le ACL) invece di un predicato nel motore – stessa garanzia, meccanismo diverso, spiegato in dettaglio nell’articolo di ingegneria di Back4app.
Le insidie, finalmente in un unico posto
- Le sorprese del rifiuto predefinito. Abilitare RLS senza nessuna policy blocca tutti tranne il proprietario – metà delle storie “RLS ha rotto la produzione” è questa.
- La matrice di aggiramento. Superutenti, ruoli
BYPASSRLSe proprietari delle tabelle ignorano le policy – conviene impostareFORCE ROW LEVEL SECURITYse il proprietario è anche l’utente dell’applicazione, e verificare chi detiene cosa. - Le viste si eseguono con i diritti del proprietario per impostazione predefinita – una vista su una tabella con RLS può aggirarlo silenziosamente, a meno che non sia creata con i diritti del chiamante (
security_invokerin PostgreSQL). - I vincoli tradiscono l’esistenza. Una violazione di unicità o di chiave esterna può rivelare che esiste una riga invisibile – il canale nascosto documentato; l’unicità sui valori protetti va trattata di conseguenza.
- Gli errori possono tradire i valori. Espressioni costruite per fallire su dati precisi (una divisione per zero quando un valore nascosto corrisponde) esfiltrano attraverso i messaggi di errore – la classe di canali laterali descritta dalla documentazione di SQL Server.
- Dump e pool hanno delle modalità. Gli strumenti di backup possono richiedere la disattivazione della sicurezza per riga per esportare tutto; i pooler in modalità transazione richiedono un contesto con portata di transazione (
SET LOCAL), altrimenti i tenant si mescolano da una richiesta all’altra.
Prestazioni: le policy sono codice sull’hot path
Tre regole coprono quasi tutta l’esperienza di campo: indicizzare ogni colonna a cui una policy fa riferimento (il predicato viene eseguito prima dei filtri propri della query – senza indice è una scansione completa); tenere i predicati senza join, spostando le ricerche in funzioni o in controlli di ruolo; e far valutare le funzioni per richiesta una volta per query, non una volta per riga, avvolgendole in una sottoquery scalare. Misurata bene, RLS costa qualche punto percentuale; misurata male, è il rallentamento misterioso su ogni tabella messa in sicurezza.
Casi d’uso tipici
- SaaS multi-tenant. Il caso canonico – l’isolamento dei tenant applicato sotto l’applicazione, dove un filtro dimenticato non può diventare una violazione.
- Dati per utente. Messaggi, documenti, cartelle cliniche: ciascuno vede le proprie righe, punto.
- Confini fra reparti e fra regioni. I commerciali vedono la propria regione; i revisori vedono tutto, in sola lettura – la stratificazione delle policy all’opera.
- Regimi di conformità. Controllo degli accessi dimostrabile al livello dei dati, che è dove piace ai revisori.
- Accesso analitico condiviso. Chi fa analisi interroga direttamente le repliche di produzione e vede solo ciò che il proprio ruolo consente – sicuro perché è il database a farlo rispettare, non la dashboard.
Conviene applicare il controllo a livello di riga? Matrice decisionale
| Applicarlo con RLS/ACL quando… | Il filtraggio applicativo può bastare quando… |
|---|---|
| Più tenant o più utenti condividono le tabelle | Il database ha esattamente un chiamante fidato |
| Qualcosa oltre all’app tocca il database | Nessuno strumento di BI, nessun SQL amministrativo, nessun secondo servizio |
| Un filtro dimenticato significa violazione, non bug | Il partizionamento è una comodità, non sicurezza |
| La conformità vuole una prova al livello dei dati | I dati non sono sensibili |
| Conviene testare il confine una volta sola, in modo centralizzato | Non pesa verificare ogni query per sempre |
La sintesi onesta delle due colonne: tenere il partizionamento nell’applicazione per leggibilità – e farlo rispettare comunque nel database. La difesa in profondità è tutto il senso dell’operazione.
Limiti e trade-off
- Le policy sono invisibili per progetto – il che rende il debug di “dove sono finite le mie righe?” una competenza vera e propria; conviene registrare per primo il contesto di sessione.
- L’autorizzazione complessa esce dai predicati. Le regole che hanno bisogno di stato di workflow o di logica fra entità appartengono ai livelli di autorizzazione applicativi, con RLS come rete di sicurezza.
- La superficie operativa è reale. La matrice di aggiramento, le modalità di dump e il contesto di pooling sono conoscenza operativa continua, non una configurazione una volta per tutte.
- La valutazione riga per riga è una tassa – modesta quando è progettata bene, illimitata quando non lo è.
- Mette in sicurezza le righe, non le colonne. Nascondere i campi è sicurezza a livello di colonna; combinando le due si ottiene l’effetto a livello di cella.
La sicurezza a livello di riga su Back4app
Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. Il suo modello a livello di riga è il meccanismo ACL-come-dato delle schede qui sopra: ogni oggetto porta la propria lista di accessi, i ruoli esprimono tenant e team, e la piattaforma fa rispettare entrambi a ogni richiesta – SDK, REST, GraphQL e dashboard compresi, con i permessi di classe come livello restrittivo al di sopra. È la garanzia della clausola WHERE obbligatoria, consegnata su un database documentale, con l’elenco di insidie qui sopra assorbito dalla piattaforma invece di essere affidato a chi sviluppa.
Domande frequenti
Che cos'è la sicurezza a livello di riga in parole semplici?
Una clausola WHERE invisibile e obbligatoria. Si attaccano delle policy a una tabella e il database applica automaticamente le loro condizioni a ogni query: ciascun utente vede e modifica solo le righe che la policy consente, qualunque query scriva e qualunque strumento usi. Il filtro vive nel database, quindi nessun percorso di codice applicativo può dimenticarlo.
Come funziona la sicurezza a livello di riga?
Si abilita RLS su una tabella e si creano policy con predicati booleani – di solito confrontano la colonna del proprietario o del tenant di una riga con l'utente corrente o con una variabile di sessione. In esecuzione il motore valuta il predicato riga per riga prima delle condizioni scritte da chi interroga, filtrando le letture e bloccando le scritture non consentite. Una volta abilitato RLS senza nessuna policy, il comportamento predefinito è negare tutto.
Qual è la differenza tra USING e WITH CHECK?
La direzione. USING filtra ciò che esiste: le righe visibili a SELECT e quelle eleggibili per UPDATE o DELETE. WITH CHECK valida ciò che viene scritto: le righe inserite e i nuovi valori dopo un aggiornamento. Omettendo WITH CHECK il predicato di USING vale per entrambi – ma le tabelle dove si legge in modo ampio e si scrive in modo ristretto hanno bisogno di entrambi, definiti in modo diverso.
Chi può aggirare la sicurezza a livello di riga?
In PostgreSQL: i superutenti, i ruoli con BYPASSRLS e – quello che tutti dimenticano – il proprietario della tabella, a meno che non si imposti FORCE ROW LEVEL SECURITY. Verificare quella matrice di aggiramento fa parte del deployment di RLS; una policy perfetta non protegge nulla se l'applicazione si connette come proprietario della tabella.
La sicurezza a livello di riga peggiora le prestazioni?
Può farlo: il predicato della policy viene valutato sulle righe candidate a ogni query. Le contromisure sono le stesse in ogni esperienza di produzione: indicizzare le colonne a cui le policy fanno riferimento, tenere i predicati senza join (meglio delle funzioni o dei controlli di ruolo) e avvolgere le funzioni per richiesta perché vengano valutate una volta per query e non una volta per riga. Una policy su una colonna non indicizzata è una scansione completa con un distintivo di sicurezza.
Qual è la differenza tra RLS e il filtraggio a livello applicativo?
Dove si trova il confine. Il filtraggio applicativo restringe le query nel codice: flessibile, ma duplicato su ogni percorso di codice e aggirato da qualsiasi cosa parli direttamente con il database. RLS lo fa rispettare nel motore, coprendo ogni percorso di accesso, strumenti di amministrazione e altri servizi compresi. Il consenso è la difesa in profondità: restringere nell'applicazione per chiarezza, far rispettare nel database per sicurezza.
Come si comporta RLS con il pooling delle connessioni?
Con cautela. Le applicazioni che usano un pool si connettono con un solo utente del database, quindi le policy si appoggiano a una variabile di sessione per richiesta invece che all'identità della connessione: impostata al prelievo, letta dalla policy, azzerata alla restituzione. Con i pooler in modalità transazione l'impostazione deve avere portata di transazione, altrimenti il contesto di un tenant può trapelare nella richiesta successiva sulla stessa connessione. Questa interazione è il bug RLS più comune in produzione.
Come si testano le policy di sicurezza a livello di riga?
Impersonando ogni ruolo ed eseguendo tutte e quattro le operazioni – select, insert, update, delete – con la verifica sia di ciò che appare sia di ciò che viene rifiutato. Vanno aggiunti i due casi che si dimenticano: il contesto non impostato (nessuna variabile di tenant deve significare nessuna riga, non tutte le righe) e la matrice di aggiramento (proprietario, superutente, percorsi di replica). RLS si guadagna la fiducia con test avversariali, non perché la policy si legge bene.