Skip to content

Row Level Security (RLS) — Plataforma OLP

Princípio

Todas as tabelas do projeto possuem RLS habilitado. As policies seguem o princípio do menor privilégio — cada papel recebe apenas as permissões mínimas necessárias.


Padrão de Policy para Dados de Escola

A maioria das tabelas com escopo de escola segue este padrão:

sql
-- SELECT: usuário vê dados da sua escola
CREATE POLICY "select_by_escola" ON tabela
  FOR SELECT USING (
    escola_id::text = auth.jwt()->>'escola_id'
    AND lower(auth.jwt()->>'principal_role') = ANY(ARRAY['escola','coordenador','diretor'])
  );

-- INSERT: apenas gestor/coordenador
CREATE POLICY "insert_by_escola" ON tabela
  FOR INSERT WITH CHECK (
    escola_id::text = auth.jwt()->>'escola_id'
    AND lower(auth.jwt()->>'principal_role') = ANY(ARRAY['escola','coordenador'])
  );

-- UPDATE: apenas gestor/coordenador
CREATE POLICY "update_by_escola" ON tabela
  FOR UPDATE USING (
    escola_id::text = auth.jwt()->>'escola_id'
    AND lower(auth.jwt()->>'principal_role') = ANY(ARRAY['escola','coordenador'])
  );

Modelo de Permissões por Papel

Administrador

EscopoAcessoTabelas
CRUD CompletoALLescolas, endereco_escola, usuarios, usuario_papeis, papeis, escola_assinaturas, escola_faturas, planos
Leitura (Auditoria)SELECTolimpiadas, fases_olimpiada, alunos, turmas, inscricoes_olimpiada, responsaveis, logs_transacoes, mural_*
Sem AcessoINSERT/UPDATE/DELETE em dados escolares ou de olimpíadas

Especialista

EscopoAcessoTabelas
CRUD CompletoALLolimpiadas, fases_olimpiada, atividades_cronograma, niveis_competicao, banners_login, headers_novidades, cursos, cursos_videos, templates_mensagem
LeituraSELECTescolas, escola_olimpiadas, inscricoes_olimpiada, configuracoes_fase_nivel
Sem AcessoDados de alunos, turmas, responsáveis, usuários

Escola (Gestor)

EscopoAcessoTabelas
CRUD CompletoALLalunos, turmas, responsaveis, aluno_responsaveis, inscricoes_olimpiada, resultados_aluno, mural_*, mensagens_enviadas
LeituraSELECTolimpiadas, fases_olimpiada, series_escolares, niveis_competicao
Sem Acessotarefas, eventos_manuais_escola, dados de outras escolas

Coordenador

TabelaSELECTINSERTUPDATEDELETE
tarefas✅ própria escola✅ (criador/responsável/coordenador)✅ (criador ou órfã)
eventos_manuais_escola✅ própria escola✅ (criador ou órfã)✅ (criador ou órfã)
Demais tabelas operacionais✅ própria escolaDepende da tabela

Tarefas e eventos órfãos (onde criado_por IS NULL por exclusão de usuário) podem ser gerenciados por qualquer coordenador da mesma escola.

Diretor

EscopoAcessoObservação
LeituraSELECT própria escolaDashboards e relatórios apenas
Sem Acessotarefas, eventos_manuais_escola

Portal (Aluno / Responsável)

PapelAcessoFiltro
AlunoSELECTid = auth.jwt()->>'sub' (apenas seus dados)
ResponsávelSELECTaluno_id IN (SELECT get_alunos_responsavel()) (apenas filhos vinculados)

Tabelas Críticas

TabelaINSERTSELECTUPDATEDELETE
logs_transacoesSistema only (service_role)Admin + Gestor da escolaBloqueadoBloqueado
login_otps / portal_otpsSistema onlyBloqueadoSistema onlyBloqueado
cron_statusSistema onlyAdmin onlySistema onlyBloqueado
configuracoes_plataformaEspecialistaPúblico (leitura)EspecialistaBloqueado

SECURITY DEFINER — Prevenção de Recursão Infinita

Problema

Quando policies de tabelas diferentes referenciam umas às outras em subqueries, ocorre recursão infinita (Postgres error 42P17).

Exemplo do ciclo:

alunos (policy: portal_responsavel_filhos_alunos)
    → Subquery em aluno_responsaveis
        → aluno_responsaveis (policy: aluno_resp_escola_select)
            → EXISTS (SELECT 1 FROM alunos WHERE ...)
                → RECURSÃO INFINITA

Solução

Funções SECURITY DEFINER executam com privilégios do owner (superuser), bypassando RLS durante sua execução. Isso quebra o ciclo de recursão.

Funções Implementadas

1. aluno_pertence_escola(p_aluno_id uuid) → boolean

Verifica se um aluno pertence à escola do usuário logado (sistema).

sql
CREATE OR REPLACE FUNCTION public.aluno_pertence_escola(p_aluno_id uuid)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
AS $$
  SELECT EXISTS (
    SELECT 1 FROM alunos
    WHERE id = p_aluno_id AND escola_id::text = auth.jwt()->>'escola_id'
  );
$$;

Uso: Policies de sistema em aluno_responsaveis para gestores/coordenadores.

2. get_alunos_responsavel() → SETOF uuid

Lista IDs de alunos vinculados ao responsável logado.

sql
CREATE OR REPLACE FUNCTION public.get_alunos_responsavel()
RETURNS SETOF uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
AS $$
  SELECT aluno_id FROM aluno_responsaveis
  WHERE responsavel_id::text = auth.jwt()->>'sub';
$$;

Uso: Policy portal_responsavel_filhos_alunos em alunos, inscricoes_olimpiada, resultados_aluno.

3. responsavel_vinculado_aluno(p_aluno_id uuid) → boolean

Verifica se o responsável logado tem vínculo com um aluno específico.

sql
CREATE OR REPLACE FUNCTION public.responsavel_vinculado_aluno(p_aluno_id uuid)
RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
AS $$
  SELECT EXISTS (
    SELECT 1 FROM aluno_responsaveis
    WHERE aluno_id = p_aluno_id AND responsavel_id::text = auth.jwt()->>'sub'
  );
$$;

4. responsavel_tem_aluno_na_escola(p_responsavel_id uuid) → boolean

Verifica se o responsável tem algum filho na escola do usuário logado.

Por que é seguro?

RiscoMitigação
SQL InjectionSQL é hardcoded, não aceita parâmetros de texto
Privilege EscalationFunção valida claims JWT antes de retornar
Data LeakageRetorna apenas UUIDs ou booleans, não dados completos
Search Path AttackSET search_path = public força schema explícito

Histórico de Hardening

DataDescrição
2026-03-05gestao-resultados: Implementado queryInChunks para evitar limite de URL do PostgREST com arrays grandes de UUIDs (>100). Corrige bug onde escolas com 600+ inscrições viam resultados zerados.
2026-03-04FK SET NULL: 11 FKs de usuarios alteradas de NO ACTION para SET NULL para permitir exclusão de usuários preservando histórico
2026-03-04Tarefas: SELECT restrito a coordenador (removido diretor, pedagogico, professor, marketing). DELETE restrito a coordenador (criador ou órfã)
2026-03-04Eventos manuais: removida policy SELECT para escola_trial, removidas policies genéricas sem filtro de papel, UPDATE/DELETE incluem suporte a registros órfãos
2026-03-04resultados_aluno: Policy SELECT expandida para incluir coordenador e diretor (antes apenas escola)
2026-03-04importacao_resultados_sessoes: Policy SELECT adicionada para owner da sessão (usuario_id = sub)
2026-01-22Implementação de funções SECURITY DEFINER para portal
2026-01-25Hardening admin: removido CRUD em 19 tabelas fora do escopo, mantido SELECT para auditoria
2026-01-25Especialista: mantém CRUD em olimpíadas, Admin recebe SELECT-only

Checklist para Novas Tabelas

markdown
□ RLS habilitado: ALTER TABLE tabela ENABLE ROW LEVEL SECURITY
□ Policy SELECT com filtro por escola_id e papel
□ Policy INSERT com WITH CHECK por escola_id e papel
□ Policy UPDATE com USING + WITH CHECK
□ Policy DELETE apenas se necessário (preferir soft delete)
□ Admin: SELECT-only (auditoria) — nunca CRUD em dados operacionais
□ Especialista: acesso apenas se tabela é de conteúdo (olimpíadas, cursos)
□ Portal: SELECT-only com filtro por sub ou get_alunos_responsavel()
□ Testar com admin_rls_smoke via auth-diagnostics