Pular para o conteúdo principal

Matriz de acesso — Emplacamento e Intranet

Versão consolidada e commitada da matriz de acesso do Emplacamento e da Intranet, de-para Vona 1.0 → Vona 2.0. Autoria original: Victor Melo, 29/07/2026. Este documento reconstrói e versiona o conteúdo referenciado pelas tasks TEC-542 (de-para dos perfis, done), TEC-543 (telas administrativas, done), TEC-556 (tela Usuários, code review), TEC-557 (permissões e allowlist, este documento) e TEC-563 (vínculo de acesso à Intranet, fechada). Antes desta versão, o documento existia só como texto solto nas tasks do ClickUp, sem histórico no git.

Pendência conhecida: este documento ainda não foi validado formalmente com o time técnico (item que ficou desmarcado no fechamento da TEC-543) — o conteúdo abaixo reflete o estado decidido e já implementado no código até 31/07/2026, não substitui essa validação.

§4 — Quem acessa o painel administrativo (tela Usuários)

PapelAcesso à tela de Usuários
super-admin / owner / adminTotal — cria, edita, ativa/desativa, exclui (via inativação), exporta
conc_grupoVê e convida apenas usuários do próprio grupo (tem invites.manage, não tem members.manage)
conc_lojaSem acesso — rota redireciona (D4: conc_loja não convida)
comunicacao / montadora / parceiroSem acesso

Diverge de propósito do Belite: lá o conc_loja tem leitura escopada nessa tela; no Emplacamento/Intranet, não entra.

member nunca aparece nesta tela nem em nenhum seletor de perfil do Emplacamento/Intranet (D2 — member existe só por exigência do MakerKit, nunca é atribuído de fato). O associado entra sempre como conc_grupo ou conc_loja.

§4.2 — Escopo de dado por perfil (correção de 30/07/2026)

Correção aplicada nesta versão: a redação anterior descrevia o perfil montadora como "rede nacional, agregado / opções de filtro completas (visão de rede)". Decisão de produto (30/07/2026): montadora vê apenas os dados da(s) própria(s) marca(s) — ex.: Fiat vê só Fiat, inclusive nas opções de filtro. O código já foi implementado assumindo esta correção (perfil-flag, sem junção de fabricante desde a decisão de 31/07 — ver §6).

PerfilEscopo de dado
admin / ownerGlobal, sem junção de escopo
conc_grupoTodas as lojas dos grupos vinculados, resolvido em runtime (junção de grupos, não expansão a partir de "loja âncora" — loja nova que entre no grupo depois aparece sozinha)
conc_lojaExatamente as concessionárias vinculadas
montadoraSó os dados da(s) marca(s) vinculada(s) (inclusive nas opções de filtro)

Seguem valendo para montadora em qualquer versão: não vê detalhamento de chassi nem dado sensível (CNPJ, faturamento individual).

§6 — Modelagem de escopo (3 junções N:N)

emplacamento_account_users -- perfil + status + dados cadastrais + boletim
emplacamento_user_concessionarias -- N:N usuário ↔ concessionária (conc_loja)
emplacamento_user_grupos -- N:N usuário ↔ grupo econômico (conc_grupo)
emplacamento_user_fabricantes -- N:N usuário ↔ fabricante/marca — LEGADA desde 31/07:
montadora virou perfil-flag puro, sem associação
de fabricante; tabela mantida só por dado histórico.

O lojas text[] do belite_account_users não serve de modelo aqui: não expressa expansão por grupo econômico nem multi-vínculo.

§8/§9 — Vínculo de acesso à Intranet

Usuários do painel administrativo do Emplacamento podem ganhar acesso à conta-irmã de Intranet do mesmo cliente (mesmo client_id, ver public.get_sibling_account_id). A concessão usa o mesmo perfil do Emplacamento (admin/conc_grupo/conc_loja/montadora) na conta Intranet — não um role fixo — com sincronização contínua nas duas direções (editar o perfil no Emplacamento propaga pra Intranet; trocar o role direto na tela de Members da Intranet espelha de volta pro perfil do Emplacamento). Inativar o usuário no Emplacamento revoga o acesso à Intranet concedido por essa tela.

Permissões (TEC-557)

As 8 permissões originais (roles.manage, billing.manage, settings.manage, members.manage, invites.manage, content.manage, reports.read, goals.manage) não sustentavam a matriz: reports.read misturava relatório editorial, BI de emplacamento, auditoria de acesso e dado financeiro sensível. TEC-557 adiciona 3 permissões, sem redefinir reports.read:

PermissãoSituaçãoRecebe
reports.emplacamento.readNova — BI de emplacamentoowner, admin, montadora, conc_grupo, conc_loja
reports.audit.readNova — auditoria de acessoowner, admin
reports.financial.readNova — libera CNPJ e faturamento individualowner, admin
reports.readMantida, semântica inalterada — relatório genérico (Belite)owner, admin, comunicacao, consultor_fi

Estado-alvo completo de role_permissions (implementado nas migrations 20260731210000/20260731220000 e em apps/web/supabase/schemas/17-roles-seed.sql):

PapelPermissões
owner (1)Todas as 11 (8 originais + 3 novas)
admin (2)settings.manage, members.manage, invites.manage, content.manage, reports.read, goals.manage + 3 novas
comunicacao (3)content.manage, reports.read (sem mudança)
montadora (4)reports.emplacamento.read
conc_grupo (5)reports.emplacamento.read, invites.manage
conc_loja (6)reports.emplacamento.read
parceiro (7)Nenhuma (representa o estado real — a área de parceiros não existe ainda)
member (8)Nenhuma (defesa em profundidade — nunca é atribuído de fato)
consultor_fi (9)reports.read, goals.manage, invites.manage (sem mudança)

§13 — Princípio de autorização

Policy e gate checam permissão, nunca nome de papel. Violar esse princípio foi a causa raiz de pelo menos um defeito real neste sistema: a policy de voto em enquete (23-polls.sql, poll_votes_insert) checa has_role_on_account(account_id, 'member') fixo — quando a decisão de produto mudou quem é o "associado" elegível, a regra quebrou silenciosamente (rastreado na TEC-547). RLS e gates novos devem usar has_permission(user_id, account_id, '<permissão>'), nunca comparar account_role a uma string fixa.

Allowlist de papéis por produto (TEC-557, parte 3)

public.roles é uma tabela global compartilhada entre Emplacamento, Intranet, Belite e Ganhaz. O seletor de convite/troca-de-role da tela genérica de Membros (packages/features/team-accounts) filtra por hierarchy_level, sem noção de produto — sem allowlist, oferece member e consultor_fi (papéis de outros produtos) em contas de Emplacamento/Intranet. Allowlist aplicada (admin, comunicacao, montadora, conc_grupo, conc_loja, parceiro — sem owner, sem member, sem consultor_fi): ver apps/web/app/[locale]/_shared/team-workspace/members/_lib/allowed-roles-by-product.ts.

Não confundir com o seletor de perfil da tela Usuários do Emplacamento (TEC-556): lá o select é próprio, com os 4 perfis do produto, e não passa por este mecanismo — esta allowlist cobre só a tela genérica Membros da conta.

§14 — Escopo de dado nos relatórios (TEC-558)

Resolver único (reports/_lib/server/emplacamento-scope.ts, resolveEmplacamentoReportScope) consumido por todos os relatórios do Emplacamento — devolve kind: 'full' | 'conc_grupo' | 'conc_loja' + hasFinancialAccess, aplicado como predicado SQL contra o DW (nunca RLS — o DW usa conexão direta fora do Supabase Auth, ver contexto original da task). Gate de rota por reports.emplacamento.read em reports/layout.tsx (cobre também daily-report e duplo-emplacamento, sem item de menu).

montadora sem recorte de marca (débito técnico confirmado, não omissão). Decisão de 31/07 (§4.2) fez montadora virar perfil-flag sem nenhum vínculo de marca no banco — o resolver não tem dado nenhum pra restringir por, então trata montadora como kind: 'full' (mesmo predicado de admin/owner). hasFinancialAccess continua false pra montadora (só owner/admin têm reports.financial.read), o que já bloqueia sozinho o detalhamento de chassi e deveria bloquear CNPJ em todo lugar que aplicar a mesma regra (ver achado do Ranking abaixo).

Por Ranking fica global, por design — única exceção. Comparar contra o mercado inteiro é o propósito de um ranking; escopar por conc_loja/ conc_grupo o descaracterizaria. Só o gate de ROTA vale para o Ranking — o dado em si não é filtrado por chamador. Ver comentário em por-ranking/_lib/server/ranking-service.ts.

Achados documentados, não corrigidos nesta leva (ficam para PR separado, sob risco de escopo crescer demais para revisão em um momento de urgência):

  • CNPJ ainda sai cru como value de opção de filtro (territory-service.ts::getFilterOptions) e como coluna direta no ranking de concessionárias (concessionarias-ranking.ts) — precisa de identificador opaco com resolução reversa no server, e mudança correspondente no client (territory-filter-bar.tsx).
  • 4 pontos em 22-belite-account-users.sql (+ 2 em 20-blog.sql) ainda checam nome de papel fixo (has_role_on_account(..., 'consultor_fi'/'conc_loja'/'comunicacao')) em vez de permissão — corrigir exigiria permissões novas no enum app_permissions, sem semântica pronta; decisão de criar essas permissões não foi tomada nesta sessão.