KB-PLUGIN-034 + nota menu_toadd no 028 (itemtype de config de plugin)

- KB-PLUGIN-034: cifrar credenciais externas em itemtype via GLPIKey
  (padrão prepareInput + senha-vazia-mantem-atual). Validado no plugin sync.
- KB-PLUGIN-028: seção sobre o mecanismo menu_toadd (seções válidas + estrutura).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Rodolpho Lopes 2026-06-16 19:56:59 +00:00
parent 79c19c7425
commit 28dcfbca94
3 changed files with 141 additions and 0 deletions

View file

@ -608,6 +608,25 @@
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-033-glpi11-plugin-console-commands.md",
"summary": "id: KB-PLUGIN-033"
},
{
"id": "KB-PLUGIN-034",
"title": "\"GLPI 11 — Cifrar credenciais de sistemas externos em itemtype de plugin (GLPIKey)\"",
"domain": "plugin-dev",
"tags": [
"glpi11",
"plugin",
"security",
"credentials",
"glpikey",
"encryption",
"commondbtm",
"itemtype"
],
"status": "active",
"severity": "high",
"path": "records/plugin-dev/KB-PLUGIN-034-glpi11-encrypt-external-credentials-itemtype.md",
"summary": "id: KB-PLUGIN-034"
}
]
}

View file

@ -162,6 +162,30 @@ function plugin_meuplugin_install(): bool
}
```
## Mecanismo do menu (menu_toadd) — como o itemtype entra na sidebar
Registrar o right faz o menu *poder* aparecer; para ele *de fato* aparecer, o
itemtype precisa ser injetado numa seção do menu via o hook `menu_toadd` (lido em
`Html::generateMenuSession()` / `Html.php`):
```php
// setup.php, dentro de plugin_init_<key>()
Plugin::registerClass('GlpiPlugin\\Meuplugin\\MeuItemtype');
$PLUGIN_HOOKS['menu_toadd']['meuplugin'] = [
'config' => 'GlpiPlugin\\Meuplugin\\MeuItemtype',
// ou vários: 'config' => ['Classe1', 'Classe2']
];
```
Seções válidas (chave do array): **`assets`, `helpdesk`, `management`, `tools`,
`admin`, `config`** (Setup), `plugins`. Itemtype administrativo de configuração
`config`; ver taxonomia em [KB-PLUGIN-029].
O `getMenuContent()` herdado de `CommonGLPI`/`CommonDBTM` já monta a entrada
(título, ícone, links search/add) a partir de `getTypeName()`, `getIcon()`,
`getSearchURL()` e `canCreate()`**gated por `canView()`** (que depende do right).
Não precisa sobrescrever `getMenuContent()` para um CRUD simples.
## Gotcha — Instalação via CLI
`bin/console glpi:plugin:install -f <plugin>` roda **sem sessão GLPI**, então `$_SESSION['glpiactiveprofile']['id']` é `null` e `createFirstAccess()` não é executado. Resultado: rights são registrados na tabela mas com valor `0` (sem permissão real).

View file

@ -0,0 +1,98 @@
---
id: KB-PLUGIN-034
title: "GLPI 11 — Cifrar credenciais de sistemas externos em itemtype de plugin (GLPIKey)"
domain: plugin-dev
tags:
- glpi11
- plugin
- security
- credentials
- glpikey
- encryption
- commondbtm
- itemtype
status: active
severity: high
created_at: 2026-06-16
updated_at: 2026-06-16
applies_to:
- plugins GLPI 11.x que guardam senha/token de sistema externo
- plugin Sync (conexão com o PROD legado)
related_records:
- KB-PLUGIN-028
- KB-PLUGIN-032
---
# GLPI 11 — Cifrar credenciais externas em itemtype de plugin
## Contexto
Plugins que se conectam a sistemas externos (banco do PROD, API de terceiros,
LDAP secundário) precisam guardar uma senha/token. Guardar em texto puro na
tabela é inaceitável. O GLPI já oferece a mesma cifra que usa para senhas de
e-mail/config: a classe `\GLPIKey` (chave em `GLPI_CONFIG_DIR/glpicrypt.key`).
## API
```php
$key = new \GLPIKey();
$cipher = $key->encrypt($plaintext); // string -> string cifrada
$plain = $key->decrypt($cipher); // string|null -> string|null
```
## Padrão no itemtype (CommonDBTM)
Cifrar no `prepareInputForAdd/Update`, decifrar sob demanda. **Senha vazia no
update = manter a atual** (não sobrescrever com vazio):
```php
public function prepareInputForAdd($input) { return $this->encryptPasswordInput($input); }
public function prepareInputForUpdate($input) { return $this->encryptPasswordInput($input); }
private function encryptPasswordInput(array $input): array
{
if (!array_key_exists('db_password', $input)) {
return $input;
}
if ($input['db_password'] === '' || $input['db_password'] === null) {
unset($input['db_password']); // mantém a senha atual
return $input;
}
$input['db_password'] = (new \GLPIKey())->encrypt((string) $input['db_password']);
return $input;
}
public function getDecryptedPassword(): string
{
$enc = (string) ($this->fields['db_password'] ?? '');
return $enc === '' ? '' : (string) (new \GLPIKey())->decrypt($enc);
}
```
No form Twig, o campo de senha deve renderizar **sempre vazio** (nunca ecoar a
cifra) e avisar que vazio mantém a atual:
```twig
{{ fields.passwordField('db_password', '', __('Senha', 'sync'),
{'helper': __('Deixe em branco para manter a senha atual', 'sync')}) }}
```
## Gotchas
- A coluna do banco guarda a **cifra** (use `TEXT`/`VARCHAR` amplo; a cifra é
maior que o texto puro).
- `decrypt()` pode retornar `null` — sempre fazer cast/checagem.
- Não logar nem expor a cifra em search options visíveis ao usuário comum.
- Cuidado se a `glpicrypt.key` mudar entre ambientes: cifra feita num GLPI não
decifra noutro com chave diferente (relevante em migração/cutover).
## Validação
Comprovado no plugin `sync` (2026-06-16): teste E2E criou Source com senha,
confirmou que `db_password` em repouso ≠ texto puro, que `getDecryptedPassword()`
devolve o original, e que update com senha vazia preserva a cifra anterior.
## Classificação
- Tipo: padrão de segurança reutilizável.
- Reutilização: qualquer plugin que persista credencial de sistema externo.