Perché servono controlli a livello di riga e di colonna
In uno scenario di data engineering capita spesso che una singola tabella — pensa a una fact table di vendite o a un’anagrafica clienti — debba essere consultata da utenti con visibilità diversa. Un venditore deve vedere solo i propri ordini, un analista di supporto deve poter interrogare i dati senza però leggere i numeri di carta di credito in chiaro. Duplicare le tabelle o creare una vista per ogni ruolo non scala. Row-level security (RLS) e dynamic data masking (DDM) risolvono questi due problemi a livello di motore, sia su Azure SQL Database sia su dedicated SQL pool in Azure Synapse Analytics.
Row-level security
La RLS filtra le righe restituite da una query in funzione dell’identità o del contesto di chi esegue, senza modificare l’applicazione. Il cuore è una security policy che collega la tabella a una inline table-valued function (TVF) — il predicato.
Filter e block predicate
- Il filter predicate agisce silenziosamente sulle operazioni di lettura (
SELECT,UPDATE,DELETE): le righe non conformi semplicemente non compaiono, senza errore. È il meccanismo più comune. - Il block predicate blocca esplicitamente le operazioni di scrittura che violerebbero la regola (
AFTER INSERT,AFTER UPDATE,BEFORE UPDATE,BEFORE DELETE), sollevando un errore. Serve a impedire che un utente inserisca o sposti una riga verso un valore che poi non potrebbe più vedere.
La funzione predicato tipicamente confronta una colonna (es. SalesRep) con funzioni di contesto come USER_NAME(), SESSION_CONTEXT() o SUSER_SNAME().
CREATE FUNCTION Security.fn_securitypredicate(@SalesRep AS sysname)
RETURNS TABLE WITH SCHEMABINDING
AS RETURN SELECT 1 AS result
WHERE @SalesRep = USER_NAME() OR USER_NAME() = 'Manager';
CREATE SECURITY POLICY Security.SalesFilter
ADD FILTER PREDICATE Security.fn_securitypredicate(SalesRep) ON dbo.Sales
WITH (STATE = ON);
Nota architetturale: su dedicated SQL pool la RLS è supportata, ma pattern multi-tenant che sfruttano SESSION_CONTEXT() sono tipici di Azure SQL Database; nel pool si tende a usare USER_NAME() con utenti/gruppi mappati.
Dynamic data masking
Il DDM offusca i dati sensibili al momento della query per gli utenti non privilegiati, lasciando il dato invariato a riposo. È fondamentale capire che è offuscamento di presentazione, non cifratura: il valore reale resta in chiaro nello storage e nei backup, e chi ha il permesso UNMASK (o è db_owner) vede tutto.
Le funzioni di mascheramento principali:
- default: maschera completa in base al tipo (
XXXXper stringhe,0per numerici,01.01.1900per date). - email: mostra la prima lettera e il suffisso
@xxxx.com(es.aXXX@XXXX.com). - random: sostituisce i numerici con un valore casuale entro un intervallo.
- custom (partial): definisce prefisso, padding e suffisso, es.
partial(0,"XXX-XX-",4)per lasciare visibili solo le ultime 4 cifre.
Si applica in CREATE/ALTER TABLE ... ADD MASKED WITH (FUNCTION = 'email()').
RLS e DDM confrontati con le viste
Storicamente si usavano viste con clausole WHERE e SELECT di colonne parziali per filtrare dati. Rispetto alle viste, RLS e DDM hanno vantaggi netti:
- La logica di sicurezza è centralizzata sull’oggetto tabella, non replicata in decine di viste che qualcuno può dimenticare o aggirare accedendo alla tabella base.
- Non richiedono di riscrivere le query dell’applicazione: agiscono in modo trasparente.
- Le viste restano utili per semplificare schemi complessi, ma per la sicurezza sono fragili perché basta un permesso diretto sulla tabella per bypassarle.
Attenzione però: RLS e DDM non sostituiscono la gestione dei permessi. Sono un layer aggiuntivo, non l’autorizzazione di base.
Trappole tipiche d’esame
- Serve nascondere il PAN della carta ma il dato deve restare recuperabile a chi ha diritto → dynamic data masking (custom partial), non cifratura: DDM offusca solo in visualizzazione e chi ha
UNMASKvede il valore reale. - Un analista con permessi di sola lettura può ricostruire i dati mascherati con
WHERE Salary > 100000o confronti inferenziali → il DDM è aggirabile: per proteggere davvero il dato serve Always Encrypted o restrizione dei permessi, non DDM. - Impedire che un venditore inserisca ordini attribuiti ad altri → block predicate (AFTER INSERT/UPDATE); il solo filter predicate nasconderebbe la riga in lettura ma non impedirebbe la scrittura.
- Il venditore deve vedere solo i propri record senza modificare l’app → RLS con filter predicate e funzione di contesto (
USER_NAME()/SESSION_CONTEXT()), non una vista per ruolo. - Query lente dopo l’attivazione della RLS su una fact table enorme → la TVF predicato impatta le performance: mantieni il predicato semplice, indicizza la colonna filtrata ed evita join costosi dentro la funzione.