# Gestão de Permissões — Migração para o V7

> **Slug:** `infraestrutura-gestaopermissoes` · **Alvo:** Laravel 12 / PHP 8.4,
> DDD-lite (`core`/`services`/`layout`/`modules`), sem traits/abstract/generics.
> Plano suficiente para escrever a migration, o model e a autorização novos
> **sem abrir o legado**.

## 1. Equivalência funcional (o que não pode mudar)

| Comportamento legado | Equivalente no V7 | Risco de divergência |
|---|---|---|
| Um perfil por usuário | `cms_users.role_id` (nullable, FK) | herança acidental se o V7 aceitar múltiplos perfis |
| Matriz `módulo → none|read|write` | lista de tokens `{modulo}.{acao}` em `core_roles.permissoes` | `write` legado não indica **quais** ações editoriais o módulo suporta |
| Chave canônica + aliases + normalização | token canônico declarado no manifesto do módulo (ADR-004/014) | perfis migrados com alias precisam ser convertidos para o token canônico |
| `read` bloqueia POST/PUT/DELETE | ação `ver` não autoriza `editar`; autorização por ação, não por verbo | rotas GET que mutam estado precisam ser corrigidas ou removidas |
| `none`/ausência = sem acesso | `Permission::authorize()` nega por padrão (ADR-033) | nenhum “fail-open” para rota não mapeada |
| E-mail de suporte com bypass | perfil reservado `suporte`, vinculado por código à equipe | **não** manter bypass por endereço de e-mail |
| Grupo `Support` invisível | grupo `Suporte` invisível e inacessível por guard (ADR-033/043) | esconder apenas na UI não basta |
| Auditoria `created/updated/deleted` | `AuditWriter` / subscriber global (ADR-035) | garantir trilha de mudança de permissão e de vínculo |
| `super-admin` do Sentinel | perfil de maior privilégio **sem** bypass de grupo Suporte | super-admin não pode acessar módulo de suporte |

## 2. Schema de migração

O V7 não replica `v2_roles`. O ADR-033 define `core_roles` com uma **lista** de
tokens, não um mapa:

```php
// exemplo ilustrativo
Schema::create('core_roles', function (Blueprint $table) {
    $table->id();
    $table->string('nome');
    $table->string('slug')->unique();
    $table->jsonb('permissoes')->default('[]');
    $table->timestamps();
});
```

```sql
-- exemplo ilustrativo
ALTER TABLE core_roles
  ADD CONSTRAINT core_roles_permissoes_array
  CHECK (jsonb_typeof(permissoes) = 'array');

-- exemplo ilustrativo
CREATE UNIQUE INDEX core_roles_permissoes_sem_duplicata
  ON core_roles (slug);
```

O vínculo com o usuário do CMS:

```php
// exemplo ilustrativo
Schema::table('cms_users', function (Blueprint $table) {
    $table->foreignId('role_id')
          ->nullable()
          ->constrained('core_roles')
          ->nullOnDelete();
});
```

| Tabela legada | Tabela nova | Mudança | Justificativa |
|---|---|---|---|
| `v2_roles` | `core_roles` | `permissions_v2` (objeto) → `permissoes` (array de tokens) | tokens `{modulo}.{acao}` são o vocabulário do ADR-033 |
| `users.v2_role_id` | `cms_users.role_id` | UUID → `bigint`/id nativo; FK `nullOnDelete` | simplicidade e integridade |
| `users.permissions_v2` | — | descartar após conferência | coluna órfã no legado |
| `v2_roles.deleted_at` | — | sem soft delete | ADR-017/041: roles são dado de seed; exclusão exige tratamento explícito |

### Conversão de dados

Converter o mapa legado em tokens conforme as ações que o módulo declara no
manifesto:

| Valor legado | Tokens gerados |
|---|---|
| `none` ou ausente | nenhum |
| `read` | `{Modulo}.ver` |
| `write` | `{Modulo}.ver` + `{Modulo}.editar`; somar `{Modulo}.aprovar` e `{Modulo}.arquivar` se o módulo declarar essas ações |

Regra de segurança: **nunca** gerar um token para uma ação que o módulo não
declara. Token órfão é inerte no V7 (ADR-033), mas suja a auditoria e a tela.

Pseudocódigo de migração de dados (executado uma vez, fora de migration de
schema):

```php
// exemplo ilustrativo
$acoesPorModulo = $manifesto->permissions(); // [modulo => ['ver','editar','aprovar','arquivar']]

foreach ($rolesLegados as $role) {
    $tokens = [];

    foreach ($role->permissions_v2 as $modulo => $nivel) {
        $canonico = $resolverTokenCanonico($modulo);
        $acoes = $acoesPorModulo[$canonico] ?? [];

        if ($nivel === 'read' && in_array('ver', $acoes, true)) {
            $tokens[] = "{$canonico}.ver";
        }

        if ($nivel === 'write') {
            foreach (['ver', 'editar', 'aprovar', 'arquivar'] as $acao) {
                if (in_array($acao, $acoes, true)) {
                    $tokens[] = "{$canonico}.{$acao}";
                }
            }
        }
    }

    $tokens = array_values(array_unique($tokens));
    $roleNovo->permissoes = $tokens;
}
```

## 3. Modelos

| Modelo legado | Modelo novo | Mudanças |
|---|---|---|
| `V2Role` | model de `core_roles` | sem `SoftDeletes`, sem UUID manual; `permissoes` com cast `array` |
| `App\Admin\User` (Sentinel) | `Core\Area\Cms\Models\User` | guard `web` nativo (ADR-037), sem Sentinel |
| — | `Core\Access\Permission` (helper/serviço estático) | vocabulário e `authorize()` (ADR-033) |

Exemplo de model novo (sem trait, sem abstract):

```php
// exemplo ilustrativo
final class Role extends Model
{
    protected $table = 'core_roles';

    protected $fillable = ['nome', 'slug', 'permissoes'];

    protected function casts(): array
    {
        return ['permissoes' => 'array'];
    }
}
```

**Enums sugeridos:**

| Valor legado | Enum | Casos |
|---|---|---|
| `none` | não existe | ausência de token |
| `read` | `Permission::VIEW` (`ver`) | consulta/listagem/detalhe |
| `write` | `Permission::EDIT` (`editar`) | criar e editar |
| — (não existia) | `Permission::APPROVE` (`aprovar`) | publicar/aprovar, quando o módulo declarar |
| — (não existia) | `Permission::ARCHIVE` (`arquivar`) | arquivar/desarquivar, quando o módulo declarar |

## 4. Form Requests

| Request legado | Request novo | Regras |
|---|---|---|
| validação inline de perfil | `StoreRoleRequest` | `nome` obrigatório; `slug` único; `permissoes` array de tokens válidos, sem duplicata |
| validação inline de perfil | `UpdateRoleRequest` | idem, ignorando o próprio id no `unique` |
| validação inline de usuário | `StoreCmsUserRequest` | `nome`, `email` único, `senha` confirmada e política do ADR-020, `role_id` existente |
| validação inline de usuário | `UpdateCmsUserRequest` | idem, senha opcional, `role_id` existente |

A validação de tokens precisa consultar o manifesto **no servidor**:

```php
// exemplo ilustrativo
'permissoes' => ['array'],
'permissoes.*' => ['string', Rule::in($acoesValidas)],
```

`$acoesValidas` é a lista achatada de `{modulo}.{acao}` declarada pelos
módulos. Token fora da lista → 422, nunca gravação silenciosa.

## 5. Autorização

Substituir o `PermissionService` por `Gate` nativo + helper declarativo
(ADR-033). O Core **não** mantém serviço próprio de resolução.

```php
// exemplo ilustrativo — no ModuleProvider do módulo
return [
    'title' => 'Notícias',
    'permissions' => Permission::ACTIONS,
];

// exemplo ilustrativo — enforcement
Permission::authorize('Noticias', Permission::APPROVE);
```

Regras:

- **Negar por padrão**: ação não declarada pelo módulo nunca vira ability.
- Perfil `suporte`: reservado, não editável, não listável e não atribuível por
  outro usuário; garantido por código, não só por tela.
- Grupo `Suporte`: guard de rota responde **404** para quem não é da equipe,
  mesmo com o perfil mais alto (ADR-033/043).
- `super-admin` não é bypass de grupo restrito.
- Sem bypass por e-mail fixo. A identidade de suporte é uma conta reservada,
  não um endereço que qualquer cadastro pode assumir.

| Recurso | Policy/Gate | Regras |
|---|---|---|
| `core_roles` | `RolePolicy` | `viewAny`/`view` → `core.ver`; `create`/`update`/`delete` → `core.editar`; perfil suporte bloqueado sempre |
| `cms_users` | `CmsUserPolicy` | `viewAny`/`view` → `core.ver`; `create`/`update` → `core.editar`; alvo suporte/super-admin exige regra explícita; ninguém edita a própria permissão |
| módulos | `Permission::authorize` no controller/action | quatro ações fixas; ação não declarada → 403 |

## 6. Endpoints sugeridos

| Legado | Novo | Tipo | Breaking? |
|---|---|---|---|
| `GET /cms/permissoes/roles` | `GET /cms/roles` | HTML | não |
| `GET /cms/permissoes/roles/create` | `GET /cms/roles/create` | HTML | não |
| `POST /cms/permissoes/roles` | `POST /cms/roles` | HTML | não |
| `GET /cms/permissoes/roles/{id}/edit` | `GET /cms/roles/{role}/edit` | HTML | não |
| `POST /cms/permissoes/roles/{id}` | `PUT/PATCH /cms/roles/{role}` | HTML | sim (verbo) |
| `POST /cms/permissoes/roles/{id}/delete` | `DELETE /cms/roles/{role}` | HTML | sim (verbo) |
| `GET /cms/permissoes/permissions` | `GET /cms/usuarios` | HTML | sim (nome) |
| `POST /cms/permissoes/permissions` | `POST /cms/usuarios` | HTML | sim (nome) |
| `POST /cms/permissoes/permissions/{id}` | `PUT/PATCH /cms/usuarios/{user}` | HTML | sim (verbo) |

Manter URLs antigas como redirect 301/302 apenas se houver links externos
documentados; caso contrário, cortar (ADR-042).

## 7. API Resources

O módulo é HTML puro. Se o V7 expuser uma API administrativa, o recurso deve
ser somente leitura e mascarar dados sensíveis:

```php
// exemplo ilustrativo
final class RoleResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'nome' => $this->nome,
            'slug' => $this->slug,
            'permissoes' => $this->permissoes,
        ];
    }
}
```

Não expor `password`, tokens de sessão ou IDs internos de Sentinel.

## 8. Views

| Blade legado | Blade novo / componente | Observação |
|---|---|---|
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/index.blade.php` | `cms::roles.index` + `<x-table>` | busca e paginação server-side; sem JS de ordenação |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/create.blade.php` | `cms::roles.form` + `<x-permission-matrix>` | matriz gerada do manifesto, não de `v2.modules.configs` |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/roles/edit.blade.php` | `cms::roles.form` | mesmo componente, valores do perfil |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/permissions/index.blade.php` | `cms::users.index` | badge de perfil; sem badge Sentinel |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/permissions/create.blade.php` | `cms::users.form` | sem select de papel V1 |
| `V2/Modules/Infraestrutura/GestaoPermissoes/Views/permissions/edit.blade.php` | `cms::users.form` | sem troca de papel V1 |

A matriz deve ser um componente único, alimentado por um DTO do manifesto
(módulo, grupo, ações suportadas). Nada de `@php function mapModuleIcon()` em
view: ícones pertencem ao DTO/manifesto.

## 9. Fila, cache e storage

| Necessidade | Solução no V7 | Configuração |
|---|---|---|
| Catálogo de módulos/ações | manifesto cacheado no boot (`core:cache-modules`) | ADR-014; sem consulta a banco por request |
| Permissões do usuário | `cms_users.role_id` + `core_roles.permissoes` no guard | cast `array`; sem cache por request no MVP |
| Invalidação após editar perfil | `Cache::forget` das chaves derivadas (sidebar/manifesto de navegação) | ADR-034 |
| Auditoria | `AuditWriter` (DB + sinks) | ADR-035 |
| Storage | não se aplica | — |

## 10. Jobs e eventos

| Evento | Quando dispara | Efeito |
|---|---|---|
| `RolePermissionsChanged` | após salvar `core_roles.permissoes` | invalida cache de navegação; auditoria `core.role_updated` |
| `UserRoleChanged` | após trocar `cms_users.role_id` | auditoria `core.user_role_changed`; invalida sessão se necessário |
| `Login`/`Logout` | autenticação nativa | `AuditAuthSubscriber` grava `core.login`/`core.logout` (ADR-037) |

## 11. Front-end

| Abordagem legada | Abordagem nova | Justificativa |
|---|---|---|
| Bootstrap 5 por classes + script inline de slug | Tailwind/daisyUI + JS mínimo (ADR-009/010) | CSP sem inline (ADR-022) |
| `onsubmit="return confirm('Deseja realmente remover este perfil de acesso?')"` | diálogo acessível em `<dialog>`/componente | CSP e acessibilidade (ADR-025) |
| Rádios `none/read/write` | checkboxes por ação declarada (`ver/editar/aprovar/arquivar`) | o modelo do ADR-033 é por ação, não por nível |
| `@v2can` | `@can`/`Gate` nativo | sem helper global próprio |

## 12. Ordem de implementação

| # | Passo | Depende de | Observação |
|---|---|---|---|
| 1 | Definir `Permission` (4 ações) e o manifesto de módulos | ADR-033/014 | vocabulário antes de qualquer tela |
| 2 | Migrar `core_roles` e `cms_users.role_id` | passo 1 | schema + CHECK de array |
| 3 | Converter dados legados (`permissions_v2` → tokens) | passos 1–2 | script único, com relatório de tokens órfãos |
| 4 | Implementar `Gate`/`RolePolicy`/`CmsUserPolicy` | passos 1–2 | negar por padrão |
| 5 | Telas de perfis e usuários | passos 1–4 | matriz gerada do manifesto |
| 6 | Perfil e grupo de suporte | passos 1–4 | guard 404 + perfil não atribuível |
| 7 | Auditoria e testes | passos 1–6 | eventos e testes de regressão |

## 13. Riscos e decisões de equivalência

1. **`write` é ambíguo**: no legado significa “ler e escrever”. No V7 pode
   significar `ver+editar`, e aprovar/arquivar exigem decisão por módulo. Não
   converter `write` em todas as quatro ações por padrão; isso ampliaria
   privilégio.
2. **Rotas GET que mutam estado**: a regra “GET passa com read” não existe no
   V7. Mapear cada GET de mutação para a ação correta antes de desligar o
   legado.
3. **Perfis com slug legado**: converter para o token canônico antes de
   descartar os aliases. Perfil não convertido vira `[]` e perde acesso.
4. **Contas de suporte**: a conta reservada precisa ser recriada com o perfil
   `suporte`; não migrar o bypass por e-mail.
5. **Auditoria de leitura**: o legado não registra leitura de perfis/usuários;
   o ADR-035 pede leitura sensível no CMS. Isso é adição deliberada, não
   regressão.
