Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## ChangeLog

--- 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
* dev: Cover the realm checks, the grant on upgrade (once, and not undone) with unit and end-to-end tests

--- 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
Expand Down
2 changes: 1 addition & 1 deletion INFO
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[info]
name = slowlog
version = 2.7
version = 2.8
longname = MySQL/MariaDB Slow Log Viewer
author = The Cacti Group
email =
Expand Down
64 changes: 64 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,70 @@ function slowlog_time_expression(string $line): string {
return db_qstr('20' . substr($time, 0, 2) . '-' . substr($time, 2, 2) . '-' . substr($time, 4, 2) . (isset($parts[3]) ? ' ' . $parts[3] : ''));
}

// 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
* 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;

Expand Down
4 changes: 4 additions & 0 deletions locales/po/cacti.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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/functions.php
msgid "Read Error - Import Aborted, Log May Be Truncated"
msgstr ""
Expand Down
48 changes: 47 additions & 1 deletion setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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');

Expand Down Expand Up @@ -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 = ?',
Expand All @@ -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
Expand Down
49 changes: 42 additions & 7 deletions slowlog.php
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -57,6 +56,8 @@

break;
case 'edit':
slowlog_require_management();

slowlog_render_with_layout('slowlog_import');

break;
Expand Down Expand Up @@ -86,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
-------------------------- */
Expand All @@ -109,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']);

Expand Down Expand Up @@ -218,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')) ?: [];
Expand Down Expand Up @@ -1666,7 +1691,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();

Expand Down Expand Up @@ -1722,7 +1749,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;

Expand Down Expand Up @@ -1763,7 +1794,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();
}
Expand All @@ -1789,7 +1822,9 @@ function slowlog_view(): void {
<?php
}

draw_actions_dropdown($actions);
if ($can_manage) {
draw_actions_dropdown($actions);
}

form_end();
}
Expand Down
25 changes: 24 additions & 1 deletion tests/E2E/AccessFlowTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
63 changes: 63 additions & 0 deletions tests/E2E/RealmFlowTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
| http://www.cacti.net/ |
+-------------------------------------------------------------------------+

/*
* The upgrade to 2.8 registers the management realm and grants it to every user and group that holds the
* viewing realm, once. Needs SLOWLOG_E2E=1 and an installed Cacti with the plugin enabled; the CI integration job
* provides both. Without them these tests are skipped.
*/

require_once __DIR__ . '/e2e_common.php';

describe('management realm upgrade against a real Cacti', function () {
beforeEach(function () {
if (($reason = slowlog_e2e_skip_reason()) !== null) {
$this->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]);
});
});
Loading
Loading