# Gestão de Permissões — Regras e Portabilidade

> Regras não óbvias, acoplamentos e armadilhas que precisam sobreviver (ou
> serem deliberadamente abandonadas) na reconstrução.

## 1. Regras de negócio

### 1.1 Um perfil por usuário

`users.v2_role_id` é uma coluna única. Não há múltiplos perfis, herança,
composição nem perfil “global”. Trocar o perfil substitui o conjunto inteiro de
permissões. Um usuário sem perfil resolve `[]` e, em qualquer módulo, cai em
`none` (a menos que seja suporte ou `super-admin`).

### 1.2 Três níveis, sem hierarquia formal

| Nível | Leitura | Escrita | Efeito prático |
|---|---|---|---|
| `none` | não | não | nenhum acesso ao módulo |
| `read` | sim | não | GET/HEAD passam; POST/PUT/PATCH/DELETE bloqueiam |
| `write` | sim | sim | acesso completo ao módulo |

Não existe nível “aprovar”, “publicar” ou “arquivar”. Módulos que precisam
dessas distinções implementam controles próprios fora do RBAC.

### 1.3 Resolução tolerante da chave

Ordem em `PermissionService::resolvePermission()` (`PermissionService.php:105-134`):

1. **Chave exata** em `permissions_v2`.
2. **Aliases explícitos**, nos dois sentidos, definidos em `$moduleAliases`
   (`:26-40`). Exemplo: `operadores → Operators`.
3. **Comparação normalizada**: minúsculas e remoção de tudo que não é
   `[a-z0-9]`. Cobre `mail-audit → MailAudit`,
   `certificado-digital → CertificadoDigital`,
   `autoteste-jogo-responsavel → AutoTesteJogoResponsavel`.
4. Nenhum match → `none`.

O fallback normalizado percorre **todas** as chaves armazenadas e retorna o
primeiro valor compatível. Se duas chaves diferentes normalizarem para o mesmo
token, a primeira na ordem do objeto JSON vence.

### 1.4 Sanitização na escrita

`sanitizePermissions()` (`:391-399`) percorre o array recebido e mantém apenas
valores estritamente iguais a `none`, `read` ou `write`. Chaves são mantidas
como vieram; não há validação contra o catálogo de módulos.

Consequências:

- Um POST direto com `permissions[ModuloInexistente]=write` grava a chave.
  Ela fica inerte.
- Um valor `"super"` é descartado silenciosamente; o módulo passa a não ter
  chave e resolve `none`.
- Um array aninhado como valor é descartado (não é string).

### 1.5 `read` é decidido pelo verbo HTTP

O middleware não distingue “ação de leitura” de “ação que muda estado”. A
regra é literalmente:

```php
if ($request->isMethod('GET') || $request->isMethod('HEAD')) {
    return $next($request);
}
```

(`V2PermissionMiddleware.php:102-105`)

Portanto qualquer endpoint de mutação exposto por GET (download que gera
arquivo, “toggle” por link, confirmação de exclusão por GET) passa com
`read`. O parâmetro `,write` declarado nas rotas não corrige isso, porque é
ignorado.

### 1.6 O parâmetro do middleware não é usado

As rotas declaram `v2.permission:Operators,read`,
`v2.permission:Operators,write` e `v2.permission:gestao-permissoes`, mas a
assinatura é `handle($request, Closure $next)` (`:22`). O PHP aceita argumentos
extras em funções de usuário e os descarta. O módulo efetivo vem sempre do
`active_pattern`; a ação efetiva vem do método HTTP. Há **208** ocorrências de
`v2.permission:Operators,write` no repositório, todas sem efeito sobre a ação.

### 1.7 Mapa rota → módulo → permissão

O `active_pattern` de cada `ModuleProvider` é o mapa. O primeiro módulo cuja
glob casar com o path da requisição define a chave; a ordem é a ordem de
iteração de `File::directories()` no boot, não uma prioridade declarada.

```php
'active_pattern' => ['cms/permissoes/permissions*', 'cms/permissoes/roles*'],
```

(`ModuleProvider.php:22`)

`Request::is()` aceita string ou array, então o array funciona. Padrões usam
`*` (glob) e `|` em alguns módulos (por exemplo
`cms/infra/support-configs*|cms/infra/user-configs*`), que é sintaxe de regex
interna do Laravel para `Request::is`.

### 1.8 Bypass de suporte e super-admin

Três caminhos independentes:

| Caminho | Verificação | Onde |
|---|---|---|
| Helper | `$user->email === config('v2.support.email')` | `helpers.php:21` |
| Serviço | e-mail de suporte **ou** `$user->inRole('super-admin')` | `PermissionService.php:72` |
| Middleware | e-mail de suporte **ou** `$user->inRole('super-admin')` | `V2PermissionMiddleware.php:70` |

O e-mail é fixo por configuração (`V2_SUPPORT_EMAIL`, default
`suporte@maxima.inf.br`). Qualquer conta que use esse e-mail — inclusive criada
pela tela de usuários — recebe acesso total.

### 1.9 Grupo `Support`

Módulos cujo diretório pai é `Support` recebem `group = 'Support'` no boot
(`V2ServiceProvider.php:99-107`). O middleware bloqueia o grupo inteiro para
quem não é o e-mail de suporte, **antes** de considerar perfil ou super-admin:

```php
if ($matched && isset($currentModuleConfig['group']) && strtoupper($currentModuleConfig['group']) === 'SUPPORT') {
    if ($user->email !== config('v2.support.email')) {
        return redirect()->route('v2.cms.dashboard')
            ->with('error', 'Acesso negado. Esta área é restrita ao suporte técnico autorizado.');
    }
}
```

(`:51-67`)

A sidebar aplica a mesma regra (`SidebarComposer.php:43-49`). Um `super-admin`
que não seja o e-mail de suporte **não** entra no grupo `Support`.

### 1.10 Módulos `restricted`

Módulos declaram `'restricted' => config('v2.support.email')` no provider. A
tela de perfis remove essas chaves da matriz (`GestaoPermissoesRolesController.php:136-146`).
A sidebar também oculta o módulo para quem não é o e-mail restrito ou
super-admin (`SidebarComposer.php:51-58`).

Observação: o middleware **não** verifica `restricted`; ele verifica `group`.
Um módulo `restricted` fora do grupo `Support` depende da sidebar + ausência na
matriz para não ser acessado — mas se o perfil tiver a chave gravada por SQL,
o middleware libera.

### 1.11 Exclusão de perfil

`deleteRole()` recusa perfis com usuários vinculados
(`PermissionService.php:358-361`) e faz soft delete. A FK `nullOnDelete` nunca
é exercitada por esse caminho. A UI desabilita o botão quando `users_count > 0`.

### 1.12 Auditoria

Criação, atualização e exclusão de perfil geram `V2AuditLogger::activity()`
com `created`/`updated`/`deleted` (`PermissionService.php:290-300,327-337,366-376`).
Edição de usuário gera `updated` (`GestaoPermissoesPermissionsController.php:156-167`).
Falhas de auditoria são capturadas e logadas como `warning`; nunca quebram a
operação.

## 2. Armadilhas e riscos

### 2.1 Fail-open para rota fora de módulo

```php
if (!$matched) {
    return $next($request);
}
```

(`V2PermissionMiddleware.php:74-77`)

Toda rota autenticada que não caia em nenhum `active_pattern` é liberada. Isso
inclui `cms/search`, `cms/organizar` e qualquer rota nova cujo provider esqueça
o padrão ou cujo prefixo não corresponda. A proteção, nesses casos, depende de
checagem dentro do controller.

### 2.2 GET/HEAD sempre passam

Ver 1.5. Um `read` pode, por exemplo, abrir a tela de edição (GET) e ver dados,
mesmo que o POST seja bloqueado. No módulo, o controller compensa chamando
`v2_can('GestaoPermissoes', 'write')` nas telas de formulário — mas isso é exceção, não regra
geral.

### 2.3 Aliases manuais

`$moduleAliases` é uma lista hardcoded de 13 entradas
(`PermissionService.php:26-40`). Um módulo novo cujo slug de rota não seja
derivável por normalização exige edição de código. Esquecer o alias produz
`403` mesmo com a permissão gravada. Os testes E2E existem justamente para
cobrir essa classe de regressão
(`tests/Feature/V2/Rbac/CommonUserModuleAccessE2ETest.php`).

### 2.4 `update()` de usuário não protege o alvo

`GestaoPermissoesPermissionsController@update()` valida apenas os campos. Não
verifica se o `$id` pertence a:

- o e-mail de suporte;
- um usuário com `inRole('super-admin')`;
- o próprio usuário logado.

Um POST direto para `/cms/permissoes/permissions/{id}` pode alterar nome,
e-mail, senha e `v2_role_id` de qualquer conta, inclusive a de suporte. A UI
esconde o link, mas não há barreira de servidor. O mesmo vale para
`GestaoPermissoesRolesController@update()`: não impede editar um perfil
existente cujo slug seja `super-admin` (a checagem só impede **atribuir** esse
slug).

### 2.5 Criação de usuário com efeitos parciais

`store()` cria e ativa o usuário no Sentinel antes de validar se o papel V1 é
`super-admin` (`GestaoPermissoesPermissionsController.php:228-253`). Se a
checagem falhar, o usuário já existe. Não há transação entre `users`, Sentinel
e `v2_role_id`.

### 2.6 Dois RBACs simultâneos

- **V1 (Sentinel)**: `roles`, `role_users`, `inRole()`. Decide quem entra no
  CMS (`CmsAuthenticate` exige ≥ 1 papel) e quem é `super-admin`.
- **V2 (`v2_roles`)**: decide o que cada usuário pode fazer por módulo.

Um usuário pode ter perfil V2 `write` e nenhum papel V1 → é barrado no login
do CMS. Pode ter papel V1 `operador` e nenhum perfil V2 → entra, mas vê apenas
dashboard. A tela de criação exige os dois conceitos, o que confunde o
operador.

### 2.7 `isHighAdmin()` tem um terceiro papel

Além de suporte e `super-admin`, aceita `inRole('administrador')`
(`PermissionService.php:197-210`). Isso não passa pelo RBAC V2 e não aparece na
matriz de permissões. É usado para organizar o dashboard
(`DashboardCmsController.php:82,160`).

### 2.8 Dupla execução do middleware

`CmsAuthenticate` chama o `V2PermissionMiddleware` no fim do seu `handle()`
(`CmsAuthenticate.php:40`). As rotas do módulo também declaram
`v2.permission`. O middleware roda duas vezes por requisição: dois scans de
todos os providers, duas leituras do perfil e dois logs potenciais. Funciona,
mas é custo e superfície de divergência (por exemplo, se um dos caminhos mudar
de assinatura).

### 2.9 Erros engolidos

`getUserPermissions()` captura `\Exception` e retorna `[]`
(`PermissionService.php:245-248`). Uma falha de banco é convertida em “sem
permissão”, o que é seguro do ponto de vista de acesso, mas invisível para o
operador e potencialmente um `none` global se a tabela estiver indisponível.

### 2.10 `deleteRole()` e auditoria antes da exclusão

A auditoria é gravada **antes** do `delete()` (`:366-378`). Se o delete falhar,
a trilha registra uma exclusão que não ocorreu. Baixo impacto, mas relevante
para evidência.

## 3. Acoplamentos

| Direção | Dependência |
|---|---|
| Módulo → Core | `PermissionService`, `V2Role`, `V2AuditLogger`, `BaseController` |
| Módulo → todos os módulos | lista de `ModuleProvider` via `v2.modules.configs` |
| Core → módulo | nenhum acoplamento direto; o Core não conhece as telas |
| Sidebar | usa `resolvePermission()` e o mesmo catálogo de módulos |
| Middleware | usa `active_pattern` de todos os providers |
| Sentinel | autenticação e papéis V1; sem ele, o CMS não abre |
| `users` | coluna `v2_role_id` e FK vinda do módulo Suporte |

## 4. Onde as versões divergiram

O repositório analisado é a V2 dentro do projeto `lotep`; não há uma segunda
cópia de `GestaoPermissoes` para comparar. O que se observa é uma evolução
interna:

- O alias e a normalização foram adicionados para corrigir `403` em perfis
  antigos (comentário em `PermissionService.php:18-24`).
- A edição de perfil passou a resolver a permissão antes de renderizar
  (`GestaoPermissoesRolesController.php:187-191`), para não sobrescrever slugs
  legados com `none`.
- A coluna `users.permissions_v2` (migration `000004`) ficou órfã quando o
  modelo migrou para perfil centralizado em `v2_roles` (migration `000005`).

## 5. Regras que **não** podem ser perdidas na portabilidade

1. Um usuário tem **um** conjunto efetivo de permissões; trocar de perfil
   substitui tudo.
2. Ausência de chave = `none`; nunca “herda do perfil anterior”.
3. `read` não pode executar mutação. No V7 isso é garantido pela ação
   (`ver` ≠ `editar`), não pelo verbo HTTP.
4. Módulos do grupo de suporte são invisíveis e inacessíveis fora da equipe de
   suporte, **inclusive** para o maior privilégio.
5. O perfil reservado de suporte não pode ser criado, editado nem atribuído por
   outro usuário.
6. Toda mudança de permissão ou de vínculo de usuário deve gerar trilha de
   auditoria.
