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 (XXXX per stringhe, 0 per numerici, 01.01.1900 per 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 UNMASK vede il valore reale.
  • Un analista con permessi di sola lettura può ricostruire i dati mascherati con WHERE Salary > 100000 o 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.