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)
| Papel | Acesso à tela de Usuários |
|---|---|
| super-admin / owner / admin | Total — cria, edita, ativa/desativa, exclui (via inativação), exporta |
| conc_grupo | Vê e convida apenas usuários do próprio grupo (tem invites.manage, não tem members.manage) |
| conc_loja | Sem acesso — rota redireciona (D4: conc_loja não convida) |
| comunicacao / montadora / parceiro | Sem 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
montadoracomo "rede nacional, agregado / opções de filtro completas (visão de rede)". Decisão de produto (30/07/2026):montadoravê 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).
| Perfil | Escopo de dado |
|---|---|
admin / owner | Global, sem junção de escopo |
conc_grupo | Todas 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_loja | Exatamente as concessionárias vinculadas |
montadora | Só 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ão | Situação | Recebe |
|---|---|---|
reports.emplacamento.read | Nova — BI de emplacamento | owner, admin, montadora, conc_grupo, conc_loja |
reports.audit.read | Nova — auditoria de acesso | owner, admin |
reports.financial.read | Nova — libera CNPJ e faturamento individual | owner, admin |
reports.read | Mantida, 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):
| Papel | Permissõ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
valuede 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 em20-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 enumapp_permissions, sem semântica pronta; decisão de criar essas permissões não foi tomada nesta sessão.