# Fase 2 — a décima sexta fatia: privilégios e funcionalidades

Escolhida em 09/09/2026. A oitava fatia devolvia **quem atende** — duas leituras,
e nada de escrita. O que ficou de fora dela foi tudo o que a tela de usuários
faz além de listar: senha, foto, comissão, horário de agenda, personificação —
e **permissão**.

Esta fatia pega a permissão. Não por ser a mais fácil das que sobraram, mas
porque é a tela onde a varredura de guardas achou o pior defeito do sistema
(**D148**): `permissoesSalvarAction` não conferia nada, e qualquer uma das 370
sessões ativas concedia a si mesma qualquer uma das 126 funcionalidades.

---

## O recorte

| endpoint | verbo | origem no ERP |
|---|---|---|
| `/adm/api/funcionalidades` | `GET` | `FuncionalidadeRepository::popularCombobox()` |
| `/adm/api/usuarios/:id/privilegios` | `GET` | `permissoesAction` |
| `/adm/api/usuarios/:id/privilegios/:funcionalidade` | `PUT` | `permissoesSalvarAction` |
| `/adm/api/usuarios/:id/privilegios/:funcionalidade/exclusao` | `POST` | `permissoesRemoverAction` |
| `/adm/api/grupos/:grupo/privilegios` | `GET` | `PrivilegiosController::indexAction` |

Cinco endpoints. **Quatro leituras e uma escrita**, e a proporção é o assunto
desta fatia.

---

## São duas camadas, e a de cima apaga a de baixo — D157

Foi o que a leitura achou antes de escrever qualquer linha.

| tabela | o que é | linhas |
|---|---|---:|
| `privilegios` | o **molde do grupo** | 139 |
| `privilegios_usuarios` | as linhas **operativas**, por pessoa | 15.495 |

O `Acl` lê **só a segunda**. `SessaoAcl` carrega
`PrivilegioUsuario::selecionarPorUsuario()` e nada mais — a tabela de grupo
nunca é consultada para decidir um acesso.

Ela é um **gerador** — e, até 09/09/2026, um destruidor:
`PrivilegioService::insert()` e `update()` terminavam apagando **todas** as
linhas daquele par e recriando uma por usuário do grupo.

Das 15.495 linhas, **347 divergem** do molde e **342 não têm molde nenhum** —
**689 decisões individuais** que sumiam sem aviso, **225 delas tirando** acesso.
A correção do D157 trocou a regra: a propagação passou a tocar só a linha que
estava em dia com o molde.

### O que a fatia faz com isso

**Não expõe a escrita da camada de grupo.** `GET /adm/api/grupos/:grupo/privilegios`
lê o molde; não há `PUT` nem `POST`. Quando a fatia foi escrita, oferecer por
endereço uma operação destrutiva que ninguém tinha decidido manter seria o
silêncio do ERP com uma porta a mais.

**O D157 foi decidido e corrigido no mesmo dia**, e a razão mudou de natureza: a
propagação passou a tocar só a linha que estava em dia com o molde, então
escrever a camada de grupo deixou de ser destrutivo. Ela continua fora desta
fatia **por escopo** — é a tela do `PrivilegiosController` do ERP, e vira fatia
própria quando for a vez dela.

E o campo `molde` mudou de sentido junto: `divergente: true` deixou de querer
dizer *"esta linha some na próxima edição"* e passou a querer dizer *"esta linha
sobrevive a uma edição de grupo"*. A pergunta que ele responde é a mesma; a
consequência é a oposta.

**E conta o que o ERP não conta.** Cada linha sai com o campo `molde`:

```json
{
  "id": 4711,
  "funcionalidade": { "id": 21, "nome": "Configurações usuários" },
  "ler": true,
  "escrever": true,
  "molde": { "ler": true, "escrever": false, "divergente": true }
}
```

- `molde: null` — não há linha de grupo para o par. A concessão **só existe por
  pessoa**.
- `molde.divergente: true` — há molde, e esta linha não bate com ele: alguém
  ajustou aquela pessoa à mão.

A coleção ainda traz `emRisco`, a contagem das duas situações juntas. O nome
ficou de antes do D157, quando essas linhas de fato corriam risco; hoje ele
conta **o que a propagação preserva**, que é a mesma medida vista do outro
lado — e continua sendo o número que uma tela precisa para distinguir o que veio
do grupo do que foi decidido pessoa a pessoa.

---

## As guardas, que a tela não tinha

Toda ação exige `configuracoes/usuarios` **e** que o usuário alvo seja da mesma
franquia. São ~32 franquias no mesmo banco, e **107 pessoas não administradoras,
em 54 unidades**, têm `escrever` nessa funcionalidade.

Usuário de outra unidade responde **404**, e não 403 — dizer "existe, mas não é
da sua unidade" conta ao pedinte que aquele id existe. É a mesma regra da oitava
fatia.

---

## Os dois campos são obrigatórios no `PUT`

`permissoesSalvar()` do ERP escreve `ler` e `escrever` sem perguntar se vieram.
Um pedido parcial gravaria `false` no que não foi mandado — e apagaria um
privilégio por omissão.

O recurso exige os dois e recusa com **422** quando falta um, nomeando o campo
(regra 3). A omissão vira erro em vez de apagamento silencioso.

---

## `PUT` e não `POST`, e o motivo é a idempotência

O par (usuário, funcionalidade) tem no máximo uma linha, e mandar o mesmo corpo
duas vezes deixa o mesmo estado. O ERP usa um `salvar` que cria ou atualiza
conforme o `id` vier ou não — a mesma coisa, dita por um campo escondido no
corpo em vez de pelo endereço.

A exclusão é `POST .../exclusao`, e não `DELETE`: é a forma que as quinze fatias
anteriores usam para "ação sobre o recurso", e mantê-la faz a API inteira ler
igual. Responde **409** quando não há o que excluir — estado, e não forma.

---

## O catálogo é só leitura, e não é omissão

As 126 funcionalidades são **dado semeado**: não há tela no ERP que crie ou
apague uma, e não há serviço para isso. Cada uma só significa alguma coisa
porque um controller escreve o nome dela em `$this->funcionalidade` — criar uma
linha nova sem o código correspondente produziria um privilégio que não guarda
nada.

Uma API que oferecesse a escrita estaria oferecendo o que o sistema não faz.

E a funcionalidade que **guarda** o catálogo é `configuracoes/usuarios`, não uma
própria: ele só serve para montar a tela de permissão, e quem pode ver aquela
tela é quem tem essa. Inventar uma funcionalidade nova criaria uma linha que
ninguém tem, e a API responderia 403 para todo mundo menos administrador.

---

## Sem paginação nas duas coleções

São 126 linhas no máximo, e quem monta um editor de permissão precisa das linhas
todas de uma vez para desenhar a tela. Página de vinte aqui seriam seis pedidos
para montar uma lista suspensa.

O envelope da regra 5 continua igual — `dados`, `total`, `pagina`, `porPagina` —
com `pagina: 1` e `porPagina` igual ao total. A forma não muda; só o tamanho da
página.

---

## O parâmetro se chama `:id`, e não `:usuario`

A rota da oitava fatia já chama o segmento de `:id`, e
`UsuariosController::verAction` lê `fromRoute('id')`. Renomeá-lo para caber
melhor neste controller deixaria aquela ação sem parâmetro.

**O sub-recurso se adapta ao nome que existe**, e não o contrário. Escrito no
código, porque é o tipo de coisa que a próxima pessoa "arruma" sem perceber o
que quebra.

---

## A auto-concessão foi decidida — 29/09/2026, com a tela nova (D288)

Até aqui quem tinha `configuracoes/usuarios`/`escrever` editava o **próprio**
privilégio, e esta seção registrava a escolha "de olhos abertos". A tela nova de
usuários é a primeira que a oferece sem o disfarce da janela do ERP, e a decisão foi
tomada antes dela — por mim, com o caminho de volta no D288:

1. **ninguém altera os próprios privilégios** — nem o administrador, que já passa
   pelo `Acl` sem linha nenhuma;
2. quem **não** é administrador só **concede** o que ele mesmo tem, no mesmo nível;
3. quem não é administrador **nunca** concede uma funcionalidade de rede — as nove de
   `RegraDoPrivilegio::DE_REDE`, pelo nome: as cinco que abrem outras franquias no
   `SessaoAcl` e as quatro que administram o que é da rede.

"Conceder" é subir de falso a verdadeiro. **Tirar e manter não passam pela 2 nem pela
3**: o gerente que tira de um colega o que não tem não ganha poder nenhum com isso. O
que quem pede tem é lido **do banco**, e não da sessão — a sessão é a foto do login.

A recusa é **403** com `detalhes.motivo` (`proprio`, `de_rede`, `acima_do_seu`) e a
frase da regra. As duas portas — este `PUT` e o `permissoesSalvar` da janela do ERP —
chamam a mesma regra, e o `conferir-autorizacao-privilegiada.php` vigia as duas
chamadas: nenhum ensaio HTTP escreve privilégio.

## O par é uma linha só — D289

"O par tem no máximo uma linha", escrito acima, **não era verdade**: há 117 pares com
mais de uma, e o `Acl` soma as linhas. Desde 29/09/2026:

- **a leitura soma** — uma entrada por par, com `ler`/`escrever` somados e `linhas`
  dizendo quantas havia; o `id` é o da mais antiga;
- **a gravação deixa uma** — a mais antiga recebe o valor e as gêmeas saem, numa
  transação (`UsuarioService::gravarPrivilegio`);
- **a exclusão tira o par inteiro** (`removerPrivilegio`), e responde `linhas`.

Com isso o `PUT` é idempotente de verdade.

## O que a leitura passou a dizer

O envelope de `GET /adm/api/usuarios/:id/privilegios` ganhou dois campos:

- `proprio` — o alvo é quem pede, e nada se altera;
- `concediveis` — o catálogo inteiro, na ordem do ERP, com o que **quem pede** pode
  ligar em cada funcionalidade e se ela é de rede. É a regra perguntada para "subir
  de falso a verdadeiro"; a tela desliga o resto e diz por quê.

## A tela velha é 409 — D293

O `PUT` e a exclusão aceitam `antes` — o que a tela mostrava. O banco diferente é
**409** com `motivo: mudou`, e nada se grava: sem isso, desmarcar um nível numa tela
velha religava o outro que outra aba tinha acabado de tirar. Sem `antes`, vale o
contrato de antes.

## A troca de grupo também passa pela regra

Cadastrar alguém ou trocar o grupo dele reescreve os privilégios pelo molde, e é uma
terceira porta. Nela valem a 1 e a 3 — ninguém troca o próprio grupo; o molde de rede
é só do administrador — **e não a 2**: medido, 99 dos 107 que escrevem em usuários não
poderiam mais cadastrar alguém no grupo Colaborador. Ver o D288.

## E quem tem o acesso mudado sai do sistema — D291

O `SessaoAcl` é montado na entrada: até 17/10/2026, o privilégio tirado não saía da
sessão aberta, e a tela dizia "vale no próximo login". Desde a resposta 102 (b) do
dono, a gravação que muda o efetivo da pessoa tira o `token_web` dela
(`UsuarioService::tirarDoSistema`): a sessão aberta cai no pedido seguinte, o cookie
de entrar sem senha deixa de achá-la, e ela entra de novo com a senha, já com o que
mudou. Sem coluna. A tela diz isso antes de gravar. Ver o D291.
