diff --git a/MEMORY.md b/MEMORY.md index 731fa6b..ee019f8 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -2,6 +2,18 @@ ## Ultima modifica: 31/08/2026 +## 31/08/2026 — Audit statico: fix 7 bug (solo UI/codice, nessuna modifica DB) +Audit statico (IA) + confronto contro sorgente C++ del backend UrBackup e core GLPI 11. Nessun bug di classe critico (crash/XSS/SQLi), ma diversi output errati corretti. +1. **Bug 1 — Livello log sempre vuoto** (`AssetTab.php:728`): l'API `livelog` restituisce `loglevel` (int 1=ERROR,2=WARNING,3=INFO,4=DEBUG), il codice leggeva `level`/`severity` (mai presenti). Aggiunto `formatLogLevel()` che mappa `loglevel`→stringa. +2. **Bug 2 — "No client logs available" fuorviante** (`AssetTab.php:733`): se `client_found=false` il client non è stato trovato per nome e i log NON vengono mai estratti. Ora mostra avviso "Client not found on UrBackup server. Check that the asset name matches the client name." Il matching resta per NOME client (confermato dall'utente come corretto). +3. **Bug 3 — JOIN IP invertito** (`Server.php` `batchLoadIps` ~1516 e `getAssetIp` ~1614): JOIN glpi_ipaddresses↔glpi_networknames generava `nn.items_id = ipa.id`; corretto a `nn.id = ipa.items_id` (con `ipa.itemtype='NetworkName'`), coerente con core `glpi/src/Report.php`. Colonna IP di Linked/Missing ora corretta. +4. **Bug 4 — Chiave cache sessione non univoca** (`AssetTab.php:374`): `$cache_key` includeva solo server id+nome client; ora `urbackup_data_{serverid}_{itemtype}_{items_id}`, evita cross-contaminazione tra asset omonimi. +5. **Bug 5 — Query Missing/Unlinked non filtrate per server** (`Server.php:1047` e `:1245`): aggiunto `WHERE plugin_urbackup_servers_id = server corrente` in `showUnlinkedClientsTab()` e `showMissingClientsTab()`, così asset collegati ad altri server non interferiscono. +6. **Bug 6 — server_test.ajax.php auth + CSRF** (`front/server_test.ajax.php`): sostituito `Profile::canCurrentUser(UPDATE)` (non entity-aware) con `$server->check($id, UPDATE)` in try/catch → 403 JSON; creato `public/js/urbackup.js` (registrato via `Hooks::ADD_JAVASCRIPT` in setup.php) che legge meta `glpi:csrf_token` e invia header `X-Glpi-Csrf-Token` su ogni POST AJAX del plugin (pre-requisito del listener GLPI 11 `CheckCsrfListener`). +7. **Bug 7 — dropdown_host.ajax.php info-disclosure** (`front/dropdown_host.ajax.php`): aggiunto check `Profile::canCurrentUser(READ)`. +8. **Traduzioni**: aggiunta la nuova stringa "Client not found on UrBackup server. Check that the asset name matches the client name." a it_IT/de_DE/en_GB `.po`, ricompilati i `.mo` (msgfmt). Versione header coerente (0.7.3). Changelog README aggiornato. +- Verifica IA: `php -l` OK su tutti i file; `git diff` autorevisione OK; `Hooks::ADD_JAVASCRIPT` verificato in `src/Glpi/Plugin/Hooks.php:60`; meta `glpi:csrf_token` verificato in `templates/layout/parts/head.html.twig:68`. **Verifica UI utente OBBLIGATORIA** (vedi checklist). + ## 31/08/2026 — Fix campi API username/password non editabili in prod (solo UI) - **Sintomo utente**: installato il plugin sul server produttivo, nel form Server non si vede il campo API username e il campo API password è bloccato (asterischi fissi non editabili), anche per l'utente che ha installato il plugin. - **Causa**: divergenza nel controllo diritti. I front usavano `Profile::canCurrentUser()` (legge i diritti da `glpi_profilerights` su DB), ma `Server::showFormFields()` (form server) usava `Session::haveRight(self::$rightname, UPDATE)` per calcolare `$canEdit` (e idem `rawSearchOptions()` riga 308 e `showUnlinkedClientsTab()`/`showMissingClientsTab()` righe 1155/1222). `Session::haveRight` dipende dalla **cache dei diritti in sessione** (`$_SESSION['glpiactiveprofile']['rights']`), popolata al login. Dopo l'installazione/aggiornamento del plugin i diritti sono scritti in DB ma la **sessione corrente non viene ricostruita**, quindi `haveRight` resta false → ramo read-only: username stampato come testo (invisibile se vuoto), password `******` fissa. diff --git a/README.md b/README.md index 7111280..46d2846 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,11 @@ plugin_urbackup/ - The asset tab now shows a "This asset hosts the UrBackup server" block with links to the hosted servers (always visible, even when the asset is also a linked client) - Server save validates the host link (unknown/disabled itemtype or missing item resets the link) - Added it_IT/de_DE/en_GB translations (2 new strings) and recompiled locales +- Fixed the plugin right check on the server form and internal tabs (`Session::haveRight` → `Profile::canCurrentUser`), so API username/password become editable right after plugin installation without re-login +- Client logs: fixed the log level column (API returns `loglevel`, now mapped to ERROR/WARNING/INFO/DEBUG); show a clear warning when the client is not found by name instead of the misleading "No client logs available" +- Fixed the inverted IP lookup JOIN (`glpi_networknames` ↔ `glpi_ipaddresses`) so the IP column in Linked/Missing clients is correct +- Made the missing/unlinked clients queries server-scoped (`plugin_urbackup_servers_id`) and the API data session cache key unique per asset (itemtype+id) +- `server_test.ajax.php`: entity-aware `check()` authorization; added `public/js/urbackup.js` (registered via `ADD_JAVASCRIPT`) that sends the `X-Glpi-Csrf-Token` header on plugin AJAX POSTs; `dropdown_host.ajax.php` now requires READ right ### 0.7.2 - UrBackup on Computer is now configurable: new `enable_computer` setting on the plugin configuration page (previously always enabled, hardcoded) diff --git a/front/dropdown_host.ajax.php b/front/dropdown_host.ajax.php index 7fd62d9..a951362 100644 --- a/front/dropdown_host.ajax.php +++ b/front/dropdown_host.ajax.php @@ -11,6 +11,7 @@ declare(strict_types=1); */ use GlpiPlugin\Urbackup\Config; +use GlpiPlugin\Urbackup\Profile; if (!defined('GLPI_ROOT')) { define('GLPI_ROOT', dirname(__DIR__, 4)); @@ -22,6 +23,11 @@ Html::header_nocache(); Session::checkLoginUser(); +if (!Profile::canCurrentUser(READ)) { + http_response_code(403); + exit; +} + if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET') { http_response_code(405); exit; diff --git a/front/server_test.ajax.php b/front/server_test.ajax.php index e727dd2..642d526 100644 --- a/front/server_test.ajax.php +++ b/front/server_test.ajax.php @@ -10,7 +10,6 @@ declare(strict_types=1); * only accepts POST requests (GET requests must not trigger state changes). */ -use GlpiPlugin\Urbackup\Profile; use GlpiPlugin\Urbackup\Server; use GlpiPlugin\Urbackup\UrbackupApiClient; @@ -31,12 +30,6 @@ if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { exit; } -if (!Profile::canCurrentUser(UPDATE)) { - http_response_code(403); - echo json_encode(['success' => false, 'message' => __('No permission', 'urbackup')]); - exit; -} - $server_id = (int) ($_POST['id'] ?? 0); if ($server_id <= 0) { @@ -46,8 +39,13 @@ if ($server_id <= 0) { $server = new Server(); -if (!$server->getFromDB($server_id)) { - echo json_encode(['success' => false, 'message' => __('Server not found', 'urbackup')]); +try { + // Entity-aware authorization: checks UPDATE right AND access to the server's entity + // (returning the server on success, throwing otherwise). + $server->check($server_id, UPDATE); +} catch (Throwable $e) { + http_response_code(403); + echo json_encode(['success' => false, 'message' => $e->getMessage()]); exit; } diff --git a/locales/de_DE.mo b/locales/de_DE.mo index c61d79c..30b89fb 100644 Binary files a/locales/de_DE.mo and b/locales/de_DE.mo differ diff --git a/locales/de_DE.po b/locales/de_DE.po index 6ceba11..bf7df9b 100644 --- a/locales/de_DE.po +++ b/locales/de_DE.po @@ -106,6 +106,13 @@ msgstr "" msgid "Client not found on UrBackup server." msgstr "Client auf dem UrBackup-Server nicht gefunden." +msgid "" +"Client not found on UrBackup server. Check that the asset name matches the " +"client name." +msgstr "" +"Client auf dem UrBackup-Server nicht gefunden. Überprüfen Sie, ob der Asset-" +"Name mit dem Client-Namen übereinstimmt." + msgid "Client state" msgstr "Client-Status" diff --git a/locales/en_GB.mo b/locales/en_GB.mo index 8a269bb..6a60c44 100644 Binary files a/locales/en_GB.mo and b/locales/en_GB.mo differ diff --git a/locales/en_GB.po b/locales/en_GB.po index 6e8442e..8b0bf11 100644 --- a/locales/en_GB.po +++ b/locales/en_GB.po @@ -101,6 +101,13 @@ msgstr "" msgid "Client not found on UrBackup server." msgstr "Client not found on UrBackup server." +msgid "" +"Client not found on UrBackup server. Check that the asset name matches the " +"client name." +msgstr "" +"Client not found on UrBackup server. Check that the asset name matches the " +"client name." + msgid "Client state" msgstr "Client state" diff --git a/locales/it_IT.mo b/locales/it_IT.mo index 15c77b7..ae65a19 100644 Binary files a/locales/it_IT.mo and b/locales/it_IT.mo differ diff --git a/locales/it_IT.po b/locales/it_IT.po index 3bfe06d..26fc430 100644 --- a/locales/it_IT.po +++ b/locales/it_IT.po @@ -105,6 +105,13 @@ msgstr "" msgid "Client not found on UrBackup server." msgstr "Client non trovato sul server UrBackup." +msgid "" +"Client not found on UrBackup server. Check that the asset name matches the " +"client name." +msgstr "" +"Client non trovato sul server UrBackup. Verifica che il nome dell'asset " +"corrisponda al nome del client." + msgid "Client state" msgstr "Stato client" diff --git a/public/js/urbackup.js b/public/js/urbackup.js new file mode 100644 index 0000000..efad1e9 --- /dev/null +++ b/public/js/urbackup.js @@ -0,0 +1,31 @@ +(function ($, document) { + 'use strict'; + + function getCsrfToken() { + var meta = document.querySelector('meta[property="glpi:csrf_token"]'); + return meta ? meta.getAttribute('content') : ''; + } + + // GLPI 11 validates the CSRF token of every AJAX POST through the + // X-Glpi-Csrf-Token header (see CheckCsrfListener). Register the token + // on all plugin AJAX requests so endpoints such as server_test.ajax.php + // are accepted instead of being rejected with a 403. + $(document).on('ajaxSend', function (event, xhr, settings) { + var token = getCsrfToken(); + if (token !== '' && typeof settings !== 'undefined' && settings.type && settings.type.toUpperCase() === 'POST') { + xhr.setRequestHeader('X-Glpi-Csrf-Token', token); + } + }); + + // Also cover direct $.ajax/{$.get[Script]/-free POST} + var origAjax = $.ajax; + $.ajax = function (url, options) { + var settings = $.isPlainObject(url) ? url : $.extend({ url: url }, options || {}); + var token = getCsrfToken(); + if (settings.type && settings.type.toUpperCase() === 'POST' && token !== '') { + settings.headers = settings.headers || {}; + settings.headers['X-Glpi-Csrf-Token'] = token; + } + return origAjax.call(this, settings); + }; +})(jQuery, document); diff --git a/setup.php b/setup.php index 2790ed4..cc99d6d 100644 --- a/setup.php +++ b/setup.php @@ -93,6 +93,10 @@ function plugin_init_urbackup(): void $PLUGIN_HOOKS[Hooks::ADD_CSS]['urbackup'] = [ 'public/css/urbackup.css', ]; + + $PLUGIN_HOOKS[Hooks::ADD_JAVASCRIPT]['urbackup'] = [ + 'public/js/urbackup.js', + ]; } /** diff --git a/src/AssetTab.php b/src/AssetTab.php index 6de1df9..41da905 100644 --- a/src/AssetTab.php +++ b/src/AssetTab.php @@ -371,7 +371,7 @@ class AssetTab extends CommonDBTM $client_name = (string) ($item->fields['name'] ?? ''); $asset_ip = ServerAsset::extractAssetIp($item); - $cache_key = 'urbackup_data_' . $server->fields['id'] . '_' . $client_name; + $cache_key = 'urbackup_data_' . $server->fields['id'] . '_' . $item::class . '_' . $item->fields['id']; if (isset($_SESSION[$cache_key]) && $_SESSION[$cache_key]['time'] > time() - 30) { return $_SESSION[$cache_key]['data']; } @@ -725,14 +725,18 @@ class AssetTab extends CommonDBTM foreach ($logs as $log) { echo "