# Acesso de IA ao CMS (CmsAiToken)

Canal **stateless** e controlado por `.env` que permite a ferramentas de IA (agentes, navegação headless)
acessarem o `/cms` e todas as subpastas com um **token fixo e duradouro**, sem sessão, sem cookies e sem
conflitos de HTTP.

## Objetivo

Permitir que a IA verifique e interaja com o UI da área administrativa **tal qual um usuário acessaria**:

- Navegar em qualquer rota `/cms/*` sem manter sessão/CSRF.
- Enviar POST/PUT/DELETE sem fricção de CSRF.
- **Impersonar usuários específicos** (via allow-list) para testar permissões e RBAC.
- O token concede as permissões **do usuário impersonado** — não é acesso root automático.

## Como funciona

Todo o fluxo passa por um único middleware: `V2/Core/Middleware/CmsAiToken.php`, registrado no grupo
`web` do `app/Http/Kernel.php` **antes** do `VerifyCsrfToken`.

Fluxo de uma request em rota `/cms`:

1. **Escopo:** se a rota não é `cms*`, o middleware não faz nada.
2. **Guarda de produção:** se `APP_ENV=production`, o canal é **impossível de ativar** (guarda em código,
   não em configuração). Isso vale para qualquer `.env`, sem exceção.
3. **Fail-closed:** `V2_CMS_AI_ENABLED=false` ou token vazio/`< 48` chars → não intercepta.
4. **Token ausente** → fluxo normal (login/sessão tradicional).
5. **IP fora da allow-list** (se configurada) → `403`.
6. **Token inválido** (fornecido mas errado) → `403` (comparação `hash_equals`, timing-safe).
7. **Token válido**:
   - Resolve o usuário alvo (`X-CMS-User` ou padrão).
   - `Sentinel::setUser($user)` — autentica **em memória para o ciclo da request** (sem criar sessão nem
     linha em `persistences`; nada persistido entre requests).
   - Marca `cms_ai_token_valid` → o `VerifyCsrfToken` pula a verificação **só nesta request**.
8. Auditoria em log (método/path/IP/usuário alvo — **nunca o token**).

Como o Sentinel passa a reportar o usuário autenticado, todo o restante do pipeline
(`CmsAuthenticate`, `V2PermissionMiddleware`, views, `@v2can`) funciona **sem nenhuma alteração**, com as
permissões exatas do perfil impersonado.

## Configuração (`.env`)

| Variável | Padrão | Descrição |
|----------|--------|-----------|
| `V2_CMS_AI_ENABLED` | `false` | Liga/desliga o canal. **Nunca `true` em produção** (guarda em código). |
| `V2_CMS_AI_TOKEN` | vazio | Token fixo. Gerar com `openssl rand -hex 32` (mín. 48 chars). |
| `V2_CMS_AI_USER_EMAIL` | email de suporte | Usuário impersonado por padrão. |
| `V2_CMS_AI_IMPERSONATE_EMAILS` | vazio | CSV de emails que o token pode "ver como" via `X-CMS-User`. |
| `V2_CMS_AI_ALLOWED_IPS` | vazio | CSV de IPs permitidos. Vazio = qualquer IP. Ver nota sobre NAT Docker abaixo. |
| `V2_CMS_AI_AUDIT_LOG` | `true` | Audita cada acesso autorizado/negado. |
| `V2_CMS_AI_HEADER` | `X-CMS-AI-Token` | Header de transporte do token. |
| `V2_CMS_AI_USER_HEADER` | `X-CMS-User` | Header que seleciona o usuário impersonado (email). |
| `V2_CMS_AI_ALLOW_QUERY` | `false` | Permite transporte do token via query param (mais exposto). |
| `V2_CMS_AI_QUERY_PARAM` | `v2_ai_token` | Nome do query param quando `ALLOW_QUERY=true`. |

A aplicação usa `config:cache` em produção — após alterar o `.env`, rode
`docker compose exec app php artisan config:cache`.

## Como usar

### curl (HTTP client)

```bash
TOKEN="<V2_CMS_AI_TOKEN>"

# Navegar no CMS (GET)
curl -H "X-CMS-AI-Token: $TOKEN" http://localhost:10011/cms/

# Enviar POST sem CSRF (stateless)
curl -X POST -H "X-CMS-AI-Token: $TOKEN" http://localhost:10011/cms/noticias

# Impersonar outro usuário (email deve estar em IMPERSONATE_EMAILS)
curl -H "X-CMS-AI-Token: $TOKEN" -H "X-CMS-User: operador@maxima.inf.br" http://localhost:10011/cms/
```

### Playwright / Puppeteer (navegação headless)

```js
const context = await browser.newContext({
    extraHTTPHeaders: { 'X-CMS-AI-Token': process.env.V2_CMS_AI_TOKEN },
});
// Todas as navegações e XHR na área /cms já vêm autenticadas.
```

### Sem header (navegação pura)

Habilite `V2_CMS_AI_ALLOW_QUERY=true` e use `?v2_ai_token=<TOKEN>` (maior exposição — só em dev).

## Segurança

- **Desligado por padrão** e **impossível em produção** (guarda dura em código no middleware).
- Token mínimo de 48 chars, comparação `hash_equals` (timing-safe), nunca logado.
- **Impersonação restrita**: o `X-CMS-User` só aceita emails da allow-list `V2_CMS_AI_IMPERSONATE_EMAILS`
  (o usuário padrão é sempre permitido). Email fora da lista → `403`.
- Allow-list de IPs opcional (`V2_CMS_AI_ALLOWED_IPS`).
- Auditoria de cada acesso (informação de auditoria não contém o token).
- Acesso stateless: `Sentinel::setUser()` não grava sessão nem cria linha em `persistences` (sem lixo no banco).
- Falhas: token ausente → fluxo normal; token errado → `403`; usuário alvo inexistente → `403`.

## Testes

- `tests/Unit/V2/Core/Middleware/CmsAiTokenTest.php` — guarda de produção, fail-closed, token inválido,
  allow-list de impersonação, restrição de IP, transporte por query.
- `tests/Feature/V2/Core/Middleware/CmsAiTokenTest.php` — integração HTTP real (kernel + CSRF ativo):
  200 stateless, redirect p/ login sem token, 403 token errado, permissões do alvo, POST sem CSRF.

```bash
docker compose exec app vendor/bin/phpunit --testsuite "V2 Unit"
docker compose exec app vendor/bin/phpunit --testsuite "V2 Feature"
```

## Observações operacionais

- **Allow-list de IPs e NAT Docker:** requisições originadas na própria máquina host chegam ao
  container com IP do **gateway da rede compose** (ex.: `10.199.0.1`), não `127.0.0.1` — o Docker faz
  SNAT no port publishing. Por isso a rede `portal_sucesu_network` tem **subnet fixa** no
  `docker-compose.yml` (`10.199.0.0/24`, gateway `10.199.0.1`) e o `.env` de referência usa
  `V2_CMS_AI_ALLOWED_IPS=127.0.0.1,::1,10.199.0.1`. Se alterar a subnet, atualize o allowlist junto.
  Máquinas externas (LAN) aparecem com o IP real de origem → ficam fora da lista → `403`.
- **Rotação do token (procedimento):**
  1. Gere um novo token: `openssl rand -hex 32`.
  2. Atualize `V2_CMS_AI_TOKEN` no `.env` **e todos os consumidores no mesmo passo**
     (agentes IA, configs de Playwright/Puppeteer, scripts) — não deixe janela com token antigo válido.
  3. Aplique: `docker compose exec app php artisan config:cache`.
  4. Valide: request com o token antigo deve receber `403`; com o novo, `200`. A rotação é stateless —
     sem migração e sem afetar sessões.
- **Higiene do canal:** quando não houver uso por longos períodos, mantenha `V2_CMS_AI_ENABLED=false`
  e ligue apenas sob demanda. O canal continua **impossível em produção** (guarda dura em código).
- **Não usar em ambiente real:** o requisito é que esse tipo de acesso **não esteja disponível em
  produção** — garantido em código por `APP_ENV=production`.
