@luanpdd/kit-mcp 1.30.2 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +168 -168
- package/gates/agent-no-recursive-dispatch.md +84 -82
- package/kit/COMANDOS.md +138 -138
- package/kit/README.md +76 -76
- package/kit/agents/advisor-researcher.md +107 -106
- package/kit/agents/ai-mutation-tester.md +1 -0
- package/kit/agents/assumptions-analyzer.md +108 -107
- package/kit/agents/audit-log-implementer.md +314 -313
- package/kit/agents/auditor-consistencia-isolamento.md +414 -413
- package/kit/agents/b2b-saas-architect.md +157 -156
- package/kit/agents/burn-rate-forecaster.md +1 -0
- package/kit/agents/cascading-failures-auditor.md +299 -298
- package/kit/agents/codebase-mapper.md +769 -768
- package/kit/agents/crm-pipeline-implementer.md +257 -256
- package/kit/agents/debugger.md +814 -813
- package/kit/agents/detector-tenant-quente.md +338 -337
- package/kit/agents/evolution-go-integrator.md +201 -200
- package/kit/agents/example-reviewer.md +22 -21
- package/kit/agents/executor.md +565 -564
- package/kit/agents/golden-signals-instrumenter.md +1 -0
- package/kit/agents/incident-investigator.md +1 -0
- package/kit/agents/integration-checker.md +201 -200
- package/kit/agents/invite-flow-implementer.md +190 -189
- package/kit/agents/legacy-characterizer.md +369 -368
- package/kit/agents/lgpd-compliance-auditor.md +296 -295
- package/kit/agents/load-shedding-instrumenter.md +1 -0
- package/kit/agents/multi-tenant-isolation-auditor.md +254 -253
- package/kit/agents/multi-tenant-rls-writer.md +341 -340
- package/kit/agents/nyquist-auditor.md +179 -178
- package/kit/agents/observability-coverage-auditor.md +316 -315
- package/kit/agents/observability-instrumenter.md +1 -0
- package/kit/agents/omm-auditor.md +1 -0
- package/kit/agents/org-onboarding-implementer.md +224 -223
- package/kit/agents/payload-capture-instrumenter.md +274 -273
- package/kit/agents/phase-researcher.md +697 -696
- package/kit/agents/plan-checker.md +273 -272
- package/kit/agents/planner.md +923 -922
- package/kit/agents/postmortem-writer.md +1 -0
- package/kit/agents/project-researcher.md +653 -652
- package/kit/agents/prr-conductor.md +1 -0
- package/kit/agents/refactor-safety-auditor.md +405 -404
- package/kit/agents/release-pipeline-auditor.md +1 -0
- package/kit/agents/research-synthesizer.md +246 -245
- package/kit/agents/roadmapper.md +678 -677
- package/kit/agents/schema-checker.md +1 -0
- package/kit/agents/seam-finder.md +360 -359
- package/kit/agents/shotgun-surgery-detector.md +350 -349
- package/kit/agents/slo-engineer.md +1 -0
- package/kit/agents/storytelling-analyst.md +1 -0
- package/kit/agents/supabase-architect.md +1 -0
- package/kit/agents/supabase-auth-bootstrapper.md +1 -0
- package/kit/agents/supabase-branching-architect.md +563 -562
- package/kit/agents/supabase-cicd-pipeline-implementer.md +778 -777
- package/kit/agents/supabase-column-privileges-writer.md +400 -399
- package/kit/agents/supabase-edge-fn-tester.md +2 -1
- package/kit/agents/supabase-edge-fn-writer.md +2 -1
- package/kit/agents/supabase-migration-writer.md +386 -385
- package/kit/agents/supabase-rbac-implementer.md +393 -392
- package/kit/agents/supabase-realtime-implementer.md +364 -363
- package/kit/agents/supabase-rls-hardener.md +522 -521
- package/kit/agents/supabase-rls-writer.md +324 -323
- package/kit/agents/supabase-roles-implementer.md +356 -355
- package/kit/agents/supabase-storage-implementer.md +1 -0
- package/kit/agents/super-admin-implementer.md +282 -281
- package/kit/agents/toil-auditor.md +1 -0
- package/kit/agents/ui-auditor.md +438 -437
- package/kit/agents/ui-checker.md +303 -302
- package/kit/agents/ui-researcher.md +356 -355
- package/kit/agents/user-profiler.md +176 -175
- package/kit/agents/validador-evolucao-schema.md +336 -335
- package/kit/agents/verifier.md +729 -728
- package/kit/commands/adicionar-backlog.md +75 -75
- package/kit/commands/adicionar-fase.md +42 -42
- package/kit/commands/adicionar-tarefa.md +45 -45
- package/kit/commands/adicionar-testes.md +41 -41
- package/kit/commands/ajuda.md +21 -21
- package/kit/commands/atualizar.md +37 -37
- package/kit/commands/auditar-cascading.md +111 -111
- package/kit/commands/auditar-marco.md +179 -179
- package/kit/commands/auditar-observabilidade-cobertura.md +183 -183
- package/kit/commands/auditar-refactor.md +219 -219
- package/kit/commands/auditar-release.md +109 -109
- package/kit/commands/auditar-uat.md +23 -23
- package/kit/commands/autonomo.md +40 -40
- package/kit/commands/branch-pr.md +24 -24
- package/kit/commands/burn-rate-status.md +408 -408
- package/kit/commands/capturar-payloads.md +193 -193
- package/kit/commands/caracterizar.md +212 -212
- package/kit/commands/concluir-marco.md +247 -247
- package/kit/commands/configuracoes.md +36 -36
- package/kit/commands/dados-distribuidos.md +188 -188
- package/kit/commands/definir-perfil.md +10 -10
- package/kit/commands/depurar.md +190 -190
- package/kit/commands/detectar-duplicacao.md +197 -197
- package/kit/commands/discutir-fase.md +131 -131
- package/kit/commands/encontrar-seams.md +136 -136
- package/kit/commands/entrar-discord.md +17 -17
- package/kit/commands/estatisticas.md +18 -18
- package/kit/commands/example-greeting.md +33 -33
- package/kit/commands/executar-fase.md +58 -58
- package/kit/commands/expresso.md +56 -56
- package/kit/commands/fase-ui.md +34 -34
- package/kit/commands/fazer.md +57 -57
- package/kit/commands/fio.md +125 -125
- package/kit/commands/fluxos-trabalho.md +64 -64
- package/kit/commands/forense.md +176 -176
- package/kit/commands/gerenciador.md +38 -38
- package/kit/commands/inserir-fase.md +31 -31
- package/kit/commands/legacy.md +263 -263
- package/kit/commands/limpeza.md +17 -17
- package/kit/commands/listar-hipoteses-fase.md +45 -45
- package/kit/commands/listar-workspaces.md +18 -18
- package/kit/commands/load-shedding.md +117 -117
- package/kit/commands/mapear-codebase.md +70 -70
- package/kit/commands/multi-tenant.md +163 -163
- package/kit/commands/nota.md +33 -33
- package/kit/commands/novo-marco.md +43 -43
- package/kit/commands/novo-projeto.md +41 -41
- package/kit/commands/novo-workspace.md +43 -43
- package/kit/commands/pausar-trabalho.md +37 -37
- package/kit/commands/perfil-usuario.md +45 -45
- package/kit/commands/pesquisar-fase.md +195 -195
- package/kit/commands/planejar-fase.md +67 -67
- package/kit/commands/planejar-lacunas.md +33 -33
- package/kit/commands/plantar-ideia.md +25 -25
- package/kit/commands/progresso.md +24 -24
- package/kit/commands/proximo.md +30 -30
- package/kit/commands/publicar.md +490 -490
- package/kit/commands/rapido.md +35 -35
- package/kit/commands/reaplicar-patches.md +124 -124
- package/kit/commands/refactor-seguro.md +321 -321
- package/kit/commands/relatorio-sessao.md +19 -19
- package/kit/commands/remover-fase.md +31 -31
- package/kit/commands/remover-workspace.md +26 -26
- package/kit/commands/resumo-marco.md +50 -50
- package/kit/commands/retomar-trabalho.md +40 -40
- package/kit/commands/revisar-backlog.md +60 -60
- package/kit/commands/revisar-ui.md +32 -32
- package/kit/commands/revisar.md +37 -37
- package/kit/commands/saude.md +21 -21
- package/kit/commands/setup-notion.md +93 -93
- package/kit/commands/storytelling.md +179 -179
- package/kit/commands/sync-main.md +68 -68
- package/kit/commands/validar-fase.md +35 -35
- package/kit/commands/verificar-tarefas.md +44 -44
- package/kit/commands/verificar-trabalho.md +64 -64
- package/kit/file-manifest.json +82 -81
- package/kit/framework/bin/lib/commands.cjs +959 -959
- package/kit/framework/bin/lib/config.cjs +442 -442
- package/kit/framework/bin/lib/core.cjs +1230 -1230
- package/kit/framework/bin/lib/frontmatter.cjs +336 -336
- package/kit/framework/bin/lib/init.cjs +1442 -1442
- package/kit/framework/bin/lib/milestone.cjs +252 -252
- package/kit/framework/bin/lib/model-profiles.cjs +68 -68
- package/kit/framework/bin/lib/phase.cjs +888 -888
- package/kit/framework/bin/lib/profile-output.cjs +952 -952
- package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
- package/kit/framework/bin/lib/roadmap.cjs +329 -329
- package/kit/framework/bin/lib/security.cjs +382 -382
- package/kit/framework/bin/lib/state.cjs +1031 -1031
- package/kit/framework/bin/lib/template.cjs +222 -222
- package/kit/framework/bin/lib/uat.cjs +282 -282
- package/kit/framework/bin/lib/verify.cjs +888 -888
- package/kit/framework/bin/lib/workstream.cjs +491 -491
- package/kit/framework/bin/tools.cjs +918 -918
- package/kit/framework/commands/workstreams.md +63 -63
- package/kit/framework/references/checkpoints.md +778 -778
- package/kit/framework/references/continuation-format.md +249 -249
- package/kit/framework/references/decimal-phase-calculation.md +64 -64
- package/kit/framework/references/git-integration.md +295 -295
- package/kit/framework/references/git-planning-commit.md +38 -38
- package/kit/framework/references/model-profile-resolution.md +36 -36
- package/kit/framework/references/model-profiles.md +139 -139
- package/kit/framework/references/phase-argument-parsing.md +61 -61
- package/kit/framework/references/planning-config.md +202 -202
- package/kit/framework/references/questioning.md +162 -162
- package/kit/framework/references/tdd.md +263 -263
- package/kit/framework/references/ui-brand.md +160 -160
- package/kit/framework/references/user-profiling.md +657 -657
- package/kit/framework/references/verification-patterns.md +612 -612
- package/kit/framework/references/workstream-flag.md +58 -58
- package/kit/framework/templates/DEBUG.md +164 -164
- package/kit/framework/templates/UAT.md +265 -265
- package/kit/framework/templates/UI-SPEC.md +100 -100
- package/kit/framework/templates/VALIDATION.md +76 -76
- package/kit/framework/templates/claude-md.md +122 -122
- package/kit/framework/templates/codebase/architecture.md +185 -185
- package/kit/framework/templates/codebase/concerns.md +205 -205
- package/kit/framework/templates/codebase/conventions.md +204 -204
- package/kit/framework/templates/codebase/integrations.md +192 -192
- package/kit/framework/templates/codebase/stack.md +158 -158
- package/kit/framework/templates/codebase/structure.md +199 -199
- package/kit/framework/templates/codebase/testing.md +301 -301
- package/kit/framework/templates/config.json +44 -44
- package/kit/framework/templates/context.md +352 -352
- package/kit/framework/templates/continue-here.md +78 -78
- package/kit/framework/templates/copilot-instructions.md +7 -7
- package/kit/framework/templates/debug-subagent-prompt.md +91 -91
- package/kit/framework/templates/dev-preferences.md +20 -20
- package/kit/framework/templates/discovery.md +146 -146
- package/kit/framework/templates/discussion-log.md +63 -63
- package/kit/framework/templates/milestone-archive.md +123 -123
- package/kit/framework/templates/milestone.md +115 -115
- package/kit/framework/templates/phase-prompt.md +610 -610
- package/kit/framework/templates/planner-subagent-prompt.md +117 -117
- package/kit/framework/templates/project.md +186 -186
- package/kit/framework/templates/requirements.md +231 -231
- package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
- package/kit/framework/templates/research-project/FEATURES.md +147 -147
- package/kit/framework/templates/research-project/PITFALLS.md +200 -200
- package/kit/framework/templates/research-project/STACK.md +120 -120
- package/kit/framework/templates/research-project/SUMMARY.md +170 -170
- package/kit/framework/templates/research.md +419 -419
- package/kit/framework/templates/retrospective.md +54 -54
- package/kit/framework/templates/roadmap.md +202 -202
- package/kit/framework/templates/state.md +176 -176
- package/kit/framework/templates/summary-complex.md +59 -59
- package/kit/framework/templates/summary-minimal.md +41 -41
- package/kit/framework/templates/summary-standard.md +48 -48
- package/kit/framework/templates/summary.md +209 -209
- package/kit/framework/templates/user-profile.md +146 -146
- package/kit/framework/templates/user-setup.md +256 -256
- package/kit/framework/templates/verification-report.md +258 -258
- package/kit/framework/workflows/add-phase.md +112 -112
- package/kit/framework/workflows/add-tests.md +351 -351
- package/kit/framework/workflows/add-todo.md +158 -158
- package/kit/framework/workflows/audit-milestone.md +340 -340
- package/kit/framework/workflows/audit-uat.md +109 -109
- package/kit/framework/workflows/autonomous.md +891 -891
- package/kit/framework/workflows/check-todos.md +177 -177
- package/kit/framework/workflows/cleanup.md +152 -152
- package/kit/framework/workflows/complete-milestone.md +696 -696
- package/kit/framework/workflows/diagnose-issues.md +231 -231
- package/kit/framework/workflows/discovery-phase.md +289 -289
- package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
- package/kit/framework/workflows/discuss-phase.md +784 -784
- package/kit/framework/workflows/do.md +104 -104
- package/kit/framework/workflows/execute-phase.md +838 -838
- package/kit/framework/workflows/execute-plan.md +510 -510
- package/kit/framework/workflows/fast.md +102 -102
- package/kit/framework/workflows/forensics.md +265 -265
- package/kit/framework/workflows/health.md +181 -181
- package/kit/framework/workflows/help.md +619 -619
- package/kit/framework/workflows/insert-phase.md +130 -130
- package/kit/framework/workflows/list-phase-assumptions.md +178 -178
- package/kit/framework/workflows/list-workspaces.md +56 -56
- package/kit/framework/workflows/manager.md +362 -362
- package/kit/framework/workflows/map-codebase.md +377 -377
- package/kit/framework/workflows/milestone-summary.md +223 -223
- package/kit/framework/workflows/new-milestone.md +486 -486
- package/kit/framework/workflows/new-project.md +1159 -1159
- package/kit/framework/workflows/new-workspace.md +237 -237
- package/kit/framework/workflows/next.md +97 -97
- package/kit/framework/workflows/node-repair.md +92 -92
- package/kit/framework/workflows/note.md +156 -156
- package/kit/framework/workflows/pause-work.md +176 -176
- package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
- package/kit/framework/workflows/plan-phase.md +765 -765
- package/kit/framework/workflows/plant-seed.md +169 -169
- package/kit/framework/workflows/pr-branch.md +129 -129
- package/kit/framework/workflows/profile-user.md +450 -450
- package/kit/framework/workflows/progress.md +507 -507
- package/kit/framework/workflows/quick.md +757 -757
- package/kit/framework/workflows/remove-phase.md +155 -155
- package/kit/framework/workflows/remove-workspace.md +90 -90
- package/kit/framework/workflows/research-phase.md +82 -82
- package/kit/framework/workflows/resume-project.md +326 -326
- package/kit/framework/workflows/review.md +228 -228
- package/kit/framework/workflows/session-report.md +146 -146
- package/kit/framework/workflows/settings.md +283 -283
- package/kit/framework/workflows/ship.md +228 -228
- package/kit/framework/workflows/stats.md +60 -60
- package/kit/framework/workflows/transition.md +671 -671
- package/kit/framework/workflows/ui-phase.md +302 -302
- package/kit/framework/workflows/ui-review.md +165 -165
- package/kit/framework/workflows/update.md +323 -323
- package/kit/framework/workflows/validate-phase.md +174 -174
- package/kit/framework/workflows/verify-phase.md +252 -252
- package/kit/framework/workflows/verify-work.md +637 -637
- package/kit/hooks/check-update.js +118 -118
- package/kit/hooks/context-monitor.js +163 -163
- package/kit/hooks/kit-attribution-reminder.cjs +29 -50
- package/kit/hooks/kit-router.cjs +137 -0
- package/kit/hooks/prompt-guard.js +103 -103
- package/kit/hooks/statusline.js +125 -125
- package/kit/hooks/workflow-guard.js +101 -101
- package/kit/settings.json +45 -45
- package/kit/skills/ai-prompt-characterization/SKILL.md +335 -335
- package/kit/skills/armadilhas-sistemas-distribuidos/SKILL.md +447 -447
- package/kit/skills/audit-log-multi-tenant/SKILL.md +340 -340
- package/kit/skills/b2b-saas-architecture/SKILL.md +300 -300
- package/kit/skills/consistencia-leitura-replica/SKILL.md +385 -385
- package/kit/skills/crm-lead-pipeline-patterns/SKILL.md +343 -343
- package/kit/skills/escolha-modelo-consistencia/SKILL.md +494 -494
- package/kit/skills/evolucao-schema-compativel/SKILL.md +448 -448
- package/kit/skills/evolution-go-whatsapp-integration/SKILL.md +322 -322
- package/kit/skills/example-skill/SKILL.md +42 -42
- package/kit/skills/legacy-api-only-applications/SKILL.md +358 -358
- package/kit/skills/legacy-characterization-tests/SKILL.md +330 -330
- package/kit/skills/legacy-effect-analysis/SKILL.md +331 -331
- package/kit/skills/legacy-extract-class/SKILL.md +203 -203
- package/kit/skills/legacy-programming-by-difference/SKILL.md +252 -252
- package/kit/skills/legacy-seams-and-test-harness/SKILL.md +460 -460
- package/kit/skills/legacy-shotgun-surgery/SKILL.md +286 -286
- package/kit/skills/legacy-sprout-wrap-techniques/SKILL.md +434 -434
- package/kit/skills/legacy-storytelling-naked-crc/SKILL.md +270 -270
- package/kit/skills/lgpd-multi-tenant-compliance/SKILL.md +340 -340
- package/kit/skills/member-invite-flow/SKILL.md +305 -305
- package/kit/skills/member-management-react-shadcn/SKILL.md +328 -328
- package/kit/skills/multi-tenant-performance-scaling/SKILL.md +316 -316
- package/kit/skills/multi-tenant-rls-hierarchy/SKILL.md +342 -342
- package/kit/skills/org-onboarding-flow/SKILL.md +257 -257
- package/kit/skills/org-switcher-react-pattern/SKILL.md +349 -349
- package/kit/skills/permission-gate-react-pattern/SKILL.md +271 -271
- package/kit/skills/postgres-isolamento-concorrencia/SKILL.md +552 -552
- package/kit/skills/pre-refactor-characterization/SKILL.md +421 -421
- package/kit/skills/rbac-permissions-matrix-supabase/SKILL.md +338 -338
- package/kit/skills/streams-eventos-cdc/SKILL.md +711 -711
- package/kit/skills/supabase-branching-workflow/SKILL.md +544 -544
- package/kit/skills/supabase-ci-cd-github-actions/SKILL.md +880 -880
- package/kit/skills/supabase-column-level-security/SKILL.md +426 -426
- package/kit/skills/supabase-config-toml-remotes/SKILL.md +807 -807
- package/kit/skills/supabase-custom-claims-rbac/SKILL.md +472 -472
- package/kit/skills/supabase-edge-functions/SKILL.md +1 -1
- package/kit/skills/supabase-edge-functions-auth/SKILL.md +1 -1
- package/kit/skills/supabase-edge-functions-limits/SKILL.md +1 -1
- package/kit/skills/supabase-edge-functions-mcp-server/SKILL.md +1 -1
- package/kit/skills/supabase-edge-functions-testing/SKILL.md +1 -1
- package/kit/skills/supabase-edge-runtime-builtins/SKILL.md +1 -1
- package/kit/skills/supabase-migration-repair/SKILL.md +823 -823
- package/kit/skills/supabase-migrations/SKILL.md +297 -297
- package/kit/skills/supabase-pgtap-testing/SKILL.md +1053 -1053
- package/kit/skills/supabase-postgres-roles/SKILL.md +392 -392
- package/kit/skills/supabase-realtime/SKILL.md +460 -460
- package/kit/skills/supabase-rls-defense-in-depth/SKILL.md +418 -418
- package/kit/skills/supabase-rls-policies/SKILL.md +635 -635
- package/kit/skills/super-admin-platform-pattern/SKILL.md +326 -326
- package/kit/skills/tenant-quente-mitigacao/SKILL.md +605 -605
- package/kit/skills/whatsapp-conversation-state-machine/SKILL.md +287 -287
- package/package.json +1 -1
- package/src/core/kit.js +216 -216
- package/src/core/reflect.js +247 -247
- package/src/core/reverse-sync.js +372 -372
- package/src/core/sync.js +437 -418
- package/src/core/watch.js +121 -121
- package/src/mcp-server/index.js +794 -746
|
@@ -1,635 +1,635 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: supabase-rls-policies
|
|
3
|
-
description: Use ao criar/auditar RLS — sempre (select auth.uid()), policies separadas por operação, GRANTs antes de ENABLE RLS, IS NOT NULL para anti silent-fail, índices nas colunas, NUNCA user_meta…
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Supabase — RLS Policies
|
|
7
|
-
|
|
8
|
-
## Quando usar
|
|
9
|
-
|
|
10
|
-
LLM carrega esta skill quando criar, auditar ou debugar Row Level Security em Supabase. Trigger phrases:
|
|
11
|
-
|
|
12
|
-
- "criar policy RLS", "RLS policy", "row level security"
|
|
13
|
-
- "policies separadas por operação"
|
|
14
|
-
- "auth.uid()", "auth.jwt()"
|
|
15
|
-
- "MFA enforcement", "AAL2"
|
|
16
|
-
- "auditar segurança de tabela Supabase"
|
|
17
|
-
- "GRANT antes de ENABLE RLS", "defense in depth", "security_invoker"
|
|
18
|
-
- "anon role vs anonymous user", "raw_app_meta_data vs raw_user_meta_data"
|
|
19
|
-
|
|
20
|
-
## Defense in depth — RLS como camada (v1.23)
|
|
21
|
-
|
|
22
|
-
RLS é um **Postgres primitive** que oferece **defense in depth**: protege dados mesmo quando acessados por third-party tooling (Metabase, dbt, ferramentas BI conectadas via JDBC). Mesmo se um vazamento de chave API ou bypass na camada de aplicação acontece, RLS impede acesso indevido **no banco**.
|
|
23
|
-
|
|
24
|
-
A regra mestre:
|
|
25
|
-
|
|
26
|
-
> **RLS *must* always be enabled on any tables stored in an exposed schema. By default, this is the `public` schema.**
|
|
27
|
-
|
|
28
|
-
Se você não tem RLS na camada do banco, você está confiando que **todo cliente** (front-end, backend, third-party, scripts, MCP tools) faça filtering corretamente. Isso é frágil.
|
|
29
|
-
|
|
30
|
-
**Princípio em v1.23 (handoff cooperativo):** todo SQL gerado pelo kit passa pelo `supabase-rls-hardener` antes do output final — drafts upstream são preservados, mas hardening RLS é obrigatório.
|
|
31
|
-
|
|
32
|
-
## Regras absolutas
|
|
33
|
-
|
|
34
|
-
**WARNING — REGRA #1 (segurança crítica):** **NUNCA** referencie `user_metadata` em policy de autorização. `user_metadata` é editável pelo cliente via `auth.updateUser({data: {...}})` — usuário pode auto-elevar `role: 'admin'` ou `plan: 'premium'`. Use **`app_metadata`** (set apenas via service_role) para roles/permissions. Splinter linter 0015 detecta automaticamente.
|
|
35
|
-
|
|
36
|
-
**REGRA #2 (performance crítica):** **SEMPRE** envolva `auth.uid()` em `(select auth.uid())`. Sem o wrapper, Postgres reavalia a função **uma vez por linha** — degrada queries com filtro RLS em **até 1000×**. Documentado nos benchmarks oficiais (`test2a-wrappedSQL-uid`: 179ms → 9ms, 94.97% improvement).
|
|
37
|
-
|
|
38
|
-
**REGRA #3 (anti silent-fail anônimo — v1.23):** Para policies que dependem de identidade autenticada, use **`auth.uid() IS NOT NULL AND auth.uid() = user_id`** ao invés de apenas `(select auth.uid()) = user_id`. Quando o usuário não está logado, `auth.uid()` retorna `null`, e `null = user_id` é **sempre false** silenciosamente — a policy "funciona" mas confunde debugging. O check explícito de `IS NOT NULL` deixa intent claro.
|
|
39
|
-
|
|
40
|
-
**Outras regras:**
|
|
41
|
-
|
|
42
|
-
- **`GRANT` antes de `ENABLE RLS`** (v1.23) — sempre conceda privilégios necessários aos roles `anon`/`authenticated`/`service_role` ANTES de habilitar RLS. Sem GRANT, mesmo policies "permissive" falham porque o role não tem permissão de tabela.
|
|
43
|
-
- **`policies separadas por operação`** — uma `for select`, uma `for insert`, uma `for update`, uma `for delete`. **Nunca** `for all` cobrindo CRUD inteiro.
|
|
44
|
-
- **`TO authenticated`** ou **`to anon`** sempre explícito — nunca deixar implícito (default `to public` é insecure; impede otimização do executor que skipa execução de policy para roles fora do TO clause).
|
|
45
|
-
- `for select` e `for delete` usam **apenas `using`** (sem `with check`).
|
|
46
|
-
- `for insert` usa **apenas `with check`** (sem `using`).
|
|
47
|
-
- `for update` usa **`using` + `with check`** (using para qual linha pode ser atualizada, with check para qual estado a linha pode assumir).
|
|
48
|
-
- Índice obrigatório nas colunas referenciadas pela policy: `create index on public.tasks (user_id);`. Sem index, scan full em cada query.
|
|
49
|
-
- `permissive` é default e preferido. `restrictive` é raro e exige justificativa explícita.
|
|
50
|
-
- Para MFA enforcement: `(auth.jwt()->>'aal')::text = 'aal2'` em policies que exigem 2FA ativo.
|
|
51
|
-
- **Views** com `security_invoker=true` (Postgres 15+) — por padrão views bypassam RLS (criadas como `security_definer` rodando como `postgres`). Ver seção "Views" abaixo.
|
|
52
|
-
|
|
53
|
-
## Setup canônico — GRANTs + ENABLE RLS (v1.23)
|
|
54
|
-
|
|
55
|
-
O setup completo para uma tabela em schema exposto é:
|
|
56
|
-
|
|
57
|
-
```sql
|
|
58
|
-
-- 1. GRANTs por role (faça ANTES de ENABLE RLS)
|
|
59
|
-
grant select on public.tasks to anon;
|
|
60
|
-
grant select, insert, update, delete on public.tasks to authenticated;
|
|
61
|
-
grant select, insert, update, delete on public.tasks to service_role;
|
|
62
|
-
|
|
63
|
-
-- 2. Enable RLS
|
|
64
|
-
alter table public.tasks enable row level security;
|
|
65
|
-
|
|
66
|
-
-- 3. Policies granulares (sem isso, nada é acessível com publishable key)
|
|
67
|
-
create policy "users_select_own_tasks"
|
|
68
|
-
on public.tasks for select to authenticated
|
|
69
|
-
using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
|
|
70
|
-
|
|
71
|
-
-- ... INSERT/UPDATE/DELETE policies análogos
|
|
72
|
-
|
|
73
|
-
-- 4. Índice obrigatório
|
|
74
|
-
create index tasks_user_id_idx on public.tasks (user_id);
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
**Por que GRANTs antes de ENABLE RLS:** RLS é uma camada de filtragem de linhas **adicionada** sobre as permissões de tabela. Sem `GRANT SELECT TO authenticated`, mesmo se a policy retorna true, a query falha com "permission denied". Os GRANTs estabelecem o que o role *pode tentar* fazer; RLS estabelece *quais linhas* ele vê.
|
|
78
|
-
|
|
79
|
-
**Roles canônicos Supabase:**
|
|
80
|
-
|
|
81
|
-
- `anon` — requisição sem autenticação (usuário deslogado)
|
|
82
|
-
- `authenticated` — requisição com JWT válido de Supabase Auth
|
|
83
|
-
- `service_role` — bypassa RLS (ver "Bypassing RLS" abaixo); use APENAS em backend/admin
|
|
84
|
-
|
|
85
|
-
## Auto-enable RLS para tabelas novas (v1.23)
|
|
86
|
-
|
|
87
|
-
Para evitar esquecimento humano, instale event trigger que ativa RLS automaticamente em `CREATE TABLE`:
|
|
88
|
-
|
|
89
|
-
```sql
|
|
90
|
-
create or replace function rls_auto_enable()
|
|
91
|
-
returns event_trigger
|
|
92
|
-
language plpgsql
|
|
93
|
-
security definer
|
|
94
|
-
set search_path = pg_catalog
|
|
95
|
-
as $$
|
|
96
|
-
declare
|
|
97
|
-
cmd record;
|
|
98
|
-
begin
|
|
99
|
-
for cmd in
|
|
100
|
-
select *
|
|
101
|
-
from pg_event_trigger_ddl_commands()
|
|
102
|
-
where command_tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
|
|
103
|
-
and object_type in ('table','partitioned table')
|
|
104
|
-
loop
|
|
105
|
-
if cmd.schema_name is not null and cmd.schema_name in ('public') and cmd.schema_name not in ('pg_catalog','information_schema') and cmd.schema_name not like 'pg_toast%' and cmd.schema_name not like 'pg_temp%' then
|
|
106
|
-
begin
|
|
107
|
-
execute format('alter table if exists %s enable row level security', cmd.object_identity);
|
|
108
|
-
raise log 'rls_auto_enable: enabled RLS on %', cmd.object_identity;
|
|
109
|
-
exception
|
|
110
|
-
when others then
|
|
111
|
-
raise log 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;
|
|
112
|
-
end;
|
|
113
|
-
else
|
|
114
|
-
raise log 'rls_auto_enable: skip % (system schema or not in enforced list)', cmd.object_identity;
|
|
115
|
-
end if;
|
|
116
|
-
end loop;
|
|
117
|
-
end;
|
|
118
|
-
$$;
|
|
119
|
-
|
|
120
|
-
drop event trigger if exists ensure_rls;
|
|
121
|
-
create event trigger ensure_rls
|
|
122
|
-
on ddl_command_end
|
|
123
|
-
when tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
|
|
124
|
-
execute function rls_auto_enable();
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
**Caveats:**
|
|
128
|
-
- Aplica-se a tabelas criadas **após** o trigger ser instalado — existentes precisam de `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` manual.
|
|
129
|
-
- Tabelas em `pg_catalog`, `information_schema`, `pg_toast*`, `pg_temp*` são skipped (sistema).
|
|
130
|
-
- Padrão completo + variações em [`supabase-rls-defense-in-depth`](../supabase-rls-defense-in-depth/SKILL.md) (v1.23).
|
|
131
|
-
|
|
132
|
-
## `anon` Postgres role vs anonymous Auth user (v1.23)
|
|
133
|
-
|
|
134
|
-
Confusão comum: **`anon` Postgres role** ≠ **anonymous user de Supabase Auth**.
|
|
135
|
-
|
|
136
|
-
- **`anon` Postgres role** — requisição sem JWT (usuário deslogado). É um Postgres role como qualquer outro; aparece em `TO anon` clauses.
|
|
137
|
-
- **anonymous user de Supabase Auth** — usuário criado via `supabase.auth.signInAnonymously()` (Auth feature). Tem JWT válido, assume o role `authenticated`, e pode ser diferenciado por checar a claim `is_anonymous` no JWT.
|
|
138
|
-
|
|
139
|
-
**Implicação em policies:**
|
|
140
|
-
|
|
141
|
-
```sql
|
|
142
|
-
-- visivel a qualquer cliente (anon Postgres ou authenticated com is_anonymous=true ou regular)
|
|
143
|
-
create policy "public_profiles_view"
|
|
144
|
-
on public.profiles for select
|
|
145
|
-
to authenticated, anon
|
|
146
|
-
using (true);
|
|
147
|
-
|
|
148
|
-
-- bloqueia anonymous Auth users mesmo que estejam autenticados
|
|
149
|
-
create policy "premium_features_no_anonymous"
|
|
150
|
-
on public.premium_data for select
|
|
151
|
-
to authenticated
|
|
152
|
-
using (
|
|
153
|
-
(select auth.jwt()->>'is_anonymous')::boolean is not true
|
|
154
|
-
and (select auth.uid()) = user_id
|
|
155
|
-
);
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Patterns canônicos
|
|
159
|
-
|
|
160
|
-
### SELECT — usuário lê apenas suas próprias linhas
|
|
161
|
-
|
|
162
|
-
```sql
|
|
163
|
-
-- política de SELECT com wrapper (select auth.uid()) + IS NOT NULL anti silent-fail
|
|
164
|
-
create policy "users_select_own_tasks"
|
|
165
|
-
on public.tasks
|
|
166
|
-
for select
|
|
167
|
-
to authenticated
|
|
168
|
-
using (
|
|
169
|
-
(select auth.uid()) is not null
|
|
170
|
-
and (select auth.uid()) = user_id
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
-- index obrigatório (sem isso, scan full)
|
|
174
|
-
create index tasks_user_id_idx on public.tasks (user_id);
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### INSERT, UPDATE, DELETE separados
|
|
178
|
-
|
|
179
|
-
```sql
|
|
180
|
-
-- INSERT — usuário só pode criar linhas com user_id = ele mesmo
|
|
181
|
-
create policy "users_insert_own_tasks"
|
|
182
|
-
on public.tasks
|
|
183
|
-
for insert
|
|
184
|
-
to authenticated
|
|
185
|
-
with check (
|
|
186
|
-
(select auth.uid()) is not null
|
|
187
|
-
and (select auth.uid()) = user_id
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
-- UPDATE — restringe quais linhas (using) E qual estado novo (with check)
|
|
191
|
-
-- IMPORTANTE: UPDATE também exige policy SELECT correspondente (sem ela, UPDATE não funciona)
|
|
192
|
-
create policy "users_update_own_tasks"
|
|
193
|
-
on public.tasks
|
|
194
|
-
for update
|
|
195
|
-
to authenticated
|
|
196
|
-
using (
|
|
197
|
-
(select auth.uid()) is not null
|
|
198
|
-
and (select auth.uid()) = user_id
|
|
199
|
-
)
|
|
200
|
-
with check (
|
|
201
|
-
(select auth.uid()) is not null
|
|
202
|
-
and (select auth.uid()) = user_id
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
-- DELETE — apenas a coluna using (sem with check)
|
|
206
|
-
create policy "users_delete_own_tasks"
|
|
207
|
-
on public.tasks
|
|
208
|
-
for delete
|
|
209
|
-
to authenticated
|
|
210
|
-
using (
|
|
211
|
-
(select auth.uid()) is not null
|
|
212
|
-
and (select auth.uid()) = user_id
|
|
213
|
-
);
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### Role admin via `app_metadata`
|
|
217
|
-
|
|
218
|
-
```sql
|
|
219
|
-
-- segurança: app_metadata é set apenas via service_role (admin API)
|
|
220
|
-
-- cliente NÃO pode mutá-lo
|
|
221
|
-
create policy "admins_manage_all_tasks"
|
|
222
|
-
on public.tasks
|
|
223
|
-
for update
|
|
224
|
-
to authenticated
|
|
225
|
-
using (
|
|
226
|
-
(select auth.jwt()->'app_metadata'->>'role') = 'admin'
|
|
227
|
-
)
|
|
228
|
-
with check (
|
|
229
|
-
(select auth.jwt()->'app_metadata'->>'role') = 'admin'
|
|
230
|
-
);
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
### Team membership via `app_metadata` (array)
|
|
234
|
-
|
|
235
|
-
```sql
|
|
236
|
-
-- exemplo: app_metadata.teams = ["team_a", "team_b"]
|
|
237
|
-
create policy "team_members_view"
|
|
238
|
-
on public.team_resources
|
|
239
|
-
for select
|
|
240
|
-
to authenticated
|
|
241
|
-
using (
|
|
242
|
-
team_id::text in (select jsonb_array_elements_text((select auth.jwt()->'app_metadata'->'teams')))
|
|
243
|
-
);
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### MFA enforcement (AAL2)
|
|
247
|
-
|
|
248
|
-
```sql
|
|
249
|
-
-- exigir 2FA ativo para acessar dados sensíveis
|
|
250
|
-
-- restrictive force AND com policy SELECT base
|
|
251
|
-
create policy "mfa_required_for_billing"
|
|
252
|
-
on public.billing_records
|
|
253
|
-
as restrictive
|
|
254
|
-
for select
|
|
255
|
-
to authenticated
|
|
256
|
-
using (
|
|
257
|
-
(select (auth.jwt()->>'aal')::text) = 'aal2'
|
|
258
|
-
);
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### Views com `security_invoker=true` (Postgres 15+) — v1.23
|
|
262
|
-
|
|
263
|
-
Por padrão, views são criadas com `security_definer` (rodam com permissões do criador, geralmente `postgres`) — **bypassam RLS** das tabelas subjacentes. Em Postgres 15+, use `security_invoker=true` para fazer a view respeitar as policies RLS do role chamador:
|
|
264
|
-
|
|
265
|
-
```sql
|
|
266
|
-
create view public.user_active_tasks
|
|
267
|
-
with (security_invoker = true)
|
|
268
|
-
as
|
|
269
|
-
select id, title, status, created_at
|
|
270
|
-
from public.tasks
|
|
271
|
-
where status = 'active';
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
**Em versões < Postgres 15:** revoke acesso de `anon`/`authenticated` à view, ou crie em schema não-exposto:
|
|
275
|
-
|
|
276
|
-
```sql
|
|
277
|
-
-- alternativa pré-Postgres 15: revoke acesso
|
|
278
|
-
revoke select on public.legacy_view from anon, authenticated;
|
|
279
|
-
|
|
280
|
-
-- ou crie em schema privado:
|
|
281
|
-
create view private.internal_view as ...;
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
## `app_metadata` vs `user_metadata` — caveats canônicos (v1.23)
|
|
285
|
-
|
|
286
|
-
A função `auth.jwt()` retorna o JWT do usuário fazendo a requisição. Existem duas claims relacionadas a metadata:
|
|
287
|
-
|
|
288
|
-
- **`raw_user_meta_data`** — pode ser atualizado pelo authenticated end-user via `supabase.auth.updateUser({ data: { ... } })`. **NÃO é seguro para authorization.** Use apenas para preferences (tema, idioma, avatar URL).
|
|
289
|
-
- **`raw_app_meta_data`** — **NÃO pode ser atualizado pelo cliente** — só via service_role + admin API. **É o lugar correto** para roles, permissions, team memberships, plan tier.
|
|
290
|
-
|
|
291
|
-
**Caveat #1 (JWT freshness):** JWT nem sempre está "fresh". Se você remove um user de um team e atualiza `app_metadata`, isso não reflete em `auth.jwt()` até o JWT ser refreshed (geralmente 1h TTL). Para invalidação imediata, force logout do user via `auth.admin.signOut()`.
|
|
292
|
-
|
|
293
|
-
**Caveat #2 (cookie 4096 bytes):** Se você usa Cookies para Auth, esteja atento ao JWT size. Browsers limitam cada cookie a 4096 bytes. Se você embarca arrays grandes em `app_metadata.teams` ou similar, o JWT pode passar do limite. Mitigação: store apenas IDs, busque membership via SQL com policy/RPC.
|
|
294
|
-
|
|
295
|
-
**Caveat #3 (NULL handling):** Para requests sem auth, `auth.uid()` retorna `null`. `null = user_id` é sempre **false silenciosamente** em SQL — a policy não erra, só não match. Sempre use `IS NOT NULL AND ...` (REGRA #3) para deixar intent claro.
|
|
296
|
-
|
|
297
|
-
## Performance — recomendações canônicas (v1.23)
|
|
298
|
-
|
|
299
|
-
Toda authorization tem custo. RLS é poderoso mas pode ser caro em queries que scaneam muitas linhas. Recomendações baseadas em benchmarks oficiais Supabase:
|
|
300
|
-
|
|
301
|
-
### 1. Index nas colunas usadas em policies (99.94% improvement)
|
|
302
|
-
|
|
303
|
-
```sql
|
|
304
|
-
-- antes: scan full
|
|
305
|
-
-- depois: index scan
|
|
306
|
-
create index tasks_user_id_idx on public.tasks (user_id);
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
### 2. Envolva funções em `(select ...)` para caching de plano (até 99.99% improvement)
|
|
310
|
-
|
|
311
|
-
```sql
|
|
312
|
-
-- antes: re-executa auth.uid() por linha (179ms para 100k rows)
|
|
313
|
-
using (auth.uid() = user_id)
|
|
314
|
-
|
|
315
|
-
-- depois: initPlan, executa 1 vez e reusa (9ms)
|
|
316
|
-
using ((select auth.uid()) = user_id)
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
Aplicável a qualquer função que não muda baseado em row data: `auth.uid()`, `auth.jwt()`, `security definer` functions. **Não funciona** se o resultado depende da linha (ex: `is_owner(row_id)` precisa rodar por linha).
|
|
320
|
-
|
|
321
|
-
### 3. Adicione filtros redundantes nas queries client-side (94.74% improvement)
|
|
322
|
-
|
|
323
|
-
Mesmo com policy aplicada, adicione `.eq()` ou `where` explícito no client:
|
|
324
|
-
|
|
325
|
-
```js
|
|
326
|
-
// errado — confia 100% na policy
|
|
327
|
-
const { data } = supabase.from('tasks').select()
|
|
328
|
-
|
|
329
|
-
// certo — Postgres usa o filtro para construir query plan melhor
|
|
330
|
-
const { data } = supabase.from('tasks').select().eq('user_id', userId)
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
Policy é "implicit where clause"; filtro explícito ajuda o planner Postgres a escolher index scan ao invés de seq scan.
|
|
334
|
-
|
|
335
|
-
### 4. Specify role com `TO` clause (99.78% improvement)
|
|
336
|
-
|
|
337
|
-
```sql
|
|
338
|
-
-- antes (sem TO): policy roda para todos roles, inclusive anon
|
|
339
|
-
create policy "rls_test_select" on rls_test
|
|
340
|
-
using ((select auth.uid()) = user_id);
|
|
341
|
-
|
|
342
|
-
-- depois: anon skipa execução
|
|
343
|
-
create policy "rls_test_select" on rls_test
|
|
344
|
-
to authenticated
|
|
345
|
-
using ((select auth.uid()) = user_id);
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### 5. Use `security definer` functions para policies caras
|
|
349
|
-
|
|
350
|
-
Se a policy precisa fazer JOIN ou query custosa, encapsule em função `security definer` que bypassa RLS interno:
|
|
351
|
-
|
|
352
|
-
```sql
|
|
353
|
-
-- função pode acessar roles_table sem aplicar RLS recursivamente
|
|
354
|
-
create function private.has_good_role()
|
|
355
|
-
returns boolean
|
|
356
|
-
language plpgsql
|
|
357
|
-
security definer
|
|
358
|
-
set search_path = ''
|
|
359
|
-
as $$
|
|
360
|
-
begin
|
|
361
|
-
return exists (
|
|
362
|
-
select 1 from public.roles_table
|
|
363
|
-
where (select auth.uid()) = user_id and role = 'good_role'
|
|
364
|
-
);
|
|
365
|
-
end;
|
|
366
|
-
$$;
|
|
367
|
-
|
|
368
|
-
-- policy fica simples e cacheável
|
|
369
|
-
create policy "rls_test_select"
|
|
370
|
-
on public.test_table
|
|
371
|
-
to authenticated
|
|
372
|
-
using ((select private.has_good_role()));
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
**IMPORTANTE:** funções `security definer` NUNCA em schema exposto (`public`). Sempre em `private` ou similar (não exposto via API settings).
|
|
376
|
-
|
|
377
|
-
### 6. Minimize joins (99.78% improvement)
|
|
378
|
-
|
|
379
|
-
Reescreva policies que fazem join na source table para usar `IN` ao invés:
|
|
380
|
-
|
|
381
|
-
```sql
|
|
382
|
-
-- antes: join entre test_table (source) e team_user (target)
|
|
383
|
-
create policy "rls_test_select" on public.test_table
|
|
384
|
-
to authenticated
|
|
385
|
-
using (
|
|
386
|
-
(select auth.uid()) in (
|
|
387
|
-
select user_id
|
|
388
|
-
from public.team_user
|
|
389
|
-
where team_user.team_id = team_id -- join com source!
|
|
390
|
-
)
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
-- depois: filtra primeiro, depois IN
|
|
394
|
-
create policy "rls_test_select" on public.test_table
|
|
395
|
-
to authenticated
|
|
396
|
-
using (
|
|
397
|
-
team_id in (
|
|
398
|
-
select team_id
|
|
399
|
-
from public.team_user
|
|
400
|
-
where user_id = (select auth.uid()) -- sem join
|
|
401
|
-
)
|
|
402
|
-
);
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
Se a lista interna pode passar de 1000 items, considere abordagem com `security definer` function ao invés.
|
|
406
|
-
|
|
407
|
-
## Anti-patterns
|
|
408
|
-
|
|
409
|
-
### Anti-pattern 1: `auth.uid()` sem `(select)` wrapper
|
|
410
|
-
|
|
411
|
-
**Errado:**
|
|
412
|
-
```sql
|
|
413
|
-
create policy "users_select_own_tasks"
|
|
414
|
-
on public.tasks
|
|
415
|
-
for select
|
|
416
|
-
to authenticated
|
|
417
|
-
using (auth.uid() = user_id); -- sem (select) — re-executa por linha
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
**Por quê:** Postgres reavalia `auth.uid()` para cada linha sendo testada. Em tabela com 100k linhas, isso é 100k chamadas. O `(select)` permite Postgres executar **uma vez** e reusar — degradação de até **1000×** sem o wrapper. Documentado em [RLS Performance](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv).
|
|
421
|
-
|
|
422
|
-
**Certo:**
|
|
423
|
-
```sql
|
|
424
|
-
using ((select auth.uid()) = user_id)
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### Anti-pattern 2: `WARNING user_metadata` em autorização — privilege escalation
|
|
428
|
-
|
|
429
|
-
**Errado:**
|
|
430
|
-
```sql
|
|
431
|
-
create policy "admins_manage_all"
|
|
432
|
-
on public.tasks
|
|
433
|
-
for update
|
|
434
|
-
to authenticated
|
|
435
|
-
using (
|
|
436
|
-
(auth.jwt()->'user_metadata'->>'role') = 'admin' -- editável pelo cliente!
|
|
437
|
-
);
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
**Por quê:** o cliente pode chamar `supabase.auth.updateUser({ data: { role: 'admin' } })` e instantaneamente ganhar privilégios de admin. `user_metadata` é projetado para preferences do usuário (tema, idioma), não para autorização. Documentado em [Splinter linter 0015](https://supabase.github.io/splinter/0015_rls_references_user_metadata/).
|
|
441
|
-
|
|
442
|
-
**Certo:** ver "Role admin via `app_metadata`" acima — `app_metadata` requer service_role para mutar.
|
|
443
|
-
|
|
444
|
-
### Anti-pattern 3: `for all` em vez de policies granulares
|
|
445
|
-
|
|
446
|
-
**Errado:**
|
|
447
|
-
```sql
|
|
448
|
-
create policy "users_manage_own_tasks"
|
|
449
|
-
on public.tasks
|
|
450
|
-
for all -- cobre CRUD inteiro com mesma regra
|
|
451
|
-
to authenticated
|
|
452
|
-
using ((select auth.uid()) = user_id);
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
**Por quê:** semântica de `for all` mistura `using` (que controla SELECT/UPDATE/DELETE) com `with check` (que controla INSERT/UPDATE), levando a confusão. Em UPDATE você pode querer regras diferentes para "qual linha tocar" vs "qual estado novo". Granularidade explícita previne erros sutis.
|
|
456
|
-
|
|
457
|
-
**Certo:** ver pattern com 4 policies separadas acima (SELECT, INSERT, UPDATE, DELETE).
|
|
458
|
-
|
|
459
|
-
### Anti-pattern 4: Sem índice nas colunas da policy
|
|
460
|
-
|
|
461
|
-
**Errado:**
|
|
462
|
-
```sql
|
|
463
|
-
-- policy referencia user_id mas não há index
|
|
464
|
-
create policy "users_select_own_tasks" on public.tasks
|
|
465
|
-
for select to authenticated
|
|
466
|
-
using ((select auth.uid()) = user_id);
|
|
467
|
-
|
|
468
|
-
-- (esqueceu) create index on public.tasks (user_id);
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
**Por quê:** cada query com filtro RLS força sequential scan. Em produção com 100k+ linhas, isso é lentidão crônica.
|
|
472
|
-
|
|
473
|
-
**Certo:**
|
|
474
|
-
```sql
|
|
475
|
-
create index tasks_user_id_idx on public.tasks (user_id);
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### Anti-pattern 5: ENABLE RLS sem GRANT — query falha silenciosa (v1.23)
|
|
479
|
-
|
|
480
|
-
**Errado:**
|
|
481
|
-
```sql
|
|
482
|
-
alter table public.tasks enable row level security;
|
|
483
|
-
-- esqueceu de grant select on public.tasks to authenticated;
|
|
484
|
-
create policy "users_select" ... using (...);
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
**Por quê:** RLS é camada *sobre* permissões de tabela. Sem GRANT, role não pode tentar acessar a tabela — query retorna "permission denied" mesmo se policy permitiria.
|
|
488
|
-
|
|
489
|
-
**Certo:** sempre GRANT primeiro, depois ENABLE RLS, depois policies (ver "Setup canônico" acima).
|
|
490
|
-
|
|
491
|
-
### Anti-pattern 6: View sem `security_invoker=true` em Postgres 15+ — bypass de RLS (v1.23)
|
|
492
|
-
|
|
493
|
-
**Errado:**
|
|
494
|
-
```sql
|
|
495
|
-
-- view criada como postgres user — bypassa RLS de public.tasks
|
|
496
|
-
create view public.user_tasks_view as
|
|
497
|
-
select id, title from public.tasks where user_id = auth.uid();
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
**Por quê:** views por default são `security_definer` (rodam com permissões do criador). Cliente acessando a view bypass RLS das tabelas underlying. Atacante consegue ler dados de outros users via SELECT na view.
|
|
501
|
-
|
|
502
|
-
**Certo:**
|
|
503
|
-
```sql
|
|
504
|
-
create view public.user_tasks_view
|
|
505
|
-
with (security_invoker = true)
|
|
506
|
-
as
|
|
507
|
-
select id, title from public.tasks
|
|
508
|
-
where user_id = (select auth.uid());
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
### Anti-pattern 7: `null = user_id` silent-fail (v1.23)
|
|
512
|
-
|
|
513
|
-
**Errado:**
|
|
514
|
-
```sql
|
|
515
|
-
using ((select auth.uid()) = user_id)
|
|
516
|
-
-- se user não logado, auth.uid() é null
|
|
517
|
-
-- null = user_id é always false → policy "funciona" mas confunde
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
**Por quê:** intent ambíguo — você queria bloquear não-logados ou tratar como caso especial? `null = X` retornar false silenciosamente esconde o intent e dificulta debug.
|
|
521
|
-
|
|
522
|
-
**Certo:**
|
|
523
|
-
```sql
|
|
524
|
-
using (
|
|
525
|
-
(select auth.uid()) is not null
|
|
526
|
-
and (select auth.uid()) = user_id
|
|
527
|
-
)
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
## Postgres Roles vs RLS — quando usar qual (v1.26)
|
|
531
|
-
|
|
532
|
-
Postgres roles e RLS são **conceitos complementares**:
|
|
533
|
-
|
|
534
|
-
| | Postgres Roles | RLS |
|
|
535
|
-
|---|---|---|
|
|
536
|
-
| Escopo | System access (service accounts, cron, BI) | Application access (end-users) |
|
|
537
|
-
| Identidade | Login Postgres (`SET ROLE`) | JWT (`auth.uid()`) |
|
|
538
|
-
| Granularidade | Per schema/table/function | Per linha + coluna |
|
|
539
|
-
| Audit | pg_stat_statements por role | RLS denial logs |
|
|
540
|
-
| Use case | "Cron job pode SELECT em todas tabs" | "User vê apenas próprias rows" |
|
|
541
|
-
|
|
542
|
-
**Princípio canônico v1.26:**
|
|
543
|
-
|
|
544
|
-
- **Para end-users:** RLS + Custom Claims (v1.25) — NÃO criar role Postgres por user
|
|
545
|
-
- **Para service accounts:** Postgres roles dedicados (NÃO usar service_role API key sempre)
|
|
546
|
-
- **Para column-level access:** Postgres roles + column-level GRANTs (skill `supabase-column-level-security` v1.24)
|
|
547
|
-
|
|
548
|
-
Padrão completo de Postgres roles em [`supabase-postgres-roles`](../supabase-postgres-roles/SKILL.md) (v1.26).
|
|
549
|
-
|
|
550
|
-
## RBAC via Custom Claims + authorize() function (v1.25)
|
|
551
|
-
|
|
552
|
-
A partir de v1.25, o pattern **canônico** de RBAC em Supabase é via **Custom Access Token Auth Hook** que injeta `user_role` no JWT durante geração do token. Em vez de policies fazendo JOIN custoso em `user_roles` table, a policy lê o claim direto via `auth.jwt() ->> 'user_role'` (ou via `authorize()` function que abstrai role → permission lookup).
|
|
553
|
-
|
|
554
|
-
```sql
|
|
555
|
-
-- Pattern v1.25 — RLS policy usando authorize() (claim do JWT consultado)
|
|
556
|
-
create policy "Allow authorized delete access" on public.channels for delete
|
|
557
|
-
to authenticated
|
|
558
|
-
using ((SELECT authorize('channels.delete')));
|
|
559
|
-
|
|
560
|
-
-- vs. pattern v1.21 — RLS policy usando helper function STABLE (JOIN em DB)
|
|
561
|
-
create policy "Allow admin delete" on public.channels for delete
|
|
562
|
-
to authenticated
|
|
563
|
-
using ((SELECT private.has_role('admin')));
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
**Vantagens canônicas do pattern v1.25:**
|
|
567
|
-
|
|
568
|
-
- **Performance:** claim no JWT é zero-JOIN; helper function STABLE faz query em user_roles table
|
|
569
|
-
- **Composability:** `authorize(permission)` abstrai role → permission; trocar quem tem permission = UPDATE em role_permissions (sem alterar policies)
|
|
570
|
-
- **Type safety:** `app_permission` enum garante consistência cross-policy
|
|
571
|
-
|
|
572
|
-
**Caveat JWT freshness:** mudanças em `user_roles` só refletem no JWT após refresh (TTL 1h). Para revogação imediata, force logout via `auth.admin.signOut(userId)`. Em multi-tenant complexo (role por org), **combine** custom claim (role global) + helper function PG (role context-aware) — claim sozinho não cobre per-org context.
|
|
573
|
-
|
|
574
|
-
Padrão completo (7 passos + anti-patterns + caveats) em [`supabase-custom-claims-rbac`](../supabase-custom-claims-rbac/SKILL.md) (v1.25).
|
|
575
|
-
|
|
576
|
-
## Combining RLS with Column-Level Privileges (v1.24)
|
|
577
|
-
|
|
578
|
-
RLS row-level e column-level privileges são **camadas complementares**:
|
|
579
|
-
|
|
580
|
-
- **RLS** filtra **quais linhas** o role vê/modifica
|
|
581
|
-
- **Column privileges** filtra **quais colunas** o role pode acessar dentro da linha
|
|
582
|
-
|
|
583
|
-
Combinação canônica: RLS + column-level (Camada 8 de defense-in-depth, skill `supabase-rls-defense-in-depth` v1.24).
|
|
584
|
-
|
|
585
|
-
```sql
|
|
586
|
-
-- 1. RLS row-level — user só vê próprias posts
|
|
587
|
-
create policy "users_select_own_posts"
|
|
588
|
-
on public.posts for select to authenticated
|
|
589
|
-
using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
|
|
590
|
-
|
|
591
|
-
-- 2. Column-level — mesmo nas próprias posts, user não vê coluna sensível (ex: admin_notes)
|
|
592
|
-
revoke select on table public.posts from authenticated;
|
|
593
|
-
grant select (id, user_id, title, content, created_at) on table public.posts to authenticated;
|
|
594
|
-
|
|
595
|
-
-- 3. service_role / admin_role vê tudo (incluindo admin_notes)
|
|
596
|
-
grant select on table public.posts to service_role;
|
|
597
|
-
|
|
598
|
-
-- Cliente DEVE listar colunas explicitamente:
|
|
599
|
-
-- ❌ supabase.from('posts').select() — FALHA (wildcard expansion → admin_notes)
|
|
600
|
-
-- ✅ supabase.from('posts').select('id, user_id, title, content, created_at')
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
**Quando combinar:**
|
|
604
|
-
|
|
605
|
-
- Compliance LGPD/GDPR onde algumas colunas (PII) precisam restrição extra além do RLS
|
|
606
|
-
- Audit log com payload sanitizado — RLS filtra por org, column priv filtra payload
|
|
607
|
-
- Billing data — RLS filtra por owner, column priv filtra credit_card_token
|
|
608
|
-
|
|
609
|
-
**Quando NÃO combinar:**
|
|
610
|
-
|
|
611
|
-
- Caso comum (admin/user roles) — use dedicated role table (skill [`supabase-column-level-security`](../supabase-column-level-security/SKILL.md)) ao invés
|
|
612
|
-
- Tabelas sem PII real — overhead sem benefício
|
|
613
|
-
|
|
614
|
-
**Caveat crítico:** com column privileges, **todo SELECT deve listar colunas explicitamente** — `SELECT *` falha. Atualize SDK calls + queries SQL ad-hoc + ferramentas BI conectadas.
|
|
615
|
-
|
|
616
|
-
Padrão completo + 4 patterns canônicos em [`supabase-column-level-security`](../supabase-column-level-security/SKILL.md) (v1.24).
|
|
617
|
-
|
|
618
|
-
## Bypassing RLS — quando e como
|
|
619
|
-
|
|
620
|
-
3 mecanismos canônicos para bypass de RLS:
|
|
621
|
-
|
|
622
|
-
1. **`service_role`** — chave Supabase com bypass automático. **NUNCA** exponha ao cliente. Use APENAS em backend (Edge Functions com env var `SUPABASE_SERVICE_ROLE_KEY`, scripts admin, migrations). Caveat: ao chamar SDK Supabase com service_role mas com `Authorization: Bearer <user_jwt>` ainda set, RLS do user é aplicado (override).
|
|
623
|
-
2. **`alter role <name> with bypassrls`** — privilégio Postgres que permite role bypass RLS sempre. Use para roles internos (`postgres`, custom admin role). NUNCA conceda a um role que recebe requisições de cliente.
|
|
624
|
-
3. **`security definer` functions** — função roda com permissões do criador (geralmente `postgres` = bypassrls). Encapsule lógica admin/cross-tenant em função `security definer` no schema `private`.
|
|
625
|
-
|
|
626
|
-
Padrões avançados em [`supabase-rls-defense-in-depth`](../supabase-rls-defense-in-depth/SKILL.md) (v1.23).
|
|
627
|
-
|
|
628
|
-
## Ver também
|
|
629
|
-
|
|
630
|
-
- [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) — event trigger, BYPASSRLS, service_role caveat, security definer, views security_invoker (v1.23)
|
|
631
|
-
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''` que respeitam RLS
|
|
632
|
-
- [supabase-storage](../supabase-storage/SKILL.md) — RLS sobre `storage.objects` (multi-tenant path isolation)
|
|
633
|
-
- [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — autenticação que popula `auth.uid()`
|
|
634
|
-
- [supabase-migrations](../supabase-migrations/SKILL.md) — migrations sempre com GRANT + RLS habilitado em novas tabelas
|
|
635
|
-
- [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + roles + comandos CLI
|
|
1
|
+
---
|
|
2
|
+
name: supabase-rls-policies
|
|
3
|
+
description: Use ao criar/auditar RLS — sempre (select auth.uid()), policies separadas por operação, GRANTs antes de ENABLE RLS, IS NOT NULL para anti silent-fail, índices nas colunas, NUNCA user_meta…
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Supabase — RLS Policies
|
|
7
|
+
|
|
8
|
+
## Quando usar
|
|
9
|
+
|
|
10
|
+
LLM carrega esta skill quando criar, auditar ou debugar Row Level Security em Supabase. Trigger phrases:
|
|
11
|
+
|
|
12
|
+
- "criar policy RLS", "RLS policy", "row level security"
|
|
13
|
+
- "policies separadas por operação"
|
|
14
|
+
- "auth.uid()", "auth.jwt()"
|
|
15
|
+
- "MFA enforcement", "AAL2"
|
|
16
|
+
- "auditar segurança de tabela Supabase"
|
|
17
|
+
- "GRANT antes de ENABLE RLS", "defense in depth", "security_invoker"
|
|
18
|
+
- "anon role vs anonymous user", "raw_app_meta_data vs raw_user_meta_data"
|
|
19
|
+
|
|
20
|
+
## Defense in depth — RLS como camada (v1.23)
|
|
21
|
+
|
|
22
|
+
RLS é um **Postgres primitive** que oferece **defense in depth**: protege dados mesmo quando acessados por third-party tooling (Metabase, dbt, ferramentas BI conectadas via JDBC). Mesmo se um vazamento de chave API ou bypass na camada de aplicação acontece, RLS impede acesso indevido **no banco**.
|
|
23
|
+
|
|
24
|
+
A regra mestre:
|
|
25
|
+
|
|
26
|
+
> **RLS *must* always be enabled on any tables stored in an exposed schema. By default, this is the `public` schema.**
|
|
27
|
+
|
|
28
|
+
Se você não tem RLS na camada do banco, você está confiando que **todo cliente** (front-end, backend, third-party, scripts, MCP tools) faça filtering corretamente. Isso é frágil.
|
|
29
|
+
|
|
30
|
+
**Princípio em v1.23 (handoff cooperativo):** todo SQL gerado pelo kit passa pelo `supabase-rls-hardener` antes do output final — drafts upstream são preservados, mas hardening RLS é obrigatório.
|
|
31
|
+
|
|
32
|
+
## Regras absolutas
|
|
33
|
+
|
|
34
|
+
**WARNING — REGRA #1 (segurança crítica):** **NUNCA** referencie `user_metadata` em policy de autorização. `user_metadata` é editável pelo cliente via `auth.updateUser({data: {...}})` — usuário pode auto-elevar `role: 'admin'` ou `plan: 'premium'`. Use **`app_metadata`** (set apenas via service_role) para roles/permissions. Splinter linter 0015 detecta automaticamente.
|
|
35
|
+
|
|
36
|
+
**REGRA #2 (performance crítica):** **SEMPRE** envolva `auth.uid()` em `(select auth.uid())`. Sem o wrapper, Postgres reavalia a função **uma vez por linha** — degrada queries com filtro RLS em **até 1000×**. Documentado nos benchmarks oficiais (`test2a-wrappedSQL-uid`: 179ms → 9ms, 94.97% improvement).
|
|
37
|
+
|
|
38
|
+
**REGRA #3 (anti silent-fail anônimo — v1.23):** Para policies que dependem de identidade autenticada, use **`auth.uid() IS NOT NULL AND auth.uid() = user_id`** ao invés de apenas `(select auth.uid()) = user_id`. Quando o usuário não está logado, `auth.uid()` retorna `null`, e `null = user_id` é **sempre false** silenciosamente — a policy "funciona" mas confunde debugging. O check explícito de `IS NOT NULL` deixa intent claro.
|
|
39
|
+
|
|
40
|
+
**Outras regras:**
|
|
41
|
+
|
|
42
|
+
- **`GRANT` antes de `ENABLE RLS`** (v1.23) — sempre conceda privilégios necessários aos roles `anon`/`authenticated`/`service_role` ANTES de habilitar RLS. Sem GRANT, mesmo policies "permissive" falham porque o role não tem permissão de tabela.
|
|
43
|
+
- **`policies separadas por operação`** — uma `for select`, uma `for insert`, uma `for update`, uma `for delete`. **Nunca** `for all` cobrindo CRUD inteiro.
|
|
44
|
+
- **`TO authenticated`** ou **`to anon`** sempre explícito — nunca deixar implícito (default `to public` é insecure; impede otimização do executor que skipa execução de policy para roles fora do TO clause).
|
|
45
|
+
- `for select` e `for delete` usam **apenas `using`** (sem `with check`).
|
|
46
|
+
- `for insert` usa **apenas `with check`** (sem `using`).
|
|
47
|
+
- `for update` usa **`using` + `with check`** (using para qual linha pode ser atualizada, with check para qual estado a linha pode assumir).
|
|
48
|
+
- Índice obrigatório nas colunas referenciadas pela policy: `create index on public.tasks (user_id);`. Sem index, scan full em cada query.
|
|
49
|
+
- `permissive` é default e preferido. `restrictive` é raro e exige justificativa explícita.
|
|
50
|
+
- Para MFA enforcement: `(auth.jwt()->>'aal')::text = 'aal2'` em policies que exigem 2FA ativo.
|
|
51
|
+
- **Views** com `security_invoker=true` (Postgres 15+) — por padrão views bypassam RLS (criadas como `security_definer` rodando como `postgres`). Ver seção "Views" abaixo.
|
|
52
|
+
|
|
53
|
+
## Setup canônico — GRANTs + ENABLE RLS (v1.23)
|
|
54
|
+
|
|
55
|
+
O setup completo para uma tabela em schema exposto é:
|
|
56
|
+
|
|
57
|
+
```sql
|
|
58
|
+
-- 1. GRANTs por role (faça ANTES de ENABLE RLS)
|
|
59
|
+
grant select on public.tasks to anon;
|
|
60
|
+
grant select, insert, update, delete on public.tasks to authenticated;
|
|
61
|
+
grant select, insert, update, delete on public.tasks to service_role;
|
|
62
|
+
|
|
63
|
+
-- 2. Enable RLS
|
|
64
|
+
alter table public.tasks enable row level security;
|
|
65
|
+
|
|
66
|
+
-- 3. Policies granulares (sem isso, nada é acessível com publishable key)
|
|
67
|
+
create policy "users_select_own_tasks"
|
|
68
|
+
on public.tasks for select to authenticated
|
|
69
|
+
using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
|
|
70
|
+
|
|
71
|
+
-- ... INSERT/UPDATE/DELETE policies análogos
|
|
72
|
+
|
|
73
|
+
-- 4. Índice obrigatório
|
|
74
|
+
create index tasks_user_id_idx on public.tasks (user_id);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Por que GRANTs antes de ENABLE RLS:** RLS é uma camada de filtragem de linhas **adicionada** sobre as permissões de tabela. Sem `GRANT SELECT TO authenticated`, mesmo se a policy retorna true, a query falha com "permission denied". Os GRANTs estabelecem o que o role *pode tentar* fazer; RLS estabelece *quais linhas* ele vê.
|
|
78
|
+
|
|
79
|
+
**Roles canônicos Supabase:**
|
|
80
|
+
|
|
81
|
+
- `anon` — requisição sem autenticação (usuário deslogado)
|
|
82
|
+
- `authenticated` — requisição com JWT válido de Supabase Auth
|
|
83
|
+
- `service_role` — bypassa RLS (ver "Bypassing RLS" abaixo); use APENAS em backend/admin
|
|
84
|
+
|
|
85
|
+
## Auto-enable RLS para tabelas novas (v1.23)
|
|
86
|
+
|
|
87
|
+
Para evitar esquecimento humano, instale event trigger que ativa RLS automaticamente em `CREATE TABLE`:
|
|
88
|
+
|
|
89
|
+
```sql
|
|
90
|
+
create or replace function rls_auto_enable()
|
|
91
|
+
returns event_trigger
|
|
92
|
+
language plpgsql
|
|
93
|
+
security definer
|
|
94
|
+
set search_path = pg_catalog
|
|
95
|
+
as $$
|
|
96
|
+
declare
|
|
97
|
+
cmd record;
|
|
98
|
+
begin
|
|
99
|
+
for cmd in
|
|
100
|
+
select *
|
|
101
|
+
from pg_event_trigger_ddl_commands()
|
|
102
|
+
where command_tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
|
|
103
|
+
and object_type in ('table','partitioned table')
|
|
104
|
+
loop
|
|
105
|
+
if cmd.schema_name is not null and cmd.schema_name in ('public') and cmd.schema_name not in ('pg_catalog','information_schema') and cmd.schema_name not like 'pg_toast%' and cmd.schema_name not like 'pg_temp%' then
|
|
106
|
+
begin
|
|
107
|
+
execute format('alter table if exists %s enable row level security', cmd.object_identity);
|
|
108
|
+
raise log 'rls_auto_enable: enabled RLS on %', cmd.object_identity;
|
|
109
|
+
exception
|
|
110
|
+
when others then
|
|
111
|
+
raise log 'rls_auto_enable: failed to enable RLS on %', cmd.object_identity;
|
|
112
|
+
end;
|
|
113
|
+
else
|
|
114
|
+
raise log 'rls_auto_enable: skip % (system schema or not in enforced list)', cmd.object_identity;
|
|
115
|
+
end if;
|
|
116
|
+
end loop;
|
|
117
|
+
end;
|
|
118
|
+
$$;
|
|
119
|
+
|
|
120
|
+
drop event trigger if exists ensure_rls;
|
|
121
|
+
create event trigger ensure_rls
|
|
122
|
+
on ddl_command_end
|
|
123
|
+
when tag in ('CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO')
|
|
124
|
+
execute function rls_auto_enable();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Caveats:**
|
|
128
|
+
- Aplica-se a tabelas criadas **após** o trigger ser instalado — existentes precisam de `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` manual.
|
|
129
|
+
- Tabelas em `pg_catalog`, `information_schema`, `pg_toast*`, `pg_temp*` são skipped (sistema).
|
|
130
|
+
- Padrão completo + variações em [`supabase-rls-defense-in-depth`](../supabase-rls-defense-in-depth/SKILL.md) (v1.23).
|
|
131
|
+
|
|
132
|
+
## `anon` Postgres role vs anonymous Auth user (v1.23)
|
|
133
|
+
|
|
134
|
+
Confusão comum: **`anon` Postgres role** ≠ **anonymous user de Supabase Auth**.
|
|
135
|
+
|
|
136
|
+
- **`anon` Postgres role** — requisição sem JWT (usuário deslogado). É um Postgres role como qualquer outro; aparece em `TO anon` clauses.
|
|
137
|
+
- **anonymous user de Supabase Auth** — usuário criado via `supabase.auth.signInAnonymously()` (Auth feature). Tem JWT válido, assume o role `authenticated`, e pode ser diferenciado por checar a claim `is_anonymous` no JWT.
|
|
138
|
+
|
|
139
|
+
**Implicação em policies:**
|
|
140
|
+
|
|
141
|
+
```sql
|
|
142
|
+
-- visivel a qualquer cliente (anon Postgres ou authenticated com is_anonymous=true ou regular)
|
|
143
|
+
create policy "public_profiles_view"
|
|
144
|
+
on public.profiles for select
|
|
145
|
+
to authenticated, anon
|
|
146
|
+
using (true);
|
|
147
|
+
|
|
148
|
+
-- bloqueia anonymous Auth users mesmo que estejam autenticados
|
|
149
|
+
create policy "premium_features_no_anonymous"
|
|
150
|
+
on public.premium_data for select
|
|
151
|
+
to authenticated
|
|
152
|
+
using (
|
|
153
|
+
(select auth.jwt()->>'is_anonymous')::boolean is not true
|
|
154
|
+
and (select auth.uid()) = user_id
|
|
155
|
+
);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Patterns canônicos
|
|
159
|
+
|
|
160
|
+
### SELECT — usuário lê apenas suas próprias linhas
|
|
161
|
+
|
|
162
|
+
```sql
|
|
163
|
+
-- política de SELECT com wrapper (select auth.uid()) + IS NOT NULL anti silent-fail
|
|
164
|
+
create policy "users_select_own_tasks"
|
|
165
|
+
on public.tasks
|
|
166
|
+
for select
|
|
167
|
+
to authenticated
|
|
168
|
+
using (
|
|
169
|
+
(select auth.uid()) is not null
|
|
170
|
+
and (select auth.uid()) = user_id
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
-- index obrigatório (sem isso, scan full)
|
|
174
|
+
create index tasks_user_id_idx on public.tasks (user_id);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### INSERT, UPDATE, DELETE separados
|
|
178
|
+
|
|
179
|
+
```sql
|
|
180
|
+
-- INSERT — usuário só pode criar linhas com user_id = ele mesmo
|
|
181
|
+
create policy "users_insert_own_tasks"
|
|
182
|
+
on public.tasks
|
|
183
|
+
for insert
|
|
184
|
+
to authenticated
|
|
185
|
+
with check (
|
|
186
|
+
(select auth.uid()) is not null
|
|
187
|
+
and (select auth.uid()) = user_id
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
-- UPDATE — restringe quais linhas (using) E qual estado novo (with check)
|
|
191
|
+
-- IMPORTANTE: UPDATE também exige policy SELECT correspondente (sem ela, UPDATE não funciona)
|
|
192
|
+
create policy "users_update_own_tasks"
|
|
193
|
+
on public.tasks
|
|
194
|
+
for update
|
|
195
|
+
to authenticated
|
|
196
|
+
using (
|
|
197
|
+
(select auth.uid()) is not null
|
|
198
|
+
and (select auth.uid()) = user_id
|
|
199
|
+
)
|
|
200
|
+
with check (
|
|
201
|
+
(select auth.uid()) is not null
|
|
202
|
+
and (select auth.uid()) = user_id
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
-- DELETE — apenas a coluna using (sem with check)
|
|
206
|
+
create policy "users_delete_own_tasks"
|
|
207
|
+
on public.tasks
|
|
208
|
+
for delete
|
|
209
|
+
to authenticated
|
|
210
|
+
using (
|
|
211
|
+
(select auth.uid()) is not null
|
|
212
|
+
and (select auth.uid()) = user_id
|
|
213
|
+
);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Role admin via `app_metadata`
|
|
217
|
+
|
|
218
|
+
```sql
|
|
219
|
+
-- segurança: app_metadata é set apenas via service_role (admin API)
|
|
220
|
+
-- cliente NÃO pode mutá-lo
|
|
221
|
+
create policy "admins_manage_all_tasks"
|
|
222
|
+
on public.tasks
|
|
223
|
+
for update
|
|
224
|
+
to authenticated
|
|
225
|
+
using (
|
|
226
|
+
(select auth.jwt()->'app_metadata'->>'role') = 'admin'
|
|
227
|
+
)
|
|
228
|
+
with check (
|
|
229
|
+
(select auth.jwt()->'app_metadata'->>'role') = 'admin'
|
|
230
|
+
);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Team membership via `app_metadata` (array)
|
|
234
|
+
|
|
235
|
+
```sql
|
|
236
|
+
-- exemplo: app_metadata.teams = ["team_a", "team_b"]
|
|
237
|
+
create policy "team_members_view"
|
|
238
|
+
on public.team_resources
|
|
239
|
+
for select
|
|
240
|
+
to authenticated
|
|
241
|
+
using (
|
|
242
|
+
team_id::text in (select jsonb_array_elements_text((select auth.jwt()->'app_metadata'->'teams')))
|
|
243
|
+
);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### MFA enforcement (AAL2)
|
|
247
|
+
|
|
248
|
+
```sql
|
|
249
|
+
-- exigir 2FA ativo para acessar dados sensíveis
|
|
250
|
+
-- restrictive force AND com policy SELECT base
|
|
251
|
+
create policy "mfa_required_for_billing"
|
|
252
|
+
on public.billing_records
|
|
253
|
+
as restrictive
|
|
254
|
+
for select
|
|
255
|
+
to authenticated
|
|
256
|
+
using (
|
|
257
|
+
(select (auth.jwt()->>'aal')::text) = 'aal2'
|
|
258
|
+
);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Views com `security_invoker=true` (Postgres 15+) — v1.23
|
|
262
|
+
|
|
263
|
+
Por padrão, views são criadas com `security_definer` (rodam com permissões do criador, geralmente `postgres`) — **bypassam RLS** das tabelas subjacentes. Em Postgres 15+, use `security_invoker=true` para fazer a view respeitar as policies RLS do role chamador:
|
|
264
|
+
|
|
265
|
+
```sql
|
|
266
|
+
create view public.user_active_tasks
|
|
267
|
+
with (security_invoker = true)
|
|
268
|
+
as
|
|
269
|
+
select id, title, status, created_at
|
|
270
|
+
from public.tasks
|
|
271
|
+
where status = 'active';
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Em versões < Postgres 15:** revoke acesso de `anon`/`authenticated` à view, ou crie em schema não-exposto:
|
|
275
|
+
|
|
276
|
+
```sql
|
|
277
|
+
-- alternativa pré-Postgres 15: revoke acesso
|
|
278
|
+
revoke select on public.legacy_view from anon, authenticated;
|
|
279
|
+
|
|
280
|
+
-- ou crie em schema privado:
|
|
281
|
+
create view private.internal_view as ...;
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## `app_metadata` vs `user_metadata` — caveats canônicos (v1.23)
|
|
285
|
+
|
|
286
|
+
A função `auth.jwt()` retorna o JWT do usuário fazendo a requisição. Existem duas claims relacionadas a metadata:
|
|
287
|
+
|
|
288
|
+
- **`raw_user_meta_data`** — pode ser atualizado pelo authenticated end-user via `supabase.auth.updateUser({ data: { ... } })`. **NÃO é seguro para authorization.** Use apenas para preferences (tema, idioma, avatar URL).
|
|
289
|
+
- **`raw_app_meta_data`** — **NÃO pode ser atualizado pelo cliente** — só via service_role + admin API. **É o lugar correto** para roles, permissions, team memberships, plan tier.
|
|
290
|
+
|
|
291
|
+
**Caveat #1 (JWT freshness):** JWT nem sempre está "fresh". Se você remove um user de um team e atualiza `app_metadata`, isso não reflete em `auth.jwt()` até o JWT ser refreshed (geralmente 1h TTL). Para invalidação imediata, force logout do user via `auth.admin.signOut()`.
|
|
292
|
+
|
|
293
|
+
**Caveat #2 (cookie 4096 bytes):** Se você usa Cookies para Auth, esteja atento ao JWT size. Browsers limitam cada cookie a 4096 bytes. Se você embarca arrays grandes em `app_metadata.teams` ou similar, o JWT pode passar do limite. Mitigação: store apenas IDs, busque membership via SQL com policy/RPC.
|
|
294
|
+
|
|
295
|
+
**Caveat #3 (NULL handling):** Para requests sem auth, `auth.uid()` retorna `null`. `null = user_id` é sempre **false silenciosamente** em SQL — a policy não erra, só não match. Sempre use `IS NOT NULL AND ...` (REGRA #3) para deixar intent claro.
|
|
296
|
+
|
|
297
|
+
## Performance — recomendações canônicas (v1.23)
|
|
298
|
+
|
|
299
|
+
Toda authorization tem custo. RLS é poderoso mas pode ser caro em queries que scaneam muitas linhas. Recomendações baseadas em benchmarks oficiais Supabase:
|
|
300
|
+
|
|
301
|
+
### 1. Index nas colunas usadas em policies (99.94% improvement)
|
|
302
|
+
|
|
303
|
+
```sql
|
|
304
|
+
-- antes: scan full
|
|
305
|
+
-- depois: index scan
|
|
306
|
+
create index tasks_user_id_idx on public.tasks (user_id);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### 2. Envolva funções em `(select ...)` para caching de plano (até 99.99% improvement)
|
|
310
|
+
|
|
311
|
+
```sql
|
|
312
|
+
-- antes: re-executa auth.uid() por linha (179ms para 100k rows)
|
|
313
|
+
using (auth.uid() = user_id)
|
|
314
|
+
|
|
315
|
+
-- depois: initPlan, executa 1 vez e reusa (9ms)
|
|
316
|
+
using ((select auth.uid()) = user_id)
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Aplicável a qualquer função que não muda baseado em row data: `auth.uid()`, `auth.jwt()`, `security definer` functions. **Não funciona** se o resultado depende da linha (ex: `is_owner(row_id)` precisa rodar por linha).
|
|
320
|
+
|
|
321
|
+
### 3. Adicione filtros redundantes nas queries client-side (94.74% improvement)
|
|
322
|
+
|
|
323
|
+
Mesmo com policy aplicada, adicione `.eq()` ou `where` explícito no client:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
// errado — confia 100% na policy
|
|
327
|
+
const { data } = supabase.from('tasks').select()
|
|
328
|
+
|
|
329
|
+
// certo — Postgres usa o filtro para construir query plan melhor
|
|
330
|
+
const { data } = supabase.from('tasks').select().eq('user_id', userId)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Policy é "implicit where clause"; filtro explícito ajuda o planner Postgres a escolher index scan ao invés de seq scan.
|
|
334
|
+
|
|
335
|
+
### 4. Specify role com `TO` clause (99.78% improvement)
|
|
336
|
+
|
|
337
|
+
```sql
|
|
338
|
+
-- antes (sem TO): policy roda para todos roles, inclusive anon
|
|
339
|
+
create policy "rls_test_select" on rls_test
|
|
340
|
+
using ((select auth.uid()) = user_id);
|
|
341
|
+
|
|
342
|
+
-- depois: anon skipa execução
|
|
343
|
+
create policy "rls_test_select" on rls_test
|
|
344
|
+
to authenticated
|
|
345
|
+
using ((select auth.uid()) = user_id);
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 5. Use `security definer` functions para policies caras
|
|
349
|
+
|
|
350
|
+
Se a policy precisa fazer JOIN ou query custosa, encapsule em função `security definer` que bypassa RLS interno:
|
|
351
|
+
|
|
352
|
+
```sql
|
|
353
|
+
-- função pode acessar roles_table sem aplicar RLS recursivamente
|
|
354
|
+
create function private.has_good_role()
|
|
355
|
+
returns boolean
|
|
356
|
+
language plpgsql
|
|
357
|
+
security definer
|
|
358
|
+
set search_path = ''
|
|
359
|
+
as $$
|
|
360
|
+
begin
|
|
361
|
+
return exists (
|
|
362
|
+
select 1 from public.roles_table
|
|
363
|
+
where (select auth.uid()) = user_id and role = 'good_role'
|
|
364
|
+
);
|
|
365
|
+
end;
|
|
366
|
+
$$;
|
|
367
|
+
|
|
368
|
+
-- policy fica simples e cacheável
|
|
369
|
+
create policy "rls_test_select"
|
|
370
|
+
on public.test_table
|
|
371
|
+
to authenticated
|
|
372
|
+
using ((select private.has_good_role()));
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**IMPORTANTE:** funções `security definer` NUNCA em schema exposto (`public`). Sempre em `private` ou similar (não exposto via API settings).
|
|
376
|
+
|
|
377
|
+
### 6. Minimize joins (99.78% improvement)
|
|
378
|
+
|
|
379
|
+
Reescreva policies que fazem join na source table para usar `IN` ao invés:
|
|
380
|
+
|
|
381
|
+
```sql
|
|
382
|
+
-- antes: join entre test_table (source) e team_user (target)
|
|
383
|
+
create policy "rls_test_select" on public.test_table
|
|
384
|
+
to authenticated
|
|
385
|
+
using (
|
|
386
|
+
(select auth.uid()) in (
|
|
387
|
+
select user_id
|
|
388
|
+
from public.team_user
|
|
389
|
+
where team_user.team_id = team_id -- join com source!
|
|
390
|
+
)
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
-- depois: filtra primeiro, depois IN
|
|
394
|
+
create policy "rls_test_select" on public.test_table
|
|
395
|
+
to authenticated
|
|
396
|
+
using (
|
|
397
|
+
team_id in (
|
|
398
|
+
select team_id
|
|
399
|
+
from public.team_user
|
|
400
|
+
where user_id = (select auth.uid()) -- sem join
|
|
401
|
+
)
|
|
402
|
+
);
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Se a lista interna pode passar de 1000 items, considere abordagem com `security definer` function ao invés.
|
|
406
|
+
|
|
407
|
+
## Anti-patterns
|
|
408
|
+
|
|
409
|
+
### Anti-pattern 1: `auth.uid()` sem `(select)` wrapper
|
|
410
|
+
|
|
411
|
+
**Errado:**
|
|
412
|
+
```sql
|
|
413
|
+
create policy "users_select_own_tasks"
|
|
414
|
+
on public.tasks
|
|
415
|
+
for select
|
|
416
|
+
to authenticated
|
|
417
|
+
using (auth.uid() = user_id); -- sem (select) — re-executa por linha
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Por quê:** Postgres reavalia `auth.uid()` para cada linha sendo testada. Em tabela com 100k linhas, isso é 100k chamadas. O `(select)` permite Postgres executar **uma vez** e reusar — degradação de até **1000×** sem o wrapper. Documentado em [RLS Performance](https://supabase.com/docs/guides/troubleshooting/rls-performance-and-best-practices-Z5Jjwv).
|
|
421
|
+
|
|
422
|
+
**Certo:**
|
|
423
|
+
```sql
|
|
424
|
+
using ((select auth.uid()) = user_id)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Anti-pattern 2: `WARNING user_metadata` em autorização — privilege escalation
|
|
428
|
+
|
|
429
|
+
**Errado:**
|
|
430
|
+
```sql
|
|
431
|
+
create policy "admins_manage_all"
|
|
432
|
+
on public.tasks
|
|
433
|
+
for update
|
|
434
|
+
to authenticated
|
|
435
|
+
using (
|
|
436
|
+
(auth.jwt()->'user_metadata'->>'role') = 'admin' -- editável pelo cliente!
|
|
437
|
+
);
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Por quê:** o cliente pode chamar `supabase.auth.updateUser({ data: { role: 'admin' } })` e instantaneamente ganhar privilégios de admin. `user_metadata` é projetado para preferences do usuário (tema, idioma), não para autorização. Documentado em [Splinter linter 0015](https://supabase.github.io/splinter/0015_rls_references_user_metadata/).
|
|
441
|
+
|
|
442
|
+
**Certo:** ver "Role admin via `app_metadata`" acima — `app_metadata` requer service_role para mutar.
|
|
443
|
+
|
|
444
|
+
### Anti-pattern 3: `for all` em vez de policies granulares
|
|
445
|
+
|
|
446
|
+
**Errado:**
|
|
447
|
+
```sql
|
|
448
|
+
create policy "users_manage_own_tasks"
|
|
449
|
+
on public.tasks
|
|
450
|
+
for all -- cobre CRUD inteiro com mesma regra
|
|
451
|
+
to authenticated
|
|
452
|
+
using ((select auth.uid()) = user_id);
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Por quê:** semântica de `for all` mistura `using` (que controla SELECT/UPDATE/DELETE) com `with check` (que controla INSERT/UPDATE), levando a confusão. Em UPDATE você pode querer regras diferentes para "qual linha tocar" vs "qual estado novo". Granularidade explícita previne erros sutis.
|
|
456
|
+
|
|
457
|
+
**Certo:** ver pattern com 4 policies separadas acima (SELECT, INSERT, UPDATE, DELETE).
|
|
458
|
+
|
|
459
|
+
### Anti-pattern 4: Sem índice nas colunas da policy
|
|
460
|
+
|
|
461
|
+
**Errado:**
|
|
462
|
+
```sql
|
|
463
|
+
-- policy referencia user_id mas não há index
|
|
464
|
+
create policy "users_select_own_tasks" on public.tasks
|
|
465
|
+
for select to authenticated
|
|
466
|
+
using ((select auth.uid()) = user_id);
|
|
467
|
+
|
|
468
|
+
-- (esqueceu) create index on public.tasks (user_id);
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Por quê:** cada query com filtro RLS força sequential scan. Em produção com 100k+ linhas, isso é lentidão crônica.
|
|
472
|
+
|
|
473
|
+
**Certo:**
|
|
474
|
+
```sql
|
|
475
|
+
create index tasks_user_id_idx on public.tasks (user_id);
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Anti-pattern 5: ENABLE RLS sem GRANT — query falha silenciosa (v1.23)
|
|
479
|
+
|
|
480
|
+
**Errado:**
|
|
481
|
+
```sql
|
|
482
|
+
alter table public.tasks enable row level security;
|
|
483
|
+
-- esqueceu de grant select on public.tasks to authenticated;
|
|
484
|
+
create policy "users_select" ... using (...);
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Por quê:** RLS é camada *sobre* permissões de tabela. Sem GRANT, role não pode tentar acessar a tabela — query retorna "permission denied" mesmo se policy permitiria.
|
|
488
|
+
|
|
489
|
+
**Certo:** sempre GRANT primeiro, depois ENABLE RLS, depois policies (ver "Setup canônico" acima).
|
|
490
|
+
|
|
491
|
+
### Anti-pattern 6: View sem `security_invoker=true` em Postgres 15+ — bypass de RLS (v1.23)
|
|
492
|
+
|
|
493
|
+
**Errado:**
|
|
494
|
+
```sql
|
|
495
|
+
-- view criada como postgres user — bypassa RLS de public.tasks
|
|
496
|
+
create view public.user_tasks_view as
|
|
497
|
+
select id, title from public.tasks where user_id = auth.uid();
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Por quê:** views por default são `security_definer` (rodam com permissões do criador). Cliente acessando a view bypass RLS das tabelas underlying. Atacante consegue ler dados de outros users via SELECT na view.
|
|
501
|
+
|
|
502
|
+
**Certo:**
|
|
503
|
+
```sql
|
|
504
|
+
create view public.user_tasks_view
|
|
505
|
+
with (security_invoker = true)
|
|
506
|
+
as
|
|
507
|
+
select id, title from public.tasks
|
|
508
|
+
where user_id = (select auth.uid());
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Anti-pattern 7: `null = user_id` silent-fail (v1.23)
|
|
512
|
+
|
|
513
|
+
**Errado:**
|
|
514
|
+
```sql
|
|
515
|
+
using ((select auth.uid()) = user_id)
|
|
516
|
+
-- se user não logado, auth.uid() é null
|
|
517
|
+
-- null = user_id é always false → policy "funciona" mas confunde
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Por quê:** intent ambíguo — você queria bloquear não-logados ou tratar como caso especial? `null = X` retornar false silenciosamente esconde o intent e dificulta debug.
|
|
521
|
+
|
|
522
|
+
**Certo:**
|
|
523
|
+
```sql
|
|
524
|
+
using (
|
|
525
|
+
(select auth.uid()) is not null
|
|
526
|
+
and (select auth.uid()) = user_id
|
|
527
|
+
)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## Postgres Roles vs RLS — quando usar qual (v1.26)
|
|
531
|
+
|
|
532
|
+
Postgres roles e RLS são **conceitos complementares**:
|
|
533
|
+
|
|
534
|
+
| | Postgres Roles | RLS |
|
|
535
|
+
|---|---|---|
|
|
536
|
+
| Escopo | System access (service accounts, cron, BI) | Application access (end-users) |
|
|
537
|
+
| Identidade | Login Postgres (`SET ROLE`) | JWT (`auth.uid()`) |
|
|
538
|
+
| Granularidade | Per schema/table/function | Per linha + coluna |
|
|
539
|
+
| Audit | pg_stat_statements por role | RLS denial logs |
|
|
540
|
+
| Use case | "Cron job pode SELECT em todas tabs" | "User vê apenas próprias rows" |
|
|
541
|
+
|
|
542
|
+
**Princípio canônico v1.26:**
|
|
543
|
+
|
|
544
|
+
- **Para end-users:** RLS + Custom Claims (v1.25) — NÃO criar role Postgres por user
|
|
545
|
+
- **Para service accounts:** Postgres roles dedicados (NÃO usar service_role API key sempre)
|
|
546
|
+
- **Para column-level access:** Postgres roles + column-level GRANTs (skill `supabase-column-level-security` v1.24)
|
|
547
|
+
|
|
548
|
+
Padrão completo de Postgres roles em [`supabase-postgres-roles`](../supabase-postgres-roles/SKILL.md) (v1.26).
|
|
549
|
+
|
|
550
|
+
## RBAC via Custom Claims + authorize() function (v1.25)
|
|
551
|
+
|
|
552
|
+
A partir de v1.25, o pattern **canônico** de RBAC em Supabase é via **Custom Access Token Auth Hook** que injeta `user_role` no JWT durante geração do token. Em vez de policies fazendo JOIN custoso em `user_roles` table, a policy lê o claim direto via `auth.jwt() ->> 'user_role'` (ou via `authorize()` function que abstrai role → permission lookup).
|
|
553
|
+
|
|
554
|
+
```sql
|
|
555
|
+
-- Pattern v1.25 — RLS policy usando authorize() (claim do JWT consultado)
|
|
556
|
+
create policy "Allow authorized delete access" on public.channels for delete
|
|
557
|
+
to authenticated
|
|
558
|
+
using ((SELECT authorize('channels.delete')));
|
|
559
|
+
|
|
560
|
+
-- vs. pattern v1.21 — RLS policy usando helper function STABLE (JOIN em DB)
|
|
561
|
+
create policy "Allow admin delete" on public.channels for delete
|
|
562
|
+
to authenticated
|
|
563
|
+
using ((SELECT private.has_role('admin')));
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Vantagens canônicas do pattern v1.25:**
|
|
567
|
+
|
|
568
|
+
- **Performance:** claim no JWT é zero-JOIN; helper function STABLE faz query em user_roles table
|
|
569
|
+
- **Composability:** `authorize(permission)` abstrai role → permission; trocar quem tem permission = UPDATE em role_permissions (sem alterar policies)
|
|
570
|
+
- **Type safety:** `app_permission` enum garante consistência cross-policy
|
|
571
|
+
|
|
572
|
+
**Caveat JWT freshness:** mudanças em `user_roles` só refletem no JWT após refresh (TTL 1h). Para revogação imediata, force logout via `auth.admin.signOut(userId)`. Em multi-tenant complexo (role por org), **combine** custom claim (role global) + helper function PG (role context-aware) — claim sozinho não cobre per-org context.
|
|
573
|
+
|
|
574
|
+
Padrão completo (7 passos + anti-patterns + caveats) em [`supabase-custom-claims-rbac`](../supabase-custom-claims-rbac/SKILL.md) (v1.25).
|
|
575
|
+
|
|
576
|
+
## Combining RLS with Column-Level Privileges (v1.24)
|
|
577
|
+
|
|
578
|
+
RLS row-level e column-level privileges são **camadas complementares**:
|
|
579
|
+
|
|
580
|
+
- **RLS** filtra **quais linhas** o role vê/modifica
|
|
581
|
+
- **Column privileges** filtra **quais colunas** o role pode acessar dentro da linha
|
|
582
|
+
|
|
583
|
+
Combinação canônica: RLS + column-level (Camada 8 de defense-in-depth, skill `supabase-rls-defense-in-depth` v1.24).
|
|
584
|
+
|
|
585
|
+
```sql
|
|
586
|
+
-- 1. RLS row-level — user só vê próprias posts
|
|
587
|
+
create policy "users_select_own_posts"
|
|
588
|
+
on public.posts for select to authenticated
|
|
589
|
+
using ((select auth.uid()) is not null and (select auth.uid()) = user_id);
|
|
590
|
+
|
|
591
|
+
-- 2. Column-level — mesmo nas próprias posts, user não vê coluna sensível (ex: admin_notes)
|
|
592
|
+
revoke select on table public.posts from authenticated;
|
|
593
|
+
grant select (id, user_id, title, content, created_at) on table public.posts to authenticated;
|
|
594
|
+
|
|
595
|
+
-- 3. service_role / admin_role vê tudo (incluindo admin_notes)
|
|
596
|
+
grant select on table public.posts to service_role;
|
|
597
|
+
|
|
598
|
+
-- Cliente DEVE listar colunas explicitamente:
|
|
599
|
+
-- ❌ supabase.from('posts').select() — FALHA (wildcard expansion → admin_notes)
|
|
600
|
+
-- ✅ supabase.from('posts').select('id, user_id, title, content, created_at')
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**Quando combinar:**
|
|
604
|
+
|
|
605
|
+
- Compliance LGPD/GDPR onde algumas colunas (PII) precisam restrição extra além do RLS
|
|
606
|
+
- Audit log com payload sanitizado — RLS filtra por org, column priv filtra payload
|
|
607
|
+
- Billing data — RLS filtra por owner, column priv filtra credit_card_token
|
|
608
|
+
|
|
609
|
+
**Quando NÃO combinar:**
|
|
610
|
+
|
|
611
|
+
- Caso comum (admin/user roles) — use dedicated role table (skill [`supabase-column-level-security`](../supabase-column-level-security/SKILL.md)) ao invés
|
|
612
|
+
- Tabelas sem PII real — overhead sem benefício
|
|
613
|
+
|
|
614
|
+
**Caveat crítico:** com column privileges, **todo SELECT deve listar colunas explicitamente** — `SELECT *` falha. Atualize SDK calls + queries SQL ad-hoc + ferramentas BI conectadas.
|
|
615
|
+
|
|
616
|
+
Padrão completo + 4 patterns canônicos em [`supabase-column-level-security`](../supabase-column-level-security/SKILL.md) (v1.24).
|
|
617
|
+
|
|
618
|
+
## Bypassing RLS — quando e como
|
|
619
|
+
|
|
620
|
+
3 mecanismos canônicos para bypass de RLS:
|
|
621
|
+
|
|
622
|
+
1. **`service_role`** — chave Supabase com bypass automático. **NUNCA** exponha ao cliente. Use APENAS em backend (Edge Functions com env var `SUPABASE_SERVICE_ROLE_KEY`, scripts admin, migrations). Caveat: ao chamar SDK Supabase com service_role mas com `Authorization: Bearer <user_jwt>` ainda set, RLS do user é aplicado (override).
|
|
623
|
+
2. **`alter role <name> with bypassrls`** — privilégio Postgres que permite role bypass RLS sempre. Use para roles internos (`postgres`, custom admin role). NUNCA conceda a um role que recebe requisições de cliente.
|
|
624
|
+
3. **`security definer` functions** — função roda com permissões do criador (geralmente `postgres` = bypassrls). Encapsule lógica admin/cross-tenant em função `security definer` no schema `private`.
|
|
625
|
+
|
|
626
|
+
Padrões avançados em [`supabase-rls-defense-in-depth`](../supabase-rls-defense-in-depth/SKILL.md) (v1.23).
|
|
627
|
+
|
|
628
|
+
## Ver também
|
|
629
|
+
|
|
630
|
+
- [supabase-rls-defense-in-depth](../supabase-rls-defense-in-depth/SKILL.md) — event trigger, BYPASSRLS, service_role caveat, security definer, views security_invoker (v1.23)
|
|
631
|
+
- [supabase-database-functions](../supabase-database-functions/SKILL.md) — funções com `set search_path = ''` que respeitam RLS
|
|
632
|
+
- [supabase-storage](../supabase-storage/SKILL.md) — RLS sobre `storage.objects` (multi-tenant path isolation)
|
|
633
|
+
- [supabase-auth-ssr](../supabase-auth-ssr/SKILL.md) — autenticação que popula `auth.uid()`
|
|
634
|
+
- [supabase-migrations](../supabase-migrations/SKILL.md) — migrations sempre com GRANT + RLS habilitado em novas tabelas
|
|
635
|
+
- [glossário](../_shared-supabase/glossary.md) — termos PT-BR↔EN + roles + comandos CLI
|