Neste artigo
ToggleFala pessoal!!
Nesse artigo, eu gostaria de apresentar para vocês um recurso de segurança muito interessante que o Microsoft SQL Server ganhou a partir da versão 2016, que é o Row Level Security (RLS) ou Segurança em nível de linha.
Como funciona o Row Level Security (RLS)
Visualizar conteúdoIsso simplifica o design e a segurança dos sistemas e aplicações, garantindo que, por exemplo, os funcionários possam acessar somente as linhas de dados que são relevantes para seu departamento ou restringir o acesso do cliente a somente aos dados relevantes para a empresa desse cliente. Tudo isso, feito à nível de banco de dados, sem precisar realizar qualquer alteração na aplicação.
Vale ressaltar que existem 2 tipos de uso do RLS:
- Filtro de linhas (FILTER PREDICATE): Permite aplicar um filtro de linhas nas consultas realizadas com SELECT, retornando apenas os registros que o usuário pode visualizar
- Bloqueio de operações (BLOCK PREDICATE): Permite bloquear determinadas operações a nível de linha, no qual o usuário em questão não poderia realizar (Ex: INSERT numa tabela utilizando um Id_Usuario diferente do dele)
Como implementar o Row Level Security (RLS)
Visualizar conteúdoCriação dos dados de teste para o exemplo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
USE [dirceuresende] GO ------------------------------------------------------------------------ -- Criação das tabelas de Usuários e Pedidos ------------------------------------------------------------------------ IF (OBJECT_ID('dbo.Usuarios') IS NOT NULL) DROP TABLE dbo.Usuarios CREATE TABLE dbo.Usuarios ( Id_Usuario INT NOT NULL IDENTITY(1,1) PRIMARY KEY, Cd_Usuario NVARCHAR(50) NOT NULL, Nm_Usuario NVARCHAR(100) NOT NULL ) INSERT INTO dbo.Usuarios ( Cd_Usuario, Nm_Usuario ) VALUES ('dirceu.resende', 'Dirceu Resende'), ('tiago.neves', 'Tiago Neves'), ('fabricio.lima', 'Fabrício Lima'), ('vithor.silva', 'Vithor Silva') IF (OBJECT_ID('dbo.Pedidos') IS NOT NULL) DROP TABLE dbo.Pedidos CREATE TABLE dbo.Pedidos ( Id_Pedido INT NOT NULL IDENTITY(1,1) PRIMARY KEY, Dt_Pedido DATETIME DEFAULT GETDATE() NOT NULL, Id_Usuario INT NOT NULL CONSTRAINT [FK_Usuario] FOREIGN KEY(Id_Usuario) REFERENCES dbo.Usuarios(Id_Usuario), Id_Cliente INT NOT NULL, Id_Produto INT NOT NULL ) GO INSERT INTO dbo.Pedidos ( Dt_Pedido, Id_Usuario, Id_Cliente, Id_Produto ) SELECT (SELECT DATEADD(DAY, CAST(RAND() * 365 AS INT), '2017-01-01')) AS Dt_Pedido, (SELECT TOP 1 Id_Usuario FROM dbo.Usuarios ORDER BY NEWID()) AS Id_Usuario, (SELECT CAST(RAND() * 1000 AS INT)) AS Id_Cliente, (SELECT CAST(RAND() * 1000 AS INT)) AS Id_Produto GO 15 SELECT * FROM dbo.Pedidos |
Criação dos usuários utilizados nos testes e permissões
Agora vou criar alguns usuários no banco de dados para testar o RLS e dar permissão para esses usuários poderem acessar as tabelas Pedidos e Usuarios.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
CREATE USER [dirceu.resende] WITHOUT LOGIN GO CREATE USER [tiago.neves] WITHOUT LOGIN GO CREATE USER [fabricio.lima] WITHOUT LOGIN GO CREATE USER [vithor.silva] WITHOUT LOGIN GO GRANT SELECT ON dbo.Pedidos TO [dirceu.resende] GRANT SELECT ON dbo.Usuarios TO [dirceu.resende] GRANT SELECT ON dbo.Pedidos TO [tiago.neves] GRANT SELECT ON dbo.Usuarios TO [tiago.neves] GRANT SELECT ON dbo.Pedidos TO [fabricio.lima] GRANT SELECT ON dbo.Usuarios TO [fabricio.lima] GRANT SELECT ON dbo.Pedidos TO [vithor.silva] GRANT SELECT ON dbo.Usuarios TO [vithor.silva] |
Criação do schema “rls”
Como boa prática, vou criar também um novo schema (rls) para agregar as funções que serão criadas para uso com o RLS. Como será necessário criar 1 função para cada critério ou filtro que você queira aplicar o RLS e 1 policy para cada tabela, é mais organizado que todas essas funções fiquem em um esquema dedicado.
1 2 3 4 5 |
USE [dirceuresende] GO CREATE SCHEMA [rls] GO |
Criação da função fncId_Usuario
Utilizando a função fncId_Usuario, posso aplicar um filtro em todas as tabelas (se eu criar policies para isso) que tenham um campo com o identificador do usuário. Nessa função, é informado o Id_Usuario que está na tabela em questão e a função irá buscar na tabela de usuários qual o identificador (Id_Usuario) do usuário logado que está realizando o SELECT na tabela.
Vale lembrar que o filtro é aplicado mesmo para usuários sysadmin. Ou seja, mesmo o usuário sysadmin só poderá visualizar os registros onde o Id_Usuario seja igual ao Id desse usuário (ou ele não poderá visualizar nenhum registro).
1 2 3 4 5 6 7 8 9 10 11 12 13 |
IF (OBJECT_ID('rls.fncId_Usuario') IS NOT NULL) DROP FUNCTION [rls].fncId_Usuario GO CREATE FUNCTION [rls].fncId_Usuario ( @Id_Usuario INT ) RETURNS TABLE WITH SCHEMABINDING AS RETURN SELECT 1 AS Retorno WHERE @Id_Usuario = (SELECT Id_Usuario FROM dbo.Usuarios WHERE Cd_Usuario = USER_NAME()) GO |
Criação do policy rls.Pedidos
Com o comando abaixo, vamos aplicar a policy rls.Pedidos utilizando a função fncId_Usuario e assim, ativar o Row Level Security (RLS) na tabela Pedidos.
1 2 3 4 5 |
IF (OBJECT_ID('rls.Pedidos') IS NOT NULL) DROP SECURITY POLICY [rls].Pedidos GO CREATE SECURITY POLICY [rls].Pedidos ADD FILTER PREDICATE [rls].fncId_Usuario(Id_Usuario) ON dbo.Pedidos WITH (STATE = ON) |
Testes do Row Level Security:
Agora, vamos aos testes práticos e ver como a Segurança em nível de linha funciona realmente.
1 2 3 4 5 6 7 |
EXEC('SELECT * FROM dbo.Pedidos') AS USER = 'dirceu.resende' EXEC('SELECT * FROM dbo.Pedidos') AS USER = 'fabricio.lima' EXEC('SELECT * FROM dbo.Pedidos') AS USER = 'tiago.neves' EXEC('SELECT * FROM dbo.Pedidos') AS USER = 'vithor.silva' -- Consulta utilizando um login sysadmin (Sem personificar outro usuário) SELECT * FROM dbo.Pedidos |
Outro exemplo para o Row Level Security (RLS)
Visualizar conteúdoCriação da função e da policy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
IF (OBJECT_ID('rls.fncUsuario') IS NOT NULL) DROP FUNCTION [rls].fncUsuario GO CREATE FUNCTION [rls].fncUsuario ( @Login NVARCHAR(100) ) RETURNS TABLE WITH SCHEMABINDING AS RETURN SELECT 1 AS Retorno WHERE @Login = USER_NAME() OR IS_MEMBER('db_owner') = 1 OR IS_SRVROLEMEMBER('sysadmin') = 1 GO IF (OBJECT_ID('rls.Usuarios') IS NOT NULL) DROP SECURITY POLICY [rls].Usuarios GO CREATE SECURITY POLICY [rls].Usuarios ADD FILTER PREDICATE [rls].fncUsuario(Cd_Usuario) ON dbo.Usuarios WITH (STATE = ON) |
Testes reais do RLS
1 2 3 4 5 6 7 8 9 10 11 |
------------------------------------------------------------------------------- -- Testes do Row Level Security em um cenário real ------------------------------------------------------------------------------- EXEC('SELECT * FROM dbo.Usuarios') AS USER = 'dirceu.resende' EXEC('SELECT * FROM dbo.Usuarios') AS USER = 'tiago.neves' EXEC('SELECT * FROM dbo.Usuarios') AS USER = 'fabricio.lima' EXEC('SELECT * FROM dbo.Usuarios') AS USER = 'vithor.silva' -- Consulta utilizando um login sysadmin (Sem personificar outro usuário) SELECT * FROM dbo.Usuarios |
Utilizando o BLOCK PREDICATE do Row Level Security (RLS)
Visualizar conteúdoVale lembrar que podemos utilizar os predicados FILTER e BLOCK ao mesmo tempo, na mesma policy.
Código-fonte da policy
1 2 3 4 5 6 7 8 9 10 11 |
IF (OBJECT_ID('rls.Usuarios_Blk') IS NOT NULL) DROP SECURITY POLICY [rls].Usuarios_Blk GO CREATE SECURITY POLICY [rls].Usuarios_Blk ADD BLOCK PREDICATE [rls].fncUsuario(Cd_Usuario) ON dbo.Usuarios AFTER INSERT, ADD BLOCK PREDICATE [rls].fncUsuario(Cd_Usuario) ON dbo.Usuarios AFTER UPDATE WITH (STATE = ON) -- Libera as permissões para permitir o insert e update na tabela Usuarios para o login "dirceu.resende" GRANT INSERT, UPDATE ON dbo.Usuarios TO [dirceu.resende] GO |
Agora, vamos impersonar o usuário “dirceu.resende” e tentar inserir um usuário com outro login na tabela que não seja o “dirceu.resende”:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
EXECUTE AS USER = 'dirceu.resende' GO INSERT INTO dbo.Usuarios ( Cd_Usuario, Nm_Usuario ) VALUES ( 'edvaldo.castro', -- Cd_Usuario - nvarchar(60) 'Edvaldo Castro' -- Nm_Usuario - varchar(60) ) |
Result:
The attempted operation failed because the target object ‘dirceuresende.dbo.Usuarios’ has a block predicate that conflicts with this operation. If the operation is performed on a view, the block predicate might be enforced on the underlying table. Modify the operation to target only the rows that are allowed by the block predicate.
The statement has been terminated.
Como vocês puderam observar, não foi possível inserir o registro, pois o BLOCK PREDICATE evitou a inserção uma vez que o Cd_Usuario que estava sendo inserido não era o que o usuário “dirceu.resende” tinha permissão para inserir.
O mesmo acontece quando tentamos realizar um UPDATE:
1 2 3 4 5 6 |
EXECUTE AS USER = 'dirceu.resende' GO UPDATE dbo.Usuarios SET Nm_Usuario = 'Teste' WHERE Cd_Usuario = 'fabricio.lima' |
Result:
The attempted operation failed because the target object ‘dirceuresende.dbo.Usuarios’ has a block predicate that conflicts with this operation. If the operation is performed on a view, the block predicate might be enforced on the underlying table. Modify the operation to target only the rows that are allowed by the block predicate.
The statement has been terminated.
Entretanto, se tentarmos atualizar os registros do próprio usuário logado, vemos que a operação é realizada com sucesso:
1 2 3 4 5 6 |
EXECUTE AS USER = 'dirceu.resende' GO UPDATE dbo.Usuarios SET Nm_Usuario = 'Teste' WHERE Cd_Usuario = 'dirceu.resende' |
Como criamos mais 2 condições na função fncUsuario para permitir que usuários sysadmin e/ou db_owner possam acessar os dados, esses 2 perfis de usuários também poderão inserir/atualizar os dados normalmente:
Boas práticas para o Row Level Security
Visualizar conteúdo- Evite conversões de tipo em funções de predicado para evitar possíveis erros de tempo de execução.
- Evite utilizar outras funções dentro da função de filtragem, para evitar a degradação do desempenho. O otimizador de consulta tentará detectar recursões diretas, mas não há garantia de que encontrará recursões indiretas (isto é, onde uma segunda função chama a função de predicado).
- Evite usar joins de tabelas em excesso em funções de predicado, para maximizar o desempenho.
- Evite criar funções de filtragens que dependam de determinadas configurações de sessão aplicadas utilizando o comando SET (Comparações/operações com NULL, funções de primeiro dia da semana e funções que dependam da linguagem atual ou DATEFORMAT).
Row Level Security e seu impacto na performance
Visualizar conteúdoComo desativar o Row Level Security (RLS)
Visualizar conteúdo
1 |
ALTER SECURITY POLICY [rls].Usuarios WITH (STATE = OFF) |
E para excluir os objetos do RLS, você pode utilizar os comandos abaixo:
1 2 3 4 5 |
DROP SECURITY POLICY [rls].Usuarios GO DROP FUNCTION [rls].fncUsuario GO |
Row Level Security (RLS) e compatibilidade entre recursos
Visualizar conteúdo- DBCC SHOW_STATISTICS: Os relatórios de estatísticas podem exibir informações que estariam protegidas por uma política de segurança. Por esse motivo, para exibir um as estatísticas de uma tabela protegidas com o RLS, o usuário deve ter ser proprietário da tabela ou deve ser um membro da role sysadmin, db_owner ou db_ddladmin.
- Filestream: RLS não é compatível com Filestream.
- Polybase: RLS não é compatível com o Polybase.
- Memory-Optimized Tables: A função usada como um predicado de segurança em uma tabela com otimização de memória deve ser definida usando a opção WITH NATIVE_COMPILATION. Com essa opção, os recursos de linguagem não permitidos pelas tabelas com otimização de memória serão banidos e o erro apropriado será emitido no momento da criação.
- Views indexadas: De modo geral, as políticas de segurança podem ser criadas sobre as views, e as views podem ser criadas sobre as tabelas que são associadas pelo RLS. No entanto, as views indexadas não podem ser criadas sobre as tabelas que têm uma política de segurança RLS, pois as pesquisas de linha pelo índice contornariam a política.
- Change Data Capture (CDC): O CDC pode permitir o vazamento das informações de linhas inteiras, uma vez que, mesmo utilizando o RLS, todas as alterações realizadas serão armazenadas na tabela de histórico, e qualquer usuário com permissão nessa tabela poderá visualizar essas informações
- Change Tracking: O Controle de Alterações (Change Tracking) pode deixar vazar a chave primária de linhas que deve ser filtrada para usuários com as permissões SELECT e VIEW CHANGE TRACKING. Os valores de dados reais não vazam; apenas o fato de que a coluna A foi atualizada/inserida/excluída para a linha com a chave primária B. Isso será um problema se a chave primária contiver um elemento confidencial, como um Número de Seguro Social. No entanto, na prática, esse CHANGETABLE é quase sempre unido à tabela original para obtenção de dados mais recentes.
- Full-Text Search: Uma queda no desempenho é esperada em consultas que usam as funções de Full-Text Search e Pesquisa Semântica devido a uma junção extra apresentada para aplicar a segurança em nível de linha e evitar a perda das chaves primárias de linhas que devem ser filtradas: CONTAINSTABLE, FREETEXTTABLE, semantickeyphrasetable, semanticsimilaritydetailstable, semanticsimilaritytable.
- Índices Columnstore: A RLS é compatível com índices columnstore clusterizados e não clusterizados. No entanto, como a segurança no nível de linha se aplica a uma função, é possível que o otimizador possa modificar o plano de consulta, de modo que ele não use o modo de lote (batch mode).
- Views Particionadas: Os predicados de bloqueio não podem ser definidos em views particionadas, e as views particionadas não podem ser criadas sobre as tabelas que usam predicados de bloqueio. Os predicados de filtro são compatíveis com views particionadas.
- Temporal tables: As tabelas temporais são compatíveis com a RLS. No entanto, os predicados de segurança na tabela atual não são replicados automaticamente na tabela de histórico. Para aplicar uma política de segurança às tabelas atual e de histórico, você deverá adicionar individualmente um predicado de segurança em cada tabela.
Referências: https://docs.microsoft.com/en-us/sql/relational-databases/security/row-level-security
And that's it, folks!
Um abraço e até a próxima.