Controle de Acesso Baseado em Funções Avançado no PostgreSQL: Segurança em Nível de Linha e Gerenciamento de Políticas
Use a Segurança em Nível de Linha do PostgreSQL com políticas RBAC para isolamento de inquilinos, verificações de propriedade e acesso mais seguro à aplicação.
Controle de Acesso Avançado Baseado em Funções no PostgreSQL: Segurança em Nível de Linha e Gerenciamento de Políticas
A Segurança em Nível de Linha (RLS) do PostgreSQL permite que seu banco de dados filtre linhas com base na função atual e no contexto da sessão. Se sua aplicação atende a vários inquilinos ou mistura acesso de administrador, gerente e usuário regular, a RLS pode evitar que uma cláusula WHERE tenant_id = ... esquecida se torne um vazamento de dados.
A RLS não substitui a autorização da aplicação. Ela fornece uma rede de segurança em nível de banco de dados para o limite mais sensível: quais linhas uma conexão pode ler ou escrever.
Por que Segurança em Nível de Linha?
Problemas da Abordagem Tradicional
Filtragem em nível de aplicação:
# Toda consulta deve lembrar de filtrar
query = "SELECT * FROM documents WHERE tenant_id = %s"
results = db.execute(query, [current_user_tenant_id])
Problemas:
- Facilidade de esquecer filtros (vulnerabilidades de segurança)
- Duplicação de código em toda a aplicação
- Nenhuma proteção para acesso direto ao banco de dados
- Requisitos de auditoria complexos
Benefícios da Segurança em Nível de Linha
- Aplicação automática: Políticas aplicadas de forma transparente
- Regras centralizadas: Lógica de segurança em um único lugar
- Isolamento multi-inquilino: Perfeito para aplicações SaaS
- Conformidade de auditoria: Garantias de segurança integradas
- Desempenho: Planejamento eficiente de consultas
Configuração Básica da RLS
Passo 1: Habilitar RLS na Tabela
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
content TEXT,
owner_id INTEGER NOT NULL,
department VARCHAR(50),
is_public BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
-- Habilitar segurança em nível de linha
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
Passo 2: Criar Funções
-- Criar diferentes funções de usuário
CREATE ROLE regular_user;
CREATE ROLE department_manager;
CREATE ROLE admin;
-- Conceder acesso à tabela
GRANT SELECT, INSERT, UPDATE, DELETE ON documents TO regular_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON documents TO department_manager;
GRANT ALL ON documents TO admin;
Passo 3: Criar Políticas
-- Política: Usuários podem ver seus próprios documentos
CREATE POLICY user_own_documents ON documents
FOR SELECT
TO regular_user
USING (owner_id = current_setting('app.user_id')::INTEGER);
-- Política: Usuários podem inserir documentos de sua propriedade
CREATE POLICY user_insert_own ON documents
FOR INSERT
TO regular_user
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
-- Política: Usuários podem atualizar seus próprios documentos
CREATE POLICY user_update_own ON documents
FOR UPDATE
TO regular_user
USING (owner_id = current_setting('app.user_id')::INTEGER)
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
Passo 4: Definir Variáveis de Sessão
No código da aplicação:
import psycopg2
# Conectar como função
conn = psycopg2.connect(
host="localhost",
database="mydb",
user="regular_user",
password="password"
)
# Definir contexto do usuário
cursor = conn.cursor()
cursor.execute("SELECT set_config('app.user_id', %s, false)", [str(current_user_id)])
# Agora as consultas são filtradas automaticamente
cursor.execute("SELECT * FROM documents")
# Retorna apenas documentos onde owner_id = current_user_id
Aplicação SaaS Multi-Inquilino
Configuração Completa Multi-Inquilino
-- Criar tabela de inquilinos
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Criar tabela de usuários
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
tenant_id INTEGER REFERENCES tenants(id),
role VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Criar tabela de dados da aplicação
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL REFERENCES tenants(id),
customer_name VARCHAR(100),
amount DECIMAL(10,2),
status VARCHAR(20),
created_by INTEGER REFERENCES users(id),
created_at TIMESTAMP DEFAULT NOW()
);
-- Habilitar RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Política: Usuários veem apenas pedidos de seu inquilino
CREATE POLICY tenant_isolation ON orders
FOR ALL
TO public
USING (tenant_id = current_setting('app.tenant_id')::INTEGER)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::INTEGER);
Integração com a Aplicação
Exemplo em Django:
from django.db import connection
class TenantMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
with connection.cursor() as cursor:
cursor.execute(
"SELECT set_config('app.tenant_id', %s, false)",
[str(request.user.tenant_id)]
)
response = self.get_response(request)
return response
Exemplo em Node.js:
const { Pool } = require('pg');
const pool = new Pool();
async function setTenantContext(tenantId) {
const client = await pool.connect();
try {
await client.query('SELECT set_config($1, $2, false)', ['app.tenant_id', String(tenantId)]);
return client;
} catch (err) {
client.release();
throw err;
}
}
// Uso
app.use(async (req, res, next) => {
if (req.user) {
req.dbClient = await setTenantContext(req.user.tenantId);
}
next();
});
Padrões Avançados de Políticas
1. Controle de Acesso Hierárquico
-- Usuários podem ver documentos de seu departamento e públicos
CREATE POLICY department_access ON documents
FOR SELECT
TO regular_user
USING (
department = current_setting('app.user_department', true)
OR is_public = true
OR owner_id = current_setting('app.user_id')::INTEGER
);
2. Acesso Baseado em Tempo
CREATE TABLE subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
content TEXT,
valid_from TIMESTAMP NOT NULL,
valid_until TIMESTAMP NOT NULL
);
ALTER TABLE subscriptions ENABLE ROW LEVEL SECURITY;
-- Política: Usuários podem ver apenas assinaturas ativas
CREATE POLICY active_subscriptions ON subscriptions
FOR SELECT
TO public
USING (
user_id = current_setting('app.user_id')::INTEGER
AND NOW() BETWEEN valid_from AND valid_until
);
3. Acesso Baseado em Função com Lógica Complexa
CREATE TABLE projects (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
owner_id INTEGER,
team_id INTEGER,
visibility VARCHAR(20) -- 'private', 'team', 'public'
);
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Política de visibilidade complexa
CREATE POLICY project_visibility ON projects
FOR SELECT
TO public
USING (
CASE current_setting('app.user_role', true)
WHEN 'admin' THEN true
WHEN 'manager' THEN (
team_id = current_setting('app.user_team_id')::INTEGER
OR visibility = 'public'
)
ELSE (
owner_id = current_setting('app.user_id')::INTEGER
OR visibility = 'public'
)
END
);
4. Listas de Acesso Compartilhado
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
owner_id INTEGER NOT NULL
);
CREATE TABLE document_shares (
document_id INTEGER REFERENCES documents(id),
shared_with_user_id INTEGER,
permission VARCHAR(20), -- 'read', 'write'
PRIMARY KEY (document_id, shared_with_user_id)
);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Política: Usuários podem ver documentos próprios ou compartilhados
CREATE POLICY document_access ON documents
FOR SELECT
TO public
USING (
owner_id = current_setting('app.user_id')::INTEGER
OR id IN (
SELECT document_id
FROM document_shares
WHERE shared_with_user_id = current_setting('app.user_id')::INTEGER
)
);
-- Política: Usuários podem modificar documentos próprios ou compartilhados com permissão de escrita
CREATE POLICY document_modify ON documents
FOR UPDATE
TO public
USING (
owner_id = current_setting('app.user_id')::INTEGER
OR id IN (
SELECT document_id
FROM document_shares
WHERE shared_with_user_id = current_setting('app.user_id')::INTEGER
AND permission = 'write'
)
);
Acesso Administrativo e RLS
Proprietários de tabelas e superusuários podem contornar a RLS, a menos que você force. Trate caminhos administrativos como uma decisão de design separada, não uma reflexão tardia.
Opção 1: Atributo de Função BYPASSRLS
CREATE ROLE admin_user WITH LOGIN PASSWORD 'secure_password';
ALTER ROLE admin_user BYPASSRLS;
Use isso com moderação. BYPASSRLS é poderoso e não deve ser concedido a funções normais da aplicação.
Opção 2: Políticas Permissivas para Administradores
-- Criar política permissiva para função de administrador
CREATE POLICY admin_all_access ON documents
FOR ALL
TO admin
USING (true)
WITH CHECK (true);
Opção 3: Forçar RLS para Proprietários de Tabelas
ALTER TABLE documents FORCE ROW LEVEL SECURITY;
FORCE ROW LEVEL SECURITY faz com que os proprietários de tabelas também sigam as políticas. Isso geralmente é mais seguro para tabelas de propriedade da aplicação. Para trabalhos de manutenção, use uma função controlada separada em vez de desabilitar a RLS dentro de um script ad hoc.
Tipos de Políticas: USING vs WITH CHECK
Cláusula USING
Controla quais linhas existentes são visíveis/modificáveis:
CREATE POLICY select_own ON documents
FOR SELECT
USING (owner_id = current_setting('app.user_id')::INTEGER);
-- Usuário pode apenas SELECT linhas onde owner_id corresponde
Cláusula WITH CHECK
Controla quais novas/atualizadas linhas são permitidas:
CREATE POLICY insert_own ON documents
FOR INSERT
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
-- Usuário pode apenas INSERT linhas onde owner_id corresponde
Políticas Combinadas
CREATE POLICY update_own ON documents
FOR UPDATE
USING (owner_id = current_setting('app.user_id')::INTEGER)
WITH CHECK (owner_id = current_setting('app.user_id')::INTEGER);
Considerações de Desempenho
1. Indexar Colunas de Políticas
-- Criar índices para colunas usadas em políticas
CREATE INDEX idx_documents_owner ON documents(owner_id);
CREATE INDEX idx_documents_tenant ON documents(tenant_id);
CREATE INDEX idx_documents_department ON documents(department);
2. Analisar Planos de Consulta
EXPLAIN ANALYZE
SELECT * FROM documents
WHERE title LIKE '%report%';
-- Verificar se o filtro da política é aplicado eficientemente
3. Cuidado com Subconsultas em Políticas
Subconsultas são às vezes o modelo certo para acesso compartilhado, mas precisam de índices e testes. Uma política que consulta outra tabela para cada linha candidata pode se tornar cara.
Arriscado sem índices:
CREATE POLICY slow_policy ON documents
USING (
id IN (
SELECT document_id
FROM shares
WHERE user_id = current_setting('app.user_id')::INTEGER
)
);
Muitas vezes mais claro com índices de suporte:
CREATE POLICY faster_policy ON documents
USING (
EXISTS (
SELECT 1 FROM shares
WHERE document_id = documents.id
AND user_id = current_setting('app.user_id')::INTEGER
)
);
Crie índices como shares(user_id, document_id) e verifique o plano com EXPLAIN (ANALYZE, BUFFERS).
Monitoramento e Auditoria
Visualizar Políticas Ativas
-- Listar todas as políticas RLS
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
ORDER BY tablename, policyname;
Auditar Mudanças de Políticas
Rastreie DDL de políticas em migrações e logs de auditoria do banco de dados. Se seu ambiente usa gatilhos de eventos, teste-os cuidadosamente em relação à sua versão do PostgreSQL e inclua eventos CREATE POLICY, ALTER POLICY e DROP POLICY. Não confie em um trecho de gatilho não testado para evidência de conformidade.
Testar Eficácia das Políticas
-- Criar função de teste
CREATE OR REPLACE FUNCTION test_user_access(
test_user_id INTEGER,
test_tenant_id INTEGER
)
RETURNS TABLE(document_id INTEGER, title VARCHAR) AS $$
BEGIN
-- Definir variáveis de sessão
PERFORM set_config('app.user_id', test_user_id::text, false);
PERFORM set_config('app.tenant_id', test_tenant_id::text, false);
-- Retornar documentos acessíveis
RETURN QUERY SELECT id, documents.title FROM documents;
END;
$$ LANGUAGE plpgsql SECURITY INVOKER;
-- Testar acesso
SELECT * FROM test_user_access(123, 5);
Solução de Problemas
Problema 1: Políticas Não Aplicadas
Verificar se RLS está habilitada:
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND tablename = 'documents';
Habilitar se desabilitada:
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
Problema 2: Variáveis de Sessão Não Definidas
Depurar configurações atuais:
SHOW app.user_id;
SHOW app.tenant_id;
-- Ou usar current_setting
SELECT current_setting('app.user_id', true);
Problema 3: Degradação de Desempenho
Identificar políticas lentas:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM documents WHERE title = 'test';
-- Procurar operações "Filter" com condições de política
Solução: Simplificar políticas ou adicionar índices
Problema 4: Acesso Negado Inesperado
Testar como usuário específico:
SET ROLE regular_user;
SELECT set_config('app.user_id', '123', false);
SELECT * FROM documents;
-- Mostra o que aquele usuário pode realmente ver
RESET ROLE;
Melhores Práticas
- Sempre definir variáveis de sessão: Garantir que os valores de
current_setting()estejam preenchidos - Indexar colunas de políticas: Criar índices em todas as colunas usadas em USING/WITH CHECK
- Testar minuciosamente: Verificar políticas com diferentes funções e cenários
- Manter políticas simples: Lógica complexa impacta o desempenho
- Usar EXPLAIN: Analisar planos de consulta com RLS habilitada
- Documentar políticas: Comentar a lógica da política para manutenibilidade
- Separar preocupações: Usar políticas diferentes para SELECT, INSERT, UPDATE, DELETE
- Monitorar desempenho: Configurar alertas para consultas lentas
Conclusão
Comece com uma política simples: isolamento de inquilino em suas tabelas de maior risco. Defina o contexto da sessão com set_config, indexe cada coluna usada pelas políticas e teste leituras e escritas como funções reais da aplicação.
Depois que o limite básico funcionar, adicione políticas de propriedade, gerente ou acesso compartilhado apenas onde o produto precisar delas. Políticas pequenas e testadas são mais fáceis de raciocinar do que uma regra inteligente que tenta codificar todo o seu modelo de permissão.