Pular para conteúdo

0035 — Autorização: fechar os portões, sanear as permissões, e o menu obedecer a elas

Status CONCLUÍDA
Branch spec/0035-menu-por-permissao
Criada em 2026-08-11
Apps afetados api · web · e2e

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

A spec cresceu durante a análise, e o motivo importa. Ela nasceu como "ajustar o menu de acordo com as permissões". Ao levantar a matriz para escrever o mapa do menu, a auditoria encontrou 13 endpoints sem verificação de permissão, e num deles uma escalada de privilégio explorável em produção. Ajustar a vitrine antes da fechadura seria inverter a ordem: o menu é conveniência, a autorização é o que segura. O escopo passou a ser o modelo de autorização inteiro, e o menu é o último item dele.

1. Problema

1.0 Qualquer usuário do tenant vira administrador dele

POST /api/v1/onboarding/users exige apenas estar autenticado, e OnboardingService.invite_team_user grava o papel que veio no corpo da requisição, sem olhar quem pediu:

await self._create_invite(..., role_name=data.role)   # data.role vem do cliente

A cadeia inteira: o solicitante escolhe um e-mail que controla, pede papel ADMIN_TENANT, recebe o link de convite, define a senha e entra como administrador da garantidora. Nenhuma etapa exige nada além de uma sessão válida.

Verificado, não deduzido. No dev-remote, rival@e2e.test — um AGENCY_USER, isto é, alguém de uma imobiliária parceira, externo à empresa — convidou um ADMIN_TENANT e recebeu HTTP 201. O usuário criado no teste foi removido.

Pior: data.role não é validado contra lista nenhuma, então ADMIN_GLOBAL também passa — o que transforma administrador de uma garantidora em administrador da plataforma.

O mesmo desenho aparece em POST /onboarding/partner-agencies/{id}/invite: qualquer autenticado convida usuário para qualquer imobiliária do tenant, inclusive a concorrente.

1.1 Treze endpoints sem porteiro

Endpoint Situação
POST /onboarding/users escalada de privilégio (1.0)
POST /onboarding/partner-agencies/{id}/invite idem, escopado a qualquer imobiliária
POST /onboarding/partner-agencies qualquer autenticado cria imobiliária
PATCH /onboarding/tenant qualquer autenticado altera dados da garantidora
POST /onboarding/activate qualquer autenticado ativa o ambiente
DELETE /onboarding/ qualquer autenticado cancela o onboarding
POST /tenants, POST/DELETE /tenants/{id}/purge cobertos pelo serviço (_require_admin_global)
POST /auth/register, GET /onboarding/summary, GET /onboarding/tenant, GET /tenant-invites/validate corretos por desenho

1.2 Cinco permissões que não governam nada

São 59 definidas e 52 exigidas por algum endpoint. As cinco restantes existem no papel de alguém e não têm efeito: credit_analyses:approve, credit_analyses:reject, credit_analyses:delete, decisions:list e partner_agencies:create.

As duas primeiras são as mais enganosas: a matriz sugere que aprovar e reprovar crédito são permissões próprias, quando na prática quem governa as duas é decisions:update. Uma permissão declarada e não aplicada é pior que ausente — ela responde "sim" à pergunta "isto está protegido?" sem proteger.

1.3 O menu é uma lista fixa

buildNavGroups (components/navigation/nav-groups.tsx) devolve os mesmos 14 itens para todo usuário autenticado do tenant, sem consultar permissão nenhuma. A sider do desktop e a gaveta do mobile consomem essa lista, então o defeito aparece nos dois.

Quem sente primeiro é a imobiliária parceira. O usuário dela abre o app e vê, no grupo ADMINISTRAÇÃO, Minha Garantidora, Equipe, Perfis de Acesso, Parâmetros, Tipos de Documento, Documentos da Análise e Auditoria — a estrutura administrativa da garantidora, oferecida a quem é de fora dela. Clicar não vaza dado (a API responde 403), mas o menu já revela como a empresa se organiza, e todo item é um convite a um beco sem saída.

Não é só a imobiliária. O AGENCY_USER também vê Garantias e Imobiliárias, que ele não pode listar; o OPERATOR vê Equipe, Perfis de Acesso e Auditoria; o VIEWER vê tudo isso e ainda Parâmetros. Em nenhum caso o menu diz a verdade sobre o que a pessoa pode fazer.

E esconder não bastaria. A rota continua registrada: quem digitar /audit-logs na barra de endereços chega à tela, que monta e só então falha com 403 da API — quando falha. Uma tela que só sabe carregar dado proibido fica em esqueleto, como aconteceu com o Centro de Operações na jornada da imobiliária (spec 0034).

O Painel virou privilégio de administrador sem que se decidisse isso

O Centro de Operações se alimenta do log de auditoria e o backend o fecha com audit:list (routers/operations_center.py:23), permissão que só ADMIN_GLOBAL e ADMIN_TENANT possuem. Enquanto o HomeGuard entregava a tela a todos, o efeito ficou escondido: analista, operador e visualizador recebiam a página, ela falhava por baixo e mostrava esqueleto.

A correção da spec 0034 passou a redirecionar quem não tem a permissão para /credit-analyses. Isso consertou a imobiliária — e, no mesmo movimento, tirou o Painel de analista, operador e visualizador, que é uma decisão de produto que ninguém tomou. Está no ar desde 2026-08-11.

2. Objetivo

Que a navegação diga a verdade: cada pessoa vê no menu exatamente o que pode usar, nos dois viewports, e a rota correspondente recusa quem não deveria chegar nela — com uma tela que explica, não com um erro cru.

3. Fora de escopo

  • Os 10 papéis area-scoped de docs/navigation-brief.md. Esta spec faz o menu obedecer à matriz de permissões que existe; redesenhar a matriz é outra feature.
  • Reescrever a matriz de permissões dos papéis atuais. A única permissão criada aqui é a do Painel (RN3), porque sem ela não há como responder à pergunta que a spec levanta. Nada mais é concedido nem retirado.
  • Menu do SaaS admin (buildAdminNavGroups). Aquela área é exclusiva de ADMIN_GLOBAL, que tem todas as permissões — filtrar lá não muda nada.
  • Autorização no backend. Já existe e é a que vale; o menu é conveniência de interface. Esta spec não afrouxa nem endurece nenhum require_permission, com a única exceção do Painel (RN3).
  • Itens "Em breve" (Cobranças, Sinistros) deixarem de ser placeholders. Continuam desabilitados; ganham só o recorte por permissão (RN6).

4. Regras de negócio

Autorização — todo endpoint tem porteiro

  • RN0. Nenhum endpoint autenticado fica sem verificação. Ou exige permissão (require_permission/require_any_permission), ou exige papel de sistema (require_system_role), ou está numa lista de exceção nomeada e justificada, coberta por teste. Estar autenticado prova quem você é, não o que você pode.
  • RN0a. As exceções legítimas, e por quê:
  • POST /auth/register e GET /tenant-invites/{token}/validate — públicos por natureza; a credencial é o próprio token.
  • POST /onboarding/tenant — o usuário acabou de se registrar e ainda não tem tenant nem papel nenhum; exigir permissão aqui tornaria o cadastro impossível. É ele que cria o tenant e recebe ADMIN_TENANT.
  • GET /onboarding/summary e GET /onboarding/tenant — leem o próprio tenant do solicitante, sem parâmetro que permita apontar para outro.
  • RN0b. Convite não concede o que quem convida não tem. O papel pedido no corpo é validado contra uma lista fechada, e ADMIN_GLOBAL nunca é concedível por convite — administrador de plataforma se cria por outro caminho. Papel desconhecido responde 422; ADMIN_GLOBAL responde 403.
  • RN0c. Toda permissão definida em _PERMISSIONS é exigida por pelo menos um endpoint. Permissão que não governa nada é removida; a que deveria governar passa a governar. Coberto por teste, para não voltar a divergir.

O princípio

  • RN1. Todo item de navegação declara a permissão que o habilita. Sem a permissão, o item não é renderizado — não aparece desabilitado, não aparece esmaecido, some. Menu que mostra o que não se pode usar ensina o usuário a tentar e falhar, e revela a estrutura interna a quem está de fora.
  • RN2. Navegar pede a permissão de leitura; administrar pede a de alteração. Um item que leva a uma tela de consulta é habilitado pelo :list do recurso. Um item que leva a uma tela de parametrização é habilitado pela permissão que permite mudá-la, não pela de lê-la. É o que separa "preciso do nome do tipo de documento para montar um checklist" de "administro os tipos de documento da garantidora" — o AGENCY_USER tem a primeira necessidade e nenhuma das duas telas.

O Painel

  • RN3. Nasce a permissão operations_center:read, concedida a ADMIN_TENANT, ANALYST, OPERATOR e VIEWER (e a ADMIN_GLOBAL, que tem todas). Os endpoints de /operations-center passam a exigi-la no lugar de audit:list.
  • RN4. O painel deixa de depender de audit:list porque auditoria sai do painel. O bloco "Atividade recente" lia a tabela audit_logs direto — era o log com outro nome —, e enquanto ele morava ali não havia resposta boa: ou o painel inteiro exigia permissão de auditoria e sumia para quem opera, que é o público da tela, ou o log vazava para quem não pode lê-lo. Auditoria fica na área administrativa, na tela de Auditoria, que já existe e é fechada por audit:list.

Some, portanto: o endpoint GET /operations-center/activity-feed, o método get_activity_feed, os schemas ActivityItem/ActivityFeedResponse e o bloco na interface. Sobram seis endpoints no painel, nenhum lendo auditoria — que é o que torna correto conceder operations_center:read a quem opera.

Esta regra nasceu errada e foi corrigida depois de a implementação já estar feita. A primeira versão dizia que amarrar painel e auditoria "foi atalho de implementação" — e isso era falso para um dos sete endpoints, justamente o que lia o log. Trocar o portão de todos por operations_center:read abriu o feed de auditoria para ANALYST, OPERATOR e VIEWER: fechou uma porta e abriu a janela ao lado. A pergunta do usuário — "por que o audit:list é necessário no painel?" — é o que expôs isso. - RN5. O HomeGuard passa a testar operations_center:read. Quem não a tem continua indo para /credit-analyses — hoje, só a imobiliária parceira.

O mapa

  • RN6. A tabela abaixo é normativa. Cada item existe no menu se e somente se o usuário tem a permissão da coluna.
Item Grupo Habilitado por
Painel — operations_center:read
Análise de Crédito OPERAÇÃO credit_analyses:list
Garantias OPERAÇÃO guarantees:list
Cobranças (em breve) OPERAÇÃO billings:list
Sinistros (em breve) OPERAÇÃO guarantees:list
Inquilinos CADASTROS renters:list
Imóveis CADASTROS properties:list
Imobiliárias CADASTROS partner_agencies:list
Minha Garantidora ADMINISTRAÇÃO tenants:read
Equipe ADMINISTRAÇÃO users:list
Perfis de Acesso ADMINISTRAÇÃO roles:list
Parâmetros ADMINISTRAÇÃO papel de sistema ADMIN_TENANT
Tipos de Documento ADMINISTRAÇÃO document_types:create
Documentos da Análise ADMINISTRAÇÃO credit_analysis_document_requirements:update
Auditoria ADMINISTRAÇÃO audit:list

Três escolhas dentro da tabela merecem o porquê:

  • Sinistros herda guarantees:list porque o sinistro nasce de uma garantia; quem não enxerga a garantia não terá o que acionar.
  • Parâmetros é a única linha gated por papel, e não por permissão, porque a API também é (require_system_role("ADMIN_TENANT") em routers/tenant_settings.py). O menu espelha o backend; inventar uma permissão que o backend ignora faria o item aparecer para quem tomaria 403.
  • Equipe fica em users:list, e não na permissão de alteração como manda a RN2, porque a tela é a lista do time — consulta, com ações para quem pode. A matriz concede users:list a ANALYST e VIEWER deliberadamente, e escondê-la deles retiraria um acesso que hoje existe e ninguém pediu para tirar.

  • RN7. Grupo cujos itens sumiram todos não renderiza o rótulo da seção. Um "ADMINISTRAÇÃO" seguido de nada é pior que a ausência: parece defeito de carregamento.

  • RN8. As abas da barra inferior do mobile (buildNavTabs) seguem a mesma tabela. A barra se recompõe com as abas que sobrarem; a aba "Mais", que abre a gaveta, nunca some — é o acesso ao resto do menu.

As permissões saneadas

  • RN11. Os endpoints de onboarding passam a exigir:
Endpoint Passa a exigir Por quê
POST /onboarding/users users:create + RN0b criar usuário é criar usuário, esteja no onboarding ou não
POST /onboarding/partner-agencies/{id}/invite users:create + RN0b idem — o convite cria conta
POST /onboarding/partner-agencies partner_agencies:create dá uso à permissão que existia sem dono (1.2)
PATCH /onboarding/tenant tenants:update é a mesma alteração que PATCH /tenants/{id} faz
POST /onboarding/activate papel ADMIN_TENANT ativar o ambiente é ato do dono da garantidora
DELETE /onboarding/ papel ADMIN_TENANT cancelar o onboarding apaga o tenant

users:create está hoje só com ADMIN_TENANT e ADMIN_GLOBAL — exigi-la já fecha a escalada da seção 1.0 para os outros quatro papéis, e a RN0b fecha o degrau que resta (o ADMIN_TENANT fabricando um ADMIN_GLOBAL).

  • RN12. credit_analyses:approve e credit_analyses:reject passam a valer: os endpoints /decision/approve e /decision/reject passam a exigi-las, além de decisions:update. Decidir crédito é a operação de maior consequência do produto e merece permissão própria — que já existia na matriz e não fazia nada. Os papéis que as têm hoje (ADMIN_TENANT, ADMIN_GLOBAL) e o ANALYST, que precisa delas para trabalhar, as recebem.
  • RN13. credit_analyses:delete e decisions:list são removidas: não há endpoint que as justifique, e inventar um só para dar emprego a uma permissão seria o rabo abanando o cachorro. Se a exclusão de análise vier a existir, a permissão volta com ela.

As rotas

  • RN9. Toda rota correspondente a um item da tabela exige a mesma permissão. Acessada sem ela — por URL digitada, link antigo ou favorito —, a aplicação mostra a tela de acesso negado que já existe (pages/unauthorized), sem montar a tela nem disparar a requisição que falharia.
  • RN10. A verificação acontece depois de a identidade carregar. Enquanto useCurrentUser está em voo, a rota mostra carregamento — nunca acesso negado, que seria uma acusação falsa e piscaria na tela a cada navegação.

5. Impacto por app

apps/api

  • app/db/rbac_seed.py — nova permissão operations_center:read em _PERMISSIONS, concedida a ADMIN_TENANT, ANALYST, OPERATOR e VIEWER em _ROLE_PERMISSIONS.
  • app/api/v1/routers/operations_center.py — _REQUIRE_AUDIT vira _REQUIRE_PAINEL = require_permission("operations_center", "read"), aplicado aos seis endpoints do painel.
  • Migration: não há. Permissões e vínculos vivem em tabela e são semeados pelo seed_rbac, que é idempotente (ON CONFLICT DO NOTHING) e roda por make seed. O papel de sistema é global (tenant_id IS NULL), então uma execução atende todos os tenants. Consequência para o deploy: não basta make migrate — produção precisa de make seed para o painel voltar a analista, operador e visualizador.
  • Testes: tests/test_rbac_security.py ganha a nova permissão na matriz esperada; teste novo garantindo que AGENCY_USER continua recusado no painel e que os outros quatro papéis passam.
  • Swagger: as descrições dos endpoints de /operations-center passam a citar operations_center:read, e a seção de papéis em main.py ganha a permissão nova.

apps/web

  • components/navigation/nav-groups.tsx — NavItem ganha o campo que declara a exigência (permissão ou papel de sistema). buildNavGroups e buildNavTabs passam a receber as permissões do usuário e devolvem só o que ele pode ver, descartando grupos que ficaram vazios (RN7).
  • components/custom-sider/index.tsx e components/mobile-shell/index.tsx — passam as permissões para os construtores. Nenhuma lógica de permissão dentro dos componentes: quem decide é o construtor, que é a fonte única (RN6).
  • App.tsx — um guarda de rota por permissão, aplicado às rotas da tabela; HomeGuard passa a testar operations_center:read (RN5).
  • shared/utils/roleUtils.ts — o teste de papel de sistema, para a linha Parâmetros.
  • data-testid novos: nav-hidden-<recurso> não existe (item ausente não tem nó); a asserção do e2e é sobre a ausência do nav-<path> que a sider já gera. Entram unauthorized-page na tela de acesso negado e nav-section-<slug> nos rótulos de seção, para o e2e afirmar a RN7.
  • Responsividade & PWA: a RN8 é especificamente de mobile — a barra inferior muda de composição por papel e precisa continuar legível com 1, 2 ou 3 abas. PWA: só muda bundle; a tela instalada reflete após reabrir.

apps/e2e

  • Specs: menu-por-permissao.spec.ts — para imob (parceira), analista, financeiro (OPERATOR) e gestor (ADMIN_TENANT), afirma quais itens aparecem e quais não aparecem, nos dois viewports; e que /audit-logs digitado por quem não pode mostra a tela de acesso negado.
  • Seeds: sem dados novos — os papéis do seed atual já cobrem os quatro casos.

6. Contrato de API

Nenhuma rota nova, nenhuma removida. Muda a permissão exigida pelo painel:

GET /api/v1/operations-center/overview
Authorization: Bearer <token>
Antes Depois
audit:list operations_center:read

Vale para overview, alerts, work-queue, activity-feed, filters e quick-actions. Quem não tem a permissão recebe 403 (FORBIDDEN) — é falta de permissão no próprio tenant, não acesso cruzado, então não é 404 (spec 0034, RN4).

7. Critérios de aceite

  • [x] CA1. Dado o usuário da imobiliária autenticado, quando abro o app em desktop e em mobile, então o menu não contém Minha Garantidora, Equipe, Perfis de Acesso, Parâmetros, Tipos de Documento, Documentos da Análise, Auditoria, Garantias nem Imobiliárias — e o rótulo ADMINISTRAÇÃO também não aparece (RN7).
  • [x] CA2. Dado o mesmo usuário, quando abro o menu, então vejo Análise de Crédito, Inquilinos e Imóveis.
  • [x] CA3. Dado um ADMIN_TENANT, quando abro o menu, então vejo os 15 itens da tabela da RN6 — nenhum sumiu para quem pode tudo no tenant.
  • [x] CA4. Dado um OPERATOR, quando abro o menu, então vejo Tipos de Documento (tem document_types:create) e não vejo Documentos da Análise, Perfis de Acesso, Minha Garantidora nem Auditoria.
  • [x] CA5. Dado um ANALYST, quando abro o menu, então vejo Painel e Equipe, e não vejo Tipos de Documento.
  • [x] CA6. Dado um ANALYST, OPERATOR ou VIEWER, quando entro no app, então caio no Painel e ele carrega com dados — não em /credit-analyses, e não em esqueleto.
  • [x] CA7. Dado o usuário da imobiliária, quando entro no app, então caio em /credit-analyses, e nenhuma requisição a /operations-center/* é disparada.
  • [ ] CA8. Dado o usuário da imobiliária, quando digito /audit-logs na barra de endereços, então vejo a tela de acesso negado e nenhuma requisição à API de auditoria é disparada.
  • [ ] CA9. Dado qualquer papel, quando a identidade ainda está carregando, então a rota mostra carregamento e nunca pisca a tela de acesso negado (RN10).
  • [x] CA10. Dado o usuário da imobiliária no mobile, quando olho a barra inferior, então ela mostra as abas que sobraram mais "Mais", sem espaço vazio nem sobreposição.
  • [x] CA11. Dado make test, quando rodo a suíte, então há teste afirmando que AGENCY_USER recebe 403 no painel e que os outros quatro papéis recebem 200.
  • [ ] CA12. Dada a jornada da imobiliária recapturada, quando abro docs/journeys/index.html, então os prints não mostram nenhum item de ADMINISTRAÇÃO no menu do parceiro.

8. Plano de implementação

Ordem obrigatória no monorepo: api → web → e2e.

  • [ ] apps/api — permissão operations_center:read no rbac_seed e nos quatro papéis
  • [ ] apps/api — endpoints do painel exigindo a permissão nova
  • [ ] apps/api — testes (matriz de permissões + painel por papel)
  • [ ] apps/api — Swagger dos endpoints do painel e a seção de papéis em main.py
  • [ ] apps/api — rodar seed_rbac no dev-remote e conferir no banco
  • [ ] apps/web — NavItem declara a exigência; construtores filtram e descartam grupo vazio
  • [ ] apps/web — sider e casca mobile passando as permissões
  • [ ] apps/web — guarda de rota por permissão + HomeGuard na permissão nova
  • [ ] apps/web — responsivo (desktop e mobile), com a barra inferior recomposta
  • [ ] apps/e2e — menu-por-permissao.spec.ts cobrindo os quatro papéis
  • [ ] make test e make lint passando (sem apontamento novo nos arquivos tocados)
  • [ ] Validado no browser em desktop e mobile, com os quatro papéis
  • [ ] Jornada da imobiliária recapturada (CA12)

9. Decisões em aberto

Nenhuma. As quatro que existiam foram fechadas em 2026-08-11:

  • Painel para quem opera — permissão própria (operations_center:read), em vez de emprestar audit:list. O painel volta a analista, operador e visualizador sem ninguém ganhar acesso ao log de auditoria.
  • Telas de parametrização — habilitadas pela permissão de alteração, não pela de leitura. Virou a RN2 e vale para além do caso que a originou.
  • Item sem permissão — some do menu.
  • Alcance — menu e rotas protegidas. Esconder sem barrar seria tirar a placa e deixar a porta destrancada.

10. Registro

O que divergiu do planejado

Dois endpoints voltaram atrás, e é correção de erro meu. POST /onboarding/activate e DELETE /onboarding receberam require_system_role("ADMIN_TENANT") na primeira passada. A suíte mostrou o preço: as duas operações agem apenas sobre o próprio solicitante, e o cancelamento é o caminho pelo qual alguém que se registrou e nunca criou garantidora apaga a própria conta. Essa pessoa não tem papel nenhum — o gate a trancaria para sempre. Voltaram a ser só-autenticado, agora com justificativa registrada na lista de exceções (RN0a), que é o que diferencia "esquecemos" de "decidimos".

O seed precisou aprender a remover. Não estava previsto. seed_rbac só inseria (ON CONFLICT DO NOTHING), então extinguir credit_analyses:delete e decisions:list do código não as tirava de ambiente nenhum: seguiam concedidas em 2 e 4 papéis no dev-remote. Sem reconciliação, metade desta spec não teria efeito onde já está instalada. Agora o seed retira do papel o que a declaração não lista e apaga a permissão órfã — só para papéis de sistema; perfis customizados por tenant são do cliente.

require_all_permissions é nova. A RN12 pede que aprovar crédito exija decisions:update e credit_analyses:approve, e só existiam a conjunção implícita (uma permissão) e a disjunção (require_any_permission).

Os 27 testes que quebraram

Nenhum era regressão de produto. Todos passavam por ausência de porteiro, e é isso que os torna interessantes:

  • test_credit_analyses e test_documents montavam o mundo inteiro com um OPERATOR, inclusive cadastrando imobiliária parceira — que a matriz nunca deu a esse papel. O endpoint não exigia nada, então o teste "passava".
  • test_onboarding e test_onboarding_cancel percorriam o fluxo sem semear os papéis de sistema, e o usuário terminava o onboarding sem papel algum. Em produção os papéis sempre existem; o ambiente de teste é que era irreal.
  • test_operations_center mantinha uma lista de permissões escrita à mão — o espelho manual que o conftest já havia abandonado por divergir. Ela citava decisions:list, extinta, e não tinha operations_center:read. Passou a derivar de _ROLE_PERMISSIONS.
  • test_overview_requires_admin_tenant afirmava que o VIEWER não podia ver o painel. Afirmava um acidente: o painel exigia audit:list porque foi o que estava à mão. Virou test_painel_liberado_a_quem_opera, com um par novo (test_painel_negado_a_imobiliaria) para a fronteira que de fato importa.

Aferições

  • make test: 287 testes (eram 279; os 8 novos são a suíte de autorização e o par do painel). Duas execuções completas: uma com 287 passando e 0 falhas, outra com 286 e 1 falha — a intermitente de paginação já documentada na CLAUDE.md da raiz, sem relação com esta spec.
  • Barreira exercitada, não só escrita: removi o require_permission("users","create") do convite dentro do container e a suíte acusou pelos dois caminhos — a introspecção (test_todo_endpoint_autenticado_tem_porteiro) e o comportamento (test_convite_exige_permissao_de_criar_usuario).
  • Banco reconciliado no dev-remote: 58 permissões, zero extintas, audit:list só com os dois administradores e operations_center:read com os quatro papéis que operam.
  • tsc do web: limpo.
  • Menu verificado no browser, quatro papéis × dois viewports, contra o dev-remote reconstruído:
Papel Itens Confere
imob (imobiliária) 3 — Análise, Inquilinos, Imóveis sem seção ADMINISTRAÇÃO (RN7)
analista 7, com Painel e Equipe sem parametrização nem Auditoria
financeiro (OPERATOR) 7, com Tipos de Documento sem Equipe nem Documentos da Análise
gestor (ADMIN_TENANT) 13 — todos —

Sem rolagem horizontal em nenhum. No mobile a barra inferior da imobiliária fica com duas abas (Análise + "Mais"), e a aba "Mais" sobrevive ao recorte, como a RN8 exige.

Uma armadilha de verificação, que vale registrar. A primeira checagem casava os itens pelo rótulo visível e devolveu "nenhum item" no desktop para os quatro papéis, com o menu perfeitamente renderizado: a casca mobile fica no DOM junto com a sider, oculta, e getByText(...).first() pegava o nó errado. O spec e2e foi escrito com os data-testid nav-<path>/mobile-nav-<path> por causa disso — teste que mede o nó errado é pior que teste ausente, porque dá confiança.

Pendências

  • e2e escrito, não executado (menu-por-permissao.spec.ts).
  • Produção precisa de make seed além do deploy: a permissão nova e a reconciliação são dado, não schema. Sem isso o painel não volta a analista, operador e visualizador — e as permissões extintas continuam concedidas lá.