knowledge-base/records/plugin-dev/KB-PLUGIN-044-glpi11-native-visibility-in-plugin-boards.md
2026-07-13 19:07:42 -03:00

120 lines
4.7 KiB
Markdown

---
id: KB-PLUGIN-044
title: "GLPI 11 — Visibilidade nativa em listagens de plugin (tickets/projetos) e permissionamento espelhado: receita e armadilhas"
domain: plugin-dev
tags:
- glpi11
- plugin
- security
- visibility
- rights
- profile
- sqlprovider
- ticket
- project
- hardening
- mindscrum
status: active
severity: critical
created_at: 2026-07-13
updated_at: 2026-07-13
applies_to:
- GLPI 11.0.8 dev
- plugin mindscrum 0.9.2
- qualquer plugin que liste tickets/projetos fora do Search nativo
related_records:
- KB-PLUGIN-028
- KB-PLUGIN-039
- KB-PLUGIN-043
---
# GLPI 11 — Visibilidade nativa em listagens de plugin e permissionamento espelhado
## O problema (achado em auditoria do mindscrum)
Um plugin que consulta `glpi_tickets`/`glpi_projects` direto com `$DB->request()`
e filtra só por `getEntitiesRestrictCriteria()` **vaza dados**: mostra a qualquer
usuário com o right do plugin TODOS os itens da entidade — ignorando as regras
nativas ("técnico vê só os seus/do grupo", "projeto visível a gerente/equipe").
O right do plugin NÃO substitui a visibilidade por item.
## Receita — tickets (a mesma do kanban do core)
`CommonITILObject::getDataToDisplayOnKanban` faz (replicar):
```php
use Glpi\Search\Provider\SQLProvider;
$where = ['glpi_tickets.is_deleted' => 0] + getEntitiesRestrictCriteria('glpi_tickets');
$vis = SQLProvider::getDefaultWhereCriteria(\Ticket::class);
if ($vis !== []) {
$where[] = $vis;
}
$query = ['SELECT' => [/* campos PREFIXADOS glpi_tickets.x AS x */], 'FROM' => 'glpi_tickets', 'WHERE' => $where];
// ⚠️ o WHERE referencia JOINs com alias hasheado (glpi_tickets_users_<md5>...):
// SEM os joins correspondentes → "Unknown column ..._users_<hash>.users_id"
$linked = [];
$join = SQLProvider::getDefaultJoinCriteria(\Ticket::class, 'glpi_tickets', $linked);
if ($join !== []) {
$query = array_merge_recursive($query, $join);
}
// joins de atores DUPLICAM linhas → deduplicar por id ($rows[$r['id']] = $r)
```
Armadilhas: (1) `getDefaultWhereCriteria` sem `getDefaultJoinCriteria` = SQL
quebrado; (2) campos do SELECT precisam de prefixo de tabela (joins tornam `id`
ambíguo); (3) dedup por id obrigatório.
## Receita — projetos
```php
$vis = \Project::getVisibilityCriteria(); // ['LEFT JOIN'=>[], 'WHERE'=>[]]
$vis['WHERE'] += getEntitiesRestrictCriteria('glpi_projects', '', '', 'auto');
// LEFT JOIN => (seus joins) + $vis['LEFT JOIN']; WHERE => $vis['WHERE'] + (seus filtros)
// dedup por id (join com projectteams duplica)
```
## Permissionamento espelhado (rights do plugin por perfil)
Conceder o right do plugin "para todos os perfis central" (mitigação C do
KB-PLUGIN-028) dá UPDATE até a Observer/Read-Only. Correto: espelhar o nativo —
`UPDATE` do plugin somente para perfis cujo right `ticket` tem `UPDATE`:
```php
$value = READ + (($ticket_rights & UPDATE) ? UPDATE : 0);
addDefaultProfileInfos($profile_id, [RIGHT => $value], true); // drop_existing corrige base
```
Reinstalar via console aplica a correção (install idempotente).
## Guardas de item-alvo (defesa em profundidade)
Right do plugin ≠ direito no item. TODA mutação deve carregar o alvo e delegar ao
nativo: `canUpdateItem()` (criar/alterar tarefa, mover, campos), `canViewItem()`
(comentar), `Ticket::canCreate()` + `canViewItem()` do projeto (spawn). Colocar as
guardas NAS CLASSES de serviço (não só no endpoint) — valem para qualquer chamador
e são testáveis em CLI.
Teste E2E que prova: sessão com `ticket => READ` (sem READALL) deve ver só os
chamados onde é ator; sessão com rights zerados deve ter toda mutação negada.
## Right CUSTOM exige relogin de TODOS — gate por right NATIVO evita (padrão butterfly)
Incidente em produção (mindscrum 1.0.0): right custom (`plugin_mindscrum_board`)
gravado em `glpi_profilerights` no install, mas o GLPI carrega rights na sessão
**no LOGIN** — o menu apareceu SÓ para quem instalou; todos os outros usuários
(inclusive super-admins) precisariam de logout/login. Sintoma: "o plugin só
aparece para quem instalou".
Solução (1.0.1, replicando o butterfly): **gate por right NATIVO** já presente
em toda sessão — `Board::$rightname = 'ticket'` (menu/leitura = `ticket READ`;
edição = `ticket UPDATE`) + guardas por item (`canUpdateItem` etc.). Sem classe
Profile, sem `change_profile`, sem relogin. Regra prática: só crie right custom
se o plugin precisa de permissão que NENHUM right nativo representa — e aceite o
custo do relogin pós-install; para ferramentas sobre itens nativos (board de
tickets/projetos), o right nativo do item é o gate certo.
## Bônus — XSS de atributo em JS de plugin
`div.textContent = s; return div.innerHTML` escapa `& < >` mas **não aspas**
inseguro para `title="..."`/`data-x="..."`. Usar replace de `[&<>"']`.