Regras de Segurança (RLS)
Visão Geral
Seção intitulada “Visão Geral”As regras de segurança permitem definir políticas de Row Level Security (RLS) para suas tabelas do PostgreSQL diretamente nas definições de coleção. Quando o schema do Drizzle é gerado, o Rebase cria as instruções CREATE POLICY correspondentes.
import { defineCollection } from "@rebasepro/cms-types";const postsCollection = defineCollection({ slug: "posts", name: "Posts", table: "posts", properties: { /* ... */ }, securityRules: [ { operation: "select", access: "public" }, { operations: ["insert", "update", "delete"], ownerField: "authorId" } ]});Como Funciona
Seção intitulada “Como Funciona”- Você define
securityRulesem uma coleção rebase schema generatecria o schema do Drizzle com RLS habilitadorebase db pushourebase db migrateaplica as políticas ao PostgreSQL- Cada consulta é filtrada automaticamente pelo contexto do usuário atual
A identidade do usuário autenticado está disponível no SQL através de:
| Função | Retorno |
|---|---|
rebase.uid() |
O ID do usuário atual |
rebase.roles() |
IDs das roles da aplicação separados por vírgula |
rebase.jwt() |
Claims completos do JWT como JSONB |
Estes são definidos automaticamente por transação pelo backend do Rebase.
Policies que uma coleção recebe sem que você as escreva
Seção intitulada “Policies que uma coleção recebe sem que você as escreva”As policies de uma tabela são mais do que as securityRules da própria coleção:
defaultSecurityRules, exportado deconfig/collections/index.ts. Uma coleção cujosecurityRulesestá ausente ou vazio recebe todas elas no lugar, como se as tivesse escrito. Uma coleção que declara ao menos uma regra própria não recebe nenhuma: as duas listas nunca são mescladas. Elas são aplicadas onde quer que o diretório seja lido:rebase dev,rebase db push, o painel de administração e um backend que recebecollectionsDir. Desde 0.25 O índice do scaffold também as aplica ao arraycollectionsque ele exporta, então um backend que recebe esse array também as recebe. Um array montado de qualquer outra forma carrega apenas as regras que suas coleções declaram.- A base do admin. Toda coleção também recebe
<table>_default_admin_reade<table>_default_admin_write_insert,_updatee_delete: policies permissivas que deixam o contexto do servidor e a roleadminler e escrever todas as linhas. Elas ficam ao lado das regras que a coleção acabar tendo, as próprias ou as padrões herdadas, e as policies permissivas são combinadas com OR, então ampliam ambas. A coleção de usuários recebe ainda uma leitura do próprio registro e um portão de escrita restritivo que mantémrolesexclusivo do admin. - Multi-tenancy. Uma coleção com escopo de tenant recebe uma regra restritiva que confina cada operação ao tenant do chamador; o contexto do servidor passa por ela.
disableDefaultPolicies: true em uma coleção remove a base permissiva
e mantém os portões restritivos. É por coleção: defaultSecurityRules
não carrega esse sinalizador, então uma coleção que herda os padrões mantém a
base a menos que defina o sinalizador ela mesma. O DDL gerado marca cada
policy injetada, e rebase db push recoloca uma removida à mão.
Atalhos de Conveniência
Seção intitulada “Atalhos de Conveniência”Acesso Baseado em Proprietário (Owner)
Seção intitulada “Acesso Baseado em Proprietário (Owner)”O padrão mais simples — os usuários só podem acessar as linhas das quais são proprietários:
securityRules: [ { operation: "all", ownerField: "user_id" }]Isso gera: USING ((user_id)::text = (SELECT rebase.uid())) — veja
Avaliado uma vez por instrução para o SELECT.
Acesso Público
Seção intitulada “Acesso Público”Permitir que qualquer pessoa (incluindo usuários não autenticados) leia:
securityRules: [ { operation: "select", access: "public" }]Isso gera: USING (true)
Acesso Autenticado
Seção intitulada “Acesso Autenticado”Permite qualquer usuário conectado (signed-in). Este é um condition em vez de um atalho access — access tem exatamente um valor, "public" — porque “conectado” é um teste contra o chamador, e o construtor é onde ficam os testes contra o chamador:
import { policy } from "@rebasepro/types";
securityRules: [ { operation: "select", condition: policy.authenticated() }]policy.authenticated() também é verdadeiro para o login anônimo (sign-in), que gera uma linha de usuário real e uma sessão real. Use policy.registered() onde um convidado (guest) não deve se qualificar — escrever uma avaliação, ingressar em uma organização, gastar dinheiro.
Acesso Baseado em Funções (Role-based)
Seção intitulada “Acesso Baseado em Funções (Role-based)”Restringir operações a roles específicas:
securityRules: [ { operation: "all", roles: ["admin"] }, { operation: "select", roles: ["editor", "viewer"] }]Acesso Relacional / Associação (Membership)
Seção intitulada “Acesso Relacional / Associação (Membership)”Para delimitar o acesso pela associação em uma coleção relacionada — por exemplo, “apenas linhas cujo time o chamador pertence” — use a condition estruturada com policy.existsIn. Ela compila para uma única subconsulta EXISTS correlacionada (sem buscas por linha) e é a alternativa segura e de primeira classe para escrever o SQL puro manualmente mostrado abaixo.
import { policy } from "@rebasepro/types";
// documents visible only to members of the document's team:securityRules: [ { operation: "select", condition: policy.existsIn({ collection: "team_members", // the join / membership collection where: policy.and( // correlate to the row being checked: policy.compare(policy.field("team_id"), "eq", policy.outerField("team_id")), // …and to the caller: policy.compare(policy.field("user_id"), "eq", policy.authUid()), ), }), },]Dentro de where, policy.field(...) refere-se a uma coluna da coleção associada (team_members), enquanto policy.outerField(...) refere-se a uma coluna da linha que está sendo verificada (documents). Combine com policy.authUid() para delimitar ao usuário atual. Como é imposto pelo banco de dados, a interface administrativa trata isso como de autoridade do servidor (server-authoritative).
O construtor policy, completo
Seção intitulada “O construtor policy, completo”Importado de @rebasepro/types. Expressões se compõem; operandos são as folhas.
| Expressão | Compila para |
|---|---|
policy.true() / policy.false() |
true / false |
policy.and(…) / policy.or(…) |
conjunção / disjunção |
policy.not(e) |
negação |
policy.compare(left, op, right) |
uma comparação entre dois operandos |
policy.rolesOverlap(roles) |
o chamador possui qualquer uma destas roles da aplicação |
policy.rolesContain(roles) |
o chamador possui todas estas roles da aplicação |
policy.authenticated() |
conectado — rebase.uid() está definido e não é uma sentinela anônima. IS NOT NULL sozinho seria uma tautologia, já que uma requisição anônima define uma sentinela em vez de deixá-la indefinida |
policy.registered() |
conectado com uma conta — authenticated() e não um convidado. Veja abaixo |
policy.serverContext() |
rebase.uid() IS NULL — veja o aviso abaixo |
policy.existsIn({ collection, where }) |
uma subconsulta EXISTS correlacionada |
policy.raw(sql) |
uma válvula de escape, inserida literalmente |
| Operando | Significado |
|---|---|
policy.field(name) |
uma coluna da coleção sendo verificada — ou, dentro de existsIn, da coleção associada |
policy.outerField(name) |
dentro de existsIn, uma coluna da linha externa |
policy.literal(value) |
uma string, número, booleano ou null |
policy.authUid() |
rebase.uid() |
policy.authRoles() |
rebase.roles() |
Avaliado uma vez por instrução
Seção intitulada “Avaliado uma vez por instrução”Toda parte de uma regra que não lê a linha — o id do chamador, uma verificação de
roles, uma claim, serverContext() — é compilada dentro de uma subquery escalar. Uma regra de
proprietário vira (user_id)::text = (SELECT rebase.uid()), e a leitura padrão de admin
vira
(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin'])).
O Postgres calcula uma subquery dessas uma vez por instrução (statement; um initPlan) em vez de uma vez por linha, e planeja como se a verificação deixasse passar metade da tabela em vez de 1,5% dela. Essa estimativa é o que mantém uma página profunda de uma tabela grande na sua chave primária: sem ela, a página 401 de um milhão de linhas era uma varredura sequencial e uma ordenação de 640 MB. Quem pode ler ou gravar o quê não muda.
O SQL puro de using / withCheck recebe o mesmo tratamento quando o Rebase consegue ler a sua
estrutura (owner_id = rebase.uid() consegue). Qualquer outra coisa é inserida como
foi escrita, então envolva você mesmo as chamadas: (SELECT rebase.uid()).
authenticated() e registered()
Seção intitulada “authenticated() e registered()”Duas coisas diferentes são chamadas de anônimas, e vale a pena ser preciso sobre qual delas uma regra se refere.
Uma requisição não autenticada não carrega nenhuma sessão. Ela recebe um ID sentinela para que rebase.uid() nunca seja NULL no fluxo do usuário, e policy.authenticated() a exclui — é isso que faz com que signifique “conectado” em vez de “qualquer pessoa”.
Um convidado (guest) é a outra coisa: uma sessão sem ninguém por trás dela. POST /auth/anonymous cria uma linha de usuário real com um uid real, então um convidado passa em todos os testes que verificam o id. Esse é o objetivo do recurso — um carrinho antes do checkout, um rascunho antes do cadastro — e isso significa que authenticated() é verdadeiro para qualquer pessoa que clicou em Continuar como convidado, o que não exige e-mail, nem senha e nenhum aceite de termos.
policy.registered() é authenticated() mais “não é um convidado”. Use-o sempre que uma regra for sobre uma pessoa que possa ser responsabilizada por algo: escrever uma avaliação, ingressar em uma organização, gastar dinheiro. Recorra a authenticated() onde um convidado for genuinamente bem-vindo.
// Anyone with a session, guests included — a draft cart.{ operation: "insert", check: policy.authenticated() }
// Someone with an account.{ operation: "insert", check: policy.registered() }Nos bastidores, a flag de convidado viaja com a sessão — ela está no token de acesso e chega ao banco de dados como rebase.is_anonymous() —, portanto, uma política pode fazer a verificação sem uma consulta extra. Um banco de dados atendido por um servidor antigo demais para defini-la lê cada sessão como uma conta, que é o comportamento que a implantação já possuía.
Expressões SQL Puras
Seção intitulada “Expressões SQL Puras”Para lógicas complexas, use using e withCheck:
securityRules: [ { operation: "select", using: "EXISTS (SELECT 1 FROM org_members WHERE org_members.org_id = {org_id} AND org_members.user_id = rebase.uid())" }]using— Filtra quais linhas existentes são visíveis (aplica-se a SELECT, UPDATE, DELETE)withCheck— Valida novos valores de linha (aplica-se a INSERT, UPDATE)
As referências a colunas usam a sintaxe {column_name}, que é resolvida para a coluna totalmente qualificada com a tabela.
Combinando Atalhos e SQL
Seção intitulada “Combinando Atalhos e SQL”Misture atalhos de conveniência com SQL puro:
securityRules: [ // Admins can do anything { operation: "all", roles: ["admin"], using: "true" }, // Regular users can only see their own rows { operation: "select", ownerField: "user_id" }, // Users can insert, but only for themselves { operation: "insert", withCheck: "{user_id} = rebase.uid()" }, // Locked rows cannot be updated { operation: "update", mode: "restrictive", using: "{is_locked} = false" }]Permissivo vs Restritivo
Seção intitulada “Permissivo vs Restritivo”O PostgreSQL possui dois modos de política:
- Permissivo (padrão) — Múltiplas políticas permissivas são combinadas com OR. Se qualquer uma passar, o acesso é concedido.
- Restritivo — Políticas restritivas são combinadas com AND. Todas devem passar.
securityRules: [ // Permissive: owners can access their rows { operation: "all", ownerField: "user_id" }, // Restrictive: but locked rows cannot be updated { operation: "update", mode: "restrictive", using: "{is_locked} = false", withCheck: "{is_locked} = false" }]Operações
Seção intitulada “Operações”| Operação | Equivalente em SQL | Descrição |
|---|---|---|
"select" |
SELECT |
Ler linhas |
"insert" |
INSERT |
Criar novas linhas |
"update" |
UPDATE |
Modificar linhas existentes |
"delete" |
DELETE |
Remover linhas |
"all" |
Todas as anteriores | Abreviação para todas as operações |
Você também pode usar operations (plural) para aplicar uma regra a múltiplas operações:
{ operations: ["insert", "update", "delete"], ownerField: "authorId" }Interface Completa de SecurityRule
Seção intitulada “Interface Completa de SecurityRule”SecurityRule é uma union, não um único objeto aberto: uma regra escolhe exatamente uma forma de expressar seu predicado, e as outras são tipadas como never, de modo que misturá-las gera um erro de compilação em vez de uma política que ignora silenciosamente metade do que você escreveu.
// Shared by every variantinterface SecurityRuleBase { name?: string; // Policy name. Omit it and one is derived operation?: SecurityOperation; // "select" | "insert" | "update" | "delete" | "all" operations?: SecurityOperation[]; // …or several at once mode?: "permissive" | "restrictive"; // Default: "permissive" roles?: string[]; // App roles, via rebase.roles() pgRoles?: string[]; // Native Postgres roles — the CREATE POLICY `TO` clause. // NOT the same as `roles`. Default: ["public"]}
// …plus exactly one of:{ ownerField: string } // <column> = rebase.uid(){ access: "public" } // the one shortcut — "no row filter"{ condition: PolicyExpression; // the structured builder — `policy.*` check?: PolicyExpression } // defaults to `condition`, as Postgres does{ using?: string; withCheck?: string } // raw SQLroles e pgRoles são as duas que geram confusão. roles é uma role da aplicação, imposta dentro da cláusula USING / WITH CHECK através de rebase.roles(). pgRoles é uma role do banco de dados e controla a quais conexões a política está vinculada. Quase todos os projetos precisam de roles.
Exemplos
Seção intitulada “Exemplos”Plataforma de Blog
Seção intitulada “Plataforma de Blog”securityRules: [ // Anyone can read published posts { operation: "select", using: "{status} = 'published'" }, // Authors can see their own drafts { operation: "select", ownerField: "authorId" }, // Authors can create and edit their own posts { operations: ["insert", "update"], ownerField: "authorId" }, // Only admins can delete { operation: "delete", roles: ["admin"] }]SaaS Multi-Tenant
Seção intitulada “SaaS Multi-Tenant”securityRules: [ { operation: "all", using: "EXISTS (SELECT 1 FROM org_members WHERE org_members.org_id = {org_id} AND org_members.user_id = rebase.uid())" }]Acesso Anônimo (Inserções Públicas)
Seção intitulada “Acesso Anônimo (Inserções Públicas)”Uma necessidade comum é permitir que usuários não autenticados enviem dados — formulários de contato, inscrições em newsletters, candidaturas públicas. O Rebase oferece um padrão limpo para isso.
Recomendado: uma regra withCheck pura
Seção intitulada “Recomendado: uma regra withCheck pura”import { defineCollection } from "@rebasepro/cms-types";
const contactMessagesCollection = defineCollection({ slug: "contact_messages", name: "Contact Messages", table: "contact_messages", securityRules: [ // Anyone can submit a contact message { operation: "insert", // A raw rule carries `using` (which rows are visible) and `withCheck` // (what a write must satisfy); an insert only exercises the latter. using: "true", withCheck: "true" }, // Only admins can read, update, or delete messages { operations: ["select", "update", "delete"], roles: ["admin"] } ], properties: { email: { name: "Email", type: "string" } }});O atalho access: "public" gera uma política que permite a operação sem exigir autenticação.
Para Captura de Leads / Cadastros
Seção intitulada “Para Captura de Leads / Cadastros”import { defineCollection } from "@rebasepro/cms-types";
const leadSignupsCollection = defineCollection({ slug: "lead_magnet_signups", name: "Lead Magnet Signups", table: "lead_magnet_signups", securityRules: [ // Allow anonymous inserts { operation: "insert", using: "true", withCheck: "true" }, // Admins can view all signups { operation: "select", roles: ["admin"] } ], properties: { email: { name: "Email", type: "string" } }});Como Funcionam as Requisições Anônimas
Seção intitulada “Como Funcionam as Requisições Anônimas”Quando uma requisição chega sem um token JWT, o backend do Rebase define as variáveis de sessão do PostgreSQL como:
| Variável | Valor |
|---|---|
app.user_id |
'anonymous' |
app.user_roles |
'' (vazio) |
Isso significa que:
rebase.uid()retorna'anonymous'rebase.roles()retorna uma string vazia- As políticas
access: "public"passam porque geramUSING (true)/WITH CHECK (true) - As condições
policy.authenticated()falham porque verificam a existência de um ID de usuário real - As políticas
ownerFieldfalham porque nenhuma linha teráuser_id = 'anonymous'(a menos que explicitamente definido)
Avançado: SQL Puro para Anônimos
Seção intitulada “Avançado: SQL Puro para Anônimos”Se precisar de um controle mais granular, use SQL puro:
securityRules: [ { operation: "insert", withCheck: "rebase.uid() = 'anonymous' OR rebase.uid() IS NOT NULL" }]Linhas aqui, campos ao lado
Seção intitulada “Linhas aqui, campos ao lado”As regras de segurança respondem a uma pergunta: a quais linhas esse chamador tem acesso. Elas são aplicadas pelo próprio Postgres, em cada instrução, independentemente da rota — e é por isso que são o modelo de autorização e tudo acima delas é conveniência.
Elas não interferem nas colunas de uma linha que o chamador acessa. Uma política que permite a um funcionário ler as linhas da sua equipe permite que ele leia todos os campos dessas linhas, salário incluído. É para isso que serve o access por propriedade:
salary: { type: "number", // Everyone the rules above let read the row; only HR gets this column. access: { read: ["hr"], write: [] }}Os dois se sobrepõem e nunca se contradizem: uma regra de campo não pode ampliar o acesso a linhas, e uma linha que você não pode ler não tem campos dos quais falar. As roles são as mesmas roles — rebase.roles() dentro de uma política, user.roles na requisição —, portanto, rolesOverlap(['hr']) em uma regra e access: { read: ["hr"] } em uma propriedade significam o mesmo hr. As regras de campo são aplicadas pelo servidor e não pelo Postgres, cobrindo assim a superfície da API; uma consulta executada via rebase.sql() vê todas as colunas, exatamente da mesma forma que ignora o RLS.
Próximos Passos
Seção intitulada “Próximos Passos”- Acesso a campos — Roles de leitura/gravação por campo
- Relações — Chaves estrangeiras e joins
- Callbacks de Entidade — Hooks de ciclo de vida
- Funções Customizadas — Endpoints de API customizados