From 5019a6fad50f08d9029aee9e9485329b1caaf08d Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 24 Oct 2026 02:58:59 -0600 Subject: [PATCH 1/3] fix: raise the PHP limits only for the import form, uploads and deletes Signed-off-by: Thomas Vincent --- CHANGELOG.md | 4 +++ includes/functions.php | 27 +++++++++++++++ slowlog.php | 5 ++- tests/Unit/RequestLimitsTest.php | 57 ++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/RequestLimitsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aae351..b1ee78e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## ChangeLog +--- 2.8 --- + +* bug: Raise the PHP execution time and memory limits only for the import form, an upload and a delete, instead of for every request to the page. Viewing a log now runs with the limits the web server is configured with; the page size is already bounded + --- 2.7 --- * security: Cast the numeric fields parsed from a slow log header (`Query_time`, `Lock_time`, `Rows_*`, `Bytes_sent`, `Thread_id`) to plain numbers before they are placed in the bulk `plugin_slowlog_details` INSERT; a non-numeric value is stored as 0 diff --git a/includes/functions.php b/includes/functions.php index da5374f..8f50ecf 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -258,6 +258,33 @@ function slowlog_log_number(string $value): string { return preg_match('/^[0-9]+(?:\.[0-9]+)?$/', $value) ? $value : '0'; } +/** + * Raises PHP's execution time and memory limits for the requests that need them, and only those: + * the import form and upload (a large file is staged and handed to the background worker) and the + * delete of a large log. Every other request keeps the limits the web server was configured with. + * + * Called from slowlog.php once the request's action is known. + * + * @param string $action The request's action. + * + * @return bool True when a limit was raised. + */ +function slowlog_apply_request_limits(string $action): bool { + $raised = false; + + if (in_array($action, ['edit', 'save', 'import', 'actions'], true)) { + ini_set('max_execution_time', '0'); + + $raised = true; + } + + if (in_array($action, ['edit', 'save', 'import'], true)) { + ini_set('memory_limit', '-1'); + } + + return $raised; +} + // the largest page the Details view will build, matching the biggest rows-per-page choice const SLOWLOG_MAX_PAGE_ROWS = 1000; diff --git a/slowlog.php b/slowlog.php index 7ece7e0..cf6cec2 100644 --- a/slowlog.php +++ b/slowlog.php @@ -29,15 +29,14 @@ require_once('./lib/timespan_settings.php'); require_once('./plugins/slowlog/includes/functions.php'); -ini_set('max_execution_time', '0'); -ini_set('memory_limit', '-1'); - $actions = [ 1 => __('Delete', 'slowlog') ]; set_default_action('select'); +slowlog_apply_request_limits(get_request_var('action')); + switch (get_request_var('action')) { case 'save': case 'import': diff --git a/tests/Unit/RequestLimitsTest.php b/tests/Unit/RequestLimitsTest.php new file mode 100644 index 0000000..cec2431 --- /dev/null +++ b/tests/Unit/RequestLimitsTest.php @@ -0,0 +1,57 @@ +toBe($raised); + expect([ini_get('max_execution_time'), ini_get('memory_limit')])->toBe(array($time, $memory)); + + ini_set('memory_limit', '-1'); +})->with(array( + 'import form' => array('edit', true, '0', '-1'), + 'upload' => array('save', true, '0', '-1'), + 'import' => array('import', true, '0', '-1'), + 'delete' => array('actions', true, '0', '256M'), + 'list' => array('select', false, '30', '256M'), + 'details' => array('details', false, '30', '256M'), + 'charts' => array('methods', false, '30', '256M'), + 'query' => array('query', false, '30', '256M'), +)); + +it('no longer raises the limits for every request', function () { + $source = file_get_contents(realpath(__DIR__ . '/../../slowlog.php')); + + expect($source)->not->toContain("ini_set('memory_limit', '-1');"); + expect($source)->toContain('slowlog_apply_request_limits(get_request_var(\'action\'));'); +}); From 27a3c992f67a3adc7efe407f46a52645e448772b Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 24 Oct 2026 03:00:05 -0600 Subject: [PATCH 2/3] fix: add a management realm for importing and deleting logs Signed-off-by: Thomas Vincent --- CHANGELOG.md | 2 + INFO | 2 +- includes/functions.php | 37 ++++++ locales/po/cacti.pot | 4 + setup.php | 48 ++++++- slowlog.php | 44 +++++- tests/Integration/SlowlogInstallHooksTest.php | 3 +- tests/Unit/ManagementRealmTest.php | 125 ++++++++++++++++++ tests/Unit/SlowlogCheckUpgradeTest.php | 52 ++++++++ 9 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 tests/Unit/ManagementRealmTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ee78e..583670f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ --- 2.8 --- +* security: Add a management realm, "Plugin -> MySQL Slow Log Management". Importing, uploading and deleting logs now need it in addition to the viewing realm; a user who holds only the viewing realm can read the list, details, query and chart pages. The import form, an upload and a delete are refused for such a user and the list no longer shows the Import link, the checkboxes or the actions menu +* bug: The upgrade to 2.8 registers the management realm and grants it to every user and every group that holds the viewing realm, once, so nobody loses import or delete when upgrading. A later narrowing by an administrator is not undone, because the grant runs only on the way up from an older version * bug: Raise the PHP execution time and memory limits only for the import form, an upload and a delete, instead of for every request to the page. Viewing a log now runs with the limits the web server is configured with; the page size is already bounded --- 2.7 --- diff --git a/INFO b/INFO index 22eb725..5b8f90a 100644 --- a/INFO +++ b/INFO @@ -1,6 +1,6 @@ [info] name = slowlog -version = 2.7 +version = 2.8 longname = MySQL/MariaDB Slow Log Viewer author = The Cacti Group email = diff --git a/includes/functions.php b/includes/functions.php index 8f50ecf..9ac88bf 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -258,6 +258,43 @@ function slowlog_log_number(string $value): string { return preg_match('/^[0-9]+(?:\.[0-9]+)?$/', $value) ? $value : '0'; } +// the pseudo file name of the management realm; no page has this name, the realm only exists so an +// administrator can grant it separately from the viewing realm +const SLOWLOG_MANAGE_FILE = 'slowlog_manage.php'; + +/** + * Returns the id of the realm that allows importing and deleting logs, as Cacti numbers it for + * is_realm_allowed() (the plugin realm number plus 100). + * + * Called from slowlog_can_manage(). + * + * @return int The realm id, or 0 when the realm is not registered. + */ +function slowlog_manage_realm(): int { + $id = db_fetch_cell_prepared('SELECT id + FROM plugin_realms + WHERE plugin = ? + AND file = ?', + ['slowlog', SLOWLOG_MANAGE_FILE]); + + return empty($id) ? 0 : (int) $id + 100; +} + +/** + * Whether the current user may import and delete logs. Viewing needs the slowlog.php realm; this + * needs the management realm as well. It fails closed when the realm is not registered. + * + * Called from slowlog.php before an import, a delete or the import form, and to decide which + * controls the list shows. + * + * @return bool True when the user holds the management realm. + */ +function slowlog_can_manage(): bool { + $realm = slowlog_manage_realm(); + + return $realm > 0 && is_realm_allowed($realm); +} + /** * Raises PHP's execution time and memory limits for the requests that need them, and only those: * the import form and upload (a large file is staged and handed to the background worker) and the diff --git a/locales/po/cacti.pot b/locales/po/cacti.pot index bee5a57..fe54ad6 100644 --- a/locales/po/cacti.pot +++ b/locales/po/cacti.pot @@ -30,6 +30,10 @@ msgstr "" msgid "Import Failed - Row Insert Error" msgstr "" +#: slowlog.php +msgid "ERROR: You do not have permission to import or delete Slowlog files." +msgstr "" + #: includes/slowlog_functions.php msgid "Bad File Format - No Slow Query Log Entries Found" msgstr "" diff --git a/setup.php b/setup.php index b8438d5..8d60405 100644 --- a/setup.php +++ b/setup.php @@ -40,7 +40,8 @@ function plugin_slowlog_csp_nonce(): string { /** * Registers this plugin's Cacti hooks (config arrays, navigation - * breadcrumbs, settings, tab display) and its slowlog.php realm, then + * breadcrumbs, settings, tab display) and its viewing and management + * realms, then * creates the plugin's database tables and seeds their reference data. * Invoked by the Cacti plugin framework when the plugin is * installed/enabled. @@ -57,6 +58,7 @@ function plugin_slowlog_install(): void { api_plugin_register_hook('slowlog', 'top_graph_header_tabs', 'slowlog_show_tab', 'setup.php'); api_plugin_register_realm('slowlog', 'slowlog.php', 'Plugin -> MySQL Slow Log Viewer', 1); + api_plugin_register_realm('slowlog', 'slowlog_manage.php', 'Plugin -> MySQL Slow Log Management', 1); require_once($config['base_path'] . '/plugins/slowlog/includes/database.php'); @@ -209,6 +211,15 @@ function slowlog_check_upgrade(): void { return; } + // 2.8 splits importing and deleting off from viewing. Everyone who can view keeps the + // access they had; an administrator narrows it afterwards. This runs once, on the way + // up from an older version, so a later change to a grant is never undone. + if (version_compare((string) $old, '2.8', '<') && !slowlog_upgrade_realms()) { + cacti_log('WARNING: slowlog realm upgrade did not complete; the installed version is left unchanged so it is retried', false, 'SLOWLOG'); + + return; + } + db_execute_prepared('UPDATE plugin_config SET version = ? WHERE directory = ?', @@ -225,6 +236,41 @@ function slowlog_check_upgrade(): void { } } +/** + * Registers the management realm and grants it to every user and group that holds the viewing + * realm, so an upgrade does not take import and delete away from anyone. Safe to run again: the + * grants are INSERT IGNORE. The name contains "upgrade" because Cacti only lets plugin install and + * upgrade functions register realms. + * + * Called from slowlog_check_upgrade() when upgrading from a version older than 2.8. + * + * @return bool True when the realm exists and every grant was made. + */ +function slowlog_upgrade_realms(): bool { + api_plugin_register_realm('slowlog', 'slowlog_manage.php', 'Plugin -> MySQL Slow Log Management', 1); + + $view = db_fetch_cell_prepared('SELECT id FROM plugin_realms WHERE plugin = ? AND file = ?', ['slowlog', 'slowlog.php']); + $manage = db_fetch_cell_prepared('SELECT id FROM plugin_realms WHERE plugin = ? AND file = ?', ['slowlog', 'slowlog_manage.php']); + + if (empty($view) || empty($manage)) { + return false; + } + + $user_grants = db_execute_prepared('INSERT IGNORE INTO user_auth_realm (user_id, realm_id) + SELECT user_id, ? + FROM user_auth_realm + WHERE realm_id = ?', + [(int) $manage + 100, (int) $view + 100]); + + $group_grants = db_execute_prepared('INSERT IGNORE INTO user_auth_group_realm (group_id, realm_id) + SELECT group_id, ? + FROM user_auth_group_realm + WHERE realm_id = ?', + [(int) $manage + 100, (int) $view + 100]); + + return $user_grants !== false && $group_grants !== false; +} + /** * No-op dependency check. Invoked by the Cacti plugin framework to * verify this plugin's dependencies are satisfied before diff --git a/slowlog.php b/slowlog.php index cf6cec2..800564f 100644 --- a/slowlog.php +++ b/slowlog.php @@ -56,6 +56,8 @@ break; case 'edit': + slowlog_require_management(); + slowlog_render_with_layout('slowlog_import'); break; @@ -85,6 +87,26 @@ break; } +/** + * Sends a user who does not hold the management realm back to the list with a message. Importing and + * deleting need that realm in addition to the one that allows viewing. + * + * Called before the import form, an upload and a delete. + * + * @return void Returns when the user may manage; otherwise redirects and exits. + */ +function slowlog_require_management(): void { + if (slowlog_can_manage()) { + return; + } + + raise_message('slowlog_manage_denied', __('ERROR: You do not have permission to import or delete Slowlog files.', 'slowlog'), MESSAGE_LEVEL_ERROR); + + header('Location: slowlog.php'); + + exit; +} + /* -------------------------- The Save Function -------------------------- */ @@ -108,6 +130,8 @@ function form_save(): void { global $config; + slowlog_require_management(); + if (isset($_POST['save_component_slowlog'])) { $logid = api_slowlog_save($_POST['logid'], $_POST['description'], $_POST['length']); @@ -217,6 +241,8 @@ function form_save(): void { function form_actions(): void { global $config, $actions; + slowlog_require_management(); + // if we are to save this form, instead of display it if (isset($_POST['selected_items'])) { $selected_items = sanitize_unserialize_selected_items(get_request_var('selected_items')) ?: []; @@ -1766,7 +1792,9 @@ function slowlog_view(): void { slowlog_tabs(); - html_start_box(__('MariaDB/MySQL SlowLog File Filters', 'slowlog'), '100%', false, 3, 'center', 'slowlog.php?action=edit'); + $can_manage = slowlog_can_manage(); + + html_start_box(__('MariaDB/MySQL SlowLog File Filters', 'slowlog'), '100%', false, 3, 'center', $can_manage ? 'slowlog.php?action=edit' : ''); filter(); html_end_box(); @@ -1822,7 +1850,11 @@ function slowlog_view(): void { ] ]; - html_header_sort_checkbox($display_text, get_request_var('sort_column'), get_request_var('sort_direction'), false, 'slowlog.php?action=select'); + if ($can_manage) { + html_header_sort_checkbox($display_text, get_request_var('sort_column'), get_request_var('sort_direction'), false, 'slowlog.php?action=select'); + } else { + html_header_sort($display_text, get_request_var('sort_column'), get_request_var('sort_direction'), 1, 'slowlog.php?action=select'); + } $has_pending = false; @@ -1863,7 +1895,9 @@ function slowlog_view(): void { form_selectable_cell(get_daysfromtime($entry['duration']), $entry['logid'], '', 'right'); form_selectable_cell($entry['start_time'], $entry['logid'], '', 'right'); form_selectable_cell($entry['end_time'], $entry['logid'], '', 'right'); - form_checkbox_cell($entry['description'], $entry['logid']); + if ($can_manage) { + form_checkbox_cell($entry['description'], $entry['logid']); + } form_end_row(); } @@ -1889,7 +1923,9 @@ function slowlog_view(): void { toBe('setup.php'); } - expect($GLOBALS['__test_registered_realms'])->toHaveCount(1); - expect($GLOBALS['__test_registered_realms'][0]['file'])->toBe('slowlog.php'); + expect(array_column($GLOBALS['__test_registered_realms'], 'file'))->toBe(array('slowlog.php', 'slowlog_manage.php')); $createdTables = array_values(array_unique(array_map(function ($call) { return $call['sql']; diff --git a/tests/Unit/ManagementRealmTest.php b/tests/Unit/ManagementRealmTest.php new file mode 100644 index 0000000..0a0e3e0 --- /dev/null +++ b/tests/Unit/ManagementRealmTest.php @@ -0,0 +1,125 @@ + $id) { + slowlog_test_mock_db('db_fetch_cell_prepared', function ($sql, $params) use ($file) { + return strpos($sql, 'plugin_realms') !== false && ($params[1] ?? '') === $file; + }, $id); + } +} + +it('numbers the management realm as Cacti does, and reports 0 when it is not registered', function () { + slowlog_test_realm_ids(array('slowlog_manage.php' => 3)); + + expect(slowlog_manage_realm())->toBe(103); + + slowlog_test_reset_db_mocks(); + slowlog_test_realm_ids(array('slowlog_manage.php' => false)); + + expect(slowlog_manage_realm())->toBe(0); +}); + +it('lets a user manage only when the management realm is allowed', function ($allowed, $expected) { + slowlog_test_realm_ids(array('slowlog_manage.php' => 3)); + $GLOBALS['__test_realm_allowed'] = $allowed; + + expect(slowlog_can_manage())->toBe($expected); + expect($GLOBALS['__test_realm_checks'])->toBe(array(103)); +})->with(array('allowed' => array(true, true), 'denied' => array(false, false))); + +it('denies management without asking Cacti when the realm is not registered', function () { + slowlog_test_realm_ids(array('slowlog_manage.php' => false)); + + expect(slowlog_can_manage())->toBeFalse(); + expect($GLOBALS['__test_realm_checks'])->toBe(array()); +}); + +it('registers both realms on install', function () { + $GLOBALS['__test_registered_realms'] = array(); + + plugin_slowlog_install(); + + expect(array_column($GLOBALS['__test_registered_realms'], 'file'))->toBe(array('slowlog.php', 'slowlog_manage.php')); +}); + +it('grants the management realm to everyone who holds the viewing realm', function () { + slowlog_test_realm_ids(array('slowlog.php' => 2, 'slowlog_manage.php' => 3)); + $GLOBALS['__test_registered_realms'] = array(); + + expect(slowlog_upgrade_realms())->toBeTrue(); + expect(array_column($GLOBALS['__test_registered_realms'], 'file'))->toBe(array('slowlog_manage.php')); + + $grants = array_values(array_filter($GLOBALS['__test_db_calls'], function ($call) { + return $call['fn'] === 'db_execute_prepared' && strpos($call['sql'], 'INSERT IGNORE INTO') === 0; + })); + + expect($grants)->toHaveCount(2); + expect($grants[0]['sql'])->toContain('user_auth_realm'); + expect($grants[1]['sql'])->toContain('user_auth_group_realm'); + expect($grants[0]['params'])->toBe(array(103, 102)); + expect($grants[1]['params'])->toBe(array(103, 102)); +}); + +it('reports failure when a realm is missing or a grant fails', function ($ids, $failing) { + slowlog_test_realm_ids($ids); + + if ($failing !== '') { + slowlog_test_mock_db('db_execute_prepared', $failing, false); + } + + expect(slowlog_upgrade_realms())->toBeFalse(); +})->with(array( + 'no viewing realm' => array(array('slowlog.php' => false, 'slowlog_manage.php' => 3), ''), + 'no management realm' => array(array('slowlog.php' => 2, 'slowlog_manage.php' => false), ''), + 'user grant fails' => array(array('slowlog.php' => 2, 'slowlog_manage.php' => 3), 'INSERT IGNORE INTO user_auth_realm'), + 'group grant fails' => array(array('slowlog.php' => 2, 'slowlog_manage.php' => 3), 'INSERT IGNORE INTO user_auth_group_realm'), +)); + +it('requires the management realm for the import form, an upload and a delete, and hides the controls from viewers', function () { + $source = file_get_contents(realpath(__DIR__ . '/../../slowlog.php')); + + expect(substr_count($source, 'slowlog_require_management();'))->toBe(3); + expect($source)->toContain("\$can_manage ? 'slowlog.php?action=edit' : ''"); + expect($source)->toContain("if (\$can_manage) {\n\t\tdraw_actions_dropdown(\$actions);"); +}); diff --git a/tests/Unit/SlowlogCheckUpgradeTest.php b/tests/Unit/SlowlogCheckUpgradeTest.php index 61ae3c2..483c7a2 100644 --- a/tests/Unit/SlowlogCheckUpgradeTest.php +++ b/tests/Unit/SlowlogCheckUpgradeTest.php @@ -39,6 +39,8 @@ beforeEach(function () { slowlog_test_reset_db_mocks(); unset($_SERVER['PHP_SELF']); + // both realm lookups find a realm, as they do once api_plugin_register_realm() has run + slowlog_test_mock_db('db_fetch_cell_prepared', 'plugin_realms', 2); // Sandbox base_path so the version-drift branch runs // slowlog_prune_files() against a throwaway tree with no @@ -200,3 +202,53 @@ expect($order)->toBe(array('schema', 'version', 'version')); }); + +it('grants the management realm before it records the version when upgrading from 2.7', function () { + $_SERVER['PHP_SELF'] = '/cacti/plugins.php'; + slowlog_test_mock_db('db_fetch_cell_prepared', 'plugin_config', '2.7'); + slowlog_test_mock_db('db_table_exists', 'plugin_slowlog_details', true); + + slowlog_check_upgrade(); + + $order = array(); + + foreach ($GLOBALS['__test_db_calls'] as $call) { + if ($call['fn'] === 'db_execute_prepared' && strpos($call['sql'], 'INSERT IGNORE INTO') === 0) { + $order[] = 'grant'; + } elseif ($call['fn'] === 'db_execute_prepared' && stripos($call['sql'], 'UPDATE plugin_config') !== false) { + $order[] = 'version'; + } + } + + expect($order)->toBe(array('grant', 'grant', 'version', 'version')); +}); + +it('does not grant the management realm again once the install is at 2.8 or later', function ($stored) { + $_SERVER['PHP_SELF'] = '/cacti/plugins.php'; + slowlog_test_mock_db('db_fetch_cell_prepared', 'plugin_config', $stored); + slowlog_test_mock_db('db_table_exists', 'plugin_slowlog_details', true); + + slowlog_check_upgrade(); + + $grants = array_filter($GLOBALS['__test_db_calls'], function ($call) { + return $call['fn'] === 'db_execute_prepared' && strpos($call['sql'], 'INSERT IGNORE INTO') === 0; + }); + + expect($grants)->toBeEmpty(); +})->with(array('2.8', '2.9', '3.0')); + +it('leaves the version unchanged and logs a warning when the realm upgrade fails', function () { + $_SERVER['PHP_SELF'] = '/cacti/plugins.php'; + slowlog_test_mock_db('db_fetch_cell_prepared', 'plugin_config', '2.7'); + slowlog_test_mock_db('db_execute_prepared', 'INSERT IGNORE INTO user_auth_realm', false); + $GLOBALS['__test_cacti_log'] = array(); + + slowlog_check_upgrade(); + + $updates = array_filter($GLOBALS['__test_db_calls'], function ($call) { + return $call['fn'] === 'db_execute_prepared' && stripos($call['sql'], 'UPDATE plugin_config') !== false; + }); + + expect($updates)->toBeEmpty(); + expect($GLOBALS['__test_cacti_log'][0])->toContain('realm upgrade did not complete'); +}); From c7fcc3e155390c6cad68023d33f4f17088dcc337 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 24 Oct 2026 03:01:12 -0600 Subject: [PATCH 3/3] test(e2e): cover the management realm and its upgrade grant Signed-off-by: Thomas Vincent --- CHANGELOG.md | 1 + tests/E2E/AccessFlowTest.php | 25 ++++++- tests/E2E/RealmFlowTest.php | 63 ++++++++++++++++++ tests/E2E/UpgradeFlowTest.php | 4 +- tests/E2E/access_flow.php | 56 ++++++++++++++-- tests/E2E/delete_flow.php | 2 +- tests/E2E/e2e_common.php | 9 +++ tests/E2E/realm_flow.php | 119 ++++++++++++++++++++++++++++++++++ tests/E2E/views_flow.php | 2 +- 9 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 tests/E2E/RealmFlowTest.php create mode 100644 tests/E2E/realm_flow.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 583670f..87ba451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * security: Add a management realm, "Plugin -> MySQL Slow Log Management". Importing, uploading and deleting logs now need it in addition to the viewing realm; a user who holds only the viewing realm can read the list, details, query and chart pages. The import form, an upload and a delete are refused for such a user and the list no longer shows the Import link, the checkboxes or the actions menu * bug: The upgrade to 2.8 registers the management realm and grants it to every user and every group that holds the viewing realm, once, so nobody loses import or delete when upgrading. A later narrowing by an administrator is not undone, because the grant runs only on the way up from an older version * bug: Raise the PHP execution time and memory limits only for the import form, an upload and a delete, instead of for every request to the page. Viewing a log now runs with the limits the web server is configured with; the page size is already bounded +* dev: Cover the realm checks, the grant on upgrade (once, and not undone) with unit and end-to-end tests --- 2.7 --- diff --git a/tests/E2E/AccessFlowTest.php b/tests/E2E/AccessFlowTest.php index b7e12bf..ad4abb7 100644 --- a/tests/E2E/AccessFlowTest.php +++ b/tests/E2E/AccessFlowTest.php @@ -50,7 +50,30 @@ expect($o['page']['none']['shows_page'])->toBeFalse(); expect($o['page']['none']['denied'])->toBeTrue(); - expect($o['kept_after_post'])->toBeTrue(); + expect($o['kept_after_post']['none'])->toBeTrue(); + expect($o['uploaded']['none'])->toBeFalse(); + }); + + it('lets a user who holds only the viewing realm read the list but not import or delete', function () { + $o = slowlog_e2e_observe('access_flow.php'); + + expect($o['page']['viewer']['shows_page'])->toBeTrue(); + expect($o['controls']['viewer'])->toBe(['add_link' => false, 'actions' => false, 'checkboxes' => false]); + expect($o['kept_after_post']['viewer'])->toBeTrue(); + expect($o['uploaded']['viewer'])->toBeFalse(); + expect($o['form_denied']['viewer'])->toBeTrue(); + }); + + it('shows the import link, the actions and the checkboxes to users who hold the management realm directly or through a group', function ($user) { + expect(slowlog_e2e_observe('access_flow.php')['controls'][$user])->toBe(['add_link' => true, 'actions' => true, 'checkboxes' => true]); + })->with(['direct', 'group']); + + it('lets a user with the management realm delete, and stops once that realm is revoked', function () { + $o = slowlog_e2e_observe('access_flow.php'); + + expect($o['manager_deleted'])->toBeTrue(); + expect($o['kept_after_manage_revoked'])->toBeTrue(); + expect($o['still_views'])->toBeTrue(); }); it('shows the tab to users who hold the realm directly or through a group, from a fresh or a cached session', function ($user) { diff --git a/tests/E2E/RealmFlowTest.php b/tests/E2E/RealmFlowTest.php new file mode 100644 index 0000000..4687245 --- /dev/null +++ b/tests/E2E/RealmFlowTest.php @@ -0,0 +1,63 @@ +markTestSkipped($reason); + } + }); + + it('runs the driver to completion', function () { + expect(slowlog_e2e_observe('realm_flow.php')['driver_failed'] ?? '')->toBe(''); + }); + + it('starts from an install with no management realm and no grants', function () { + $o = slowlog_e2e_observe('realm_flow.php')['before']; + + expect($o['registered'])->toBeFalse(); + expect($o['grants'])->toBe(['viewer' => false, 'grouped' => false, 'other' => false]); + }); + + it('registers the realm and grants it to the users and groups that hold the viewing realm, and only to them', function () { + $o = slowlog_e2e_observe('realm_flow.php'); + + expect($o['after_upgrade']['registered'])->toBeTrue(); + expect($o['after_upgrade']['grants'])->toBe(['viewer' => true, 'grouped' => true, 'other' => false]); + expect($o['after_upgrade']['version'])->toBe('2.8'); + expect($o['admin_has_it'])->toBeTrue(); + }); + + it('does not undo a narrowed grant at the current version', function () { + expect(slowlog_e2e_observe('realm_flow.php')['after_narrowing'])->toBe(['viewer' => false, 'grouped' => true, 'other' => false]); + }); + + it('grants again only on a run from an older version', function () { + expect(slowlog_e2e_observe('realm_flow.php')['rerun_from_27'])->toBe(['viewer' => true, 'grouped' => true, 'other' => false]); + }); +}); diff --git a/tests/E2E/UpgradeFlowTest.php b/tests/E2E/UpgradeFlowTest.php index 569e6bc..c51d6cf 100644 --- a/tests/E2E/UpgradeFlowTest.php +++ b/tests/E2E/UpgradeFlowTest.php @@ -38,10 +38,10 @@ expect(slowlog_e2e_observe('upgrade_flow.php')['driver_failed'] ?? '')->toBe(''); }); - it('widens the old column widths and records version 2.7', function () { + it('widens the old column widths and records version 2.8', function () { $o = slowlog_e2e_observe('upgrade_flow.php'); - expect($o['version'])->toBe('2.7'); + expect($o['version'])->toBe('2.8'); expect($o['widths'])->toBe([ 'plugin_slowlog_details.user' => 128, 'plugin_slowlog_details.ip_address' => 45, diff --git a/tests/E2E/access_flow.php b/tests/E2E/access_flow.php index 249a4e0..6811aff 100644 --- a/tests/E2E/access_flow.php +++ b/tests/E2E/access_flow.php @@ -34,13 +34,17 @@ require_once __DIR__ . '/e2e_boot.php'; -$realm = slowlog_e2e_realm(); -$users = [ - 'direct' => slowlog_e2e_user('e2e_acc_direct', [$realm]), - 'group' => slowlog_e2e_user('e2e_acc_group', [], [$realm]), +$realm = slowlog_e2e_realm(); +$manage = slowlog_e2e_manage_realm(); +$users = [ + 'direct' => slowlog_e2e_user('e2e_acc_direct', [$realm, $manage]), + 'group' => slowlog_e2e_user('e2e_acc_group', [], [$realm, $manage]), + 'viewer' => slowlog_e2e_user('e2e_acc_viewer', [$realm]), 'none' => slowlog_e2e_user('e2e_acc_none', [7]), ]; +file_put_contents($tmp . '/upload.log', "# Time: 240115 1:02:03\n# User@Host: u[u] @ h [10.0.0.1]\n# Query_time: 1.0 Lock_time: 0.0 Rows_sent: 1 Rows_examined: 1\nSET timestamp=1705280523;\nSELECT 1 FROM host;\n# Time: 240115 1:02:09\n"); + db_execute_prepared('INSERT INTO plugin_slowlog (description, import_status, import_text_status) VALUES (?, 2, ?)', [$tag . '-keep', 'All Tables Processed']); $logid = (int) db_fetch_insert_id(); @@ -87,13 +91,23 @@ function e2e_visit(string $path): array { $out['page'][$key] = e2e_visit('plugins/slowlog/slowlog.php'); - if ($key === 'none') { - // a delete request from the user without the realm, with a token from a page the user can open + $list = slowlog_e2e_request($web, 'GET', 'plugins/slowlog/slowlog.php')['body']; + + $out['controls'][$key] = ['add_link' => strpos($list, 'slowlog.php?action=edit') !== false, 'actions' => strpos($list, 'drp_action') !== false, 'checkboxes' => strpos($list, 'chk_' . $logid) !== false]; + + if ($key === 'none' || $key === 'viewer') { + // a delete request and an upload from a user who may not manage, with a token from a page the user can open $token = slowlog_e2e_token(slowlog_e2e_request($web, 'GET', 'index.php')['body']); slowlog_e2e_request($web, 'POST', 'plugins/slowlog/slowlog.php', ['action' => 'actions', 'drp_action' => '1', 'selected_items' => serialize([$logid]), '__csrf_magic' => $token]); + slowlog_e2e_request($web, 'POST', 'plugins/slowlog/slowlog.php?action=import', ['save_component_import' => '1', 'description' => $tag . '-' . $key . '-upload', 'length' => '-1', 'table_mode' => 'all', 'table_names' => '', 'import_file' => new CURLFile($tmp . '/upload.log', 'text/plain', 'upload.log'), '__csrf_magic' => $token]); + + $out['kept_after_post'][$key] = (bool) db_fetch_cell_prepared('SELECT COUNT(*) FROM plugin_slowlog WHERE logid = ?', [$logid]); + $out['uploaded'][$key] = (bool) db_fetch_cell_prepared('SELECT COUNT(*) FROM plugin_slowlog WHERE description = ?', [$tag . '-' . $key . '-upload']); + + $form = slowlog_e2e_request($web, 'GET', 'plugins/slowlog/slowlog.php?action=edit')['body']; - $out['kept_after_post'] = (bool) db_fetch_cell_prepared('SELECT COUNT(*) FROM plugin_slowlog WHERE logid = ?', [$logid]); + $out['form_denied'][$key] = strpos($form, 'import_file') === false && strpos($form, 'do not have permission to import or delete') !== false; } // the tab: looked up on the first call, taken from the session on the second @@ -103,6 +117,34 @@ function e2e_visit(string $path): array { ]; } + // a user who holds both realms can delete, then loses the ability when the management realm is revoked + $web['jar'] = $tmp . '/jar-direct'; + + db_execute_prepared('INSERT INTO plugin_slowlog (description, import_status, import_text_status) VALUES (?, 2, ?)', [$tag . '-managed', 'All Tables Processed']); + + $managed = (int) db_fetch_insert_id(); + $token = slowlog_e2e_token(slowlog_e2e_request($web, 'GET', 'plugins/slowlog/slowlog.php')['body']); + + slowlog_e2e_request($web, 'POST', 'plugins/slowlog/slowlog.php', ['action' => 'actions', 'drp_action' => '1', 'selected_items' => serialize([$managed]), '__csrf_magic' => $token]); + + $out['manager_deleted'] = !db_fetch_cell_prepared('SELECT COUNT(*) FROM plugin_slowlog WHERE logid = ?', [$managed]); + + db_execute_prepared('INSERT INTO plugin_slowlog (description, import_status, import_text_status) VALUES (?, 2, ?)', [$tag . '-revoked', 'All Tables Processed']); + + $revoked = (int) db_fetch_insert_id(); + + db_execute_prepared('DELETE FROM user_auth_realm WHERE user_id = ? AND realm_id = ?', [$users['direct']['id'], $manage]); + e2e_reset_perms($users['direct']['id']); + + slowlog_e2e_request($web, 'POST', 'plugins/slowlog/slowlog.php', ['action' => 'actions', 'drp_action' => '1', 'selected_items' => serialize([$revoked]), '__csrf_magic' => $token]); + + $out['kept_after_manage_revoked'] = (bool) db_fetch_cell_prepared('SELECT COUNT(*) FROM plugin_slowlog WHERE logid = ?', [$revoked]); + $out['still_views'] = e2e_visit('plugins/slowlog/slowlog.php')['shows_page']; + + api_slowlog_remove($revoked); + + db_execute_prepared('INSERT IGNORE INTO user_auth_realm (user_id, realm_id) VALUES (?, ?)', [$users['direct']['id'], $manage]); + // the page a slowlog URL is on uses the pressed image $_SERVER['REQUEST_URI'] = '/cacti/plugins/slowlog/slowlog.php'; $_SESSION = ['sess_user_id' => $users['direct']['id']]; diff --git a/tests/E2E/delete_flow.php b/tests/E2E/delete_flow.php index edc4bc5..08425a2 100644 --- a/tests/E2E/delete_flow.php +++ b/tests/E2E/delete_flow.php @@ -35,7 +35,7 @@ require_once __DIR__ . '/e2e_boot.php'; -$admin = slowlog_e2e_user('e2e_del_admin', [slowlog_e2e_realm()]); +$admin = slowlog_e2e_user('e2e_del_admin', [slowlog_e2e_realm(), slowlog_e2e_manage_realm()]); $web = slowlog_e2e_web_start($tmp); $ids = []; diff --git a/tests/E2E/e2e_common.php b/tests/E2E/e2e_common.php index 80c9d4d..a1dd24c 100644 --- a/tests/E2E/e2e_common.php +++ b/tests/E2E/e2e_common.php @@ -407,3 +407,12 @@ function slowlog_e2e_synthetic_log(string $path, int $entries, array $queries): function slowlog_e2e_realm(): int { return (int) db_fetch_cell_prepared('SELECT id FROM plugin_realms WHERE plugin = ? AND file = ?', ['slowlog', 'slowlog.php']) + 100; } + +/** + * @return int The realm id that allows importing and deleting (the plugin realm number plus 100), or 0. + */ +function slowlog_e2e_manage_realm(): int { + $id = db_fetch_cell_prepared('SELECT id FROM plugin_realms WHERE plugin = ? AND file = ?', ['slowlog', 'slowlog_manage.php']); + + return $id ? (int) $id + 100 : 0; +} diff --git a/tests/E2E/realm_flow.php b/tests/E2E/realm_flow.php new file mode 100644 index 0000000..409db8c --- /dev/null +++ b/tests/E2E/realm_flow.php @@ -0,0 +1,119 @@ + slowlog_e2e_user('e2e_realm_viewer', [$view]), + 'grouped' => slowlog_e2e_user('e2e_realm_grouped', [], [$view]), + 'other' => slowlog_e2e_user('e2e_realm_other', [7]), +]; +$scratch = $tmp . '/upgrade-tree'; + +/** + * @return array Whether each user holds the management realm directly or through a group. + */ +function e2e_manage_grants(array $users): array { + $manage = slowlog_e2e_manage_realm(); + $held = []; + + foreach ($users as $key => $user) { + $held[$key] = (bool) db_fetch_cell_prepared('SELECT COUNT(*) FROM user_auth_realm WHERE user_id = ? AND realm_id = ?', [$user['id'], $manage]) + || (bool) db_fetch_cell_prepared('SELECT COUNT(*) FROM user_auth_group_realm gr INNER JOIN user_auth_group_members gm ON gm.group_id = gr.group_id WHERE gm.user_id = ? AND gr.realm_id = ?', [$user['id'], $manage]); + } + + return $held; +} + +/** + * Runs the version-change check against the scratch copy of the plugin directory. + */ +function e2e_upgrade(string $scratch, string $version): void { + global $config; + + $real_base = $config['base_path']; + + db_execute_prepared('UPDATE plugin_config SET version = ? WHERE directory = ?', [$version, 'slowlog']); + + $_SERVER['PHP_SELF'] = '/cacti/plugins.php'; + $config['base_path'] = $scratch; + + try { + slowlog_check_upgrade(); + } finally { + $config['base_path'] = $real_base; + } +} + +$real_version = db_fetch_cell_prepared('SELECT version FROM plugin_config WHERE directory = ?', ['slowlog']); +$real_realm = db_fetch_row_prepared('SELECT id, display FROM plugin_realms WHERE plugin = ? AND file = ?', ['slowlog', 'slowlog_manage.php']); + +try { + slowlog_e2e_copy_tree($plugin, $scratch . '/plugins/slowlog'); + file_put_contents($scratch . '/plugins/slowlog/includes/database.php', " slowlog_e2e_manage_realm() > 0, 'grants' => e2e_manage_grants($users)]; + + e2e_upgrade($scratch, '2.7'); + + $out['after_upgrade'] = ['registered' => slowlog_e2e_manage_realm() > 0, 'grants' => e2e_manage_grants($users), 'version' => db_fetch_cell_prepared('SELECT version FROM plugin_config WHERE directory = ?', ['slowlog'])]; + $out['admin_has_it'] = (bool) db_fetch_cell_prepared('SELECT COUNT(*) FROM user_auth_realm WHERE user_id = 1 AND realm_id = ?', [slowlog_e2e_manage_realm()]); + + // an administrator narrows the grant; a later check at the current version must not undo that + db_execute_prepared('DELETE FROM user_auth_realm WHERE user_id = ? AND realm_id = ?', [$users['viewer']['id'], slowlog_e2e_manage_realm()]); + + e2e_upgrade($scratch, db_fetch_cell_prepared('SELECT version FROM plugin_config WHERE directory = ?', ['slowlog'])); + + $out['after_narrowing'] = e2e_manage_grants($users); + + // running the upgrade from 2.7 again only adds back what the viewing realm implies + e2e_upgrade($scratch, '2.7'); + + $out['rerun_from_27'] = e2e_manage_grants($users); +} finally { + db_execute_prepared('UPDATE plugin_config SET version = ? WHERE directory = ?', [$real_version, 'slowlog']); + + slowlog_e2e_drop_users($users); + slowlog_e2e_restore_log_setting($saved_log); + slowlog_e2e_remove_tree($scratch); +} + +print json_encode($out) . PHP_EOL; diff --git a/tests/E2E/views_flow.php b/tests/E2E/views_flow.php index 27c7e0e..1c4fdca 100644 --- a/tests/E2E/views_flow.php +++ b/tests/E2E/views_flow.php @@ -34,7 +34,7 @@ require_once __DIR__ . '/e2e_boot.php'; -$admin = slowlog_e2e_user('e2e_views_admin', [slowlog_e2e_realm()]); +$admin = slowlog_e2e_user('e2e_views_admin', [slowlog_e2e_realm(), slowlog_e2e_manage_realm()]); $web = slowlog_e2e_web_start($tmp); $made = []; $markers = ['Fatal error', 'Parse error', 'Warning:', 'Notice:', 'Deprecated:', 'Undefined '];