# Spec 11 — Acesso e Ativação

> **Dono:** Core/Acesso · **Status:** Vigente · **Plano:** 08
> **Relaciona:** ADR-033, ADR-034, ADR-013, ADR-014, ADR-019
> **Objetivo:** contrato de **controle de acesso** (permissões por módulo, perfis, grupo `Suporte`)
> e de **ativação de módulo em tempo real** — nativo (`Gate` + cache), previsível, com **negação por
> padrão** e invalidação imediata.

## 1. Permissões (ADR-033)

- **Token:** `{modulo}.{acao}`, com o **token canônico** do módulo (ADR-004/014) e **quatro ações**:
  `ver`, `editar`, `aprovar`, `arquivar`.
- **Declaração:** o módulo declara no `ModuleProvider::config()`:
  ```php
  use Core\Access\Permission;

  'permissions' => Permission::ACTIONS,   // ou [Permission::VIEW, Permission::EDIT]
  ```
- **Resolução:** o Core registra `Gate::define('{modulo}.{acao}', ...)` a partir do manifesto
  (`AccessGate`); a decisão lê o perfil do usuário. **Sem** `PermissionService`/sentinel.
- **Proteção no módulo** (helper `Core\Access\Permission`):
  ```php
  Permission::authorize('Noticias', Permission::APPROVE); // 403 se não puder
  if (Permission::allows('Noticias', Permission::APPROVE)) { ... }
  ```
- **Negação por padrão:** ação **não declarada** pelo módulo nunca vira ability; token inválido é
  rejeitado. Comunicação: `Permission::for()`/`Permission::isToken()`, `Permission::ACTIONS`.
- **Catálogo (P2.S2):** `Core\Access\PermissionCatalog` lê o manifesto + catálogo de services e
  devolve `{group, module, title, actions[]}` só com ações declaradas (ordem canônica, grupo
  `Suporte` fora). É a fonte da matriz de perfis e da validação de escrita — **não** do model.
  `orphans()` devolve tokens de módulo removido para a UI avisar; a escrita só os preserva se já
  existiam no perfil.

## 2. Perfis (ADR-033)

- **Um perfil por usuário:** `cms_users.role_id` (FK `nullOnDelete`).
- **Persistência:** `core.core_roles.permissoes` **jsonb** — lista de tokens, `NOT NULL DEFAULT '[]'`,
  `CHECK jsonb_typeof = 'array'`.
- **Validação na escrita** (`Role::saving`): só tokens válidos, **sem duplicata**; `null → []`.
  A **existência** no manifesto é validada pela Action com `PermissionCatalog`/`AccessPolicy`
  (`Permission` continua só sintaxe).
- **Políticas (P2.S2):** `AccessPolicy` nega **403** sem `acesso.editar`; recusa (**422** na tela,
  exceção concreta no domínio) perfil `suporte` editável/excluível/atribuível, último administrador
  ativo desativado/excluído/sem perfil/sem `acesso.editar`, e auto-lockout (desativar a própria
  conta ou perder o próprio `acesso.ver`). Último administrador trava as linhas na transação da
  escrita (`FOR UPDATE`, ordem determinística).
- **Sem herança**; menor privilégio; escopo além das quatro ações é recusado.

## 3. Perfil e grupo `Suporte` (ADR-033)

- **Fundador:** a equipe é identificada pelo **e-mail** `suporte@maxima.inf.br` (`SupportAccess::EMAIL`).
  A identidade é **reservada nos dois sentidos** (P2): a conta da equipe não troca o e-mail e
  nenhuma conta que não seja da equipe recebe esse endereço por escrita web (criação, edição
  administrativa ou perfil próprio → **422** `support_identity`). O provisionamento legítimo é o
  comando de bootstrap `core:user:create --role=suporte` (superfície confiável, fora da UI).
- O perfil `suporte` é **protegido**: imutável, não removível, **não atribuível** (`Role::assignable()`)
  e **não listado** em `/papeis` (ADR-033 §3). A **conta** também é protegida: quem detém
  `acesso.editar` não redefine a senha, não envia senha na edição e não desativa a conta da equipe
  (`AccessPolicy::assertUserNotProtected`); só o próprio suporte ou o CLI.
- O grupo `Suporte` é um **grupo de módulos normal** (`src/modules/Suporte/{Modulo}`), **ocultado à
  força pelo Core**:
  - **Navegação:** `ModuleDiscoveryService::grouped(false)` oculta o grupo por padrão; só a equipe
    (`grouped(true)`) o vê.
  - **Roteamento:** middleware `EnsureSupportGroupAccess` (alias `support.group`) em **todas as
    áreas** → **404** para quem não é da equipe, **inclusive com o perfil máximo**.
  - **Abilities:** `AccessGate` restringe abilities do grupo à equipe (`Gate::before` dá passe livre
    ao suporte).
- **404** (não 403) para não revelar a existência do grupo.

## 4. Ativação de módulo em tempo real (ADR-034)

- **Estado é dado:** `core.core_module_discovery_settings` (chave `modules_activation`,
  `{modulo: bool}`). **Não** é engine de flags.
- **Resolução:** `ModuleActivation` lê **Valkey** (TTL curto) com fallback ao banco e **fail-open**
  (default = manifesto). `ModuleDiscoveryService::isModuleActive()` usa essa resolução.
- **Guard:** middleware `CheckModuleActivation` (alias `module.activation`) em **todas as áreas**
  (portal/cms/api) → **404** quando o módulo está inativo. Efeito **imediato e global**, sem reload.
- **Rotas ficam registradas:** `AreaRegistrar` registra rotas de **todos** os módulos descobertos; o
  corte é no runtime (não removendo do router).
- **Navegação/manifesto** filtram inativos (`grouped()`/`activeModules()`).
- **Toggle (exclusivo do suporte):** grava o dado e **invalida a chave do cache** na conclusão da
  escrita (o `forget` é agendado no `afterCommit`; nunca antes do commit raiz, e rollback não
  invalida), sem rebuild/reload. Operacional via `php artisan core:module:activate {Nome}
  [--disable]`; a UI é um módulo comum do grupo `Suporte` (CMS, F4).

### 4.1 Modo de manutenção (P3.S5)

- **Estado é settings tipado (ADR-046):** seção `system` com `maintenance` (bool, default `false`),
  `maintenance_message` (texto ≤ 500), `maintenance_retry_after` (int `0..86400`, default `3600`)
  e `maintenance_allow_cms` (bool, default `true`). `timezone` e o restante da seção são
  preservados na escrita (a Action mescla o documento).
- **Escrita única:** `SetMaintenanceMode` (Core) valida, grava por `Settings` (invalidação
  síncrona), emite span + `log_event('painel.maintenance_changed')` e audita explicitamente
  `painel.maintenance_changed` com before/after sem PII.
- **Middleware `maintenance` por área** (depois do locale e dos guards `support.group`/
  `module.activation`): allowlist explícita antes de ler o banco (`/up`, `robots.txt`, `sitemap*.xml`,
  `security.txt`, manifest, favicon, `theme.css`/`cms-theme.css`, `build/*`, `midia/*` e a ponte
  pública `/cms[/...]` do portal, que é só redirect para o domínio do painel).
  Portal → **503** HTML acessível, `Retry-After`, `X-Robots-Tag: noindex`; API → **503**
  `application/problem+json` com `Retry-After`; CMS → acessível com `allow_cms`, e sem ele só a
  equipe de suporte, `login`/`logout`, `GET/PUT /suporte/manutencao` e o health. Falha ao ler
  settings é **fail-open** e logada sem segredo.
- **UI:** `GET/PUT /suporte/manutencao` (`painel.cms.maintenance[.update]`), `painel.ver|editar`,
  CSRF, confirmação explícita e validação server-side; a página 503 é uma view dedicada, sem JS.

### 4.2 Contato do fornecedor e versão (P3.S6)

- **Estado é settings tipado (ADR-046):** seção `support` com `company` (≤ 160), `site` (URL
  `http(s)`), `contact_email`, `contact_phone` e `whatsapp` (`phone`: 8–15 dígitos, `+` opcional),
  `address` (texto ≤ 500) e `hours` (texto ≤ 200). `contact_email`/`contact_phone`/`hours` mantêm o
  nome do P0; campos novos ausentes não geram default nem erro. Validação de e-mail/URL/telefone é
  do schema; valor inválido no banco é **ignorado** (nunca vira link).
- **UI somente leitura:** `GET /suporte/contato` (`painel.cms.contact`, `painel.ver` + grupo
  `Suporte`) exibe os dados como texto; site/WhatsApp são links de navegação (`target="_blank"` +
  `rel="noopener"`), telefone/e-mail são `tel:`/`mailto:`. **Sem `iframe`/mapa/CDN** (ADR-002);
  nenhuma chave livre, nenhum HTML no banco.
- **Estado vazio:** sem nenhum campo preenchido, a tela mostra um estado vazio explicativo com link
  real para `/configuracoes/support` (seção `support` do formulário genérico) — nunca em branco.
- **Versão/novidades:** a versão vem de `VERSION` (`SystemVersion`); não há changelog nem link para
  o manual quando o pacote não é publicado no deploy, e o caminho local nunca é exposto.
  O span/log `painel.contact` leva apenas `configured` e a contagem de campos (sem PII).

## 5. Onde enforcementar

- O **módulo** enforceia como achar melhor (Action/middleware/policy), sempre com o vocabulário.
- O **Core** fornece: vocabulário (`Permission`), registro/resolução (`Gate`), guard do grupo
  `Suporte`, guard de ativação e o resolver de ativação.
- **Auditoria** (ADR-019): mudanças de perfil/permissão e ativação de módulo são auditáveis (dado).

## 6. Gestão de usuários e perfis (P2)

- **Rotas CMS** (sem closure; mutação só `POST`/`PUT`/`DELETE` com CSRF; `whereUuid` no binding):
  `acesso.cms.usuarios.{index,create,store,edit,update,activate,deactivate,password}`,
  `acesso.cms.papeis.{index,create,store,edit,update,destroy}` e
  `acesso.cms.perfil.{edit,update,password}`. `acesso.ver` lista; `acesso.editar` muta; o perfil
  próprio não exige `acesso.*` (o alvo é sempre o ator da sessão).
- **Estado da conta:** `cms_users.deactivated_at timestamptz null` (data, não flag). O login inclui
  `deactivated_at IS NULL` na credencial e o middleware `EnsureActiveUser` (grupo da área CMS)
  encerra a sessão aberta no próximo request (`core.session_deactivated`).
- **Unicidade de identidade:** índice único parcial `lower(email) WHERE deleted_at IS NULL`; a
  consulta de borda inclui contas soft-deleted (a identidade não é reciclada). O login normaliza
  `mb_strtolower(trim())` na borda e o ramo "sem conta ativa" executa um `Hash::check` sentinela
  (mesmo custo Argon2id, sem oráculo de existência).
- **Políticas de escrita** (`AccessPolicy`): perfil `suporte` protegido; conta `suporte` protegida;
  último administrador ativo preservado; auto-lockout recusado. A ordem global de locks é
  **perfis administradores (`id`) → usuários administradores ativos (`id`) → perfil-alvo →
  conta-alvo** (`lockRoleForWrite` antes de `lockUserForWrite` na edição; desativar/ativar/reset
  travam só a conta); o perfil de destino é relido sob lock (snapshot do request não é
  autoridade). Recusas de política viram **422** i18n; falta de permissão, **403**.
- **Observabilidade/auditoria:** cada Action emite **span + `log_event`** com o mesmo nome
  (`acesso.user_created|user_updated|user_activated|user_deactivated|user_password_reset|
  profile_updated|password_changed|role_created|role_updated|role_deleted`). Mutações privilegiadas
  emitem ainda evento **explícito** na trilha (`acesso.user_role_changed`,
  `acesso.user_password_reset`, `acesso.role_permissions_changed`) com ids/tokens, sem PII/senha.
- **Nomes de perfil:** unicidade sem diferenciar caixa na escrita (request + Action) para não
  deixar dois `<option>` idênticos na atribuição; o `slug` segue identidade estável.

## 7. Verificação

- **Permissões/perfis:** `AccessGateTest`, `RoleProtectionTest`, `RolePermissionTest`,
  `PermissionTest`, `PermissionCatalogTest`, `AccessPolicyTest` — negado sem perfil; negado para
  ação não declarada; token inválido/órfão rejeitado em perfil novo; escalada, auto-lockout, último
  administrador e `suporte` recusados.
- **Grupo `Suporte`:** `SupportGroupAccessTest` — 404 para não-suporte; manifesto oculto; abilities
  restritas à equipe.
- **Ativação:** `ModuleActivationTest` — default ativo; desativar/reativar imediato; invalidação de
  cache; guard → 404; **rota real** (`/blueprint`) 200→404 após desativar.
- **Manutenção (P3.S5):** `MaintenanceModeTest` — portal 503/`Retry-After`/noindex, allowlist
  (SEO/health/assets), API RFC 7807, CMS 200/503 conforme `allow_cms`, login/toggle sobrevivendo,
  restauração e fail-open; `SetMaintenanceModeTest` (validação, timezone preservado, auditoria);
  `PainelMaintenanceCmsTest` (autorização, CSRF, validação, 4 locales, layout); E2E `23` (liga →
  portal 503 + axe → API 503 → desliga → 200, em `cms-serial`).
- **Contato do fornecedor (P3.S6):** `SettingsSchemaTest` (campos/ordem, URL/e-mail/telefone,
  tamanho, 4 locales) e `DatabaseSettingsTest` (documento P0 preservado, round-trip dos canais);
  `PainelContactCmsTest` (200/404, estado vazio explicativo, links seguros, sem `iframe`, versão,
  valor inválido no banco ignorado, 4 locales, layout); E2E `23` (vazio → preenchido → links → axe →
  limpeza no `finally`).
- E2E operacional: `core:module:activate Noticias --disable` grava `{"Noticias": false}` e invalida
  o Valkey; reativar volta a responder.
- **P2 (usuários/perfis):** `AcessoUsersCmsTest`, `AcessoRolesCmsTest`, `AcessoProfileCmsTest`,
  `AccessPolicyTest` (identidade reservada, conta protegida, snapshot velho, ordem de lock),
  `CmsAuthTest` (login sem caixa, custo constante), `CmsUserCommandTest` (bootstrap do suporte) e
  E2E `19`–`22` (axe + mobile + sem JS).
- **Gate a11y:** `tools/sniff-gate.sh` é **fail-closed** (sem a linha `rules: N pass | N warn |
  N fail` ou com snapshot vazio, reprova); o parser tem autoteste offline em
  `tools/gates-selftest.sh` (`--verify-summary`).

## 8. Referências

- `src/core/Access/` (`Permission`, `AccessGate`, `SupportAccess`, `Models/Role`, middleware);
  `src/core/Internal/ModuleDiscovery/` (`ModuleActivation`, `ModuleDiscoveryService`),
  `src/core/Internal/Http/Middleware/{CheckModuleActivation,EnsureSupportGroupAccess,EnforceMaintenanceMode}.php`,
  `src/core/Internal/Maintenance/` (`MaintenanceState`, `SetMaintenanceMode`) e
  `resources/views/errors/maintenance.blade.php`. O contato do fornecedor usa
  `src/core/Settings/SettingsSchema.php` (seção `support`), `Core\Internal\System\SystemVersion` e
  `src/modules/Suporte/Painel/Logic/{Actions/PainelContactAction,DTOs/PainelContact}.php`.
