Skip to content

Plano Mestre — Migração de createSupabaseSystem() para RLS

Status: Em execução. Fases 0–5 ✅ concluídas. Próxima: Fase 6 (gestao-alunos). Escopo: ~20 Edge Functions autenticadas usando createSupabaseSystem() para CRUD padrão.
Objetivo: Eliminar bypass de RLS não justificado, ativando policies que hoje são código morto.


1. Reclassificação Real (Audit Corrigido)

O audit original listou ~20 funções. Após leitura do código real, a situação é diferente:

Grupo A — Dead Code (importam createSupabaseSystem mas NÃO usam)

FunçãoSituação Real
especialista-olimpiadasCria supabaseSystem mas todas as queries usam supabase (RLS). Dead code.
especialista-headersIdem — cria mas não usa. Dead code.
especialista-cursosIdem.
escola-dashboardCria supabaseSystem mas todas as queries usam supabase (RLS). Dead code.
eventos-calendarioIdem.
eventos-calendario-trialIdem.

Ação: Remover imports/instanciações mortas. Zero risco. Nenhuma policy nova. ✅ Executado na Fase 0.

Grupo A.2 — Reclassificados para Grupo B (audit original errou)

FunçãoPlano original diziaRealidade encontrada
admin-dashboardDead codeUsa ativamente — queries em portal_rate_metrics, escolas
admin-usuariosDead codeUsa ativamente — queries em usuario_papeis, usuarios
admin-usuarios-escolaDead codeUsa ativamente — queries em papeis, usuario_papeis, usuarios

Ação: Movidas para Grupo B. Serão tratadas nas fases seguintes com policies RLS.

Grupo B — Uso Real de supabaseSystem para CRUD

FunçãoGravidadeO que usa supabaseSystem paraTabelas
gestao-alunos + helpersALTA30+ operações CRUDalunos, turmas, escolas, aluno_turma_historico, aluno_responsaveis, importacao_alunos_sessoes
gestao-turmasALTATodo CRUD de turmas + sériesturmas, series_escolares, escolas
admin-feature-flagsMÉDIACRUD de canary groups/flagscanary_groups, canary_group_escolas, canary_group_usuarios, feature_flag_canary
admin-dashboardMÉDIAQueries em portal_rate_metrics, escolasportal_rate_metrics, escolas
admin-usuariosMÉDIAQueries em usuario_papeis, usuariosusuario_papeis, usuarios
admin-usuarios-escolaMÉDIACRUD em papeis, usuario_papeis, usuariospapeis, usuario_papeis, usuarios
change-passwordMÉDIASELECT senha_hash + INSERT notificaçõesusuarios, notificacoes
set-passwordMÉDIASELECT/UPDATE senha_hash + INSERT históricousuarios, senha_historico
notificacoes (create_internal, create_batch)MÉDIAINSERT notificações + query usuario_papeisnotificacoes, usuario_papeis
escola-pagamentosBAIXAINSERT notificações (complementar)notificacoes
coordenador-videosBAIXAUPDATE visualizações (1 operação)cursos_videos
gestao-resultados (import + cleanup)MÉDIASessões import + cleanup mural_liberacoesimportacao_resultados_sessoes, configuracoes_fase_nivel, mural_liberacoes

2. Crítica da Sequência Original

A sequência proposta pelo usuário tinha problemas identificados:

Problema 1: admin-dashboard, admin-usuarios e admin-usuarios-escola são falsos positivos — não usam supabaseSystem de verdade. Gastar 3 ciclos de Cenário B neles é desperdício.

Problema 2: change-password e set-password acessam usuarios.senha_hash — campo que não deve ser acessível via RLS (expõe hash de senha via PostgREST). São casos onde supabaseSystem é possivelmente legítimo, não dívida técnica.

Problema 3: escola-pagamentos e coordenador-videos usam supabaseSystem para 1 operação cada. São micro-correções, não Cenário B completo.


3. Sequência Final Validada

Fase 0 — Limpeza de Dead Code (1 entrega) ✅ CONCLUÍDA

Removidos imports/instanciações mortas de createSupabaseSystem() em 6 funções:

  • especialista-olimpiadas, especialista-headers, especialista-cursos
  • escola-dashboard, eventos-calendario, eventos-calendario-trial

Nota: 3 funções originalmente listadas (admin-dashboard, admin-usuarios, admin-usuarios-escola) foram reclassificadas como Grupo B após análise real do código revelar uso ativo de supabaseSystem.

Risco: Zero. Removeu código morto. Nenhuma policy nova.

Fase 1 — admin-feature-flags (1 entrega) ✅ CONCLUÍDA

Justificativa de ser primeiro: Admin-only, tabelas de flags já têm SELECT policies, risco operacional baixo, CRUD real com supabaseSystem.
Desafio: Precisa de INSERT/UPDATE/DELETE policies para canary_groups, canary_group_escolas, canary_group_usuarios, feature_flag_canary — admin-only.
Dependências: Nenhuma.

Entregue em 2026-04-07:

  • 12 policies RLS admin-only criadas (UPDATE feature_flags; INSERT/UPDATE/DELETE canary_groups; SELECT/INSERT/DELETE canary_group_escolas e canary_group_usuarios; INSERT/DELETE feature_flag_canary)
  • Código migrado: createSupabaseSystem()createSupabaseClient(req), todas as referências supabaseSystemsupabase
  • Testes de segurança criados em security.test.ts (4 cenários: coordenador bloqueado 403, admin permitido 200)
  • Testes de contrato baseline: 5/5 passando

Fase 2 — notificacoes (1 entrega) ✅ CONCLUÍDA

Justificativa: Função mista — list/mark_read já usam RLS. Apenas create_internal e create_batch usam supabaseSystem. Migração parcial e simples.
Desafio: Admin cria notificações para usuários de outras escolas — precisa policy de INSERT com principal_role = 'administrador'.
Dependência: Nenhuma.

Entregue em 2026-04-07:

  • 1 policy RLS criada: notificacoes_admin_insert (FOR INSERT, admin-only, WITH CHECK)
  • Código migrado: removidas 2 instanciações de createSupabaseSystem(), ~5 referências supabaseSystemsupabase
  • Import de createSupabaseSystem removido do arquivo
  • Testes de contrato baseline: 5/5 passando
  • Testes de segurança criados em security.test.ts (4 cenários: coordenador bloqueado 403, admin permitido 200)

Fase 3 — escola-pagamentos + coordenador-videos

Entregue em 2026-04-07:

  • coordenador-videos: removido createSupabaseSystem e fallback UPDATE redundante. RPC increment_video_view (SECURITY DEFINER) chamado via supabase (RLS client). Zero policies necessárias.
  • escola-pagamentos: migração parcial. recalculate_due_dates migrado para RLS com 2 policies novas (escola_assinaturas_escola_update_own, escola_faturas_escola_update_own). request_plan_change mantém supabaseSystem — reclassificado como ADR Categoria 2 (notificação cross-tenant para admins).
  • 4 testes de contrato criados para coordenador-videos (CORS, 401, JSON shape, anti-leak). 8/8 testes passando.
  • Documentação atualizada: ADR, RLS_POLICIES, CATALOG, PLANO.

Fase 4 — change-password + set-password + reset-password ✅ CONCLUÍDA

Entregue em 2026-04-07:

  • 4 RPCs SECURITY DEFINER criadas: get_own_password_hash, update_own_password_hash, get_own_password_history, save_to_password_history
  • Guard de ownership: (auth.jwt()->>'sub')::uuid != p_usuario_id (padrão OLP, não auth.uid())
  • set-password: 100% migrado — zero createSupabaseSystem. Usa createSupabaseClient(req) + RPCs.
  • change-password: parcial — operações de senha via RPCs. supabaseSystem mantido apenas para HIBP fire-and-forget (INSERT notificacoes — ADR Cat. 2).
  • reset-password: idem change-password.
  • password-history-helper.ts: refatorado de queries diretas para supabase.rpc().
  • Testes existentes: 14/14 passando (4 auth-password + 10 modal-definir-senha).

Fase 5 — gestao-turmas (1 entrega — Cenário B completo) ✅ CONCLUÍDA

Entregue em 2026-04-07:

  • Decisão arquitetural: Admin removido do fluxo — turmas são conteúdo pedagógico, fora do escopo admin (menor privilégio). Nenhuma policy admin criada.
  • Policy legada turmas_admin_readonly removida (DROP).
  • createSupabaseSystem() eliminado — todas as operações usam createSupabaseClient(req) (RLS ativo).
  • listSeries migrado para createSupabasePublic() (tabela pública).
  • requireRole restrito a ['escola', 'coordenador'].
  • escola_id override removido — sempre user.escola_id (zero-trust).
  • Validação zero-trust: .maybeSingle() em INSERT/UPDATE/DELETE com check de resultado vazio → 403.
  • Testes de contrato endurecidos: 4/4 passando (CORS 204 + credentials, 401 auth guard, Content-Type JSON, anti-leak).
  • Documentação atualizada: ADR, RLS_POLICIES, PLANO.

Fase 6 — gestao-alunos (1 entrega — Cenário B completo, maior da lista)

Justificativa: Mesmo padrão school-scoped que gestao-turmas mas 4 arquivos de helpers, 30+ operações, 6+ tabelas.
Dependência: Fase 5 validou o padrão school-scoped sem admin. Avaliar se admin precisa de acesso a alunos (decisão a documentar).
Tabelas: alunos, turmas, escolas, aluno_turma_historico, aluno_responsaveis, importacao_alunos_sessoes.

Fase 7 — gestao-resultados (import + mutations cleanup)

Justificativa: Último. As queries e mutations normais já usam RLS. Apenas import sessions e cleanup usam supabaseSystem.
Tabelas: importacao_resultados_sessoes, configuracoes_fase_nivel, mural_liberacoes.
Nota: Import sessions podem ser reclassificadas como Categoria 2 (fire-and-forget background) no ADR.


4. Template de Entrega por Função (Cenário B)

Cada função com uso real de supabaseSystem segue estas fases:

Fase 1: Raio-X
  - Inventário: arquivos, tabelas, operações (SELECT/INSERT/UPDATE/DELETE)
  - Policies existentes no banco (query pg_policies)
  - Cobertura de testes atual (index.test.ts?)
  - Desvios de padrão vs docs/
  → Aprovação

Fase 2: Testes Baseline
  - Testes de contrato Deno (CORS, auth guard, response shape)
  - Correção de desvios identificados (se houver)
  → Aprovação

Fase 3: Planejamento da Migração
  - Quais policies RLS precisam ser criadas (SQL exato)
  - Quais linhas mudam de supabaseSystem → supabase
  - MIGRATION_SAFETY_PROTOCOL Fase 1
  → Aprovação

Fase 4: TDD — Testes + Migration SQL
  - Escrever testes de segurança IDOR (se school-scoped)
  - SQL completo das policies
  - MIGRATION_SAFETY_PROTOCOL Fase 2 + Checklist
  → Aprovação

Fase 5: Execução
  - Aplicar migration
  - Alterar código (supabaseSystem → supabase)
  - Rodar testes — todos devem passar
  - MIGRATION_SAFETY_PROTOCOL Fase 3

Fase 6: @audit + Documentação
  - AUDIT_CHECKLIST completo na função
  - Atualizar ADR_SERVICE_ROLE_USAGE.md (mover função de "dívida" para "migrado")
  - Atualizar RLS_POLICIES.md com as novas policies criadas
  - Verificar DATABASE_SCHEMA.md — novas policies ou RPCs exigem atualização?
  - Verificar EDGE_FUNCTIONS_CATALOG.md — mudança estrutural na função exige atualização?
  - Seguir checklist completo de docs/development/DOCUMENTATION_MAINTENANCE.md

5. Dependências entre Funções

Fase 0 (dead code)     ← sem dependência
Fase 1 (admin-flags)   ← sem dependência
Fase 2 (notificacoes)  ← sem dependência
Fase 3 (pagamentos)    ← depende de policy INSERT notificacoes (Fase 2)
Fase 3 (coord-videos)  ← sem dependência (usa RPC existente)
Fase 4 (passwords)     ← sem dependência (RPCs novos)
Fase 5 (turmas)        ← sem dependência ✅ concluída (school-scoped, sem admin)
Fase 6 (alunos)        ← depende de Fase 5 (valida padrão school-scoped)
Fase 7 (resultados)    ← sem dependência técnica

6. Critério de Done por Função

Uma função é declarada migrada quando:

  1. Zero uso de createSupabaseSystem() fora de registrarLog() e categorias legítimas do ADR
  2. Policies RLS cobrindo SELECT/INSERT/UPDATE/DELETE para cada tabela acessada
  3. Testes de contrato Deno passando (CORS, auth, response shape)
  4. Teste IDOR cross-escola passando (se school-scoped)
  5. @audit sem itens 🔴
  6. ADR_SERVICE_ROLE_USAGE.md atualizado (linha movida de dívida para migrado)
  7. RLS_POLICIES.md atualizado com novas policies
  8. DATABASE_SCHEMA.md e EDGE_FUNCTIONS_CATALOG.md verificados e atualizados se necessário
  9. Checklist de DOCUMENTATION_MAINTENANCE.md seguido

7. Estimativa de Complexidade

FaseFunçãoComplexidadePolicies novas
06 funções (dead code)Trivial0
1admin-feature-flagsMédia~12 (CRUD admin em 4 tabelas)
2notificacoesBaixa~4 (INSERT admin + self-access)
3pagamentos + videosTrivial0-2 (reusa policies)
4passwordsMédia0 policies, 4 RPCs novos
5gestao-turmasAlta0 novas, 1 removida (admin readonly)
6gestao-alunosMuito Alta~20+ (6 tabelas, school-scoped + admin)
7gestao-resultadosMédia~6 (sessions + config)

Referências