03 - Abordagem para Administradores (ignorando RLS)

Vide o artigo anterior:
02 - Conceito mais detalhado de RLS no PostgresSQL na prática

Imagine a situação onde iremos necessitar executar algo que precise ignorar a situação do tenant_id, ou seja, um select para mais de um tenant_id por exemplo, etc.

Essa é uma questão crucial em qualquer aplicação multi-tenant que usa RLS, pois há cenários válidos onde você precisa contornar o isolamento padrão.

A política de RLS que definimos opera no modo de isolamento estrito. Para permitir consultas que acessem dados de múltiplos tenants, você tem duas abordagens principais no PostgreSQL, dependendo de quem está fazendo a consulta:

  1. Usuário de Sistema/Administrador (BYPASSRLS): Se for um processo de background, relatório de administração ou migração de dados que precisa ver TUDO.

  2. Consulta Multi-Tenant Controlada (USING Modificado): Se for uma função específica da sua aplicação que precisa, por design, ver dados de um subconjunto de tenants.

1. 🛑 Abordagem para Administradores (Ignorando RLS)

Esta é a abordagem mais simples e segura para tarefas administrativas.

A solução: SET SESSION AUTHORIZATION e BYPASSRLS

As políticas de RLS são ignoradas por usuários com o atributo BYPASSRLS. Por padrão, apenas os Superusuários têm esse atributo.

Prática Recomendada:

  1. Crie uma ROLE dedicada para tarefas de administração (ex: app_admin).
  2. Dê a essa ROLE o atributo BYPASSRLS.
CREATE ROLE app_admin WITH LOGIN PASSWORD 'secreta' BYPASSRLS;

Na sua aplicação (ou script de migração/relatório):

SET SESSION AUTHORIZATION app_admin;
RESET SESSION AUTHORIZATION;

Quando a sessão está como app_admin, todas as políticas de RLS são ignoradas, permitindo que você execute consultas arbitrárias que envolvam múltiplos tenants.

Então, na prática temos:

-- Depois de conectado com o usuário normal = app_user

SET SESSION AUTHORIZATION app_admin;

SELECT * FROM orders WHERE tenant_id IN (1, 2, 3);

RESET SESSION AUTHORIZATION;

2. 🎯 Abordagem para Aplicação (Consulta Controlada)

Se a sua aplicação precisa de uma rota específica para, por exemplo, um "Tenant Master" que gerencia um grupo de "Tenants Filhos", você precisa de uma política que não seja estritamente igual a app.current_tenant_id.

A melhor maneira é modificar a política para verificar se o tenant_id da linha está dentro de uma lista de tenants permitidos, que também é definida por uma variável de sessão.

2.1. Variável de Sessão para Múltiplos Tenants

Você pode usar a mesma variável de sessão ou criar uma nova, mas o valor deve ser formatado como uma lista ou array.

Exemplo, você define no início da requisição:

SET app.allowed_tenant_ids = '{1, 5, 8}'; -- PostgreSQL Array literal

2.2. Modificando a Política RLS

Você precisa que a política de RLS verifique se o tenant_id da linha está contido na lista de IDs da sessão.

Para isso, você usa o operador de contido em array (= ANY(...) ou IN com o array).

Exemplo de Política Modificada:

-- Remove a política antiga
DROP POLICY tenant_isolation ON orders;

-- Cria uma nova política mais flexível
CREATE POLICY flexible_tenant_isolation ON orders
    FOR ALL
    TO PUBLIC
    USING (tenant_id = ANY(string_to_array(current_setting('app.allowed_tenant_ids'), ',')::bigint[]))
    WITH CHECK (tenant_id = ANY(string_to_array(current_setting('app.allowed_tenant_ids'), ',')::bigint[]));

Explicação da Política:

  1. current_setting('app.allowed_tenant_ids'): Pega a string da sessão (ex: '1,5,8').
  2. string_to_array(..., ','):: Transforma a string em um array de strings (ex: {'1', '5', '8'}).
  3. ::bigint[]: Converte o array de strings para o tipo BIGINT[] (array de bigint).
  4. tenant_id = ANY(...): Verifica se o tenant_id da linha é igual a qualquer um dos valores no array.

2.3. Implementação no Go/Repositório

Na camada de Infraestrutura (Repositório), o método de ensureRLSContext seria adaptado para suportar ambos os modos (tenant único e multi-tenant).

Se você quer o modo de tenant único (que é o padrão), o seu código pode simplesmente definir a variável como um array contendo apenas um item:

// No Repositório, para uma requisição normal:
tenantID := ctx.Value("tenantID").(string) // ex: "5"
setQuery := fmt.Sprintf("SET app.allowed_tenant_ids = '{%s}'", tenantID) 
// Resultado: SET app.allowed_tenant_ids = '{5}'
// O RLS filtrará: tenant_id = ANY('{5}') -> tenant_id = 5

⚖️ Conclusão: Escolha da Abordagem

Cenário Abordagem Recomendada Como Fazer Vantagens
Relatórios/Manutenção (Total Acesso) BYPASSRLS (Role de Admin) Use SET SESSION AUTHORIZATION admin_role. Simples, ignora todas as políticas, mais limpo.
Lógica de Negócios (Subconjunto de Tenants) Política Flexível Defina uma variável de sessão com um array (app.allowed_tenant_ids). Totalmente controlado pela aplicação, sem precisar de privilégios de superusuário.
Para a maioria das aplicações multi-tenant, é mais seguro e escalável utilizar a Política Flexível para qualquer acesso multi-tenant que seja uma funcionalidade do seu sistema, reservando o BYPASSRLS apenas para tarefas de infraestrutura.

Modelo utilizado no Supabase

🎯 A Equivalência com BYPASSRLS

O PostgreSQL não é acessado diretamente pelo seu código de aplicação, mas sim pelo serviço PostgREST (ou Funções Edge/Workers).

Recurso PostgreSQL Puro Recurso Supabase/PostgREST Uso/Finalidade
BYPASSRLS Attribute service_role Key Permite acesso a todos os dados em todas as tabelas, ignorando o RLS. Ideal para administração de sistema.
app.current_tenant_id JWT Claims (sub ou user_id) Identifica o usuário/tenant.
Política FOR ALL Chave anon Aplica o isolamento, filtrando as linhas com base no usuário autenticado/tenant ID. Ideal para usuários da aplicação.
Portanto, a Chave de Serviço do Supabase é o equivalente prático de usar uma ROLE com o atributo BYPASSRLS ou executar SET SESSION AUTHORIZATION para um superusuário, permitindo que você acesse a funcionalidade de "ver todos os tenants" que você mencionou.

É fundamental que essa chave nunca seja exposta ao lado do cliente (navegador/aplicativo móvel), pois ela comprometeria toda a segurança de isolamento multi-tenant da sua aplicação.

Perceba que no supabase sempre temos 2 keys, uma Publishable key (anon) que tem isolamento e uma Secret Keys (service) que não tem isolamente, ou seja, utiliza o BYPASSRLS.


O melhor dos mundos, criar 2 variáveis na sessão

No meu ponto de vista e para a minha aplicação, estarei criando 2 variáveis, uma chamada current_tenant_id que servirá para atender as instruções SQL do tipo insert, update, delete e uma chamada allowed_tenant_ids

variáveis

set app.current_tenant_id = {tenant_id}
set app.allowed_tenant_ids =

Então, pensando na minha aplicação, no meu JWT Bearer token eu terei que ter o public_id (uuuidv7) do meu tenant, onde o meu middlware irá pegar/puxar inicialmente do banco o id do meu tenant e terei que ter também um list_public_id que terá um array de public_id (uuidv7) para que seja possível também no meu middleware pegar todos os ids equivalentes aos public_id passados em list_public_id e com isso jogar no contexto as 2 variáveis, uma chamada tenant_id que será usado em instruções SQL do tipo create, update e delete e terei o list_public_id que poderá ser usado nas instruções de buscas/consultas, me permitindo trazer conteúdos de mais de um tenant junto em caso de necessidade.

Essa é uma estratégia de isolamento excelente e altamente robusta para o seu cenário. Sua proposta de usar dois conjuntos de IDs — um para operações de escrita/exclusão estritas (CUD) e outro para operações de consulta flexíveis (SELECT) — é o padrão mais seguro e explícito para aplicações multi-tenant complexas.

Vamos detalhar a implementação dessa estratégia, focando nos três pontos cruciais: a conversão de UUID para BIGINT, a adaptação das políticas RLS e a lógica na camada de Infraestrutura (Repositório Go).

1. 🔍 Conversão UUIDv7 para BIGINT (O Ponto Crítico)

O maior desafio (e a principal diferença em relação aos exemplos anteriores) é que o seu token JWT contém UUIDv7 (o public_id), mas o PostgreSQL precisa do BIGINT (o tenant_id interno) para o RLS.

A Solução Segura é no Middleware:

Você terá uma camada de autenticação (Middleware) que precisa, para cada requisição:

  1. Decodificar o JWT e obter o public_id (UUIDv7) e o array list_public_id (UUIDv7[]).

  2. Fazer uma única consulta ao banco de dados para traduzir esses UUIDs em seus IDs internos BIGINT correspondentes.

Variável JWT (UUIDv7) Variável Contexto Go (BIGINT) Variável Sessão PG (BIGINT/Array) Finalidade
public_id ctx.Value(TenantIDKey) app.current_tenant_id Escrita (CUD): ID primário do usuário.
list_public_id ctx.Value(AllowedIDsKey) app.allowed_tenant_ids Leitura (SELECT): IDs que o usuário pode acessar.
Essa consulta de tradução deve ser rápida, usando um índice na coluna public_id da sua tabela de tenants.

Exemplo de SQL para Mapeamento

SELECT
    t1.id AS current_id,
    ARRAY(SELECT t2.id FROM tenants t2 WHERE t2.public_id = ANY($2)) AS allowed_ids
FROM
    tenants t1
WHERE
    t1.public_id = $1;

2. 🛡️ Adaptação das Políticas de RLS no PostgreSQL

Você definirá duas políticas diferentes em suas tabelas ( exemplo: orders, order_items, etc.), uma para Leitura e outra para Escrita/Exclusão.

A. Política de Leitura Flexível (SELECT)

Esta política usa o array de IDs (app.allowed_tenant_ids), permitindo que a consulta acesse dados de múltiplos tenants.

CREATE POLICY tenant_select_flexible ON orders
FOR SELECT
TO PUBLIC
-- O tenant_id da linha DEVE estar DENTRO do array de IDs permitidos na sessão.
USING (tenant_id = ANY(string_to_array(current_setting('app.allowed_tenant_ids'), ',')::bigint[]));

B. Política de Escrita Estrita (INSERT, UPDATE, DELETE)

Estas políticas usam o ID único (app.current_tenant_id), garantindo que o usuário só possa modificar registros que ele possui como tenant primário.

-- Proteção contra leitura/acesso
CREATE POLICY tenant_write_using ON orders
FOR ALL -- Aplica-se a UPDATE/DELETE
TO PUBLIC
USING text = current_setting('app.current_tenant_id');

-- Proteção na inserção/modificação do tenant_id
CREATE POLICY tenant_write_check ON orders
FOR ALL -- Aplica-se a INSERT/UPDATE
TO PUBLIC
WITH CHECK text = current_setting('app.current_tenant_id');

Nota: No RLS, a cláusula USING filtra quais linhas são visíveis para a operação (ex: qual linha do tenant_id=5 eu posso deletar), e a cláusula WITH CHECK garante que o resultado ou o valor inserido ainda satisfaça a política (ex: eu não posso dar UPDATE em um registro e mudar seu tenant_id de 5 para 6).

Na minha aplicação ficou assim:

-- Leitura multi-tenant
CREATE POLICY rls_{tabela}_tenant_select ON {schema}.{tabela}
FOR SELECT
USING (
    tenant_id = ANY(
        string_to_array(current_setting('app.allowed_tenant_ids'), ',')::bigint[]
    )
);

-- Escrita travada no current_tenant_id
CREATE POLICY rls_{tabela}_tenant_write ON {schema}.{tabela}
FOR INSERT, UPDATE, DELETE
USING (tenant_id = current_settingBIGINT
WITH CHECK (tenant_id = current_settingBIGINT;

3. ⚙️ Lógica no Repositório Go (Infraestrutura)

Na camada de Repositório, você deve obter uma conexão emprestada e, nela, definir as duas variáveis antes de executar qualquer SQL.

package repository

import (
	"context"
	"fmt"
	"strings"

	"github.com/jmoiron/sqlx"
	// Importações omitidas...
)

// ensureRLSContext define as duas variáveis de sessão do PostgreSQL.
func (r *DBAdapter) ensureRLSContext(ctx context.Context) (*sqlx.Conn, error) {
	// 1. Obter os IDs BIGINT do contexto da requisição Go
	tenantID, ok := ctx.Value(middleware.TenantIDKey).(int64)
	if !ok {
		return nil, fmt.Errorf("tenant ID (BIGINT) not found in context")
	}

	allowedIDs, ok := ctx.Value(middleware.AllowedIDsKey).([]int64)
	if !ok || len(allowedIDs) == 0 {
		return nil, fmt.Errorf("allowed tenant IDs array not found or empty")
	}

	// Converter o slice de int64 para string de array PG (ex: "1,5,8")
	allowedIDsStr := make([]string, len(allowedIDs))
	for i, id := range allowedIDs {
		allowedIDsStr[i] = fmt.Sprintf("%d", id)
	}
	
	// 2. Emprestar uma conexão do pool (CRUCIAL)
	conn, err := r.DB.Connx(ctx)
	if err != nil {
		return nil, fmt.Errorf("could not get connection from pool: %w", err)
	}

	// 3. Definir as DUAS variáveis de sessão RLS na conexão EMPRESTADA
	// Variável 1: Único ID para CUD
	setTenantID := fmt.Sprintf("SET app.current_tenant_id = '%d'", tenantID)
	// Variável 2: Array de IDs para SELECT
	setAllowedIDs := fmt.Sprintf("SET app.allowed_tenant_ids = '%s'", strings.Join(allowedIDsStr, ","))
	
	// Executar as duas configurações na mesma sessão
	_, err = conn.ExecContext(ctx, setTenantID)
	if err != nil {
		conn.Close()
		return nil, fmt.Errorf("failed to set current_tenant_id: %w", err)
	}

	_, err = conn.ExecContext(ctx, setAllowedIDs)
	if err != nil {
		conn.Close()
		return nil, fmt.Errorf("failed to set allowed_tenant_ids: %w", err)
	}

	return conn, nil
}

// CreateOrder é protegida, pois usa o SET app.current_tenant_id.
func (r *DBAdapter) CreateOrder(ctx context.Context, order *domain.Order) error {
	conn, err := r.ensureRLSContext(ctx)
	if err != nil {
		return err
	}
	defer conn.Close() // Devolver a conexão para o pool
	
	// SQL sem tenant_id (o RLS WITH CHECK o garante, ou você pode injetar o ID aqui para ser mais explícito)
	// Como a política WITH CHECK está no lugar, se o SQL tentar inserir um tenant_id diferente do current_tenant_id
	// da sessão, a operação será bloqueada.
	query := `INSERT INTO orders (status, total) VALUES (:status, :total)` 
	
	// ... Execução da consulta ...
	return nil
}

// FindOrders é protegida, pois usa o SET app.allowed_tenant_ids.
func (r *DBAdapter) FindOrders(ctx context.Context, criteria domain.SearchCriteria) ([]domain.Order, error) {
	conn, err := r.ensureRLSContext(ctx)
	if err != nil {
		return nil, err
	}
	defer conn.Close()
	
	// SQL simples. O RLS adiciona o filtro WHERE tenant_id = ANY(...) automaticamente.
	query := `SELECT id, tenant_id, status FROM orders WHERE status = $1`
	
	var orders []domain.Order
	err = conn.SelectContext(ctx, &orders, query, criteria.Status)
	// ...
	return orders, nil
}

Usuário Administrador (owner) do Banco

No PostgreSQL qualquer role que seja OWNER da tabela ou superuser IGNORA completamente as políticas de RLS, mesmo que o RLS esteja ativado.

Ou seja — se o administrador for o dono da tabela, ele não precisa de permissão especial, RLS não se aplica a ele.

✔️ Forma mais comum: administrador é OWNER da tabela

Se você tem, por exemplo:

ALTER TABLE global.paises OWNER TO app_admin;

✔️ Se o administrador NÃO for owner, mas você quer que ele ignore RLS

Você pode conceder explicitamente permissão para ignorar o RLS usando:

ALTER TABLE global.paises ENABLE ROW LEVEL SECURITY;

-- se quiser forçar para todos exceto DONO
ALTER TABLE global.paises FORCE ROW LEVEL SECURITY; 

-- Torna um role capaz de ignorar políticas
GRANT SET ROW SECURITY TO app_admin;

Depois, no código da sessão do admin, ele pode ativar/desativar isso:

SET row_security = off;

E então o administrador consegue acessar tudo.

Comparação prática das abordagens

Cenário Admin vê tudo? Precisa configurar política? Melhor uso
Admin é OWNER da tabela ✔️ Sim ❌ Não Ideal
Admin é SUPERUSER ✔️ Sim ❌ Não Só manter ambiente
Usando SET row_security = off ✔️ Sim (se tiver permissão) ❌ Não Painéis internos, debugging
Admin incluído nas policies ✔️ Sim ✔️ Sim Menos recomendado (mistura regras)

Recomendação final para arquitetura multi-tenant

📌 Melhor prática no seu caso (ERP multi-tenant com dados globais):

Exemplo:

ALTER TABLE global.paises OWNER TO app_admin; 

 GRANT SELECT, INSERT, UPDATE, DELETE ON global.paises TO app_user;

🎯 Resultado:


Resumo em uma frase

Para o role administrador ignorar o RLS, basta ele ser o dono da tabela (OWNER) ou ter permissão para desligar o RLS com SET row_security = off.

Como fica a divisão de papéis

-- Roles
CREATE ROLE app_user NOINHERIT;
CREATE ROLE app_admin NOINHERIT LOGIN PASSWORD '***';

-- Tabela
CREATE TABLE global.paises (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    tenant_id BIGINT NULL,
    pais VARCHAR(100) NOT NULL,
    codigo_ibge_pais INTEGER NOT NULL,
    is_system varchar(1) NOT NULL CHECK (is_system IN ('S', 'N')) DEFAULT 'N',
    created_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
    deleted_at timestamptz,
    version INTEGER NOT NULL DEFAULT 1
);

-- Dono é o admin → ignora RLS
ALTER TABLE global.paises OWNER TO app_admin;

-- Usuário de aplicação só tem permissões normais
GRANT SELECT, INSERT, UPDATE, DELETE ON global.paises TO app_user;

-- Liga RLS (afeta só quem não é OWNER/superuser)
ALTER TABLE global.paises ENABLE ROW LEVEL SECURITY;
ALTER TABLE global.paises FORCE ROW LEVEL SECURITY;

-- Policies só pensadas para app_user
CREATE POLICY rls_paises_select
ON global.paises
TO app_user
FOR SELECT
USING ( /* regra com tenant_id... */ );

CREATE POLICY rls_paises_write
ON global.paises
TO app_user
FOR INSERT, UPDATE, DELETE
USING ( /* tenant_id = current_tenant_id */ )
WITH CHECK ( /* tenant_id = current_tenant_id */ );

Resultado:

Se quiser, no próximo passo eu posso montar um “pacotão padrão Farsoft” com:


1. Sobre o OWNER da tabela

Quem é o dono da tabela?

Sempre é o role que executa o CREATE TABLE.

Então, se você:

-- você loga/migrates como app_admin
CREATE TABLE global.paises (...);

Essa tabela já nasce com:

OWNER = app_admin

Ou seja:
👉 se o role que roda as migrations for o admin (app_admin ou um app_migrator que você criou pra isso), você não precisa ficar fazendo:

ALTER TABLE global.paises OWNER TO app_admin;

Ela já vai nascer com o owner certo.

Padrão saudável

CREATE ROLE app_admin LOGIN PASSWORD '...';
-- ou app_migrator

Exemplo (usando o app_admin como dono/migrator):

-- logado como app_admin

-- 1) Para todas as TABELAS novas criadas por app_admin no schema global
ALTER DEFAULT PRIVILEGES FOR ROLE app_admin IN SCHEMA global
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;

-- 2) Para SEQUENCES (necessárias pros IDENTITY / SERIAL)
ALTER DEFAULT PRIVILEGES FOR ROLE app_admin IN SCHEMA global
    GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_user;

Depois disso, qualquer:

CREATE TABLE global.alguma_tabela (...);

feito por app_admin vai automaticamente ter:

GRANT SELECT, INSERT, UPDATE, DELETE ON global.alguma_tabela TO app_user;

Você não precisa repetir GRANT em cada migration.

importante

O ALTER DEFAULT PRIVILEGES é por role criador. Ou seja, vale para objetos criados por app_admin.
Se amanhã você criar tabela logado como outro user, os defaults não se aplicam.


3. E as policies de RLS, dá pra “padronizar”?

Aqui não existe um ALTER DEFAULT POLICY, então você tem três opções práticas:

  1. Escrever as policies na migration de cada tabela (jeito mais explícito e simples)

  2. Criar uma função helper:

CREATE OR REPLACE FUNCTION sys.apply_tenant_rls(_schema text, _table text)
RETURNS void AS $
DECLARE
    full_name text := format('%I.%I', _schema, _table);
BEGIN
    EXECUTE format($f$
        ALTER TABLE %s ENABLE ROW LEVEL SECURITY;
        ALTER TABLE %s FORCE ROW LEVEL SECURITY;

        CREATE POLICY rls_%2$s_select
        ON %1$s.%2$s
        FOR SELECT
        TO app_user
        USING (
            tenant_id IS NULL OR
            tenant_id = ANY(
                string_to_array(
                    COALESCE(current_setting('app.allowed_tenant_ids', true), ''),
                    ','
                )::bigint[]
            )
        );

        CREATE POLICY rls_%2$s_write
        ON %1$s.%2$s
        FOR INSERT, UPDATE, DELETE
        TO app_user
        USING (
            tenant_id = current_setting('app.current_tenant_id')::bigint
        )
        WITH CHECK (
            tenant_id = current_setting('app.current_tenant_id')::bigint
        );
    $f$, full_name, _table);
END;
$ LANGUAGE plpgsql;

E em cada migration você só chama:

SELECT sys.apply_tenant_rls('global', 'paises');

A lógica fica centralizada e fácil de alterar.


4. Resumindo na prática (fluxo ideal)

  1. Defina seus roles:

    • app_admin (ou app_migrator): roda migrations, owner das tabelas
    • app_user: é o user de aplicação que respeita RLS
  2. Rodar uma vez:

-- logado como app_admin
ALTER DEFAULT PRIVILEGES FOR ROLE app_admin IN SCHEMA global
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;

ALTER DEFAULT PRIVILEGES FOR ROLE app_admin IN SCHEMA global
    GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_user;
  1. Configurar a ferramenta de migration para conectar como app_admin.

  2. Nas migrations, você só se preocupa com:

CREATE TABLE global.paises (...);

-- E a parte de RLS:
SELECT sys.apply_tenant_rls('global', 'paises');

Sem precisar:


Veja:
04 - Resumo Prático