Implementare la sicurezza a livello di riga
La sicurezza a livello di riga non usa la crittografia e opera a livello di database per limitare l'accesso a una tabella usando criteri di sicurezza basati sull'appartenenza al gruppo o sul contesto di autorizzazione. Funzionalmente è equivalente a una clausola WHERE.
Il criterio di sicurezza richiama una funzione con valori di tabella inline per proteggere l'accesso alle righe di una tabella.
A seconda dell'attributo di un utente, il predicato determina se tale l'utente ha accesso alle informazioni pertinenti. Quando si esegue una query su una tabella, il criterio di sicurezza applica la funzione del predicato. A seconda dei requisiti aziendali, la sicurezza a livello di riga può essere semplice, ad esempio WHERE CustomerId = 29, o complessa in base alle esigenze.
Esistono due tipi di criteri di sicurezza supportati dalla sicurezza a livello di riga:
Predicati di filtro: limitano l'accesso ai dati che viola il predicato.
accesso Definizione SELECT Non è possibile visualizzare le righe filtrate. UPDATE Non è possibile aggiornare le righe filtrate. DELETE Non è possibile eliminare le righe filtrate. INSERT Non applicabile. Predicati di blocco: limitano le modifiche ai dati che violano il predicato.
accesso Definizione AFTER INSERT Impedisce agli utenti di inserire righe con valori che violano il predicato. DOPO L’AGGIORNAMENTO Impedisce agli utenti di aggiornare le righe a valori che violano il predicato. PRIMA DELL'AGGIORNAMENTO Impedisce agli utenti di aggiornare le righe che attualmente violano il predicato. BEFORE DELETE Blocca le operazioni di eliminazione se la riga viola il predicato.
Poiché il controllo di accesso è configurato e applicato a livello di database, le modifiche all'applicazione sono minime, se non addirittura nulle. Gli utenti possono anche accedere direttamente alle tabelle ed eseguire una query sui dati.
La sicurezza a livello di riga viene implementata in tre passaggi principali:
- Creare gli utenti o i gruppi a cui si vuole isolare l'accesso.
- Creare la funzione inline con valori di tabella che filtra i risultati in base al predicato definito.
- Creare un criterio di sicurezza per la tabella, assegnando la funzione creata in precedenza.
I comandi T-SQL seguenti illustrano come utilizzare RLS (Row-Level Security) in uno scenario in cui l'accesso degli utenti è suddiviso per tenant.
-- Create supporting objects for this example
CREATE TABLE [Sales] (SalesID INT,
ProductID INT,
TenantName NVARCHAR(10),
OrderQtd INT,
UnitPrice MONEY)
GO
INSERT INTO [Sales] VALUES (1, 3, 'Tenant1', 5, 10.00);
INSERT INTO [Sales] VALUES (2, 4, 'Tenant1', 2, 57.00);
INSERT INTO [Sales] VALUES (3, 7, 'Tenant1', 4, 23.00);
INSERT INTO [Sales] VALUES (4, 2, 'Tenant2', 2, 91.00);
INSERT INTO [Sales] VALUES (5, 9, 'Tenant3', 5, 80.00);
INSERT INTO [Sales] VALUES (6, 1, 'Tenant3', 5, 35.00);
INSERT INTO [Sales] VALUES (7, 3, 'Tenant4', 8, 11.00);
-- View all the rows in the table
SELECT * FROM Sales;
Creare quindi gli utenti e concedere loro l'accesso alla tabella Sales. In questo esempio ogni utente è responsabile di un tenant specifico. L'utente TenantAdmin ha accesso per visualizzare i dati di tutti i tenant.
CREATE USER [TenantAdmin] WITH PASSWORD = '<strong password>'
GO
CREATE USER [Tenant1] WITH PASSWORD = '<strong password>'
GO
CREATE USER [Tenant2] WITH PASSWORD = '<strong password>'
GO
CREATE USER [Tenant3] WITH PASSWORD = '<strong password>'
GO
CREATE USER [Tenant4] WITH PASSWORD = '<strong password>'
GO
GRANT SELECT ON [Sales] TO [TenantAdmin]
GO
GRANT SELECT ON [Sales] TO [Tenant1]
GO
GRANT SELECT ON [Sales] TO [Tenant2]
GO
GRANT SELECT ON [Sales] TO [Tenant3]
GO
GRANT SELECT ON [Sales] TO [Tenant4]
GO
Successivamente si crea un nuovo schema, una funzione con valori di tabella inline e si concederà all'utente l'accesso alla nuova funzione. Il predicato WHERE @TenantName = USER_NAME() OR USER_NAME() = 'TenantAdmin' valuta se il nome utente che esegue la query corrisponde ai valori della colonna TenantName.
CREATE SCHEMA sec;
GO
--Create the filter predicate
CREATE FUNCTION sec.tvf_SecurityPredicatebyTenant(@TenantName AS NVARCHAR(10))
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS result
WHERE @TenantName = USER_NAME() OR USER_NAME() = 'TenantAdmin';
GO
--Grant users access to inline table-valued function
GRANT SELECT ON sec.tvf_SecurityPredicatebyTenant TO [TenantAdmin]
GO
GRANT SELECT ON sec.tvf_SecurityPredicatebyTenant TO [Tenant1]
GO
GRANT SELECT ON sec.tvf_SecurityPredicatebyTenant TO [Tenant2]
GO
GRANT SELECT ON sec.tvf_SecurityPredicatebyTenant TO [Tenant3]
GO
GRANT SELECT ON sec.tvf_SecurityPredicatebyTenant TO [Tenant4]
GO
--Create security policy and add the filter predicate
CREATE SECURITY POLICY sec.SalesPolicy
ADD FILTER PREDICATE sec.tvf_SecurityPredicatebyTenant(TenantName) ON [dbo].[Sales]
WITH (STATE = ON);
GO
A questo punto, è possibile testare l'accesso:
EXECUTE AS USER = 'TenantAdmin';
SELECT * FROM dbo.Sales;
REVERT;
EXECUTE AS USER = 'Tenant1';
SELECT * FROM dbo.Sales;
REVERT;
EXECUTE AS USER = 'Tenant2';
SELECT * FROM dbo.Sales;
REVERT;
EXECUTE AS USER = 'Tenant3';
SELECT * FROM dbo.Sales;
REVERT;
EXECUTE AS USER = 'Tenant4';
SELECT * FROM dbo.Sales;
REVERT;
L'utente TenantAdmin deve visualizzare tutte le righe. Gli utenti Tenant1, Tenant2, Tenant3 e Tenant4 devono visualizzare solo le proprie righe.
Se si modificano i criteri di sicurezza con WITH (STATE = OFF);, si nota che gli utenti visualizzano tutte le righe.
Nota
Esiste un rischio di perdita di informazioni se un utente malintenzionato scrive una query con una clausola WHERE appositamente creata e, ad esempio, un errore di divisione per zero, per forzare un'eccezione se la condizione WHERE è vera. Si tratta di un attacco side-channel. È consigliabile limitare la capacità degli utenti di eseguire query non pianificate quando si usa la sicurezza a livello di riga.
Caso d'uso
La sicurezza a livello di riga è ideale in molti scenari, ad esempio:
- Quando è necessario isolare l'accesso a un reparto a livello di riga.
- Quando è necessario limitare l'accesso ai dati dei clienti solo ai dati rilevanti per l'azienda.
- Quando è necessario limitare l'accesso per ragioni di conformità.
Procedure consigliate
Ecco alcune procedure consigliate da considerare quando si implementa la sicurezza a livello di riga:
- Creare uno schema separato per le funzioni di predicato e i criteri di sicurezza.
- Evitare le conversioni dei tipi nelle funzioni del predicato.
- Evitare di usare un numero eccessivo di join di tabella e ricorsione nelle funzioni del predicato.