# Gestão de Permissões — Modelagem

> Tabelas do RBAC V2, colunas legadas e a estrutura do jsonb de permissões.
> Todo o schema é criado por migrations do **Core**, não do módulo.

## 1. Tabela `v2_roles`

Criada em
`V2/Core/Migrations/2026_06_01_000005_create_v2_roles_and_add_role_id_to_users_table.php:25-36`.

| Coluna | Tipo | Nulo | Default | Observação |
|---|---|---|---|---|
| `id` | `uuid` | não | — | PK; gerado no model como UUID v4 |
| `name` | `varchar(255)` | não | — | nome exibido na UI |
| `slug` | `varchar(255)` | não | — | **unique**; identificador estável |
| `permissions_v2` | `jsonb` | sim | `null` | mapa `módulo → nível` |
| `created_at` | `timestamp` | sim | — | Eloquent |
| `updated_at` | `timestamp` | sim | — | Eloquent |
| `deleted_at` | `timestamp` | sim | — | soft delete |

Índice adicional: `v2_roles_name_index` sobre `name` (`:33`).

### DDL real

```php
Schema::create('v2_roles', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name');
    $table->string('slug')->unique();
    $table->jsonb('permissions_v2')->nullable()->default(null);
    $table->index('name');
    $table->timestamps();
    $table->softDeletes();
});
```

### Estrutura de `permissions_v2`

Objeto JSON cujas chaves são o nome canônico do módulo (nome do diretório em
`V2/Modules`) e cujos valores são exatamente um dos três
níveis:

```json
{
  "Noticias": "write",
  "Licitacoes": "read",
  "Ouvidoria": "none",
  "FormulariosDinamicos": "write"
}
```

Regras observadas no código:

- Não há validação de schema no banco: a coluna é `jsonb` livre. A sanitização
  é feita na aplicação (`PermissionService::sanitizePermissions`,
  `:391-399`) e **descarta** chaves com valor fora de `none|read|write`.
- Chaves desconhecidas são persistidas e ficam **inertes**: nunca casam com um
  `active_pattern` e não concedem acesso.
- Ausência da chave equivale a `none` (`resolvePermission` retorna `none`).
- A UI envia **sempre** as três opções por módulo; o valor `none` é gravado
  explicitamente, não omitido.

### Consulta de exemplo

```sql
-- Nível efetivo de um módulo específico para um usuário
SELECT u.id,
       u.email,
       r.name AS perfil,
       COALESCE(r.permissions_v2 ->> 'Noticias', 'none') AS nivel_noticias
FROM users u
LEFT JOIN v2_roles r ON r.id = u.v2_role_id AND r.deleted_at IS NULL
WHERE u.email = 'operador@dominio.gov.br';
```

```sql
-- Perfis que possuem escrita em Noticias (jsonb contém a chave/valor)
SELECT id, name, slug
FROM v2_roles
WHERE deleted_at IS NULL
  AND permissions_v2 @> '{"Noticias":"write"}'::jsonb;
```

## 2. Coluna `users.v2_role_id`

Adicionada na mesma migration (`:38-45`):

```php
Schema::table('users', function (Blueprint $table) {
    if (!Schema::hasColumn('users', 'v2_role_id')) {
        $table->uuid('v2_role_id')->nullable()->index();
    }
});
```

| Coluna | Tipo | Nulo | Default | Observação |
|---|---|---|---|---|
| `v2_role_id` | `uuid` | sim | `null` | um perfil por usuário |

A FK é criada em uma migration separada do módulo Suporte
(`V2/Modules/Support/SupportConfigs/Migrations/2026_07_02_000001_add_v2_foreign_keys.php:20-30`):

```php
$table->foreign('v2_role_id')
      ->references('id')->on('v2_roles')
      ->onDelete('set null');
```

Consequência: excluir um perfil **fisicamente** deixaria os usuários sem
perfil; como o código usa soft delete, a linha permanece e a FK não dispara.
Um usuário cujo perfil foi soft-deletado continua com `v2_role_id` apontando
para uma linha invisível para `V2Role::find()` (que respeita o SoftDeletes),
portanto passa a resolver `[]` → `none` em todos os módulos.

### Por que não há FK no model

O model `User` (`app/Http/Model/Admin/User.php`) não declara
`v2_role_id` em `$fillable` (`:50-55`) nem relação `belongsTo(V2Role::class)`.
Os controllers atribuem a coluna diretamente:

```php
$user->v2_role_id = $request->input('v2_role_id');
$user->save();
```

(`GestaoPermissoesPermissionsController.php:139` e `:240`)

Isso significa que `User::create(['name' => 'Fulano', 'email' => 'fulano@dominio.gov.br'])`
**não** grava `v2_role_id` por mass assignment; os testes de feature atribuem
o campo depois de criar.

## 3. Coluna legada `users.permissions_v2`

Criada em
`V2/Core/Migrations/2026_06_01_000004_add_permissions_v2_to_users_table.php:23-31`:

```php
if (!Schema::hasColumn('users', 'permissions_v2')) {
    $table->jsonb('permissions_v2')->nullable()->default(null);
}
```

Estado atual: **não é lida por nenhum código da V2**. O `PermissionService`
lê apenas `v2_roles.permissions_v2`
(`PermissionService.php:216-251`). A coluna permanece como resíduo de um
modelo anterior em que a permissão era por usuário. Não há migration que a
remova.

Risco de migração: se algum ambiente de produção ainda tiver dados nessa
coluna, eles serão ignorados na leitura e perdidos se a coluna for descartada
sem um backfill para `v2_roles`.

## 4. Relacionamentos

| Relação | Definição | Uso |
|---|---|---|
| `V2Role::users()` | `hasMany(App\Admin\User::class, 'v2_role_id')` (`V2Role.php:99-102`) | contagem de usuários por perfil no índice |
| `User → V2Role` | **não declarada**; acesso por `V2Role::find($user->v2_role_id)` | resolução de permissões |
| `User → roles()` | Sentinel V1 (`role_users`) | `CmsAuthenticate` exige ao menos um papel; `inRole()` libera `super-admin` |

Não há tabela pivot entre `v2_roles` e módulos: a matriz é um único documento
jsonb. Não há tabela de permissões individuais por usuário na V2.

## 5. Views SQL

**Nenhuma.** O módulo não cria `CREATE VIEW`, não usa `DB::raw` e não consulta
views do legado. As leituras são Eloquent (`V2Role`, `User`) e uma consulta
`whereIn` para os perfis da listagem de usuários
(`GestaoPermissoesPermissionsController.php:56-60`).

## 6. Migrations

| Migration | Data | O que faz |
|---|---|---|
| `V2/Core/Migrations/2026_06_01_000004_add_permissions_v2_to_users_table.php` | 2026-06-01 | adiciona `users.permissions_v2` jsonb (legado, sem uso) |
| `V2/Core/Migrations/2026_06_01_000005_create_v2_roles_and_add_role_id_to_users_table.php` | 2026-06-01 | cria `v2_roles` e adiciona `users.v2_role_id` |
| `V2/Modules/Support/SupportConfigs/Migrations/2026_07_02_000001_add_v2_foreign_keys.php` | 2026-07-02 | adiciona FK `users.v2_role_id → v2_roles.id` (`set null`) |
| `V2/Modules/Support/SupportConfigs/Migrations/2026_06_30_000002_create_v2_module_flags_table.php` | 2026-06-30 | cria `v2_module_flags` (ativação; não é RBAC, mas condiciona o acesso ao módulo) |

### Estratégia de rollback

As migrations do Core renomeiam tabelas/colunas em vez de apagar
(`000005:66-87`; `000004:48-69`), preservando dados em “cold storage”:

```php
$backupTableName = 'backup_v2_roles_' . date('Y_m_d_His');
$backupColumnName = 'backup_v2_role_id_' . date('Y_m_d_His');
```

Isso evita perda em produção, mas deixa objetos órfãos que precisam de limpeza
manual após um rollback definitivo.

## 7. Seeds e provisionamento

Não existe seeder do módulo nem seed de `v2_roles`. O único caminho de criação
inicial documentado é a configuração de provisionamento em `config/v2.php`
(`:43-51`):

```php
'provisioning' => [
    'admin_inicial' => [
        'name'     => env('V2_ADMIN_INICIAL_NAME', 'Admin Inicial'),
        'email'    => env('V2_ADMIN_INICIAL_EMAIL', 'admin-inicial@lotep.pb.gov.br'),
        'password' => env('V2_ADMIN_INICIAL_PASSWORD', '2026@Lotep@Admin@v2'),
    ],
],
```

Os valores são usados por uma rotina externa de provisionamento. Não há
migration que crie o perfil “admin-inicial” nem o usuário; a criação é feita
pela tela de Perfis ou por script operacional.

### Perfil `super-admin`

Não existe linha reservada em `v2_roles` criada por código. O nome
`super-admin` é protegido **contra criação/renomeação**
(`GestaoPermissoesRolesController.php:125-133,218-226`), mas a autorização de
super-admin vem do **Sentinel V1** (`inRole('super-admin')`), não de um perfil
V2. Ou seja: o usuário pode ter `v2_role_id = null` e ainda assim ser
super-admin.

## 8. Integridade e índices

| Índice/constraint | Coluna | Efeito |
|---|---|---|
| PK | `v2_roles.id` | unicidade do UUID |
| unique | `v2_roles.slug` | impede dois perfis com o mesmo slug |
| index | `v2_roles.name` | acelera busca/ordenação |
| index | `users.v2_role_id` | acelera resolução por usuário |
| FK | `users.v2_role_id` | `nullOnDelete` |

Não há `CHECK` sobre o formato de `permissions_v2`; um valor escalar ou array
inválido pode ser gravado por SQL direto e será tratado como `[]`/`none` pela
aplicação (`PermissionService.php:230-243`).

## 9. Volume e cardinalidade

- `v2_roles`: uma linha por perfil; a UI pagina de 15 em 15
  (`GestaoPermissoesRolesController.php:56`).
- `users`: a listagem pagina de 15 em 15 e faz **uma** consulta adicional para
  carregar os perfis da página (`GestaoPermissoesPermissionsController.php:53-60`).
- `permissions_v2`: no máximo uma chave por módulo registrado (39 providers no
  repositório). O documento é pequeno e não precisa de índice GIN no volume
  atual.
