---
id: MATRIZ-PERMISSOES
tipo: matriz
onda: 5
status: concluido
unidades: []
fontes:
  - V2/Core/Middleware/
  - V2/Core/Services/
  - V2/Core/Models/
  - V2/Core/Migrations/
  - V2/Modules/
  - V2/Services/
  - app/Http/Kernel.php
  - config/v2.php
depende_de: [CORE-AUTH, CORE-BOOT, CORE-DATA, CORE-CMS, MOD-INFRAESTRUTURA-GESTAOPERMISSOES]
consumidores: [MATRIZ-SINGULARIDADES, MATRIZ-FLAGS, MATRIZ-INTEGRACOES, REIMPL-ORDEM]
criticidade: alta
dono: Matriz
---

# Permissões e papéis: a matriz real de RBAC

Este documento descreve **quem entra, quem vê e quem escreve** no V2, com os
números medidos no código e nos índices. Ele separa três coisas que o sistema
mistura no mesmo request: (1) a autorização binária do Sentinel, (2) a matriz
`none|read|write` por módulo em `v2_roles.permissions_v2` e (3) os bypasses por
identidade (e-mail de suporte e `super-admin`). A quarta camada — token de API
regional e chave interna — não é RBAC e aparece em §3.6.

## 1. Efeito

### Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Decide, por requisição, se o usuário entra no CMS, qual módulo aparece na sidebar e se ele pode ler ou escrever em cada módulo; fora do RBAC, tokens de API decidem o acesso máquina-a-máquina. |
| Entrada | `POST /cms/login`; qualquer request sob `/cms`; headers `X-Access-Key`/`X-Internal-Key`/`Bearer asc_`; `permissions_v2` do `v2_roles`; `users.unidade_regional_id`. |
| Saída | `302` para login/dashboard, `401 Unauthorized.`, `403` com mensagens fixas, `404` de módulo desativado; sidebar filtrada; logs `[V2 > MIDDLEWARE > Permission]`. |
| Efeitos colaterais | `persistences` no login; `session()->regenerate()`/`invalidate()`; `last_used_at` de chave interna; `activity_log` em login e em CRUD de perfis; `markUsed()` por request autenticada por chave. |
| Particularidade central | O RBAC só é aplicado quando o path casa com um `active_pattern`. **40 endpoints de aplicação (33 com sessão e 7 sem autenticação) não casam** (§2.7); somando as 5 rotas públicas de login/logout, são 45 rotas sem pattern. Nesses casos o middleware libera e a defesa é o controller — quando existe. |
| Como se integra | `CmsAuthenticate` → `V2PermissionMiddleware::handle()`; `PermissionService::getUserPermissions`; `SidebarComposer`; `@v2can`/`v2_can()`; aliases do Kernel. |
| Se removido, quebra | Todo o CMS, a sidebar, a Gestão de Permissões, o recorte regional, a API interna e o canal IA. |
| Superfície pública | `PermissionService` (`hasPermission`, `getUserPermissions`, `getAllRoles`, `getRoleById`, `createRole`, `updateRole`, `deleteRole`, `getAssignedUsersCount`); `V2Role`; `ModuleFlagService`; `RegionalScopeService`; `v2_can()`, `v2_is_regional_operator()`, `v2_regional_id()`; diretivas `@v2can`/`@endv2can`. |

### 1.1 Atores e o que cada um pode

| Ator | Como é reconhecido | Entra no CMS? | RBAC dos módulos | Grupo `Support` | Escopo regional | Evidência |
|---|---|---|---|---|---|---|
| Público | sem sessão | não (302 `/cms/login`; 401 JSON) | não se aplica | não se aplica | não se aplica | `V2/Core/Middleware/CmsAuthenticate.php:20-26` |
| Usuário com papel Sentinel e `v2_role_id` | `roles()->count() > 0` + linha em `v2_roles` | sim | conforme `permissions_v2` | negado, salvo e-mail de suporte | irrestrito (sem `unidade_regional_id`) | `V2/Core/Services/PermissionService.php:71-103` |
| Usuário com papel Sentinel e `v2_role_id = null` | idem, perfil ausente | sim | todas as chaves `none`: sidebar vazia; core fail-open responde 200 | negado | irrestrito | `V2/Core/Services/PermissionService.php:73-77` |
| Operador regional | `users.unidade_regional_id` preenchido + papel `regional` | sim | `Noticias=write`, `Associados=write` (seed) | negado | só a própria regional | `V2/Core/Migrations/2026_09_14_000002_seed_v2_regional_role.php:21-46` |
| Suporte | `email === config('v2.support.email')` | sim (seed cria papel Sentinel `admin`) | bypass total | **permitido** | nunca restringido | `V2/Core/Middleware/V2PermissionMiddleware.php:51-72` |
| `super-admin` | `inRole('super-admin')` (papel Sentinel não semeado) | sim | bypass total | **negado** se o e-mail não for o de suporte | nunca restringido | `V2/Core/Middleware/V2PermissionMiddleware.php:51-72`; `V2/Core/Services/PermissionService.php:46-49` |
| Canal IA | `CmsAiToken` válido + e-mail impersonado | sim, stateless | herda o RBAC do e-mail impersonado | depende do e-mail | depende do e-mail | `01_arquitetura/04-middlewares-seguranca.md:51-53` |
| Chave interna | `X-Access-Key`/`X-Internal-Key`/`Bearer` + escopo | não (API) | não usa RBAC | não se aplica | `consumer` opcional | `V2/Core/Middleware/InternalApiAuth.php:37-58` |
| Token regional | `Bearer asc_` hasheado em `v2_unidade_regionais` | não (API) | sem escopos | não se aplica | só a unidade do token | `05_modulos/Conexoes/UnidadeRegional/api.md:25-31` |

### 1.2 Decisão por request (RBAC V2)

`CmsAuthenticate` valida sessão e papel Sentinel e delega a
`V2PermissionMiddleware` (`V2/Core/Middleware/CmsAuthenticate.php:44`). O
middleware percorre `v2.modules.configs` e usa o **primeiro** `active_pattern`
que casar (`V2/Core/Middleware/V2PermissionMiddleware.php:34-48`).

| Permissão do módulo | GET/HEAD | POST/PUT/PATCH/DELETE | Resposta AJAX/JSON | Resposta HTML |
|---|---|---|---|---|
| `write` | passa | passa | passa | passa |
| `read` | passa | bloqueia | `403 Forbidden. Write actions are not allowed for your profile.` | `302 back()` + `Operação negada. Seu perfil possui apenas permissão de leitura para o módulo: <title>` |
| `none` (ou chave ausente) | bloqueia | bloqueia | `403 Forbidden. Access denied for this module.` | `302 v2.cms.dashboard` + `Acesso negado. Seu perfil não possui permissão para acessar o módulo: <title>` |
| valor inválido | bloqueia | bloqueia | `403 Forbidden. Invalid permissions configuration.` | `302 dashboard` + `Configuração de permissões inválida.` |
| nenhum pattern casou | passa | passa | passa | passa (fail-open) |

Fonte: `V2/Core/Middleware/V2PermissionMiddleware.php:84-135`.

## 2. Abordagem

### 2.1 Duas autorizações, em série

```text
Request /cms/*
  └─ web (sessão + CSRF + CmsAiToken)
      └─ CmsAuthenticate
          ├─ Sentinel::check()? ............... não → 302 login / 401
          ├─ roles()->count() > 0? ............ não → logout + 302 login
          └─ V2PermissionMiddleware
              ├─ grupo SUPPORT? e email != suporte → 302/403
              ├─ email == suporte ou inRole(super-admin) → next
              ├─ nenhum active_pattern casou → next (fail-open)
              └─ permissions_v2[módulo] ∈ {none,read,write} → decide
```

### 2.2 Onde cada autorização é aplicada

| Camada | Mecanismo | Ponto | Decide |
|---|---|---|---|
| Sessão | Sentinel `persistences` + `session()` | `CmsAuthenticate` | autenticado ou anônimo |
| Papel administrativo | `roles`/`role_users` | `CmsAuthenticate` | entra ou não no CMS |
| Matriz por módulo | JSONB `v2_roles.permissions_v2` | `V2PermissionMiddleware` | `none`/`read`/`write` |
| Grupo restrito | `group === SUPPORT` + e-mail | `V2PermissionMiddleware`; `SidebarComposer` | Support só para o e-mail configurado |
| Restrição individual | chave `restricted` do provider | `SidebarComposer` | item de menu por e-mail (ou `super-admin`) |
| Ação | `authorizeSupportAccess()` | controllers de Support/SLE | 403 fora do e-mail |
| Escopo de dados | `RegionalOwnable` | `RegionalScopeService` | só registros da própria unidade |
| API interna | `scope`/`abilities`/`consumer`/`allowed_ips` | `InternalApiAuth` | operação e origem |
| API regional | token `asc_` por unidade | controllers de API | unidade dos dados |
| UI | `v2_can()`/`@v2can` | Blades | esconder botões (não proteger rota) |

### 2.3 Papéis Sentinel e perfis V2

| Objeto | Tabela | Semeadura | Quem cria | Slug/valores |
|---|---|---|---|---|
| Papel Sentinel `admin` | `roles` + `role_users` | `DatabaseSeeder` cria e associa ao usuário de suporte | Gestão de Permissões cria para novos usuários | `admin` (`database/seeds/DatabaseSeeder.php:28-39`; `GestaoPermissoesPermissionsController.php:367-400`) |
| Papel Sentinel `super-admin` | `roles` | **não é semeado** | só inserção manual fora da aplicação | `super-admin`; a UI bloqueia criar/renomear (`GestaoPermissoesRolesController.php:100-106,188-194`) |
| Perfil V2 `regional` | `v2_roles` | migration idempotente | migration | `regional`; `{"Noticias":"write","Associados":"write"}` (`2026_09_14_000002_seed_v2_regional_role.php:21-46`) |
| Demais perfis V2 | `v2_roles` | nenhum | Gestão de Permissões | `permissions_v2` JSONB por módulo (`V2/Core/Models/V2Role.php:44-56`) |

O usuário de suporte nasce **sem** `v2_role_id` de propósito
(`database/seeds/DatabaseSeeder.php:42-50`): o poder dele vem do e-mail, não da
matriz. Um `super-admin` inserido manualmente também não precisa de `v2_role_id`.

### 2.4 Slugs por módulo (as 20 chaves reais)

A chave de permissão é o **nome do diretório** do módulo, não o título nem a
rota (`V2/Core/Providers/V2ServiceProvider.php:103-110`). Slugs oferecidos pela
UI de perfis:

| # | Chave (slug) | Título | Grupo | `active_pattern` | Cobre as rotas CMS? |
|---|---|---|---|---|---|
| 1 | `Homepage` | Homepage | `Layouts` | `cms/homepage*` | sim (3) |
| 2 | `BasicPage` | Páginas do Portal | `Layouts` | `cms/paginas-fixas*` | sim (15) |
| 3 | `Noticias` | Notícias | `Publicacoes` | `cms/noticias*` | sim (21) |
| 4 | `Agenda` | Agenda | `Publicacoes` | `cms/agenda*` | sim (6) |
| 5 | `TrabalheConosco` | Trabalhe Conosco | `Publicacoes` | `cms/institucional/trabalhe-conosco*` | sim (10) |
| 6 | `Associados` | Associados Regionais | `Conexoes` | `cms/associados*` | sim (13) |
| 7 | `ParceirosInstitucionais` | Parceiros Institucionais | `Conexoes` | `cms/parceiros-institucionais*` | sim (9) |
| 8 | `UnidadeRegional` | Unidades Regionais | `Conexoes` | `cms/unidades-regionais*` | sim (16) |
| 9 | `AssocieSe` | Associe-se | `Institucional` | `cms/institucional/associe-se*` | sim (13) |
| 10 | `FaleConosco` | Fale Conosco | `Institucional` | `cms/fale-conosco*` | sim (4) |
| 11 | `Historia` | História | `Institucional` | `cms/historia*` | sim (11) |
| 12 | `SobreNos` | Sobre Nós | `Institucional` | `cms/institucional/sobre-nos*` | sim (11) |
| 13 | `CertificadoDigital` | Certificados Digitais | `Infraestrutura` | `cms/certificado-digital*` | sim (7) |
| 14 | `FeedRss` | Feed RSS | `Infraestrutura` | `cms/v2/feed-rss*` | sim (3) |
| 15 | `GestaoPermissoes` | Gestão de Permissões | `Infraestrutura` | `cms/permissoes/permissions*` + `cms/permissoes/roles*` | sim (11) |
| 16 | `MailAudit` | Auditoria de E-mails | `Infraestrutura` | `cms/mail-audit*` | sim (2) |
| 17 | `NoticiasImport` | Importação de Notícias | `Support` | `cms/importacao-noticias*` | sim (2) |
| 18 | `StructuralLandingEditor` | Editor Estrutural | `Support` | `cms/infra/structural-editor/*` | sim (17) |
| 19 | `SupportConfigs` | Suporte Técnico | `Support` | `cms/infra/support-configs*\|cms/infra/user-configs*` | **não** (11) |
| 20 | `ExemploAPI` | [DEV] Exemplo API | `_Exemplos` | `cms/exemplo-api*` | sim (2) |

Fonte das chaves/patterns: os 20 `ModuleProvider.php` em `V2/Modules/*/*/`;
contagem de endpoints por unidade em `_indices/rotas.csv`.

Chaves **usadas em Blade mas ausentes da UI de perfis**:

| Chave | Onde é usada | Por que não aparece na UI | Efeito |
|---|---|---|---|
| `LayoutBase` | 8 checagens `v2_can('LayoutBase','write')` no SLE | não é nome de módulo; `v2.modules.configs` não tem essa chave | Só suporte/`super-admin` (bypass) veem os botões; um perfil comum com `StructuralLandingEditor=write` não vê, e o grupo Support o bloqueia de qualquer forma. |
| `SobreNos` | `Views/cms/editor.blade.php:9` | é chave real, mas o módulo é do grupo `Institucional` | funciona para leitura/escrita no editor. |
| `MailAudit` (`read`) | views do módulo | é chave real | botão de detalhe exige apenas `read`. |

Evidência: `V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/layoutbase/links-header.blade.php:32,41`;
`V2/Modules/Infraestrutura/GestaoPermissoes/Controllers/GestaoPermissoesRolesController.php:71-73`.

### 2.5 Bypass: quem ignora o quê

| Bypass | Ignora matriz V2 | Ignora grupo Support | Ignora escopo regional | Ignora `restricted` do menu | Evidência |
|---|---|---|---|---|---|
| `email === v2.support.email` | sim | **não** (é a única identidade que passa) | sim | sim | `V2/Core/Middleware/V2PermissionMiddleware.php:70`; `V2/Core/Services/RegionalScopeService.php:169-177` |
| `inRole('super-admin')` | sim | **sim, é bloqueado antes** | sim | sim (no composer) | `V2/Core/Middleware/V2PermissionMiddleware.php:51-70`; `V2/Core/ViewComposers/SidebarComposer.php:51-66` |
| `CmsAiToken` | não; impersona um e-mail | depende do e-mail | depende do e-mail | depende do e-mail | `01_arquitetura/04-middlewares-seguranca.md:51-53` |
| ACCESS_KEY estática | não se aplica | não se aplica | não se aplica | não se aplica | `V2/Core/Middleware/InternalApiAuth.php:105-110` |
| Token regional | não se aplica | não se aplica | só a própria unidade | não se aplica | `05_modulos/Conexoes/UnidadeRegional/api.md:25-31` |

Consequência prática: **`super-admin` não é onipotente**. Se o e-mail não for o
de `config('v2.support.email')`, ele recebe `302 dashboard` com
`Acesso negado. Esta área é restrita ao suporte técnico autorizado.` em qualquer
rota cujo `group` seja `SUPPORT` e cujo pattern case.

### 2.6 Tenancy regional

| Peça | Regra | Evidência |
|---|---|---|
| Identificação | operador regional = `users.unidade_regional_id` não vazio, **exceto** suporte e `super-admin` | `V2/Core/Services/RegionalScopeService.php:38-60` |
| Escopo de query | só em models que implementam `RegionalOwnable`; aplica `byUnidadeRegional($id)` | `V2/Core/Services/RegionalScopeService.php:78-102` |
| Models escopáveis | `News`, `AssociadoInstituicao` (2) | `V2/Modules/Publicacoes/Noticias/Models/News.php:465`; `V2/Modules/Conexoes/Associados/Models/AssociadoInstituicao.php:104` |
| Autorização de registro | `owns()` compara a unidade; `authorize()` aborta 403 `Acesso restrito aos dados da sua Unidade Regional.` | `V2/Core/Services/RegionalScopeService.php:113-141` |
| Rotas nacional-only | `RegionalRestrictMiddleware` bloqueia 12 rotas de Notícias e 3 de Associados | `V2/Modules/Publicacoes/Noticias/Routes/cms.php:52-54`; `V2/Modules/Conexoes/Associados/Routes/cms.php:13-23,70-74` |
| Categoria forçada | `regional-<sigla>` derivada de `acronym`; `forced_news_category_slug` só para a categoria-mãe | `V2/Core/Services/RegionalIdentity/RegionalIdentityService.php:22-61`; `config/v2.php:52-55` |
| Leitura pública | não há escopo regional no portal; a regional é um filtro de UI | `05_modulos/Conexoes/Associados/portal.md:26-32` |
| API regional | token dá acesso à unidade do token, sem escopos e sem RBAC | `V2/Modules/Conexoes/UnidadeRegional/Routes/api.php:3-31` |

### 2.7 Matriz de rotas sem `active_pattern` (fail-open)

Contagem derivada de `_indices/rotas.csv` + route files: **40 endpoints de
aplicação** em grupos cujo path não casa com nenhum pattern de módulo. Destes,
33 têm `CmsAuthenticate` e 7 não têm autenticação. Somando as **5 rotas públicas
de autenticação** (`/login`, GET/POST `/cms/login`, GET/POST `/cms/logout`), são
**45 rotas** sem `active_pattern`; as 5 são a própria entrada e não contam como
fail-open.

| Grupo de rotas | Endpoints | Auth | RBAC V2 | Defesa efetiva | Observação | Evidência |
|---|---|---|---|---|---|---|
| `cms` (dashboard, redirect, search, conexao-tags) | 7 | `CmsAuthenticate` | **fail-open** | nenhuma além de “ter papel” | inclui `/cms` e `/cms/search`; qualquer usuário com papel entra | `V2/Core/Routes/cms.php:22-33`; `V2/Core/Routes/cms.php:14-19` (login/logout, 5 endpoints sem RBAC) |
| `cms/infra/support-configs*` + `cms/infra/user-configs*` | 11 | `CmsAuthenticate` | **fail-open** (pipe literal) | `authorizeSupportAccess()` em 10; **reset sem guarda** | ver §2.8 | `V2/Modules/Support/SupportConfigs/ModuleProvider.php:87`; `V2/Modules/Support/SupportConfigs/Routes/cms.php:18-64` |
| `cms/v2/editor` (CRE) | 2 | `CmsAuthenticate` | **fail-open** | nenhuma | qualquer usuário com papel faz upload e busca | `V2/Services/ContentRichEditor/Routes/cms.php:9-13` |
| `cms/v2/landing-sections` (LSE) | 10 | `CmsAuthenticate` | **fail-open** | nenhuma | form/validate/preview/mídia abertos a qualquer papel | `V2/Services/LandingSectionsEditor/Routes/cms.php:9-24` |
| `cms/v2/gallery` | 7 | **nenhuma** | **fail-open** | somente `owner_module` no service | upload/arquivar/excluir sem sessão; subcaminhos isentos de CSRF | `V2/Services/Gallery/Routes/cms.php:5-13`; `app/Http/Middleware/VerifyCsrfToken.php:15-17` |
| `api/v2/mail` | 3 | `CmsAuthenticate` | **fail-open** | nenhuma | envio/teste/histórico para qualquer papel | `V2/Services/MailService/Routes/api.php:21-25` |
| Rotas públicas de autenticação (`/login`, `cms/login`, `cms/logout`) | 5 | pública | n/a | própria autenticação | não é fail-open; é a entrada do sistema | `V2/Core/Routes/cms.php:11-19` |
| **Total** | **45** (7+11+2+10+7+3+5) | — | — | — | 33 com sessão + 7 sem sessão + 5 públicas | — |

Rotas públicas (portal, `/busca`, `/feed.xml`, `/robots.txt`, `/sitemap.xml`,
`/openapi.json`) não entram na conta: `auth=nenhuma` é o contrato.

### 2.8 O caso `SupportConfigs` (active_pattern com `|`)

```php
// V2/Modules/Support/SupportConfigs/ModuleProvider.php:87
'active_pattern' => 'cms/infra/support-configs*|cms/infra/user-configs*',
```

`V2PermissionMiddleware` faz `$request->is($pattern)`
(`V2/Core/Middleware/V2PermissionMiddleware.php:41-46`), e `Request::is()`
delega a `Str::is()`, que trata `*` como curinga e **não** interpreta `|` como
alternância. Logo o pattern nunca casa. Efeitos:

1. O grupo `SUPPORT` não é bloqueado pelo middleware nessas rotas — a única
   barreira é `authorizeSupportAccess()` no controller
   (`SupportConfigsCmsController.php:47-54`), que exige e-mail exato.
2. `POST /cms/infra/user-configs/cms-theme/reset` está na mesma condição e chama
   `resetCmsTheme()` **sem** guarda (`UserConfigsCmsController.php:38-46`): qualquer
   usuário CMS com papel pode resetar o tema do CMS (SEC-G1-04).
3. A sidebar esconde o grupo Support para não-suporte, mas isso é UI; a rota
   continua alcançável por POST/GET direto.

### 2.9 O caso `Gallery` (rotas sem autenticação)

```php
// V2/Services/Gallery/Routes/cms.php:5-13
Route::group(['prefix' => 'cms/v2/gallery'], function () {
    Route::get('/', 'GalleryController@index')->name('v2.gallery.index');
    Route::post('/', 'GalleryController@store')->name('v2.gallery.store');
    // ... update, rotate, download, archive, destroy
});
```

O `V2ServiceProvider` registra rotas CMS de serviço com apenas `web`
(`V2/Core/Providers/V2ServiceProvider.php:209-214`), e o `VerifyCsrfToken`
isenta `cms/v2/gallery/*` (`app/Http/Middleware/VerifyCsrfToken.php:15-17`).
A exceção cobre os subcaminhos com id; o `POST /cms/v2/gallery` raiz não casa
`cms/v2/gallery/*` e continua exigindo CSRF.
Não há `CmsAuthenticate` nem `V2PermissionMiddleware` no grupo. A autorização
real é a comparação de `owner_module` no `GalleryService`
(`V2/Services/Gallery/Controllers/GalleryController.php:146,194,224`), que é um
escopo de visibilidade, não autenticação. Consequência: `GET /cms/v2/gallery`
lista mídia e `GET /{id}/download` baixa arquivo sem sessão.

## 3. Particularidades

### 3.1 Contradições medidas

| Contradição | Fato A | Fato B | Consequência |
|---|---|---|---|
| Suporte × `super-admin` | `V2PermissionMiddleware` libera `super-admin` para “módulos normais” (`:70`) | O bloqueio do grupo `SUPPORT` ocorre **antes** (`:51-68`) e não isenta `super-admin` | `super-admin` sem o e-mail de suporte não acessa Support/SLE/NoticiasImport. |
| `position` do menu | 19 providers declaram `position` | `SidebarComposer` ordena por `title` (`:85-91`) | A posição é decorativa; a ordem é alfabética. |
| `active_pattern` de `FeedRss` | pattern `cms/v2/feed-rss*` | rota real `cms/v2/feed-rss` (confere) | ok; mas a autorização efetiva do controller é `isSupportUser()`, não RBAC. |
| `active_pattern` de `GestaoPermissoes` | 2 patterns explícitos | todas as 11 rotas estão sob `cms/permissoes/permissions*` ou `roles*` | cobre 100%; não há fail-open aqui. |
| `permissions_v2` por chave | UI lista os 20 módulos | `LayoutBase` é usado em Blade e não é módulo | perfil comum nunca vê os botões do SLE. |
| `role_padrao` nos índices | `_indices/permissoes.csv` traz `none` para quase tudo | o seed regional traz `write` para 2 módulos | a coluna é descritiva, não um default aplicado em runtime. |
| Logout | `GET /cms/logout` desloga | não há CSRF em GET | um `img src="/cms/logout"` de terceiros derruba a sessão. |

### 3.2 Flags de módulo × permissões

A flag (`v2_module_flags`) é avaliada **antes** do RBAC, no grupo de rotas CMS
(`module.active:<Nome>`), e não no web (`V2/Core/Providers/V2ServiceProvider.php:303-307`).
Consequências:

- módulo desativado → 404/redirect mesmo para o suporte (a flag não tem bypass);
- módulo ativo + perfil `none` → 302/403 do RBAC;
- módulo ativo + rota sem pattern → passa, mesmo com perfil `none`.
- `ModuleFlagService::isActive` considera **ativo quando não há linha**
  (`V2/Core/Services/ModuleFlagService.php:91-94`), então só as 8 chaves semeadas
  podem desligar algo: `noticias`, `licitacoes`, `diario`, `homepage`,
  `layoutbase`, `galeria`, `supportconfigs`, `trabalheconosco`
  (`V2/Modules/Support/SupportConfigs/Migrations/2026_06_30_000002_create_v2_module_flags_table.php:61-69`;
  `V2/Modules/Publicacoes/TrabalheConosco/Migrations/2026_09_03_000001_disable_trabalhe_conosco_module_flag.php:23-47`).

### 3.3 O que a UI esconde e o que a rota permite

| Ação | UI | Rota | Divergência |
|---|---|---|---|
| Criar/editar perfil | só `super-admin` vê o botão | controllers exigem `super-admin` | consistente |
| Resetar tema CMS | botão no SLE (suporte) | `POST` sem guarda de e-mail | **inconsistente** (SEC-G1-04) |
| Upload na Galeria | qualquer picker no CMS | rota sem auth | **inconsistente** (SEC-G1-01) |
| Excluir tag | botão na tela de tags | `DELETE` sem checagem de uso | consistente com o RBAC do Core (fail-open) |
| Editar notícia de outra regional | some da lista | `edit/update` chamam `RegionalScopeService::authorize` | consistente |

### 3.4 Anti-uso

- **Não** use `v2_can()` como proteção de rota: é só renderização; a rota
  continua acessível se o pattern não casar.
- **Não** assuma que `super-admin` acessa Support: a ordem dos `if` nega.
- **Não** confie em `active_pattern` com `|`: use array de patterns.
- **Não** proteja a Galeria só por `owner_module`: ela não é autenticação.
- **Não** espere `403` em módulo desativado: o prefixo `cms/<módulo>` não é
  exatamente `cms`, então o middleware aborta `404 Modulo desativado.`.
- **Não** aplique escopo regional a model que não implementa `RegionalOwnable`:
  o `RegionalScopeService` devolve a query intacta.

## 4. Integrabilidade

### 4.1 Receita: criar um perfil que só lê Notícias

```php
// via PermissionService (mesma rota da UI)
app(\V2\Core\Services\PermissionService::class)->createRole([
    'name' => 'Leitor de Notícias',
    'slug' => 'leitor-noticias',
    'permissions' => ['Noticias' => 'read'], // demais chaves ausentes = none
]);
```

Efeito: a sidebar mostra apenas `Noticias`; `GET /cms/noticias` passa;
`POST /cms/noticias/novo` responde 403/302; `/cms` e `/cms/search` continuam
acessíveis (fail-open do Core).

### 4.2 Receita: operador regional

```text
1. INSERT em v2_roles (slug 'regional') — a migration já garante.
2. UPDATE users SET v2_role_id = <id do perfil>, unidade_regional_id = <uuid>.
3. O usuário precisa de um papel Sentinel (ex.: 'admin') em role_users.
4. Noticias/Associados = write; o resto = none; rotas nacional-only = 403.
```

### 4.3 Receita: chave interna com escopo

```bash
php artisan v2:noticias-import-key --consumer=COLETOR-SP --abilities=noticias:import --ttl-days=90
curl -X POST https://host/api/v2/noticiasimport/run \
  -H "X-Access-Key: <segredo>" -H "Content-Type: application/json" \
  -d '{"path":"/lotes/2026-10","dry_run":true}'
```

## 5. Lacunas

- `LG-PERM-01` — não há teste automatizado que prove a matriz 20×3; a
  verificação é manual e por leitura de código.
- `LG-PERM-02` — `v2_roles.permissions_v2` não tem schema/validação de chaves:
  uma chave desconhecida é persistida e ignorada; não há tela para
  `LayoutBase`.
- `LG-PERM-03` — o papel Sentinel `super-admin` não é semeado nem documentado
  fora do código; não há fluxo de criação legítimo.
- `LG-PERM-04` — a contagem de 40 rotas fail-open de aplicação (45 com as públicas de login) foi derivada de route files +
  patterns; não há teste de integração que a fixe.
- `LG-PERM-05` — a API regional não tem escopos; “acesso total à unidade” é o
  único nível, e o TTL efetivo é 365 por fallback de config ausente.

## 6. Evidências

- `_indices/permissoes.csv` (67 linhas), `_indices/rotas.csv` (301 linhas),
  `_indices/tabelas.csv` (9 tabelas de `CORE-AUTH`).
- 20 `ModuleProvider.php` em `V2/Modules/`; `V2/Core/Providers/V2ServiceProvider.php`;
  `V2/Core/Middleware/{CmsAuthenticate,V2PermissionMiddleware,ModuleAccessMiddleware,RegionalRestrictMiddleware,InternalApiAuth}.php`;
  `V2/Core/Services/{PermissionService,ModuleFlagService,RegionalScopeService}.php`;
  `V2/Core/ViewComposers/SidebarComposer.php`; `config/v2.php`;
  `app/Http/Kernel.php`; `app/Http/Middleware/VerifyCsrfToken.php`.
- Docs de unidade: `01_arquitetura/03-auth-rbac-sentinel.md`,
  `01_arquitetura/04-middlewares-seguranca.md`,
  `01_arquitetura/06-tenancy-regional.md`,
  `05_modulos/Infraestrutura/GestaoPermissoes/spec.md`,
  `05_modulos/Support/SupportConfigs/{spec,rotas}.md`,
  `03_servicos-ui/gallery/spec.md`.
- Snapshot: 2026-10-21; `git -C sucesu-cms status --porcelain V2` vazio.
