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

  1. Sempre definir variáveis de sessão: Garantir que os valores de current_setting() estejam preenchidos
  2. Indexar colunas de políticas: Criar índices em todas as colunas usadas em USING/WITH CHECK
  3. Testar minuciosamente: Verificar políticas com diferentes funções e cenários
  4. Manter políticas simples: Lógica complexa impacta o desempenho
  5. Usar EXPLAIN: Analisar planos de consulta com RLS habilitada
  6. Documentar políticas: Comentar a lógica da política para manutenibilidade
  7. Separar preocupações: Usar políticas diferentes para SELECT, INSERT, UPDATE, DELETE
  8. 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.