feat: resolução de usuários self-healing + syncUsers upsert + fallback logado (1.6.0)
Causa raiz do incidente "autor = admin": cache de usuários vazio no prod. - cache miss -> lookup ao vivo por e-mail no Redmine + upsert no cache - syncUsers nunca mais esvazia a tabela (upsert por redmine_id) - fallback para admin deixa rastro no log - config exibe saúde do cache (contagem + alerta se vazio) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1b790354a3
commit
b8f89cda8d
6 changed files with 92 additions and 11 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
|
@ -4,6 +4,22 @@ Todas as mudanças relevantes deste plugin são documentadas aqui.
|
|||
O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/1.0.0/)
|
||||
e o versionamento segue [SemVer](https://semver.org/lang/pt-BR/).
|
||||
|
||||
## [1.6.0]
|
||||
|
||||
### Adicionado
|
||||
- **Resolução de usuários self-healing**: se o e-mail não estiver no cache local,
|
||||
o plugin busca o usuário **ao vivo** no Redmine e atualiza o cache — cache vazio
|
||||
ou desatualizado não gera mais lançamentos como admin.
|
||||
- A seção "Mapeamento de usuários" da config mostra a **quantidade de usuários em
|
||||
cache**, com alerta destacado quando o cache está vazio.
|
||||
|
||||
### Corrigido
|
||||
- `syncUsers` fazia delete-all antes de inserir: uma falha no meio deixava o cache
|
||||
**vazio** (causa raiz de lançamentos saindo como admin). Agora é **upsert** por
|
||||
usuário + remoção só dos ausentes — o cache nunca fica vazio por falha.
|
||||
- Fallback para admin agora é **logado** ("Sem par Redmine para o usuário GLPI #N"),
|
||||
em vez de silencioso.
|
||||
|
||||
## [1.5.7]
|
||||
|
||||
### Corrigido
|
||||
|
|
|
|||
3
hook.php
3
hook.php
|
|
@ -251,6 +251,9 @@ function _plugin_redmine_impersonation_login($glpi_users_id, $redmineProjectId)
|
|||
}
|
||||
$ru = PluginRedmineConfig::resolveRedmineUser($glpi_users_id);
|
||||
if (!$ru || empty($ru['login'])) {
|
||||
// Sem par no Redmine: o lançamento sairá como admin. Loga para não ser silencioso.
|
||||
Toolbox::logInFile('redmine', "Sem par Redmine para o usuário GLPI #{$glpi_users_id} ("
|
||||
. getUserName($glpi_users_id) . ") — lançando como admin. Verifique o e-mail do usuário e o cache (Sincronizar Metadados).\n");
|
||||
return null;
|
||||
}
|
||||
$roleId = PluginRedmineConfig::getDefaultRoleId();
|
||||
|
|
|
|||
|
|
@ -77,13 +77,29 @@ class PluginRedmineConfig extends CommonDBTM {
|
|||
}
|
||||
}
|
||||
|
||||
// 2) E-mail auto-match
|
||||
// 2) E-mail auto-match (cache local)
|
||||
$email = \UserEmail::getDefaultForUser($glpi_users_id);
|
||||
if (!empty($email)) {
|
||||
$ru = $DB->request(['FROM' => 'glpi_plugin_redmine_users', 'WHERE' => ['mail' => $email]]);
|
||||
if (count($ru) > 0) {
|
||||
return $ru->current();
|
||||
}
|
||||
|
||||
// 3) Self-healing: cache pode estar vazio/desatualizado — busca ao
|
||||
// vivo no Redmine e grava no cache para as próximas resoluções.
|
||||
$live = PluginRedmineRedmineapi::findUserByEmail($email);
|
||||
if ($live && !empty($live['login'])) {
|
||||
$row = [
|
||||
'redmine_id' => (int) $live['id'],
|
||||
'login' => $live['login'],
|
||||
'mail' => $live['mail'] ?? $email,
|
||||
'name' => trim(($live['firstname'] ?? '') . ' ' . ($live['lastname'] ?? '')),
|
||||
];
|
||||
$DB->delete('glpi_plugin_redmine_users', ['redmine_id' => $row['redmine_id']]);
|
||||
$DB->insert('glpi_plugin_redmine_users', $row);
|
||||
Toolbox::logInFile('redmine', "Cache de usuários atualizado ao vivo para '{$email}' -> {$row['login']}.\n");
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -200,6 +216,7 @@ class PluginRedmineConfig extends CommonDBTM {
|
|||
'status_map' => is_array($status_map) ? $status_map : [],
|
||||
'glpi_ticket_statuses' => \Ticket::getAllStatusArray(),
|
||||
'usermap' => $usermap,
|
||||
'user_cache_count' => count($redmine_user_options),
|
||||
]);
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -393,27 +393,65 @@ class PluginRedmineRedmineapi {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single Redmine user by e-mail, live (admin only).
|
||||
* Used as self-healing fallback when the local cache misses.
|
||||
* @param string $email
|
||||
* @return array|null the Redmine user, or null
|
||||
*/
|
||||
public static function findUserByEmail($email) {
|
||||
if (empty($email)) {
|
||||
return null;
|
||||
}
|
||||
$result = self::get("/users.json?name=" . urlencode($email) . "&limit=10");
|
||||
foreach (($result['users'] ?? []) as $u) {
|
||||
if (strcasecmp($u['mail'] ?? '', $email) === 0) {
|
||||
return $u;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the local cache of Redmine users (glpi_plugin_redmine_users).
|
||||
* Upsert per user — never truncates the table, so a mid-sync failure
|
||||
* can't leave the cache empty (which silently broke impersonation).
|
||||
* @return int number of users cached, or -1 on API failure
|
||||
*/
|
||||
public static function syncUsers() {
|
||||
global $DB;
|
||||
$users = self::getUsers();
|
||||
if (empty($users)) {
|
||||
// Distinguish "no access" from "zero users": getUsers returns [] for both,
|
||||
// but a working admin key always sees at least itself. Treat [] as failure-safe no-op.
|
||||
// A working admin key always sees at least itself: [] = API/permission
|
||||
// failure. Keep the existing cache untouched.
|
||||
Toolbox::logInFile('redmine', "syncUsers: API não retornou usuários (chave sem admin? indisponível?). Cache mantido.\n");
|
||||
return -1;
|
||||
}
|
||||
$DB->delete('glpi_plugin_redmine_users', [1]);
|
||||
|
||||
$seen = [];
|
||||
foreach ($users as $u) {
|
||||
$DB->insert('glpi_plugin_redmine_users', [
|
||||
'redmine_id' => (int) $u['id'],
|
||||
$rid = (int) $u['id'];
|
||||
$seen[] = $rid;
|
||||
$row = [
|
||||
'login' => $u['login'] ?? null,
|
||||
'mail' => $u['mail'] ?? null,
|
||||
'name' => trim(($u['firstname'] ?? '') . ' ' . ($u['lastname'] ?? '')),
|
||||
]);
|
||||
];
|
||||
$exists = $DB->request(['FROM' => 'glpi_plugin_redmine_users', 'WHERE' => ['redmine_id' => $rid]]);
|
||||
if (count($exists) > 0) {
|
||||
$DB->update('glpi_plugin_redmine_users', $row, ['redmine_id' => $rid]);
|
||||
} else {
|
||||
$DB->insert('glpi_plugin_redmine_users', $row + ['redmine_id' => $rid]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove somente quem não existe mais no Redmine.
|
||||
foreach ($DB->request(['FROM' => 'glpi_plugin_redmine_users']) as $row) {
|
||||
if (!in_array((int) $row['redmine_id'], $seen, true)) {
|
||||
$DB->delete('glpi_plugin_redmine_users', ['id' => $row['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
return count($users);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
define('PLUGIN_REDMINE_VERSION', '1.5.7');
|
||||
define('PLUGIN_REDMINE_VERSION', '1.6.0');
|
||||
|
||||
/**
|
||||
* Load the Mindplace License class if the autoloader has not run yet.
|
||||
|
|
|
|||
|
|
@ -91,8 +91,15 @@
|
|||
<h4 class="card-title mb-0"><i class="ti ti-user-cog me-1"></i>{{ __('Mapeamento de usuários (GLPI → Redmine)', 'redmine') }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if user_cache_count|default(0) == 0 %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="ti ti-alert-triangle me-1"></i>
|
||||
{{ __('Cache de usuários Redmine VAZIO — os lançamentos sairão como admin. Clique em "Sincronizar Metadados".', 'redmine') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="alert alert-info">
|
||||
{{ __('Usuários com o mesmo e-mail nos dois sistemas são associados automaticamente. Use a tabela abaixo apenas para exceções (e-mails diferentes).', 'redmine') }}
|
||||
<br><small>{{ __('Usuários Redmine em cache:', 'redmine') }} <strong>{{ user_cache_count|default(0) }}</strong></small>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm align-middle">
|
||||
|
|
|
|||
Loading…
Reference in a new issue