# ADR-033 — Controle de acesso: permissões por módulo, perfis e grupo suporte

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-15) |
| **Data** | 2026-10-15 |
| **Plano de origem** | `wiki/plan/07-core-contratos-discovery.md`, `wiki/plan/08-servicos-headless-persistencia.md` |
| **Relacionadas** | ADR-013, ADR-014, ADR-019, ADR-020 |

## Contexto

O `sucesu-cms` acumulou RBAC pesado (`PermissionService`, `V2PermissionMiddleware`, tabelas
`sentinel`, `RegionalOwnable`) e o legado usava permissões difusas. O V7 é **single-tenant** e
precisa de um modelo **simples e previsível**: o acesso é decidido por **módulo**, com um
vocabulário pequeno de ações; perfis agrupam permissões; e existe um **grupo de módulos e um perfil
de suporte** com proteção especial (equipe `@maxima.inf.br`).

## Decisão

1. **Permissão = `{modulo}.{acao}`**, com o módulo no token canônico (ADR-004/ADR-014) e **quatro
   ações fixas**:
   - `ver` — visualizar/consultar;
   - `editar` — criar **e** editar;
   - `aprovar` — publicar/aprovar;
   - `arquivar` — arquivar/desarquivar.
   Cada módulo **declara** no `ModuleProvider::config()` as ações que suporta (nem todo módulo tem
   `aprovar`).
2. **Cada módulo implementa a checagem como achar melhor** (política Laravel/Gate, middleware,
   checagem na Action), desde que use o **vocabulário comum**. O Core só fornece o vocabulário, o
   registro das permissões e a resolução via **`Gate` nativo do Laravel** — **sem** `PermissionService`
   próprio nem tabela `sentinel`. Para o caminho feliz o Core expõe o helper **`Core\Access\Permission`**:
   o módulo **declara** com `Permission::ACTIONS` (ou um subconjunto das 4 constantes) e **protege**
   com `Permission::authorize('Noticias', Permission::APPROVE)` (ou `Permission::allows(...)` em
   condição). O helper valida o vocabulário, monta o token canônico e **nega por padrão** — ação não
   declarada pelo módulo nunca vira ability.
3. **Perfil de acesso** = conjunto nomeado de permissões, persistido em `core_roles.permissoes`
   (**jsonb**, lista de tokens `{modulo}.{acao}`, `NOT NULL DEFAULT '[]'`, `CHECK jsonb_typeof =
   'array'`). **Um perfil por usuário** (`cms_users.role_id`, FK `nullOnDelete`) — simplicidade;
   múltiplos perfis só se houver ganho comprovado. Os tokens são **validados na escrita** contra o
   vocabulário (só as 4 ações, sem duplicata); a leitura usa o cast nativo (`array`/`in_array`) e,
   se algum dia houver relatório, o PG 18 responde com GIN (`permissoes @> '["{modulo}.{acao}"]'`)
   sem join.
4. **Perfil `suporte` (protegido):** existe **um** perfil reservado `suporte`, vinculado
   exclusivamente ao usuário de equipe (`suporte@maxima.inf.br`). Ele **não pode ser editado,
   visualizado nem atribuído** a outra conta — garantido por código (não só por tela).
5. **Grupo de módulos `Suporte`:** módulos sob `src/modules/Suporte/{Modulo}` formam um grupo
   **invisível e inacessível** para quem não pertence à equipe de suporte — inclusive quem tiver o
   perfil de maior privilégio. A proteção vale na **navegação** (manifesto) e no **roteamento**
   (guard), não apenas na UI.
6. **Perfis além de `suporte`** são permitidos apenas quando simples e vantajosos; a regra é
   **menor privilégio** e negação por padrão. Sem herança de perfis, sem permissões além das quatro
   ações (recusar escopo extra).

## Justificativa (alternativas descartadas)

- **RBAC pesado / `PermissionService` / sentinel (sucesu)** — descartada: cerimônia e tabelas que
  não pagam o custo num sistema single-tenant.
- **Permissões por rota/recurso granular** — descartada: explode a cardinalidade e dificulta a UX
  dos perfis; as quatro ações por módulo cobrem o fluxo editorial.
- **Herança/roles compostos** — descartada: previsibilidade > flexibilidade (ADR-001).
- **Tabela normalizada `core_role_permissions`** — descartada: o catálogo de permissões é **código**
  (manifesto dos módulos), então a tabela **não adiciona integridade referencial**; só tabela + join
  + modelo e auditoria fragmentada (N linhas) para um conjunto minúsculo. Se um dia um relatório/
  auditoria por permissão exigir escala, normalizar (novo ADR).
- **Proteger o suporte só na UI** — descartada: precisa de guard de rota + filtro de manifesto.

## Consequências

**Positivas**
- Modelo mental simples (módulo × 4 ações); perfis triviais de administrar.
- Suporte blindado por código; auditoria natural das mudanças de permissão.

**Custos / negativas**
- Módulos precisam declarar/enforcear suas permissões (convenção).
- Casos muito específicos exigem permissão customizada — tratados no módulo, não no Core.
- A integridade das permissões é de **código** (vocabulário declarado pelo módulo), não do banco;
  tokens órfãos de módulos removidos são **inertes** (nunca viram ability).

## Exemplo mínimo (comparativo)

```php
use Core\Access\Permission;

// ModuleProvider::config()
return [
    'title' => 'Notícias',
    'permissions' => Permission::ACTIONS, // ou [Permission::VIEW, Permission::EDIT]
    // ...
];

// Enforcement no módulo (nativo, via helper)
Permission::authorize('Noticias', Permission::APPROVE); // 403 se não puder
// ou: if (Permission::allows('Noticias', Permission::APPROVE)) { ... }
```

## Verificação

- **Usuário autenticado** sem perfil suporte recebe **404** em módulo/rota do grupo `Suporte`,
  mesmo com o perfil mais alto; **anônimo** é redirecionado ao login (302) — a autenticação da
  área roda antes do guard do grupo, então o 404 vale para quem já está autenticado.
- O perfil `suporte` não pode ser editado, listado nem atribuído por outro usuário (teste tenta e
  falha).
- Cada módulo expõe suas ações; permissão ausente nega por padrão (`Permission::authorize` → 403).
- `core_roles.permissoes` só aceita lista de tokens válidos (`array`, sem duplicata); token fora do
  vocabulário é rejeitado na escrita (`PermissionTest`/`RolePermissionTest`).

## Emenda (2026-10-25) — superfície do grupo `Suporte`

O grupo `Suporte` deixou de ser só uma regra de invisibilidade: a fase **P3** entregou a primeira
superfície real da equipe, `src/modules/Suporte/Painel` (token canônico `painel`, derivado da pasta
pelo ADR-004/ADR-014; URI `/suporte` no domínio do painel, `painel.cms.*`).

- O módulo **declara** `painel.ver`/`painel.editar` e **enforça** com o helper comum
  (`Permission::authorize`), como qualquer outro módulo; a equipe tem passe livre pelo
  `Gate::before` (`SupportAccess`), não por uma checagem paralela.
- A proteção do grupo continua sendo **do Core e do roteamento**: `EnsureSupportGroupAccess`
  devolve **404** (não 403) ao **usuário autenticado** que não é da equipe — inclusive ao perfil de
  maior privilégio — em **todas** as rotas do grupo, inclusive `POST` de mutação; o **anônimo**
  recebe **302 para o login** (a autenticação da área roda antes do guard) e o
  manifesto/navegação não lista o grupo.
- A área herda a postura de área restrita (ADR-043): `X-Robots-Tag: noindex`, cache privado,
  autenticação/permissão e auditoria. Ela não aparece em `robots.txt`/sitemap/canonical e não é
  mapeada pelo portal.
- Nenhum outro perfil ganha acesso ao grupo por conveniência: um caso novo exige permissão
  explícita, como já diz a decisão acima.

**Verificação:** `PainelCmsTest` (404 do perfil máximo em todas as rotas do grupo e 200 da equipe),
`PainelModulesCmsTest`/`PainelAuditCmsTest`/`PainelMaintenanceCmsTest`/`PainelContactCmsTest`
(404 de não-equipe nas telas e no toggle/escrita), `SupportGroupAccessTest` e o E2E
`23-suporte-painel.spec.ts` (perfil máximo sem item de menu e 404 digitando a URL).

## Referências

- `sucesu-cms/V2/Core/Services/{PermissionService,ModuleFlagService}.php` (referência, simplificada);
  `prefeituras_v5/.agents/rules/` (RBAC); `wiki/backlog/modulo-usuarios.md`.
