mirror of
https://github.com/Cacti/cacti.git
synced 2026-11-03 02:27:13 +09:00
* feature: user-defined filter layouts with administrator publishing
Add a render_layouts toggle to the CactiTableFilter class so list pages
offer Save / New / Rename / Delete controls for saved filter layouts,
modelled on the monitor plugin. Layouts persist to a new user_layouts
table (id, user_id, page, name, url). The toggle defaults on for every
page except the Console > Presets pages (CDEFs, VDEFs, Colors, GPRINTs,
Data Profiles, SNMP), where it is off.
Each user manages their own layouts from any filter. Administrators who
hold the Settings/Utilities realm can publish a layout to all users
(moving it to user_id = 0) from the filter, and from a new
Console > Presets > Filters management page (layouts.php) where they can
view and manage every user's layouts. Published layouts appear for all
users on that page.
Layout create/rename/delete/publish requests are handled centrally by
filter_layouts_handle_request(), invoked from include/auth.php after the
user has been authorized for the current page, so the controls work on any
filter page while publishing stays gated on the realm. layouts.php itself
is mapped to the Settings/Utilities realm so the Presets > Filters menu
pick is administrator-only.
Schema added to cacti.sql, install/upgrades/1_3_0.php, and
docs/audit_schema.sql. Adds tests/Unit/FilterLayoutsTest.php covering the
URL-safety validation, preset-page defaults, and the request/permission
wiring.
* changelog: 8185.feature fragment for filter layouts
* chore: enforce LF line endings via .gitattributes and normalize new files
* fix: address review - attribute-escape layout options, honor failed/non-editable layout ops, regenerate cacti.pot, move test to Unit/Ui/Html
* chore: renormalize pre-existing CRLF files to LF per .gitattributes
* chore: keep vendored JS (big.js, tablesorter) byte-identical to upstream and exclude from LF normalization
* style: apply php-cs-fixer formatting and resolve PHPStan level 8 findings in filter layouts
* ci: re-trigger checks on current head
* refactor: store filter layouts as a versioned JSON document in a TEXT data column
Replace the user_layouts.url varchar(1024) column with a data TEXT column holding a versioned {version,page,filter} document. The navigable url is rebuilt server-side from the document (page basename + rebuilt query) plus the filter_layout marker, removing the stored-url redirect/XSS surface and making the backend forward-compatible with a structured query builder without a further schema change.
* feature: add Modern/Legacy page filter format with an Edit dialog for filters
Modern (default) collapses the filter to a Layouts selector plus an Edit dialog holding the Filter Name and all filter fields; Save and Publish live in the dialog, with Edit/Rename/Delete/Save As on the bar and time controls/refresh kept on the bar. Legacy keeps the classic inline filter. The format is a per-user General setting (page_filter_format). Field rendering is factored into emit_field() so both layouts share one source.
* fix: render page-action buttons (import, export, sort) on the modern filter bar
Import, Export, Purge, Save, and sort ascending/descending are page actions, not filter inputs, so they render on the bar beside the Layouts selector instead of inside the Edit dialog. Only go/clear are dropped from the bar; the dialog keeps the Search/Clear footer.
* feature: render the modern Edit dialog one filter variable per row
The dialog no longer mirrors the inline filter's multi-field row packing; each filter variable renders on its own row (label + control) for a clean, responsive form. Hidden/validate fields stay in the form without an empty visible row. Rows carry a cactiFilterEditRow hook for label-column alignment styling.
* feature: align the modern edit dialog one-per-row and skin it per theme
Render all dialog fields in a single table so the label column aligns via the existing table-cell layout. Tag the dialog widget with cactiFilterDialog and add a scoped skin to the modern theme (slate titlebar, styled button pane, accented primary action). Add a bar separator between layout and page-action buttons.
* feature: skin the modern filter Edit dialog across the classic themes
Add the scoped .cactiFilterDialog skin (accent titlebar, white title/close, styled button pane with an accented primary action) to cacti, carrot, dark, hollyberry, paper-plane, paw, raspberry, and sunrise, each using its own brand accent. The skin is accent-only and inherits each theme's dialog surface, so it works on light and dark themes. MidWinter is intentionally skipped (its variable-based CSS is handled in a separate Epic).
* fix: delete a user's saved filter layouts when the user is removed
user_remove() now deletes the user's own user_layouts rows alongside their other per-user data. Scoped to user_id so published (user_id = 0) layouts remain available to everyone.
* fix: address PR review — PHPStan return type, escaping, callbacks, SQL columns, mb truncation, contrast, POT
- create_filter(): cast both ob_get_clean() returns to string and drop the now-unused PHPStan baseline entry (fixes Level 6 failure).
- Edit dialog title now uses html_escape_attr() (attribute-context double-encoding).
- Modern Search/Clear honor the page's configured go/clear callbacks instead of hardcoding applyFilter()/clearFilter().
- filter_layouts_get() and layouts.php select explicit columns instead of *.
- Layout name truncation uses mb_substr() to avoid cutting multibyte codepoints.
- layouts.php management callback checks result.ok before reloading.
- carrot/paper-plane dialog accents darkened to meet WCAG AA (>=4.5:1) with white text.
- Added the new translatable msgids (Page Filter Format, Filters, Edit Filter, Filter Name, Save As, ...) to cacti.pot.
* fix: resolve Semgrep reflected-xss by reading filter_layout inside layout_select
The request var is no longer passed into layout_select() as a parameter (which Semgrep tracked parameter-to-return to the print sink); it is read internally like create_filter(), and the option markup remains escaped. No behavior change.
---------
Co-authored-by: TheWitness <copilot@users.noreply.github.com>
Co-authored-by: TheWitness <TheWitness@users.noreply.github.com>
5166 lines
161 KiB
PHP
5166 lines
161 KiB
PHP
<?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 |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
require_once(__DIR__ . '/client_address.php');
|
|
require_once(__DIR__ . '/ldap.php');
|
|
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/FixedBitNotation.php');
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/GoogleAuthenticatorInterface.php');
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/GoogleAuthenticator.php');
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/GoogleQrUrl.php');
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/RuntimeException.php');
|
|
|
|
use Cacti\Auth\CredentialLoginProviderInterface;
|
|
use Cacti\Auth\LoginProviderFactory;
|
|
use phpseclib4\Crypt\RSA;
|
|
|
|
if (!defined('CACTI_SECRET_CIPHER')) {
|
|
define('CACTI_SECRET_CIPHER', 'aes-256-gcm');
|
|
}
|
|
|
|
if (!defined('CACTI_SECRET_TAG_LENGTH')) {
|
|
define('CACTI_SECRET_TAG_LENGTH', 16);
|
|
}
|
|
|
|
/**
|
|
* Clears a users security token
|
|
*
|
|
* @return void
|
|
*/
|
|
function clear_auth_cookie() : void {
|
|
if (isset($_COOKIE['cacti_remembers']) && read_config_option('auth_cache_enabled') == 'on') {
|
|
if (!is_string($_COOKIE['cacti_remembers'])) {
|
|
cacti_cookie_session_logout();
|
|
|
|
return;
|
|
}
|
|
|
|
$parts = explode(',', $_COOKIE['cacti_remembers']);
|
|
|
|
if (cacti_sizeof($parts) == 2) {
|
|
$user_id = $parts[0];
|
|
$realm_id = -1;
|
|
$token = $parts[1];
|
|
} elseif (cacti_sizeof($parts) == 3) {
|
|
$user_id = $parts[0];
|
|
$realm_id = $parts[1];
|
|
$token = $parts[2];
|
|
} else {
|
|
cacti_cookie_session_logout();
|
|
|
|
return;
|
|
}
|
|
|
|
// Legacy support which leaked usernames
|
|
if (!is_numeric($user_id)) {
|
|
$user_id = db_fetch_cell_prepared('SELECT id
|
|
FROM user_auth
|
|
WHERE username = ?',
|
|
[$user_id]);
|
|
}
|
|
|
|
if ($user_id > 0) {
|
|
$secret = hash('sha512', $token, false);
|
|
|
|
db_execute_prepared('DELETE FROM user_auth_cache
|
|
WHERE user_id = ?
|
|
AND token = ?',
|
|
[$user_id, $secret]);
|
|
}
|
|
|
|
// Revoke the server-side credential before clearing browser state.
|
|
cacti_cookie_session_logout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* set_auth_cookie - sets a users security token
|
|
*
|
|
* @param array $user Is the user_auth row for the user
|
|
*
|
|
* @return void
|
|
*/
|
|
function set_auth_cookie(array $user) : void {
|
|
if (db_table_exists('user_auth_cache')) {
|
|
clear_auth_cookie();
|
|
|
|
$nssecret = bin2hex(random_bytes(32));
|
|
|
|
$secret = hash('sha512', $nssecret, false);
|
|
|
|
db_execute_prepared('REPLACE INTO user_auth_cache
|
|
(user_id, hostname, last_update, token)
|
|
VALUES
|
|
(?, ?, NOW(), ?);',
|
|
[$user['id'], get_client_addr(), $secret]);
|
|
|
|
cacti_cookie_session_set($user['id'], $user['realm'], $nssecret);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the authentication cookie and removes the user session from the cache.
|
|
*
|
|
* @return int|false The user of the session cookie, otherwise false
|
|
*/
|
|
function check_auth_cookie() : int|false {
|
|
if (isset($_COOKIE['cacti_remembers']) &&
|
|
read_config_option('auth_cache_enabled') == 'on' &&
|
|
db_table_exists('user_auth_cache')) {
|
|
if (!is_string($_COOKIE['cacti_remembers'])) {
|
|
return false;
|
|
}
|
|
|
|
$parts = explode(',', $_COOKIE['cacti_remembers']);
|
|
|
|
if (cacti_sizeof($parts) == 2) {
|
|
$user_id = $parts[0];
|
|
$realm_id = -1;
|
|
$token = $parts[1];
|
|
} elseif (cacti_sizeof($parts) == 3) {
|
|
$user_id = $parts[0];
|
|
$realm_id = $parts[1];
|
|
$token = $parts[2];
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
if (!is_numeric($user_id)) {
|
|
$user_id = db_fetch_cell_prepared('SELECT id
|
|
FROM user_auth
|
|
WHERE username = ?',
|
|
[$user_id]);
|
|
}
|
|
|
|
if ($user_id > 0 && $user_id !== get_guest_account()) {
|
|
if ($realm_id == -1) {
|
|
$user_info = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
} else {
|
|
$user_info = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?
|
|
AND realm = ?',
|
|
[$user_id, $realm_id]);
|
|
}
|
|
|
|
if (cacti_sizeof($user_info)) {
|
|
$secret = hash('sha512', $token, false);
|
|
|
|
// GHSA-xq26-f282-f998: reject remember-me tokens past the 90-day retention window instead of trusting the cron purge alone.
|
|
$found = db_fetch_cell_prepared('SELECT user_id
|
|
FROM user_auth_cache
|
|
WHERE user_id = ?
|
|
AND token = ?
|
|
AND last_update >= DATE_SUB(NOW(), INTERVAL 90 DAY)',
|
|
[$user_info['id'], $secret]
|
|
);
|
|
|
|
if (empty($found)) {
|
|
return false;
|
|
}
|
|
|
|
// a disabled account must not authenticate from a cookie issued before it was disabled
|
|
if ($user_info['enabled'] != 'on') {
|
|
db_execute_prepared('DELETE FROM user_auth_cache
|
|
WHERE user_id = ?',
|
|
[$user_info['id']]);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!auth_cookie_user_currently_allowed($user_info)) {
|
|
db_execute_prepared('DELETE FROM user_auth_cache
|
|
WHERE user_id = ?',
|
|
[$user_info['id']]);
|
|
|
|
return false;
|
|
}
|
|
|
|
set_auth_cookie($user_info);
|
|
|
|
cacti_log(sprintf('LOGIN: User %s Authenticated via Authentication Cookie from IP Address %s', $user_info['username'], get_client_addr()), false, 'AUTH');
|
|
|
|
db_execute_prepared('INSERT IGNORE INTO user_log
|
|
(username, user_id, result, ip, time)
|
|
VALUES
|
|
(?, ?, 2, ?, NOW())',
|
|
[$user_info['username'], $user_info['id'], get_client_addr()]
|
|
);
|
|
|
|
return $user_info['id'];
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Confirms that a remember-me cookie still belongs to an enabled account
|
|
* with current access before the cookie is allowed to create a new session.
|
|
*
|
|
* @param array $user_info The user_auth row loaded from the cookie identity.
|
|
*
|
|
* @return bool True when the account can still authenticate with the cookie.
|
|
*/
|
|
function auth_cookie_user_currently_allowed(array $user_info) : bool {
|
|
if (($user_info['enabled'] ?? '') != 'on') {
|
|
return false;
|
|
}
|
|
|
|
if (($user_info['locked'] ?? '') == 'on') {
|
|
return false;
|
|
}
|
|
|
|
if (!auth_realm_allows_cookies((int) ($user_info['realm'] ?? 0))) {
|
|
return false;
|
|
}
|
|
|
|
return auth_user_has_access($user_info);
|
|
}
|
|
|
|
/**
|
|
* cacti_auth_transition - move a session from unauthenticated to authenticated.
|
|
*
|
|
* Issues a new session id so a session identifier planted by an attacker before
|
|
* login cannot be reused afterwards, and drops the permission caches so the new identity is
|
|
* evaluated from scratch rather than inheriting the previous one.
|
|
*
|
|
* Call this at every point that first sets the session user id, apart from the
|
|
* guest account, which is not a privilege gain.
|
|
*
|
|
* @param int $user_id The account the session is becoming.
|
|
* @param string $reason Where the transition came from, for the log.
|
|
*
|
|
* @return bool False when the account is locked and must not be let in.
|
|
*/
|
|
function cacti_auth_transition(int $user_id, string $reason = 'login') : bool {
|
|
$locked = db_fetch_cell_prepared('SELECT locked
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
if ($locked === false || $locked == 'on') {
|
|
cacti_log(sprintf('SECURITY: auth transition blocked for unavailable or locked user %d, reason %s', $user_id, $reason), false, 'AUTH');
|
|
|
|
return false;
|
|
}
|
|
|
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
|
if (!session_regenerate_id(true)) {
|
|
cacti_log(sprintf('SECURITY: auth transition blocked because session regeneration failed for user %d, reason %s', $user_id, $reason), false, 'AUTH');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
kill_session_var(SESS_TREE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS);
|
|
kill_session_var(SESS_USER_PERMS_KEY);
|
|
kill_session_var(SESS_USER_2FA);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
|
|
cacti_log(sprintf('NOTE: auth transition completed for user %d, reason %s', $user_id, $reason), false, 'AUTH', POLLER_VERBOSITY_MEDIUM);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Given a username or user_id test if this is a template account
|
|
* Template accounts could be accounts used for the administrative email, for both
|
|
* the guest and template accounts, or a user that is specified by a plugin as a
|
|
* template account.
|
|
*
|
|
* @param null|int|string $user_id The user ID or username to check.
|
|
*
|
|
* @return bool True if the user is a template account, false otherwise.
|
|
*/
|
|
function is_template_account(null|int|string $user_id) : bool {
|
|
if (is_string($user_id)) {
|
|
$user_id = db_fetch_cell_prepared('SELECT id FROM user_auth WHERE username = ?', [$user_id]);
|
|
}
|
|
|
|
if (empty($user_id)) {
|
|
return false;
|
|
}
|
|
|
|
if (read_config_option('admin_user') == $user_id) {
|
|
return true;
|
|
}
|
|
|
|
if (read_config_option('guest_user') == $user_id) {
|
|
return true;
|
|
}
|
|
|
|
if (read_config_option('user_template') == $user_id) {
|
|
return true;
|
|
} else {
|
|
$domain_template = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM login_providers
|
|
WHERE user_id = ?',
|
|
[$user_id]);
|
|
|
|
if ($domain_template > 0) {
|
|
return true;
|
|
} else {
|
|
$plugin_template = get_template_account($user_id);
|
|
|
|
if ($plugin_template == $user_id) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Whether the current request originates from a reverse proxy that config.php
|
|
* lists in $trusted_proxies. Only such proxies may assert a pre-authenticated
|
|
* user via forwarded HTTP headers. Matching is IP-normalized, so equivalent
|
|
* spellings (for example ::1 and 0:0:0:0:0:0:0:1) match regardless of text
|
|
* representation, while an IPv4-mapped entry never silently matches its bare
|
|
* IPv4 form. CIDR entries such as 10.0.0.0/8 are accepted. A malformed or
|
|
* non-IP entry falls back to exact string equality. An empty or unset list
|
|
* trusts no proxy.
|
|
*
|
|
* @return bool true when REMOTE_ADDR matches a trusted proxy entry.
|
|
*/
|
|
function is_trusted_proxy() : bool {
|
|
global $config;
|
|
|
|
if (!isset($_SERVER['REMOTE_ADDR'])) {
|
|
return false;
|
|
}
|
|
|
|
$trusted = $config['trusted_proxies'] ?? [];
|
|
|
|
if (!is_array($trusted) || !cacti_sizeof($trusted)) {
|
|
return false;
|
|
}
|
|
|
|
return cacti_trusted_proxy_match((string) $_SERVER['REMOTE_ADDR'], $trusted);
|
|
}
|
|
|
|
/**
|
|
* If basic auth is used, return the valid username
|
|
*
|
|
* @return string|false The username if found and processed, or false if no username is found.
|
|
*/
|
|
function get_basic_auth_username() : string|false {
|
|
/* Headers arriving with an HTTP_ prefix are client-supplied and spoofable.
|
|
* Only honor a forwarded identity when the request comes from a trusted
|
|
* reverse proxy. The unprefixed REMOTE_USER / REDIRECT_REMOTE_USER vars are
|
|
* set by the web server's own auth module and are always honored. */
|
|
$trusted = is_trusted_proxy();
|
|
|
|
if (isset($_SERVER['PHP_AUTH_USER'])) {
|
|
$raw = is_array($_SERVER['PHP_AUTH_USER']) ? ($_SERVER['PHP_AUTH_USER'][0] ?? '') : $_SERVER['PHP_AUTH_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif (isset($_SERVER['REMOTE_USER'])) {
|
|
$raw = is_array($_SERVER['REMOTE_USER']) ? ($_SERVER['REMOTE_USER'][0] ?? '') : $_SERVER['REMOTE_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif (isset($_SERVER['REDIRECT_REMOTE_USER'])) {
|
|
$raw = is_array($_SERVER['REDIRECT_REMOTE_USER']) ? ($_SERVER['REDIRECT_REMOTE_USER'][0] ?? '') : $_SERVER['REDIRECT_REMOTE_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif ($trusted && isset($_SERVER['HTTP_X_FORWARDED_USER'])) {
|
|
$raw = is_array($_SERVER['HTTP_X_FORWARDED_USER']) ? ($_SERVER['HTTP_X_FORWARDED_USER'][0] ?? '') : $_SERVER['HTTP_X_FORWARDED_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif ($trusted && isset($_SERVER['HTTP_PHP_AUTH_USER'])) {
|
|
$raw = is_array($_SERVER['HTTP_PHP_AUTH_USER']) ? ($_SERVER['HTTP_PHP_AUTH_USER'][0] ?? '') : $_SERVER['HTTP_PHP_AUTH_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif ($trusted && isset($_SERVER['HTTP_REMOTE_USER'])) {
|
|
$raw = is_array($_SERVER['HTTP_REMOTE_USER']) ? ($_SERVER['HTTP_REMOTE_USER'][0] ?? '') : $_SERVER['HTTP_REMOTE_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif ($trusted && isset($_SERVER['HTTP_REDIRECT_REMOTE_USER'])) {
|
|
$raw = is_array($_SERVER['HTTP_REDIRECT_REMOTE_USER']) ? ($_SERVER['HTTP_REDIRECT_REMOTE_USER'][0] ?? '') : $_SERVER['HTTP_REDIRECT_REMOTE_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} else {
|
|
$username = false;
|
|
}
|
|
|
|
if ($username !== false && is_string($username)) {
|
|
if (str_contains($username, '@')) {
|
|
$upart = explode('@', $username);
|
|
$username = $upart[0];
|
|
}
|
|
|
|
/* Handle mapping basic accounts to shortform accounts.
|
|
* Format of map file is CSV: basic,shortform */
|
|
$mapfile = read_config_option('path_basic_mapfile');
|
|
|
|
if ($mapfile != '' && file_exists($mapfile) && is_readable($mapfile)) {
|
|
$records = file($mapfile);
|
|
$found = false;
|
|
|
|
if (cacti_sizeof($records)) {
|
|
foreach (($records ?: []) as $r) {
|
|
if (trim($r) === '') {
|
|
continue;
|
|
}
|
|
|
|
[$basic, $shortform] = str_getcsv($r);
|
|
|
|
if ($basic === null || trim($basic) === '') {
|
|
continue;
|
|
}
|
|
|
|
if (trim($basic) == $username) {
|
|
$username = trim($shortform ?? '');
|
|
$found = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
cacti_log("WARNING: Username $username not found in basic mapfile.", false, 'AUTH');
|
|
}
|
|
}
|
|
}
|
|
|
|
return is_string($username) ? $username : false;
|
|
}
|
|
|
|
/**
|
|
* Copies a user from a template user to a new user, optionally overwriting an existing user.
|
|
*
|
|
* @param string $template_user The username of the template user to copy from.
|
|
* @param string $new_user The username of the new user to create.
|
|
* @param int $template_realm The realm of the template user. Default is 0.
|
|
* @param int $new_realm The realm of the new user. Default is 0.
|
|
* @param bool $overwrite Whether to overwrite an existing user with the same username and realm. Default is false.
|
|
* @param array $data_override An associative array of fields to override in the new user's data. Default is an empty array.
|
|
*
|
|
* @return mixed - The ID of the new user if successful, or false if the operation failed.
|
|
*/
|
|
function user_copy(string $template_user, string $new_user, int $template_realm = 0, int $new_realm = 0, bool $overwrite = false, array $data_override = []) : mixed {
|
|
// ================= input validation =================
|
|
input_validate_input_number($template_realm, 'template_realm');
|
|
input_validate_input_number($new_realm, 'new_realm');
|
|
// ====================================================
|
|
|
|
// Check get template users array
|
|
$user_auth = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$template_user, $template_realm]);
|
|
|
|
if (!cacti_sizeof($user_auth)) {
|
|
return false;
|
|
}
|
|
|
|
$template_id = $user_auth['id'];
|
|
|
|
// Create update/insert for new/existing user
|
|
$user_exist = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$new_user, $new_realm]);
|
|
|
|
if (cacti_sizeof($user_exist)) {
|
|
if ($overwrite === true) {
|
|
// Overwrite existing user
|
|
$user_auth['id'] = $user_exist['id'];
|
|
$user_auth['username'] = $user_exist['username'];
|
|
$user_auth['password'] = $user_exist['password'];
|
|
$user_auth['realm'] = $user_exist['realm'];
|
|
$user_auth['full_name'] = $user_exist['full_name'];
|
|
$user_auth['email_address'] = $user_exist['email_address'];
|
|
$user_auth['must_change_password'] = $user_exist['must_change_password'];
|
|
$user_auth['enabled'] = $user_exist['enabled'];
|
|
} else {
|
|
// User already exists, duplicate users are bad
|
|
raise_message(19);
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
// new user
|
|
$user_auth['id'] = 0;
|
|
$user_auth['username'] = $new_user;
|
|
$user_auth['enabled'] = 'on';
|
|
$user_auth['password'] = mt_rand(100000, 10000000);
|
|
$user_auth['email_address'] = '';
|
|
$user_auth['realm'] = $new_realm;
|
|
}
|
|
|
|
// Update data_override fields
|
|
if (cacti_sizeof($data_override)) {
|
|
foreach ($data_override as $field => $value) {
|
|
if (isset($user_auth[$field]) && $field != 'id' && $field != 'username') {
|
|
$user_auth[$field] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the user
|
|
$new_id = sql_save($user_auth, 'user_auth');
|
|
|
|
// Create/Update permissions and settings
|
|
if (cacti_sizeof($user_exist) && $overwrite === true) {
|
|
db_execute_prepared('DELETE FROM user_auth_perms WHERE user_id = ?', [$user_exist['id']]);
|
|
db_execute_prepared('DELETE FROM user_auth_realm WHERE user_id = ?', [$user_exist['id']]);
|
|
db_execute_prepared('DELETE FROM settings_user WHERE user_id = ?', [$user_exist['id']]);
|
|
db_execute_prepared('DELETE FROM settings_tree WHERE user_id = ?', [$user_exist['id']]);
|
|
}
|
|
|
|
$user_auth_perms = db_fetch_assoc_prepared('SELECT *
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($user_auth_perms)) {
|
|
foreach ($user_auth_perms as $row) {
|
|
$row['user_id'] = $new_id;
|
|
sql_save($row, 'user_auth_perms', ['user_id', 'item_id', 'type'], false);
|
|
}
|
|
}
|
|
|
|
$user_auth_realm = db_fetch_assoc_prepared('SELECT *
|
|
FROM user_auth_realm
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($user_auth_realm)) {
|
|
foreach ($user_auth_realm as $row) {
|
|
$row['user_id'] = $new_id;
|
|
sql_save($row, 'user_auth_realm', ['realm_id', 'user_id'], false);
|
|
}
|
|
}
|
|
|
|
$settings_user = db_fetch_assoc_prepared('SELECT *
|
|
FROM settings_user
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($settings_user)) {
|
|
foreach ($settings_user as $row) {
|
|
$row['user_id'] = $new_id;
|
|
sql_save($row, 'settings_user', ['user_id', 'name'], false);
|
|
}
|
|
}
|
|
|
|
$settings_tree = db_fetch_assoc_prepared('SELECT *
|
|
FROM settings_tree
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($settings_tree)) {
|
|
foreach ($settings_tree as $row) {
|
|
$row['user_id'] = $new_id;
|
|
sql_save($row, 'settings_tree', ['user_id', 'graph_tree_item_id'], false);
|
|
}
|
|
}
|
|
|
|
// apply group permissions for the user
|
|
$groups = db_fetch_assoc_prepared('SELECT group_id
|
|
FROM user_auth_group_members
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($groups)) {
|
|
$sql = [];
|
|
|
|
foreach ($groups as $g) {
|
|
$sql[] = '(' . $new_id . ', ' . $g['group_id'] . ')';
|
|
}
|
|
|
|
db_execute('INSERT IGNORE INTO user_auth_group_members
|
|
(user_id, group_id) VALUES ' . implode(',', $sql));
|
|
}
|
|
|
|
api_plugin_hook_function('copy_user', ['template_id' => $template_id, 'new_id' => $new_id]);
|
|
|
|
return $new_id;
|
|
}
|
|
|
|
/**
|
|
* Removes a user and all associated data from the database.
|
|
*
|
|
* @param int $user_id The ID of the user to be removed.
|
|
*
|
|
* @return void
|
|
*/
|
|
function user_remove(int $user_id) : void {
|
|
// ================= input validation =================
|
|
input_validate_input_number($user_id, 'user_id');
|
|
// ====================================================
|
|
|
|
// check for guest or template user
|
|
$username = db_fetch_cell_prepared('SELECT username
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
if ($username != gnrv('username')) {
|
|
if (is_template_account($user_id)) {
|
|
raise_message(21);
|
|
|
|
return;
|
|
}
|
|
|
|
if ($user_id === get_guest_account()) {
|
|
raise_message(21);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
db_execute_prepared('DELETE FROM user_auth WHERE id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_realm WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_perms WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_row_cache WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_group_members WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM settings_user WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM settings_tree WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_layouts WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM sessions WHERE user_id = ?', [$user_id]);
|
|
|
|
api_plugin_hook_function('user_remove', $user_id);
|
|
}
|
|
|
|
/**
|
|
* Disable a user account
|
|
*
|
|
* @param int $user_id The ID of the user to disable.
|
|
*
|
|
* @return void
|
|
*/
|
|
function user_disable(int $user_id) : void {
|
|
// ================= input validation =================
|
|
input_validate_input_number($user_id, 'user_id');
|
|
// ====================================================
|
|
|
|
db_execute_prepared("UPDATE user_auth SET enabled = '' WHERE id = ?", [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_row_cache WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM sessions WHERE user_id = ?', [$user_id]);
|
|
|
|
reset_user_perms($user_id);
|
|
}
|
|
|
|
/**
|
|
* Enable a user account
|
|
*
|
|
* @param int $user_id The ID of the user to enable.
|
|
*
|
|
* @return void
|
|
*/
|
|
function user_enable(int $user_id) : void {
|
|
// ================= input validation =================
|
|
input_validate_input_number($user_id, 'user_id');
|
|
// ====================================================
|
|
|
|
db_execute_prepared("UPDATE user_auth SET enabled = 'on' WHERE id = ?", [$user_id]);
|
|
|
|
reset_user_perms($user_id);
|
|
}
|
|
|
|
/**
|
|
* Replace every outstanding password-reset token for a local user.
|
|
*
|
|
* Locking the user row serializes the self-service and administrator issuance
|
|
* paths, so concurrent requests cannot leave multiple valid reset links.
|
|
*
|
|
* @param int $user_id The local user receiving the token
|
|
* @param string $hash Cryptographically random reset-token hash
|
|
* @param int $timeout_minutes Token lifetime in minutes
|
|
*
|
|
* @return bool True when the replacement token was committed
|
|
*/
|
|
function auth_reset_token_replace(int $user_id, string $hash, int $timeout_minutes) : bool {
|
|
if ($user_id <= 0 || $hash === '' || $timeout_minutes <= 0 || !db_begin_transaction()) {
|
|
return false;
|
|
}
|
|
|
|
$locked_user = db_fetch_cell_prepared('SELECT id
|
|
FROM user_auth
|
|
WHERE id = ?
|
|
AND realm = 0
|
|
AND enabled = "on"
|
|
FOR UPDATE',
|
|
[$user_id]);
|
|
|
|
$success = (int) $locked_user === $user_id &&
|
|
db_execute_prepared('DELETE FROM user_auth_reset_hashes WHERE user_id = ?', [$user_id]) !== false &&
|
|
db_execute_prepared('INSERT INTO user_auth_reset_hashes
|
|
(user_id, hash, expiry)
|
|
VALUES (?, ?, date_add(now(), interval ? minute))',
|
|
[$user_id, $hash, $timeout_minutes]) !== false;
|
|
|
|
if ($success && db_commit_transaction()) {
|
|
return true;
|
|
}
|
|
|
|
db_rollback_transaction();
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the authentication realms based on the configured authentication method.
|
|
*
|
|
* @param bool $login Whether the function is being called during a login process. Default is false.
|
|
*
|
|
* @return array An array of authentication realms
|
|
*/
|
|
function get_auth_realms(bool $login = false) : array {
|
|
if (read_config_option('auth_method') == AUTH_METHOD_PROVIDERS) {
|
|
// SAML2/OpenID are redirect-only and cannot be driven from this username
|
|
// + password realm dropdown; they get their own login page buttons instead.
|
|
$drealms = db_fetch_assoc('SELECT id, name
|
|
FROM login_providers
|
|
WHERE enabled="on"
|
|
AND type IN (' . PROVIDER_TYPE_LDAP . ', ' . PROVIDER_TYPE_AD . ')
|
|
ORDER BY name');
|
|
|
|
$drealms = is_array($drealms) ? $drealms : [];
|
|
|
|
if (cacti_sizeof($drealms)) {
|
|
if ($login) {
|
|
$new_realms['0'] = [
|
|
'name' => __('Local'),
|
|
'selected' => false
|
|
];
|
|
|
|
foreach ($drealms as $realm) {
|
|
$new_realms[1000 + $realm['id']] = [
|
|
'name' => $realm['name'],
|
|
'selected' => false
|
|
];
|
|
}
|
|
|
|
$default_realm = db_fetch_cell('SELECT id
|
|
FROM login_providers
|
|
WHERE is_default=1
|
|
AND enabled="on"
|
|
AND type IN (' . PROVIDER_TYPE_LDAP . ', ' . PROVIDER_TYPE_AD . ')');
|
|
|
|
if (!empty($default_realm)) {
|
|
$new_realms[1000 + $default_realm]['selected'] = true;
|
|
} else {
|
|
$new_realms['0']['selected'] = true;
|
|
}
|
|
} else {
|
|
$new_realms['0'] = __('Local');
|
|
|
|
foreach ($drealms as $realm) {
|
|
$new_realms[1000 + $realm['id']] = $realm['name'];
|
|
}
|
|
}
|
|
|
|
return $new_realms;
|
|
} else {
|
|
$new_realms['0'] = [
|
|
'name' => __('Local'),
|
|
'selected' => false
|
|
];
|
|
|
|
return $new_realms;
|
|
}
|
|
}
|
|
|
|
// TODO: Verify this array
|
|
return [
|
|
'0' => ['name' => __('Local')],
|
|
'3' => ['name' => __('LDAP')],
|
|
'2' => ['name' => __('Web Basic')],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Returns the enabled SAML2/OpenID Login Providers, for rendering as
|
|
* "Login with <button label>" buttons on the login page. Unlike
|
|
* get_auth_realms(), these are never part of the username/password realm
|
|
* dropdown since authenticating with them always starts with a browser
|
|
* redirect to the external IdP, handled by login_sso.php.
|
|
*
|
|
* @return array<int, array{realm: int, label: string}>
|
|
*/
|
|
function get_sso_login_providers() : array {
|
|
if (read_config_option('auth_method') != AUTH_METHOD_PROVIDERS) {
|
|
return [];
|
|
}
|
|
|
|
$providers = db_fetch_assoc('SELECT id, name, button_label
|
|
FROM login_providers
|
|
WHERE enabled = "on"
|
|
AND type IN (' . PROVIDER_TYPE_SAML2 . ', ' . PROVIDER_TYPE_OPENID . ')
|
|
ORDER BY name');
|
|
|
|
$buttons = [];
|
|
|
|
foreach ($providers as $provider) {
|
|
$buttons[] = [
|
|
'realm' => 1000 + $provider['id'],
|
|
'label' => $provider['button_label'] !== '' ? $provider['button_label'] : $provider['name'],
|
|
];
|
|
}
|
|
|
|
return $buttons;
|
|
}
|
|
|
|
/**
|
|
* Whether a login realm's provider allows "remember me" auth cookies.
|
|
* Realms below 1000 (local/basic/single-LDAP) always defer to the global
|
|
* auth_cache_enabled setting only, since they have no per-provider row.
|
|
*
|
|
* @param int $realm The login realm (0/2/3, or 1000 + login_providers.id).
|
|
*
|
|
* @return bool
|
|
*/
|
|
function auth_realm_allows_cookies(int $realm) : bool {
|
|
if ($realm < 1000) {
|
|
return true;
|
|
}
|
|
|
|
// Disabling or deleting a provider must revoke the ability for its
|
|
// existing remember-me cookies to keep restoring a session; a missing
|
|
// or disabled row is treated as "deny", not "allow".
|
|
$allowed = db_fetch_cell_prepared('SELECT allow_auth_cookies
|
|
FROM login_providers
|
|
WHERE id = ?
|
|
AND enabled = "on"',
|
|
[$realm - 1000]);
|
|
|
|
return $allowed === 'on';
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current user is allowed to view a certain graph
|
|
*
|
|
* @param int $local_graph_id The ID of the local graph to check.
|
|
* @param int $user_id The ID of the user to check permissions for.
|
|
*
|
|
* @return bool True if the graph is allowed for the user, false otherwise.
|
|
*/
|
|
function is_graph_allowed(int $local_graph_id, int $user_id = 0) : bool {
|
|
$rows = 0;
|
|
|
|
get_allowed_graphs('', '', '', $rows, $user_id, $local_graph_id);
|
|
|
|
return ($rows > 0);
|
|
}
|
|
|
|
/**
|
|
* A helper function to checking Tree permissions
|
|
*
|
|
* @param array $objects An array of objects to check permissions against.
|
|
* @param int $policy The policy to apply
|
|
*
|
|
* @return bool Returns true or false based on the policy and the presence of objects
|
|
*/
|
|
function auth_check_perms(array $objects, int $policy) : bool {
|
|
$objectSize = cacti_sizeof($objects);
|
|
|
|
// policy == allow AND matches = DENY
|
|
if ($objectSize && $policy == 1) {
|
|
return false;
|
|
// policy == deny AND matches = ALLOW
|
|
}
|
|
|
|
if ($objectSize && $policy == 2) {
|
|
return true;
|
|
// policy == allow AND no matches = ALLOW
|
|
}
|
|
|
|
if (!$objectSize && $policy == 1) {
|
|
return true;
|
|
// policy == deny AND no matches = DENY
|
|
}
|
|
|
|
if (!$objectSize && $policy == 2) {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* A helper function to extend Cacti roles with additional realms or to add a new role.
|
|
*
|
|
* @param string $role_name The name of the role to augment.
|
|
* @param array $files An array of file paths to check and associate with realm IDs.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_augment_roles(string $role_name, array $files) : void {
|
|
global $user_auth_roles, $user_auth_realm_filenames;
|
|
|
|
foreach ($files as $file) {
|
|
if (array_search($file, $user_auth_realm_filenames, true) !== false) {
|
|
if (array_search($user_auth_realm_filenames[$file], $user_auth_roles[$role_name], true) === false) {
|
|
$user_auth_roles[$role_name][] = $user_auth_realm_filenames[$file];
|
|
}
|
|
} else {
|
|
if (isset($_SESSION[SESS_AUTH_NAMES][$file])) {
|
|
$realm_id = $_SESSION[SESS_AUTH_NAMES][$file];
|
|
} else {
|
|
$realm_id = db_fetch_cell_prepared('SELECT id+100 AS realm
|
|
FROM plugin_realms
|
|
WHERE file = ?
|
|
OR file LIKE ?
|
|
OR file LIKE ?
|
|
OR file LIKE ?',
|
|
[
|
|
$file,
|
|
$file . ',%',
|
|
'%,' . $file . ',%',
|
|
'%,' . $file
|
|
]
|
|
);
|
|
|
|
if ($realm_id > 0) {
|
|
$_SESSION[SESS_AUTH_NAMES][$file] = $realm_id;
|
|
}
|
|
}
|
|
|
|
if (!empty($realm_id)) {
|
|
if (!isset($user_auth_roles[$role_name])) {
|
|
$user_auth_roles[$role_name][] = $realm_id;
|
|
} elseif (array_search($realm_id, $user_auth_roles[$role_name], true) === false) {
|
|
$user_auth_roles[$role_name][] = $realm_id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A helper function to extend Cacti roles with additional realms or to add a new role.
|
|
*
|
|
* @param string $role_name The name of the role to augment.
|
|
* @param string $auth_name The authentication name to look up the realm ID.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_augment_roles_byname(string $role_name, string $auth_name) : void {
|
|
global $user_auth_roles, $user_auth_realm_filenames;
|
|
|
|
if (isset($_SESSION[SESS_AUTH_NAMES][$auth_name])) {
|
|
$realm_id = $_SESSION[SESS_AUTH_NAMES][$auth_name];
|
|
} else {
|
|
$realm_id = db_fetch_cell_prepared('SELECT id+100 AS realm
|
|
FROM plugin_realms
|
|
WHERE display = ?',
|
|
[$auth_name]);
|
|
|
|
if ($realm_id > 0) {
|
|
$_SESSION[SESS_AUTH_NAMES][$auth_name] = $realm_id;
|
|
}
|
|
}
|
|
|
|
if (!empty($realm_id)) {
|
|
if (!isset($user_auth_roles[$role_name])) {
|
|
$user_auth_roles[$role_name][] = $realm_id;
|
|
} elseif (array_search($realm_id, $user_auth_roles[$role_name], true) === false) {
|
|
$user_auth_roles[$role_name][] = $realm_id;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current user is allowed to view a certain graph tree
|
|
*
|
|
* @param int $tree_id The ID of the tree to check access for.
|
|
* @param int $user_id The ID of the user to check access for.
|
|
*
|
|
* @return bool True if the user is allowed to access the tree, false otherwise.
|
|
*/
|
|
function is_tree_allowed(int $tree_id, int $user_id = 0) : bool {
|
|
if ($user_id == -1) {
|
|
return true;
|
|
}
|
|
|
|
if (isset($_SESSION[SESS_TREE_PERMS][$tree_id])) {
|
|
return $_SESSION[SESS_TREE_PERMS][$tree_id];
|
|
}
|
|
|
|
if ($user_id == 0) {
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = false;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$policy = db_fetch_cell_prepared('SELECT policy_trees
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
$trees = db_fetch_assoc_prepared('SELECT user_id
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?
|
|
AND type = 2
|
|
AND item_id = ?',
|
|
[$user_id, $tree_id]);
|
|
|
|
if (auth_check_perms($trees, $policy)) {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
// check for group perms
|
|
$groups = db_fetch_assoc_prepared("SELECT uag.policy_trees
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?",
|
|
[$user_id]);
|
|
|
|
if (!cacti_sizeof($groups)) {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
foreach ($groups as $g) {
|
|
if (auth_check_perms($trees, $g['policy_trees'])) {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// check for group trees
|
|
$gtrees = db_fetch_assoc_prepared("SELECT uagm.user_id
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
INNER JOIN user_auth_group_perms as uagp
|
|
ON uagp.group_id = uag.id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?
|
|
AND uagp.item_id = ?",
|
|
[$user_id, $tree_id]);
|
|
|
|
foreach ($groups as $g) {
|
|
if (auth_check_perms($gtrees, $g['policy_trees'])) {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current user is allowed to view a certain device
|
|
*
|
|
* @param int $device_id The ID of the device to check.
|
|
* @param int $user_id The ID of the user to check.
|
|
*
|
|
* @return bool True if the device is allowed for the user, false otherwise.
|
|
*/
|
|
function is_device_allowed(int $device_id, int $user_id = 0) : bool {
|
|
$total_rows = -2;
|
|
get_allowed_devices('', '', '', $total_rows, $user_id, $device_id);
|
|
|
|
return ($total_rows > 0);
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current user is allowed to view a certain graph template
|
|
*
|
|
* @param int $graph_template_id The ID of the graph template to check.
|
|
* @param int $user The ID of the user to check permissions for. Defaults to 0.
|
|
*
|
|
* @return bool Returns true if the graph template is allowed for the user, false otherwise.
|
|
*/
|
|
function is_graph_template_allowed(int $graph_template_id, int $user = 0) : bool {
|
|
$total_rows = -2;
|
|
get_allowed_graph_templates('', '', '', $total_rows, $user, $graph_template_id);
|
|
|
|
return ($total_rows > 0);
|
|
}
|
|
|
|
/**
|
|
* Returns a true or false as to whether or not a specific view type is allowed
|
|
* View options include 'show_tree', 'show_list', 'show_preview', 'graph_settings'
|
|
*
|
|
* @param string $view The view to check permissions for. Default is 'show_tree'.
|
|
*
|
|
* @return bool Returns true if the view is allowed for the user, false otherwise.
|
|
*/
|
|
function is_view_allowed(string $view = 'show_tree') : bool {
|
|
$allowed_views = ['show_tree', 'show_list', 'show_preview', 'graph_settings'];
|
|
|
|
if (!in_array($view, $allowed_views, true)) {
|
|
cacti_log("WARNING: Invalid view parameter '$view' in is_view_allowed()", false, 'AUTH');
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!isset($_SESSION[SESS_USER_ID])) {
|
|
return false;
|
|
}
|
|
|
|
if (db_table_exists('user_auth_group')) {
|
|
$values = array_rekey(
|
|
db_fetch_assoc_prepared("SELECT DISTINCT `$view`
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?",
|
|
[$_SESSION[SESS_USER_ID]]
|
|
), $view, $view
|
|
);
|
|
|
|
if (isset($values[3])) {
|
|
return false;
|
|
}
|
|
|
|
if (isset($values['on'])) {
|
|
return true;
|
|
}
|
|
|
|
if (isset($values[2])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$value = db_fetch_cell_prepared("SELECT `$view`
|
|
FROM user_auth
|
|
WHERE id = ?",
|
|
[$_SESSION[SESS_USER_ID]]
|
|
);
|
|
|
|
return ($value == 'on');
|
|
}
|
|
|
|
/**
|
|
* Given a tree id and a branch id, check if it's empty
|
|
*
|
|
* @param int $tree_id The ID of the tree to check.
|
|
* @param int $parent The parent ID of the branch to check. Default is 0.
|
|
*
|
|
* @return bool Returns true if the branch is empty, false otherwise.
|
|
*/
|
|
function is_tree_branch_empty(int $tree_id, int $parent = 0) : bool {
|
|
$graphs = array_rekey(
|
|
db_fetch_assoc_prepared('SELECT local_graph_id
|
|
FROM graph_tree_items
|
|
WHERE graph_tree_id = ?
|
|
AND local_graph_id > 0
|
|
AND parent = ?',
|
|
[$tree_id, $parent]
|
|
), 'local_graph_id', 'local_graph_id'
|
|
);
|
|
|
|
$simple_perms = get_simple_graph_perms($_SESSION[SESS_USER_ID]);
|
|
|
|
if (cacti_sizeof($graphs) && ($simple_perms || cacti_sizeof(get_allowed_graphs('gl.id IN(' . implode(',', $graphs) . ')'))) > 0) {
|
|
return false;
|
|
}
|
|
|
|
$hosts = array_rekey(
|
|
db_fetch_assoc_prepared('SELECT host_id
|
|
FROM graph_tree_items
|
|
WHERE graph_tree_id = ?
|
|
AND host_id > 0
|
|
AND parent = ?',
|
|
[$tree_id, $parent]
|
|
), 'host_id', 'host_id'
|
|
);
|
|
|
|
$sites = array_rekey(
|
|
db_fetch_assoc_prepared('SELECT site_id
|
|
FROM graph_tree_items
|
|
WHERE graph_tree_id = ?
|
|
AND site_id > 0
|
|
AND parent = ?',
|
|
[$tree_id, $parent]
|
|
), 'site_id', 'site_id'
|
|
);
|
|
|
|
if (!cacti_sizeof($sites)) {
|
|
$total_rows = -1; // Adding to fix pass by reference error in get_allowed_devices
|
|
|
|
if (cacti_sizeof($hosts) && cacti_sizeof(get_allowed_devices('h.id IN(' . implode(',', $hosts) . ')', 'description', '', $total_rows)) > 0) {
|
|
return false;
|
|
}
|
|
} else {
|
|
$site_hosts = [];
|
|
|
|
foreach ($sites as $site) {
|
|
$site_hosts += array_rekey(
|
|
db_fetch_assoc_prepared('SELECT id
|
|
FROM host
|
|
WHERE site_id = ?',
|
|
[$site]
|
|
), 'id', 'id'
|
|
);
|
|
}
|
|
$total_rows = -1; // Adding to fix pass by reference error in get_allowed_devices
|
|
|
|
if (cacti_sizeof($site_hosts) && cacti_sizeof(get_allowed_devices('h.id IN(' . implode(',', $site_hosts) . ')', 'description', '', $total_rows)) > 0) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$branches = db_fetch_assoc_prepared('SELECT id, graph_tree_id
|
|
FROM graph_tree_items
|
|
WHERE graph_tree_id = ?
|
|
AND parent = ?
|
|
AND local_graph_id = 0
|
|
AND host_id = 0',
|
|
[$tree_id, $parent]);
|
|
|
|
if (cacti_sizeof($branches)) {
|
|
foreach ($branches as $b) {
|
|
if (!is_tree_branch_empty($b['graph_tree_id'], $b['id'])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Given a realm and a user, check their permissions if the
|
|
* admin changed a users settings, setup the case to redirect by clearing
|
|
* session variables so that when an admin makes a change, the user does not
|
|
* have to login again to receive them.
|
|
*
|
|
* @param int $realm The realm to check permissions for.
|
|
* @param int|bool $check_user Optional. The user ID to check permissions for. If false, checks the current session user.
|
|
*
|
|
* @return bool True if the user has permission to access the realm, false otherwise.
|
|
*/
|
|
function is_realm_allowed(int $realm, int|bool $check_user = false) : bool {
|
|
global $config;
|
|
|
|
// if we are only checking another users permission, don't check cache
|
|
if ($check_user == false) {
|
|
// user is not set, no permissions
|
|
if (!isset($_SESSION[SESS_USER_ID])) {
|
|
return false;
|
|
}
|
|
|
|
// check to see if the admin invalidated a permission
|
|
if (!is_user_perms_valid($_SESSION[SESS_USER_ID])) {
|
|
if (db_table_exists('user_auth_cache')) {
|
|
$enabled = db_fetch_cell_prepared('SELECT enabled
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$_SESSION[SESS_USER_ID]]);
|
|
|
|
if ($enabled == '' && get_guest_account() !== $_SESSION[SESS_USER_ID]) {
|
|
db_execute_prepared('DELETE FROM user_auth_cache
|
|
WHERE user_id = ?',
|
|
[$_SESSION[SESS_USER_ID]]);
|
|
|
|
kill_session_var(SESS_USER_ID);
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
kill_session_var(SESS_TREE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS);
|
|
|
|
print '<span style="display:none;">cactiLoginSuspend</span>';
|
|
|
|
exit;
|
|
} else {
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
kill_session_var(SESS_TREE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS);
|
|
}
|
|
|
|
print '<span style="display:none;">cactiRedirect</span>';
|
|
|
|
exit;
|
|
} else {
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
kill_session_var(SESS_TREE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS);
|
|
}
|
|
}
|
|
|
|
// if the permission is already valid, the session variable will be set
|
|
if (isset($_SESSION[SESS_USER_REALMS][$realm])) {
|
|
return $_SESSION[SESS_USER_REALMS][$realm];
|
|
}
|
|
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
$user_id = $check_user;
|
|
}
|
|
|
|
/**
|
|
* check the permissions from the table, should only happen once per login
|
|
* of after a permission change by the administrator.
|
|
*/
|
|
if (cacti_version_compare($config['cacti_db_version'], '1.0.0', '>=')) {
|
|
$user_realm = db_fetch_cell_prepared("SELECT realm_id
|
|
FROM user_auth_realm
|
|
WHERE user_id = ?
|
|
AND realm_id = ?
|
|
UNION
|
|
SELECT realm_id
|
|
FROM user_auth_group_realm AS uagr
|
|
INNER JOIN user_auth_group AS uag
|
|
ON uag.id = uagr.group_id
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagr.realm_id = ?
|
|
AND uagm.user_id = ?",
|
|
[$user_id, $realm, $realm, $user_id]);
|
|
} else {
|
|
$user_realm = db_fetch_cell_prepared('SELECT realm_id
|
|
FROM user_auth_realm
|
|
WHERE user_id = ?
|
|
AND realm_id = ?',
|
|
[$user_id, $realm]);
|
|
}
|
|
|
|
if (!empty($user_realm)) {
|
|
if ($check_user == false) {
|
|
$_SESSION[SESS_USER_REALMS][$realm] = true;
|
|
} else {
|
|
return true;
|
|
}
|
|
} else {
|
|
if ($check_user == false) {
|
|
$_SESSION[SESS_USER_REALMS][$realm] = false;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $_SESSION[SESS_USER_REALMS][$realm];
|
|
}
|
|
|
|
/**
|
|
* Get the permitted tree branch data available to the user
|
|
*
|
|
* @param int $tree_id The ID of the tree.
|
|
* @param int $parent_id The ID of the parent node.
|
|
* @param bool $editing Whether the user is in editing mode (default: false).
|
|
* @param int $user_id The ID of the user (default: 0).
|
|
*
|
|
* @return array The list of allowed tree items.
|
|
*/
|
|
function get_allowed_tree_level(int $tree_id, int $parent_id, bool $editing = false, int $user_id = 0) : array {
|
|
$items = db_fetch_assoc_prepared('SELECT gti.id, gti.title, gti.host_id,
|
|
gti.site_id, gti.local_graph_id, gti.host_grouping_type,
|
|
h.description AS hostname, s.name AS sitename
|
|
FROM graph_tree_items AS gti
|
|
INNER JOIN graph_tree AS gt
|
|
ON gt.id = gti.graph_tree_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gti.host_id
|
|
LEFT JOIN sites AS s
|
|
ON s.id = gti.site_id
|
|
WHERE gti.graph_tree_id = ?
|
|
AND gti.parent = ?
|
|
ORDER BY gti.position ASC',
|
|
[$tree_id, $parent_id]);
|
|
|
|
if (!$editing) {
|
|
$i = 0;
|
|
|
|
if (cacti_sizeof($items)) {
|
|
foreach ($items as $item) {
|
|
if ($item['host_id'] > 0) {
|
|
if (!is_device_allowed($item['host_id'], $user_id)) {
|
|
unset($items[$i]);
|
|
}
|
|
} elseif ($item['local_graph_id'] > 0) {
|
|
if (!is_graph_allowed($item['local_graph_id'], $user_id)) {
|
|
unset($items[$i]);
|
|
}
|
|
}
|
|
|
|
$i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Gathers items and statistics of those items that are permitted on the tree specified
|
|
*
|
|
* @param int $tree_id The ID of the tree to retrieve content for.
|
|
* @param int $parent The parent ID to filter the tree content. Default is 0.
|
|
* @param string $sql_where Additional SQL WHERE conditions. Default is an empty string.
|
|
* @param string $sql_order SQL ORDER BY clause. Default is an empty string.
|
|
* @param string $sql_limit SQL LIMIT clause. Default is an empty string.
|
|
* @param int $total_rows Reference to a variable to store the total number of rows. Default is 0.
|
|
* @param int $user_id The ID of the user to filter the allowed trees. Default is 0.
|
|
*
|
|
* @return array - An array of allowed tree content.
|
|
*/
|
|
function get_allowed_tree_content(int $tree_id, int $parent = 0, string $sql_where = '', string $sql_order = '', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0) : array {
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE gti.local_graph_id=0
|
|
AND gti.parent=$parent
|
|
AND gti.graph_tree_id=$tree_id
|
|
AND (" . $sql_where . ')';
|
|
} else {
|
|
$sql_where = "WHERE gti.local_graph_id=0
|
|
AND gti.parent=$parent
|
|
AND gti.graph_tree_id=$tree_id";
|
|
}
|
|
|
|
$trees = array_rekey(
|
|
get_allowed_trees(false, false, '', '', '', $total_rows, $user_id),
|
|
'id', 'name'
|
|
);
|
|
|
|
$hierarchy = [];
|
|
|
|
if ($tree_id > 0) {
|
|
if (cacti_sizeof($trees)) {
|
|
$sql_where .= ' AND gt.id IN (' . implode(', ', array_keys($trees)) . ')';
|
|
}
|
|
|
|
$hierarchy = db_fetch_assoc("SELECT gti.graph_tree_id AS tree_id, gti.id, gti.title, gti.host_id, gti.site_id,
|
|
gti.local_graph_id, gti.host_grouping_type, h.description AS hostname, s.name AS sitename
|
|
FROM graph_tree_items AS gti
|
|
INNER JOIN graph_tree AS gt
|
|
ON gt.id = gti.graph_tree_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gti.host_id
|
|
LEFT JOIN sites AS s
|
|
ON gti.site_id=s.id
|
|
$sql_where
|
|
ORDER BY gti.position");
|
|
} elseif (cacti_sizeof($trees)) {
|
|
$hierarchy = db_fetch_assoc("SELECT gt.id AS tree_id, '0' AS id, gt.name AS title, '0' AS host_id, '0' AS site_id,
|
|
'0' AS local_graph_id, '1' AS host_grouping_type, '' AS hostname, '' AS sitename
|
|
FROM graph_tree AS gt
|
|
WHERE enabled='on'
|
|
AND gt.id IN (" . implode(', ', array_keys($trees)) . ')
|
|
ORDER BY gt.sequence');
|
|
}
|
|
|
|
$new_hierarchy = [];
|
|
|
|
if (cacti_sizeof($hierarchy)) {
|
|
foreach ($hierarchy as $h) {
|
|
if ($h['host_id'] > 0) {
|
|
if (is_device_allowed($h['host_id'])) {
|
|
$new_hierarchy[] = $h;
|
|
}
|
|
} elseif ($h['id'] == 0) {
|
|
if (!is_tree_branch_empty($h['tree_id'], $h['id'])) {
|
|
if (is_tree_allowed($h['tree_id'])) {
|
|
$new_hierarchy[] = $h;
|
|
}
|
|
}
|
|
} elseif ($h['site_id'] > 0) {
|
|
$new_hierarchy[] = $h;
|
|
} elseif (!is_tree_branch_empty($h['tree_id'], $h['id'])) {
|
|
$new_hierarchy[] = $h;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $new_hierarchy;
|
|
}
|
|
|
|
/**
|
|
* Searches both the users and the users groups and returns a policy
|
|
* array to be used for determining object permissions.
|
|
*
|
|
* @param int $user_id The ID of the user for whom to retrieve policies.
|
|
*
|
|
* @return array - An array of policies
|
|
*/
|
|
function get_policies(int $user_id) : array {
|
|
// get policies for all user groups
|
|
$policies = db_fetch_assoc_prepared("SELECT uag.id, 'group' AS type, uag.name,
|
|
uag.policy_graphs, uag.policy_hosts, uag.policy_graph_templates, uag.policy_trees
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?",
|
|
[$user_id]);
|
|
|
|
// get policies for the user
|
|
$policies[] = db_fetch_row_prepared("SELECT id, 'user' AS type, 'user' AS name,
|
|
policy_graphs, policy_hosts, policy_graph_templates, policy_trees
|
|
FROM user_auth
|
|
WHERE id = ?",
|
|
[$user_id]);
|
|
|
|
return $policies;
|
|
}
|
|
|
|
/**
|
|
* Returns the graphs that are permitted at the branch/leaf id specified
|
|
*
|
|
* @param int $tree_id The ID of the tree.
|
|
* @param int $leaf_id The ID of the leaf (default is 0).
|
|
* @param string $sql_where Additional SQL WHERE conditions (default is '').
|
|
* @param string $sql_order SQL ORDER BY clause (default is 'gti.position').
|
|
* @param string $sql_limit SQL LIMIT clause (default is '').
|
|
* @param int $total_rows Reference to a variable to store the total number of rows (default is 0).
|
|
* @param int $user_id The ID of the user (default is 0).
|
|
*
|
|
* @return array - An array of allowed tree header graphs.
|
|
*/
|
|
function get_allowed_tree_header_graphs(int $tree_id, int $leaf_id = 0, string $sql_where = '', string $sql_order = 'gti.position', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($user_id == -1) {
|
|
$auth_method = 0;
|
|
} else {
|
|
$auth_method = read_config_option('auth_method');
|
|
}
|
|
|
|
if ($auth_method > 0 && $user_id == 0) {
|
|
if (isset($_SESSION['sess_user_id'])) {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = " AND ($sql_where)";
|
|
}
|
|
|
|
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;
|
|
|
|
if (read_user_setting('hide_disabled', false, false, $user_id) == 'on') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . '(h.disabled = "" OR h.disabled IS NULL)';
|
|
}
|
|
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
|
|
$graphs = db_fetch_assoc("SELECT gti.id, gti.title, gtg.local_graph_id, h.description,
|
|
gt.name AS template_name, gtg.title_cache, gtg.width, gtg.height, gl.host_id, h.disabled,
|
|
gl.snmp_index, gl.snmp_query_id
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id = gtg.local_graph_id
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON gti.local_graph_id = gl.id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
$sql_where
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$sql = "SELECT COUNT(*)
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id = gtg.local_graph_id
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON gti.local_graph_id = gl.id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
$sql_where";
|
|
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'graph');
|
|
|
|
return is_array($graphs) ? $graphs : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the graphs that are permitted by the user. Used for table displays
|
|
* where users will view the graphs that they are permitted to access
|
|
*
|
|
* @param string $sql_where The SQL where when searching for specific content
|
|
* @param mixed $sql_order The SQL Order clause to use for the sorting of graphs, if using order by
|
|
* Data source, the following must be passed in an array
|
|
*
|
|
* array(
|
|
* 'data_source' => 'traffic_in',
|
|
* 'order' => 'asc'|'desc',
|
|
* 'start_time' => unix_timestamp,
|
|
* 'end_time' => unix_timestamp,
|
|
* 'cf' => avg (0) | max (1)
|
|
* 'measure' => average | peak | sum | p25n | p50n | p75n | p90n | p95n
|
|
* )
|
|
*
|
|
* @param string $sql_limit SQL LIMIT clause.
|
|
* @param int &$total_rows Reference to a variable to store the total number of rows.
|
|
* @param int $user_id User ID for which to retrieve the graphs.
|
|
* @param int $graph_id Specific graph ID to retrieve.
|
|
*
|
|
* @return array An array of allowed graphs.
|
|
*/
|
|
function get_allowed_graphs(string $sql_where = '', mixed $sql_order = 'gtg.title_cache', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0, int $graph_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($user_id == -1) {
|
|
$auth_method = 0;
|
|
} else {
|
|
$auth_method = read_config_option('auth_method');
|
|
}
|
|
|
|
if ($auth_method > 0 && $user_id == 0) {
|
|
if (isset($_SESSION['sess_user_id'])) {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if ($sql_limit != '') {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
$sql_order_join = '';
|
|
|
|
if (is_array($sql_order)) {
|
|
require_once(CACTI_PATH_LIBRARY . '/dsstats.php');
|
|
require_once(CACTI_PATH_LIBRARY . '/html_utility.php');
|
|
|
|
$table = dsstats_get_best_partition($sql_order['start_time'], $sql_order['end_time']);
|
|
|
|
if ($table != '' && $sql_order['data_source'] != '') {
|
|
if ($sql_order['cf'] == 'avg') {
|
|
$cf = 0;
|
|
} elseif ($sql_order['cf'] == 'max') {
|
|
$cf = 1;
|
|
} else {
|
|
$cf = 0;
|
|
}
|
|
|
|
$sql_order_join = "INNER JOIN
|
|
(
|
|
SELECT DISTINCT gti.local_graph_id, ot.*
|
|
FROM graph_templates_item AS gti
|
|
INNER JOIN data_template_rrd AS dtr
|
|
ON gti.task_item_id = dtr.id
|
|
INNER JOIN $table AS ot
|
|
ON dtr.local_data_id = ot.local_data_id
|
|
WHERE ot.rrd_name = " . db_qstr($sql_order['data_source']) . "
|
|
AND ot.cf = $cf
|
|
) AS rs
|
|
ON gl.id = rs.local_graph_id";
|
|
|
|
$sql_order = get_dsstats_order_string($sql_order);
|
|
} elseif ($sql_order['data_source'] == '') {
|
|
cacti_log('WARNING: Graph Order missing Data Source name for ordering.', false, 'AUTH');
|
|
cacti_log('ORDER, DETAIL: ' . json_encode($sql_order), false, 'AUTH');
|
|
} else {
|
|
cacti_log('WARNING: Graph Order unable to determine table for ordering.', false, 'AUTH');
|
|
cacti_log('ORDER DETAIL: ' . json_encode($sql_order), false, 'AUTH');
|
|
}
|
|
} elseif ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
$sql_order = is_string($sql_order) ? $sql_order : '';
|
|
|
|
if ($graph_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : ' ') . ' gl.id = ' . $graph_id;
|
|
}
|
|
|
|
if (read_user_setting('hide_disabled', false, false, $user_id) == 'on') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . '(h.disabled = "" OR h.disabled IS NULL)';
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL) AND $sql_where";
|
|
} else {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL)";
|
|
}
|
|
|
|
// see if permissions are simple
|
|
$simple_perms = get_simple_graph_perms($user_id);
|
|
|
|
// in case we need to review get the graph_auth_method
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
if (!$simple_perms) {
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
}
|
|
|
|
$graphs = db_fetch_assoc("SELECT gtg.local_graph_id, h.description, gt.name AS template_name,
|
|
gtg.title_cache, gtg.width, gtg.height, gl.snmp_index, gl.snmp_query_id, gl.host_id, h.disabled,
|
|
IF(gl.graph_template_id = 0, 0, IF(gl.snmp_query_id = 0, 2, 1)) AS graph_source,
|
|
s.id AS site_id, s.name AS site_name, h.location
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id = gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
LEFT JOIN sites AS s
|
|
ON h.site_id = s.id
|
|
$sql_order_join
|
|
$sql_where
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$sql = "SELECT COUNT(*)
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id = gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
LEFT JOIN sites AS s
|
|
ON h.site_id = s.id
|
|
$sql_order_join
|
|
$sql_where";
|
|
|
|
if ($graph_id == 0) {
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'graph');
|
|
} else {
|
|
$total_rows = db_fetch_cell($sql);
|
|
}
|
|
|
|
return is_array($graphs) ? $graphs : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the aggregate graphs that are permitted by the user.
|
|
* Used for table displays where users will view the graphs that they are permitted to access
|
|
*
|
|
* @param string $sql_where Additional SQL WHERE conditions.
|
|
* @param string $sql_order SQL ORDER BY clause.
|
|
* @param string $sql_limit SQL LIMIT clause.
|
|
* @param int &$total_rows Reference to a variable to store the total number of rows.
|
|
* @param int $user_id The ID of the user.
|
|
* @param int $graph_id The ID of the graph.
|
|
*
|
|
* @return array The list of allowed aggregate graphs.
|
|
*/
|
|
function get_allowed_aggregate_graphs(string $sql_where = '', string $sql_order = 'gtg.title_cache', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0, int $graph_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($user_id == -1) {
|
|
$auth_method = 0;
|
|
} else {
|
|
$auth_method = read_config_option('auth_method');
|
|
}
|
|
|
|
if ($auth_method > 0 && $user_id == 0) {
|
|
if (isset($_SESSION['sess_user_id'])) {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($graph_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : ' ') . ' gl.id = ' . (int) $graph_id;
|
|
}
|
|
|
|
if (read_user_setting('hide_disabled', false, false, $user_id) == 'on') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . '(h.disabled = "" OR h.disabled IS NULL)';
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL) AND $sql_where";
|
|
} else {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL)";
|
|
}
|
|
|
|
// see if permissions are simple
|
|
$simple_perms = get_simple_graph_perms($user_id);
|
|
|
|
// in case we need to review get the graph_auth_method
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
if (!$simple_perms) {
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
}
|
|
|
|
$graphs = db_fetch_assoc("SELECT gtg.local_graph_id, '' AS description, gt.name AS template_name,
|
|
gtg.title_cache, gtg.width, gtg.height, gl.snmp_index, gl.snmp_query_id, gl.host_id, h.disabled
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN (
|
|
SELECT ag.local_graph_id AS id, gl.host_id, gl.graph_template_id,
|
|
gl.snmp_query_id, gl.snmp_query_graph_id, gl.snmp_index
|
|
FROM aggregate_graphs AS ag
|
|
INNER JOIN aggregate_graphs_items AS agi
|
|
ON ag.id=agi.aggregate_graph_id
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id=agi.local_graph_id
|
|
) AS gl
|
|
ON gl.id=gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id=gl.host_id
|
|
$sql_where
|
|
GROUP BY gtg.local_graph_id
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$sql = "SELECT COUNT(DISTINCT gl.id)
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN (
|
|
SELECT ag.local_graph_id AS id, gl.host_id, gl.graph_template_id,
|
|
gl.snmp_query_id, gl.snmp_query_graph_id, gl.snmp_index
|
|
FROM aggregate_graphs AS ag
|
|
INNER JOIN aggregate_graphs_items AS agi
|
|
ON ag.id=agi.aggregate_graph_id
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id=agi.local_graph_id
|
|
) AS gl
|
|
ON gl.id=gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id=gl.host_id
|
|
$sql_where";
|
|
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'aggregate_graph');
|
|
|
|
return is_array($graphs) ? $graphs : [];
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean true or false if the user has full access to
|
|
* all devices in the system. This function is used to shortcut complex queries that may
|
|
* take multiple seconds to return an answer.
|
|
*
|
|
* @param int $user The user ID to check permissions for.
|
|
*
|
|
* @return bool True if the user has simple device permissions, false otherwise.
|
|
*/
|
|
function get_simple_device_perms(int $user) : bool {
|
|
$policy_hosts = db_fetch_cell_prepared('SELECT policy_hosts
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user]);
|
|
|
|
$perm_count = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?
|
|
AND type = 2',
|
|
[$user]);
|
|
|
|
if ($policy_hosts == 1 && $perm_count == 0) {
|
|
return true;
|
|
} else {
|
|
$policies = db_fetch_assoc_prepared('SELECT policy_hosts, COUNT(*) AS exceptions
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_perms AS uagp
|
|
ON uag.id = uagp.group_id
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uagm.group_id = uag.id
|
|
WHERE uagp.type = 2
|
|
AND uagm.user_id = ?
|
|
GROUP BY uag.id',
|
|
[$user]);
|
|
|
|
if (cacti_sizeof($policies)) {
|
|
foreach ($policies as $p) {
|
|
if ($p['policy_hosts'] == 1 && $p['exceptions'] == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean true or false if the user has full access to
|
|
* all graphs in the system. This function is used to shortcut complex queries that may
|
|
* take multiple seconds to return an answer.
|
|
*
|
|
* @param int $user_id The ID of the user whose permissions are being retrieved.
|
|
*
|
|
* @return bool True if the user has simple graph permissions, false otherwise.
|
|
*/
|
|
function get_simple_graph_perms(int $user_id) : bool {
|
|
if (isset($_SESSION[SESS_SIMPLE_PERMS])) {
|
|
return $_SESSION[SESS_SIMPLE_PERMS];
|
|
}
|
|
|
|
$policy_graphs = db_fetch_cell_prepared('SELECT policy_graphs
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
$perm_count = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?
|
|
AND type = 1',
|
|
[$user_id]);
|
|
|
|
if ($policy_graphs == 1 && $perm_count == 0) {
|
|
$_SESSION[SESS_SIMPLE_PERMS] = true;
|
|
|
|
return true;
|
|
} else {
|
|
$policies = db_fetch_assoc_prepared('SELECT policy_graphs, COUNT(*) AS exceptions
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_perms AS uagp
|
|
ON uag.id = uagp.group_id
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uagm.group_id = uag.id
|
|
WHERE uagp.type = 1
|
|
AND uagm.user_id = ?
|
|
GROUP BY uag.id',
|
|
[$user_id]);
|
|
|
|
if (cacti_sizeof($policies)) {
|
|
foreach ($policies as $p) {
|
|
if ($p['policy_graphs'] == 1 && $p['exceptions'] == 0) {
|
|
$_SESSION[SESS_SIMPLE_PERMS] = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$_SESSION[SESS_SIMPLE_PERMS] = false;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean true or false if the user has full access to
|
|
* all graphs templates in the system. This function is used to shortcut complex queries that may
|
|
* take multiple seconds to return an answer.
|
|
*
|
|
* @param int $user_id The ID of the user whose permissions are being retrieved.
|
|
*
|
|
* @return bool True if the user has simple graph template permissions, false otherwise.
|
|
*/
|
|
function get_simple_graph_template_perms(int $user_id) : bool {
|
|
if (isset($_SESSION[SESS_SIMPLE_TEMPLATE_PERMS])) {
|
|
return $_SESSION[SESS_SIMPLE_TEMPLATE_PERMS];
|
|
}
|
|
|
|
$policy_graph_templates = db_fetch_cell_prepared('SELECT policy_graph_templates
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
$perm_count = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?
|
|
AND type = 4',
|
|
[$user_id]);
|
|
|
|
if ($policy_graph_templates == 1 && $perm_count == 0) {
|
|
$_SESSION[SESS_SIMPLE_TEMPLATE_PERMS] = true;
|
|
|
|
return true;
|
|
} else {
|
|
$policies = db_fetch_assoc_prepared('SELECT policy_graph_templates, COUNT(*) AS exceptions
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_perms AS uagp
|
|
ON uag.id = uagp.group_id
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uagm.group_id = uag.id
|
|
WHERE uagp.type = 4
|
|
AND uagm.user_id = ?
|
|
GROUP BY uag.id',
|
|
[$user_id]);
|
|
|
|
if (cacti_sizeof($policies)) {
|
|
foreach ($policies as $p) {
|
|
if ($p['policy_graph_templates'] == 1 && $p['exceptions'] == 0) {
|
|
$_SESSION[SESS_SIMPLE_TEMPLATE_PERMS] = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$_SESSION[SESS_SIMPLE_TEMPLATE_PERMS] = false;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the list of Graph Templates that the user is allowed to access.
|
|
* This function is generally intended for both listbox and table displays.
|
|
*
|
|
* @param string $sql_where Additional SQL WHERE conditions.
|
|
* @param string $sql_order SQL ORDER BY clause.
|
|
* @param string $sql_limit SQL LIMIT clause.
|
|
* @param int|null &$total_rows Reference to the total number of rows.
|
|
* @param int $user_id User ID to check permissions for.
|
|
* @param int $graph_template_id Specific graph template ID to filter by.
|
|
*
|
|
* @return array List of allowed graph templates.
|
|
*/
|
|
function get_allowed_graph_templates(string $sql_where = '', string $sql_order = 'gt.name', string $sql_limit = '', int|null &$total_rows = 0, int $user_id = 0, int $graph_template_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($user_id == -1) {
|
|
$auth_method = 0;
|
|
} else {
|
|
$auth_method = read_config_option('auth_method');
|
|
}
|
|
|
|
if ($user_id == 0) {
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// For timing
|
|
$start = microtime(true);
|
|
|
|
$init_rows = $total_rows;
|
|
|
|
$templates = [];
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($graph_template_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : ' ') . "(gl.graph_template_id = $graph_template_id)";
|
|
}
|
|
|
|
$sql_where = 'WHERE ' . ($sql_where != '' ? $sql_where . ' AND ' : ' ') . '(gt.id IS NOT NULL) ';
|
|
|
|
// see if permissions are simple
|
|
$simple_perms = get_simple_graph_perms($user_id);
|
|
|
|
// in case we need to review get the graph_auth_method
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
// short circuit if we don't have a user
|
|
if ($user_id == 0) {
|
|
return [];
|
|
}
|
|
|
|
if (!$simple_perms) {
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
}
|
|
|
|
if ($total_rows != -2) {
|
|
$templates = db_fetch_assoc("SELECT gt.id, gt.name, COUNT(*) AS graphs
|
|
FROM graph_local AS gl
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
$sql_where
|
|
GROUP BY gl.graph_template_id
|
|
$sql_order
|
|
$sql_limit");
|
|
}
|
|
|
|
if ($total_rows >= 0 || $total_rows == -2) {
|
|
$sql = "SELECT COUNT(DISTINCT gl.graph_template_id) AS id
|
|
FROM graph_local AS gl
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
$sql_where";
|
|
|
|
if ($graph_template_id == 0) {
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'graph');
|
|
} else {
|
|
$total_rows = db_fetch_cell($sql);
|
|
}
|
|
}
|
|
|
|
// For timing
|
|
$end = microtime(true);
|
|
|
|
cacti_log(sprintf('The Get Templates total time was %4.2f', $end - $start), false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
|
|
if (!is_array($templates)) {
|
|
$templates = [];
|
|
}
|
|
|
|
return $templates;
|
|
}
|
|
|
|
/**
|
|
* Parse the policies in order to visually display user permissions
|
|
*
|
|
* @param array $policies The list of user and group policies. Will be reversed to show user permissions first.
|
|
*
|
|
* @return array An associative array containing both $sql_select and $sql_join
|
|
*/
|
|
function get_policy_join_select(array $policies) : array {
|
|
$sql_join = '';
|
|
$sql_select = '';
|
|
|
|
$i = 1;
|
|
$j = 1;
|
|
|
|
foreach ($policies as $p) {
|
|
$sql_join .= 'LEFT JOIN (SELECT * FROM user_auth_' . ($p['type'] == 'user' ? '' : 'group_') . 'perms WHERE ' . $p['type'] . '_id = ' . $p['id'] . ") AS uap$j ON (gl.id = uap$j.item_id AND uap$j.type = 1) ";
|
|
$sql_select .= ($sql_select != '' ? ', ' : '') . "uap$j." . $p['type'] . "_id AS graph$i";
|
|
$j++;
|
|
|
|
$sql_join .= 'LEFT JOIN (SELECT * FROM user_auth_' . ($p['type'] == 'user' ? '' : 'group_') . 'perms WHERE ' . $p['type'] . '_id = ' . $p['id'] . ") AS uap$j ON (gl.host_id = uap$j.item_id AND uap$j.type = 3) ";
|
|
$sql_select .= ($sql_select != '' ? ', ' : '') . "uap$j." . $p['type'] . "_id AS device$i";
|
|
$j++;
|
|
|
|
$sql_join .= 'LEFT JOIN (SELECT * FROM user_auth_' . ($p['type'] == 'user' ? '' : 'group_') . 'perms WHERE ' . $p['type'] . '_id = ' . $p['id'] . ") AS uap$j ON (gl.graph_template_id = uap$j.item_id AND uap$j.type = 4) ";
|
|
$sql_select .= ($sql_select != '' ? ', ' : '') . "uap$j." . $p['type'] . "_id AS template$i";
|
|
$j++;
|
|
|
|
$i++;
|
|
}
|
|
|
|
return ['sql_select' => $sql_select, 'sql_join' => $sql_join];
|
|
}
|
|
|
|
/**
|
|
* Parse the policies in order to downselect matching graphs
|
|
* without the use of the SQL Having clause which is very inefficient
|
|
*
|
|
* @param string|int $graph_auth_method The graph auth method: permissive, restrictive, device, graph_template
|
|
* @param array $policies The list of user and group policies.
|
|
* @param string $sql_where The SQL where filter provided by the calling function.
|
|
*
|
|
* @return string Updated sql_where value
|
|
*/
|
|
function get_policy_where(string|int $graph_auth_method, array $policies, string $sql_where) : string {
|
|
if ($graph_auth_method == 1) {
|
|
// Policy Rows include
|
|
// id, type (group|user), policy_graphs, policy_hosts, policy_graph_templates
|
|
|
|
// Table user_auth_perms has user_id, item_id, type
|
|
// Types can be:
|
|
// 1 - Graph
|
|
// 2 - Trees
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
|
|
// Policies are
|
|
// 1 - Default Allow, all exceptions are denied
|
|
// 2 - Default Deny, all exceptions are allowed
|
|
|
|
// Permissive means access to the graph, or access to either the host or template
|
|
|
|
$sql_where .= ($sql_where != '' ? ' AND (' : 'WHERE (');
|
|
|
|
foreach ($policies as $index => $p) {
|
|
$sql_where .= ($index > 0 ? ' OR (' : '(');
|
|
|
|
if ($p['type'] == 'user') {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ' OR ';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
|
|
$sql_where .= ' OR ';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
} else {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ' OR ';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
|
|
$sql_where .= ' OR ';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
} elseif ($graph_auth_method == 2) {
|
|
// Policy Rows include
|
|
// id, type (group|user), policy_graphs, policy_hosts, policy_graph_templates
|
|
|
|
// Table user_auth_perms has user_id, item_id, type
|
|
// Types can be:
|
|
// 1 - Graph
|
|
// 2 - Trees
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
|
|
// Policies are
|
|
// 1 - Default Allow, all exceptions are denied
|
|
// 2 - Default Deny, all exceptions are allowed
|
|
|
|
// Restrictive means access to the graph, or access to both the host and template
|
|
|
|
$sql_where .= ($sql_where != '' ? ' AND (' : 'WHERE (');
|
|
|
|
foreach ($policies as $index => $p) {
|
|
$sql_where .= ($index == 0 ? '((' : ' OR ((');
|
|
|
|
if ($p['type'] == 'user') {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
|
|
$sql_where .= ' AND ';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= ' gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= ' gl.graph_template_id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
} else {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
|
|
$sql_where .= ' AND ';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
} elseif ($graph_auth_method == 3) {
|
|
// Policy Rows include
|
|
// id, type (group|user), policy_graphs, policy_hosts, policy_graph_templates
|
|
|
|
// Table user_auth_perms has user_id, item_id, type
|
|
// Types can be:
|
|
// 1 - Graph
|
|
// 2 - Trees
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
|
|
// Policies are
|
|
// 1 - Default Allow, all exceptions are denied
|
|
// 2 - Default Deny, all exceptions are allowed
|
|
|
|
$sql_where .= ($sql_where != '' ? ' AND (' : 'WHERE (');
|
|
|
|
foreach ($policies as $index => $p) {
|
|
$sql_where .= ($index == 0 ? '((' : ') OR ((');
|
|
|
|
if ($p['type'] == 'user') {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
} else {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= '))';
|
|
} elseif ($graph_auth_method == 4) {
|
|
// Policy Rows include
|
|
// id, type (group|user), policy_graphs, policy_hosts, policy_graph_templates
|
|
|
|
// Table user_auth_perms has user_id, item_id, type
|
|
// Types can be:
|
|
// 1 - Graph
|
|
// 2 - Trees
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
|
|
// Policies are
|
|
// 1 - Default Allow, all exceptions are denied
|
|
// 2 - Default Deny, all exceptions are allowed
|
|
|
|
$sql_where .= ($sql_where != '' ? ' AND (' : 'WHERE (');
|
|
|
|
foreach ($policies as $index => $p) {
|
|
$sql_where .= ($index == 0 ? '((' : ') OR ((');
|
|
|
|
if ($p['type'] == 'user') {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
} else {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id =' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= '))';
|
|
}
|
|
|
|
return $sql_where;
|
|
}
|
|
|
|
/**
|
|
* Get the effective permission string for the graph in question. The
|
|
* logic for this is somewhat complex, but understandable. First, the $graph object will include
|
|
* three columns generally graphX, deviceX, and templateX for each of the user or groups in the collection.
|
|
* The way we assign a restrictive or permissive value is based upon the graph permission setting
|
|
* in Cacti, but also whether or not the default access for the object type is either 'Allow' or 'Deny'.
|
|
*
|
|
* - If the 'default access' for the object type is 'Deny', then a numeric value in userX means
|
|
* the user has permission to an object.
|
|
* - If the default access for the object is 'Allow', then a numeric
|
|
* value in userX means that the object is blocked.
|
|
*
|
|
* Again, each of the graphX, deviceX, and templateX will always come in three's. They equate to:
|
|
*
|
|
* - graphX - The user does or does not have permission to the Graph at the Graph Level
|
|
* - deviceX - The user does or does not have permission to the Graph at the Device Level
|
|
* - templateX - The user does or does not have permission to the Graph at the Graph Template Level
|
|
*
|
|
* Then, the effective permission are calculated by the Graph Permission Model in Cacti. They are
|
|
*
|
|
* - Permissive - If the user has access to the Graph, the Device, or Graph Template, then the
|
|
* user will have access to the Graph.
|
|
*
|
|
* - Restrictive - If the user has access to the Graph, or both the Device and Graph Template,
|
|
* then the user will have access to the Graph
|
|
*
|
|
* - Device - If the user has access to the Graph, or the Device, then the user will have
|
|
* access to the Graph
|
|
*
|
|
* - Graph Template - If the user has access to the Graph, or the Graph Template, then the user
|
|
* will have access to the Graph.
|
|
*
|
|
* This function will apply this logic, and then respond to the user a 'Granted' or 'Restricted'
|
|
* column value, and a Tooltip, that shows how the permissions were evaluated. In other words
|
|
* why was the user either permitted to or denied access to the Graph.
|
|
*
|
|
* @param array $graph The graph data.
|
|
* @param array $policies The policies to evaluate.
|
|
*
|
|
* @return string The permission string
|
|
*/
|
|
function get_permission_string(array &$graph, array &$policies) : string {
|
|
$grantStr = '';
|
|
$rejectStr = '';
|
|
$reasonStr = '';
|
|
$drejectStr = '';
|
|
|
|
// Methods:
|
|
// 1 - Permissive
|
|
// 2 - Restrictive
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
$method = read_config_option('graph_auth_method');
|
|
|
|
if ($graph['disabled'] == 'on' && read_user_setting('hide_disabled', false, false, grv('user_id'))) {
|
|
$drejectStr .= __esc('Device:(Hide Disabled)');
|
|
}
|
|
|
|
// Policies:
|
|
// 1 - Default Allow All - Numeric means blocked
|
|
// 2 - Default Deny All - Numeric means allowed
|
|
|
|
$i = 1;
|
|
|
|
foreach ($policies as $p) {
|
|
$allowed = 0;
|
|
$rejected = 0;
|
|
|
|
// Perform the Graph Check first
|
|
// If a user has access at the Graph Access, they always get Access
|
|
if ($p['policy_graphs'] == 1) {
|
|
// Default is to allow
|
|
if (empty($graph["graph$i"])) {
|
|
// Allow the access at the level
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Graph:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr .= ($rejectStr != '' ? ', ' : '') . __esc('Graph:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
// Default is to Deny
|
|
if (!empty($graph["graph$i"])) {
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Graph:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr .= ($rejectStr != '' ? ', ' : '') . __esc('Graph:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
$rejected++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Now we check at the Device and Graph Template Level. Here the permission get a bit more dicey, so we will
|
|
* Use case logic to make it read more simply.
|
|
*/
|
|
switch($method) {
|
|
case 1: // Permissive
|
|
if ($p['policy_hosts'] == 1) {
|
|
if (empty($graph["device$i"])) {
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Device:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
if (!empty($graph["device$i"])) {
|
|
$grantStr = $grantStr . ($grantStr != '' ? ', ' : '') . __esc('Device:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
}
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
if (empty($graph["template$i"])) {
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
if (!empty($graph["template$i"])) {
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
}
|
|
|
|
if ($rejected == 3) {
|
|
$rejectStr .= ($rejectStr != '' ? ', ' : '') . __esc('Graph+Device+Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
}
|
|
|
|
break;
|
|
case 2: // Restrictive
|
|
$allowed = 0;
|
|
$rejected = 0;
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
if (empty($graph["device$i"])) {
|
|
$allowed++;
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
if (!empty($graph["device$i"])) {
|
|
$allowed++;
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
}
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
if (empty($graph["template$i"])) {
|
|
$allowed++;
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
if (!empty($graph["template$i"])) {
|
|
$rejected++;
|
|
} else {
|
|
$allowed++;
|
|
}
|
|
}
|
|
|
|
if ($allowed == 2) {
|
|
$grantStr = $grantStr . ($grantStr != '' ? ', ' : '') . __esc('Device+Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr = $rejectStr . ($rejectStr != '' ? ', ' : '') . __esc('Device+Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
}
|
|
|
|
break;
|
|
case 3: // Device
|
|
if ($p['policy_hosts'] == 1) {
|
|
if (empty($graph["device$i"])) {
|
|
$grantStr = $grantStr . ($grantStr != '' ? ', ' : '') . __esc('Device:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr = $rejectStr . ($rejectStr != '' ? ', ' : '') . __esc('Device:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
}
|
|
} else {
|
|
if (!empty($graph["device$i"])) {
|
|
$grantStr = $grantStr . ($grantStr != '' ? ', ' : '') . __esc('Device:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr = $rejectStr . ($rejectStr != '' ? ', ' : '') . __esc('Device:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
}
|
|
}
|
|
|
|
break;
|
|
case 4: // Graph Template
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
if (empty($graph["template$i"])) {
|
|
$grantStr = $grantStr . ($grantStr != '' ? ', ' : '') . __esc('Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr = $rejectStr . ($rejectStr != '' ? ', ' : '') . __esc('Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
}
|
|
} else {
|
|
if (!empty($graph["template$i"])) {
|
|
$grantStr = $grantStr . ($grantStr != '' ? ', ' : '') . __esc('Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr = $rejectStr . ($rejectStr != '' ? ', ' : '') . __esc('Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
$i++;
|
|
}
|
|
|
|
$permStr = '';
|
|
|
|
if ($drejectStr != '') {
|
|
$reasonStr .= ($reasonStr != '' ? ', ' : '') . __esc('Restricted By: ') . $drejectStr;
|
|
}
|
|
|
|
if ($grantStr != '') {
|
|
$reasonStr .= ($reasonStr != '' ? ', ' : '') . __esc('Granted By: ') . trim($grantStr, ',');
|
|
|
|
if ($rejectStr != '') {
|
|
$reasonStr .= ', ' . __esc('Restricted By: ') . trim($rejectStr, ',');
|
|
}
|
|
|
|
if ($drejectStr == '') {
|
|
$permStr = "<span data-tooltip='" . trim($reasonStr) . "' class='accessGranted'>" . __('Granted') . '</span>';
|
|
} else {
|
|
$permStr = "<span data-tooltip='" . trim($reasonStr) . "' class='accessRestricted'>" . __('Restricted') . '</span>';
|
|
}
|
|
} elseif ($rejectStr != '') {
|
|
$reasonStr .= ($reasonStr != '' ? ', ' : '') . __esc('Restricted By: ') . trim($rejectStr, ',');
|
|
|
|
$permStr = "<span data-tooltip='" . $reasonStr . "' class='accessRestricted'>" . __('Restricted') . '</span>';
|
|
} else {
|
|
$permStr = __('Unknown');
|
|
}
|
|
|
|
return $permStr;
|
|
}
|
|
|
|
/**
|
|
* Returns the list of Trees that the user is allowed to access.
|
|
* This function is generally intended for both listbox and table displays as well as to build out the tree for a user.
|
|
*
|
|
* @param bool $edit If true, includes trees that are not enabled.
|
|
* @param bool $return_sql If true, returns the SQL query instead of executing it.
|
|
* @param string $sql_where Additional SQL WHERE conditions.
|
|
* @param string $sql_order SQL ORDER BY clause.
|
|
* @param string $sql_limit SQL LIMIT clause.
|
|
* @param int &$total_rows Reference to a variable to store the total number of rows.
|
|
* @param int $user_id The ID of the user. If 0, the current session user ID is used.
|
|
*
|
|
* @return array|string The allowed graph trees or the SQL query string if $return_sql is true.
|
|
*/
|
|
function get_allowed_trees(bool $edit = false, bool $return_sql = false, string $sql_where = '', string $sql_order = 'name', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0) : array|string {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
$auth_method = read_config_option('auth_method');
|
|
|
|
if ($user_id == 0) {
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// get policies for all groups and user
|
|
$policies = db_fetch_assoc_prepared("SELECT uag.id, 'group' AS type, policy_trees FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?",
|
|
[$user_id]);
|
|
|
|
$policies[] = db_fetch_row_prepared("SELECT id, 'user' as type, policy_trees FROM user_auth WHERE id = ?", [$user_id]);
|
|
|
|
$i = 0;
|
|
$sql_join = '';
|
|
$sql_where1 = '';
|
|
|
|
foreach ($policies as $policy) {
|
|
if ($policy['policy_trees'] == '1') {
|
|
$sql_where1 .= ($sql_where1 != '' ? ' OR' : '') . " uap$i." . $policy['type'] . '_id IS NULL';
|
|
} elseif ($policy['policy_trees'] == '2') {
|
|
$sql_where1 .= ($sql_where1 != '' ? ' OR' : '') . " uap$i." . $policy['type'] . '_id IS NOT NULL';
|
|
}
|
|
|
|
$sql_join .= 'LEFT JOIN user_auth_' . ($policy['type'] == 'group' ? 'group_' : '') . "perms AS uap$i
|
|
ON (gt.id=uap$i.item_id AND uap$i.type=2 AND uap$i." . $policy['type'] . '_id=' . $policy['id'] . ') ';
|
|
|
|
$i++;
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = 'WHERE ' . ($edit == false ? '(gt.enabled="on") AND ' : '') . '(' . $sql_where . ') AND (' . $sql_where1 . ')';
|
|
} else {
|
|
$sql_where = 'WHERE ' . ($edit == false ? '(gt.enabled="on") AND ' : '') . '(gt.enabled="on") AND (' . $sql_where1 . ')';
|
|
}
|
|
|
|
$sql = "SELECT id, name
|
|
FROM graph_tree AS gt
|
|
$sql_join
|
|
$sql_where
|
|
$sql_order
|
|
$sql_limit";
|
|
|
|
if ($return_sql) {
|
|
return $sql;
|
|
} else {
|
|
$trees = db_fetch_assoc($sql);
|
|
|
|
$sql = "SELECT COUNT(gt.id)
|
|
FROM graph_tree AS gt
|
|
$sql_join
|
|
$sql_where";
|
|
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'tree');
|
|
}
|
|
|
|
return is_array($trees) ? $trees : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the list of Tree branches that the user is allowed to access.
|
|
* This function is generally intended for both listbox and table displays as well as to build out the tree for a user.
|
|
*
|
|
* @param string $sql_where Optional SQL WHERE clause to filter the branches.
|
|
* @param string $sql_order Optional SQL ORDER BY clause to sort the branches. Default is 'name'.
|
|
* @param string $sql_limit Optional SQL LIMIT clause to limit the number of branches returned.
|
|
* @param int &$total_rows Reference to a variable to store the total number of rows.
|
|
* @param int $user_id Optional user ID to retrieve branches for. Default is 0.
|
|
*
|
|
* @return array An array of allowed branches.
|
|
*/
|
|
function get_allowed_branches(string $sql_where = '', string $sql_order = 'name', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$sql_join = '';
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
// suppress total rows
|
|
$total_rows = -1;
|
|
|
|
$auth_method = read_config_option('auth_method');
|
|
|
|
if ($user_id == 0) {
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
$simple_perms = get_simple_device_perms($user_id);
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
$sql_hosts_where = '';
|
|
|
|
if (!$simple_perms) {
|
|
$hosts = get_allowed_devices('', 'description', '', $total_rows);
|
|
|
|
if (cacti_sizeof($hosts)) {
|
|
$hosts = array_rekey($hosts, 'id', 'description');
|
|
}
|
|
|
|
$tree_hosts = db_fetch_assoc("SELECT DISTINCT h.id, h.description
|
|
FROM graph_tree_items AS gti
|
|
INNER JOIN graph_tree AS gt
|
|
ON gti.graph_tree_id = gt.id
|
|
INNER JOIN host AS h
|
|
ON gti.host_id = h.id
|
|
WHERE gti.host_id > 0
|
|
AND $sql_where", false);
|
|
|
|
if (cacti_sizeof($tree_hosts)) {
|
|
$tree_hosts = array_rekey($tree_hosts, 'id', 'description');
|
|
}
|
|
|
|
if (!is_array($tree_hosts)) {
|
|
$tree_hosts = [];
|
|
}
|
|
|
|
$hosts = array_intersect_key($hosts, $tree_hosts);
|
|
|
|
if (cacti_sizeof($hosts) > 0) {
|
|
$sql_hosts_where = 'AND h.id IN (' . implode(',', array_keys($hosts)) . ')';
|
|
}
|
|
}
|
|
|
|
$i = 0;
|
|
$sql_join = '';
|
|
$sql_where1 = '';
|
|
|
|
foreach ($policies as $policy) {
|
|
if ($policy['policy_trees'] == '1') {
|
|
$sql_where1 .= ($sql_where1 != '' ? ' OR' : '') . " uap$i." . $policy['type'] . '_id IS NULL';
|
|
} elseif ($policy['policy_trees'] == '2') {
|
|
$sql_where1 .= ($sql_where1 != '' ? ' OR' : '') . " uap$i." . $policy['type'] . '_id IS NOT NULL';
|
|
}
|
|
|
|
$sql_join .= 'LEFT JOIN user_auth_' . ($policy['type'] == 'group' ? 'group_' : '') . "perms AS uap$i
|
|
ON (gt.id=uap$i.item_id AND uap$i.type=2 AND uap$i." . $policy['type'] . '_id=' . $policy['id'] . ') ';
|
|
|
|
$i++;
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = 'WHERE gt.enabled="on" AND (' . $sql_where . ')' . ($sql_where1 != '' ? ' AND (' . $sql_where1 . ')' : '');
|
|
} elseif ($sql_where1 != '') {
|
|
$sql_where = 'WHERE gt.enabled="on" AND (' . $sql_where1 . ')';
|
|
}
|
|
|
|
$sql = "(SELECT gti.id, CONCAT('" . __('Branch:') . " ', gti.title) AS name
|
|
FROM graph_tree AS gt
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON gti.graph_tree_id = gt.id
|
|
AND gti.host_id = 0
|
|
AND gti.local_graph_id=0
|
|
$sql_join
|
|
$sql_where
|
|
) UNION (
|
|
SELECT gti.id, CONCAT('" . __('Device:') . " ', h.description) AS name
|
|
FROM graph_tree AS gt
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON gti.graph_tree_id = gt.id
|
|
INNER JOIN host AS h
|
|
ON h.id=gti.host_id
|
|
$sql_hosts_where
|
|
$sql_join
|
|
$sql_where
|
|
)
|
|
$sql_order
|
|
$sql_limit";
|
|
|
|
$branches = db_fetch_assoc($sql);
|
|
|
|
$sql = 'SELECT COUNT(*) FROM (' . $sql . ') AS rower';
|
|
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'branch');
|
|
|
|
return is_array($branches) ? $branches : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the list of devices that the user is allowed to access.
|
|
* This function is generally intended for both listbox and table displays as well as other tasks.
|
|
*
|
|
* @param string $sql_where Additional SQL WHERE conditions to filter devices.
|
|
* @param string $sql_order SQL ORDER BY clause to sort the results. Default is 'description'.
|
|
* @param string $sql_limit SQL LIMIT clause to limit the number of results. Default is no limit.
|
|
* @param int &$total_rows Reference to a variable to store the total number of rows found.
|
|
* @param int $user_id The ID of the user for whom to retrieve allowed devices. Default is 0.
|
|
* @param int $device_id The ID of a specific device to retrieve. Default is 0.
|
|
*
|
|
* @return array An array of allowed devices for the user.
|
|
*/
|
|
function get_allowed_devices(string $sql_where = '', string $sql_order = 'description', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0, int $device_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$auth_method = read_config_option('auth_method');
|
|
|
|
if ($user_id == 0) {
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
$simple_perms = get_simple_device_perms($user_id);
|
|
|
|
$init_rows = $total_rows;
|
|
|
|
$host_list = [];
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if (read_user_setting('hide_disabled', false, false, $user_id) == 'on') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . '(h.disabled = "" OR h.disabled IS NULL)';
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL) AND $sql_where";
|
|
} else {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL) ";
|
|
}
|
|
|
|
if ($device_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . ' h.id = ' . (int) $device_id;
|
|
}
|
|
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
if (!$simple_perms) {
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
}
|
|
|
|
if ($total_rows != -2) {
|
|
$host_list = db_fetch_assoc("SELECT h1.*
|
|
FROM host AS h1
|
|
INNER JOIN (
|
|
SELECT DISTINCT id
|
|
FROM (
|
|
SELECT h.id
|
|
FROM host AS h
|
|
LEFT JOIN graph_local AS gl
|
|
ON h.id=gl.host_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN host_template AS ht
|
|
ON h.host_template_id=ht.id
|
|
$sql_where
|
|
) AS rs1
|
|
) AS rs2
|
|
ON rs2.id=h1.id
|
|
$sql_order
|
|
$sql_limit");
|
|
}
|
|
|
|
if ($total_rows >= 0 || $total_rows == -2) {
|
|
$sql = "SELECT COUNT(DISTINCT id)
|
|
FROM (
|
|
SELECT h.id
|
|
FROM host AS h
|
|
LEFT JOIN graph_local AS gl
|
|
ON h.id=gl.host_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN host_template AS ht
|
|
ON h.host_template_id=ht.id
|
|
$sql_where
|
|
) AS rower";
|
|
|
|
if ($device_id == 0) {
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'device');
|
|
} else {
|
|
$total_rows = db_fetch_cell($sql);
|
|
}
|
|
}
|
|
|
|
return is_array($host_list) ? $host_list : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the list of sites that the user is allowed to access.
|
|
* This function is generally intended for both listbox and table displays as well as other tasks.
|
|
*
|
|
* @param string $sql_where Optional SQL WHERE clause to filter the sites.
|
|
* @param string $sql_order Optional SQL ORDER BY clause to sort the sites. Default is 'name'.
|
|
* @param string $sql_limit Optional SQL LIMIT clause to limit the number of results.
|
|
* @param int &$total_rows Reference to a variable to store the total number of rows.
|
|
* @param int $user_id Optional user ID to filter the sites by user. Default is 0.
|
|
* @param int $site_id Optional site ID to filter the sites by a specific site. Default is 0.
|
|
* @return array An associative array of allowed sites.
|
|
*/
|
|
function get_allowed_sites(string $sql_where = '', string $sql_order = 'name', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0, int $site_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE $sql_where";
|
|
}
|
|
|
|
if ($site_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . " s.id=$site_id";
|
|
}
|
|
|
|
$auth_method = read_config_option('auth_method');
|
|
|
|
if (isset($_SESSION[SESS_USER_ID]) && $user_id == 0) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
return [];
|
|
}
|
|
|
|
$sites = db_fetch_assoc("SELECT s.id, s.name
|
|
FROM sites AS s
|
|
INNER JOIN host AS h
|
|
ON s.id=h.site_id
|
|
$sql_where
|
|
GROUP BY s.id
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$sql = "SELECT COUNT(DISTINCT s.id)
|
|
FROM sites AS s
|
|
INNER JOIN host AS h
|
|
ON s.id=h.site_id
|
|
$sql_where";
|
|
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'site_device');
|
|
|
|
return is_array($sites) ? $sites : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the list of devices in a site that the user is allowed to access.
|
|
* This function is generally intended for both listbox and table displays as well as other tasks.
|
|
*
|
|
* @param int $site_id The ID of the site to filter devices by.
|
|
* @param string $sql_where Additional SQL WHERE conditions to apply.
|
|
* @param string $sql_order The column to order the results by. Default is 'description'.
|
|
* @param string $sql_limit The limit for the number of results to return.
|
|
* @param int &$total_rows The total number of rows that match the criteria.
|
|
* @param int $user_id The ID of the user to check permissions for. Default is 0.
|
|
*
|
|
* @return array An associative array of allowed site devices.
|
|
*/
|
|
function get_allowed_site_devices(int $site_id, string $sql_where = '', string $sql_order = 'description', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$auth_method = read_config_option('auth_method');
|
|
|
|
if ($user_id == 0) {
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
$simple_perms = get_simple_device_perms($user_id);
|
|
|
|
$policies = get_policies($user_id);
|
|
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if (read_user_setting('hide_disabled', false, false, $user_id) == 'on') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . '(h.disabled = "" OR h.disabled IS NULL)';
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL) AND $sql_where";
|
|
} else {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL) ";
|
|
}
|
|
|
|
if ($site_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . " h.site_id=$site_id";
|
|
}
|
|
|
|
if (!$simple_perms) {
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
}
|
|
|
|
$host_list = db_fetch_assoc("SELECT h1.*, ht.name AS host_template_name
|
|
FROM host AS h1
|
|
LEFT JOIN host_template AS ht
|
|
ON h1.host_template_id=ht.id
|
|
INNER JOIN (
|
|
SELECT DISTINCT id
|
|
FROM (
|
|
SELECT h.id
|
|
FROM host AS h
|
|
LEFT JOIN graph_local AS gl
|
|
ON h.id=gl.host_id
|
|
LEFT JOIN graph_templates_graph AS gtg
|
|
ON gl.id=gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN host_template AS ht
|
|
ON h.host_template_id=ht.id
|
|
$sql_where
|
|
) AS rs1
|
|
) AS rs2
|
|
ON rs2.id=h1.id
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$sql = "SELECT COUNT(DISTINCT id)
|
|
FROM (
|
|
SELECT h.id
|
|
FROM host AS h
|
|
LEFT JOIN graph_local AS gl
|
|
ON h.id=gl.host_id
|
|
LEFT JOIN graph_templates_graph AS gtg
|
|
ON gl.id=gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN host_template AS ht
|
|
ON h.host_template_id=ht.id
|
|
$sql_where
|
|
) AS rower";
|
|
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'site_device');
|
|
|
|
return is_array($host_list) ? $host_list : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the list of graph templates aligned with the to be able to differentiate
|
|
* between Graph Templates based on a non-data query data input mode
|
|
* and those related to data queries.
|
|
*
|
|
* @param string $sql_where Optional SQL WHERE clause to filter the results.
|
|
* @param string $sql_order Optional SQL ORDER BY clause to sort the results. Default is 'name'.
|
|
* @param string $sql_limit Optional SQL LIMIT clause to limit the number of results.
|
|
* @param int|null &$total_rows Reference to a variable to store the total number of rows.
|
|
* @param int $user_id ID of the user to validate permissions.
|
|
* @param int $graph_template_id ID of the graph template to filter the results.
|
|
*
|
|
* @return array An associative array of allowed graph templates, keyed by their IDs and names.
|
|
*/
|
|
function get_allowed_graph_templates_normalized(string $sql_where = '', string $sql_order = 'name', string $sql_limit = '', int|null &$total_rows = 0, int $user_id = 0, int $graph_template_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$templates = array_rekey(get_allowed_graph_templates($sql_where, $sql_order, $sql_limit, $total_rows, $user_id, $graph_template_id), 'id', 'name');
|
|
|
|
if (!cacti_sizeof($templates)) {
|
|
return [];
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = ' WHERE (' . $sql_where . ') AND gl.graph_template_id IN(' . implode(', ', array_keys($templates)) . ') AND (gl.snmp_query_graph_id=0 OR sqg.name IS NOT NULL) AND (gt.name IS NOT NULL)';
|
|
} else {
|
|
$sql_where = ' WHERE gl.graph_template_id IN(' . implode(', ', array_keys($templates)) . ') AND (gl.snmp_query_graph_id=0 OR sqg.name IS NOT NULL) AND (gt.name IS NOT NULL)';
|
|
}
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
$templates = db_fetch_assoc("SELECT
|
|
IF(snmp_query_graph_id=0, CONCAT('cg_',gl.graph_template_id), CONCAT('dq_', gl.snmp_query_graph_id)) AS id,
|
|
IF(snmp_query_graph_id=0, gt.name, IF(gt.name = sqg.name, gt.name, CONCAT(gt.name, ' [', sqg.name, ']'))) AS name
|
|
FROM graph_local AS gl
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN snmp_query_graph AS sqg
|
|
ON gl.snmp_query_graph_id=sqg.id
|
|
AND gl.graph_template_id=sqg.graph_template_id
|
|
$sql_where
|
|
GROUP BY id, name
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
return is_array($templates) ? $templates : [];
|
|
}
|
|
|
|
/**
|
|
* Returns true or false depending on if the user is valid for the system
|
|
* users with an id of 0 or -1 are special cases. All non-zero users should be found in the
|
|
* user_auth table.
|
|
*
|
|
* @param int $user_id The ID of the user to validate.
|
|
*
|
|
* @return bool True if the user exists, false otherwise.
|
|
*/
|
|
function auth_valid_user(int $user_id) : bool {
|
|
static $users = [];
|
|
|
|
// perform a check if the user exists
|
|
if ($user_id > 0) {
|
|
if (isset($users[$user_id]) && $users[$user_id] == true) {
|
|
return true;
|
|
}
|
|
|
|
if (isset($users[$user_id])) {
|
|
return false;
|
|
} else {
|
|
$exists = db_fetch_cell_prepared('SELECT id FROM user_auth WHERE id = ?', [$user_id]);
|
|
|
|
if (empty($exists)) {
|
|
cacti_log(sprintf('ERROR: Invalid Cacti User ID %d is being used in a permission that does not exist', $user_id), false, 'AUTH');
|
|
|
|
cacti_debug_backtrace('Invalid User Account');
|
|
|
|
$users[$user_id] = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
$users[$user_id] = true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Purge one or more row Cache classes
|
|
*
|
|
* @param int $user_id The ID of the user whose cache should be purged. If 0, purges cache for all users.
|
|
* @param string $class The class of cache to purge. Defaults to 'all'.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_row_cache_purge(int $user_id, string $class = 'all') : void {
|
|
if ($user_id > 0) {
|
|
if ($class == 'all') {
|
|
db_execute_prepared('DELETE
|
|
FROM user_auth_row_cache
|
|
WHERE user_id = ?',
|
|
[$user_id]);
|
|
} else {
|
|
db_execute_prepared('DELETE
|
|
FROM user_auth_row_cache
|
|
WHERE user_id = ?
|
|
AND class = ?',
|
|
[$user_id, $class]);
|
|
}
|
|
} else {
|
|
if ($class == 'all') {
|
|
db_execute('TRUNCATE user_auth_row_cache');
|
|
} else {
|
|
db_execute_prepared('DELETE
|
|
FROM user_auth_row_cache
|
|
WHERE class = ?',
|
|
[$class]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the total rows based upon a set of criteria
|
|
*
|
|
* This function will hash the $sql, and then search for the total
|
|
* row counter based upon that criteria and if it finds a unexpired
|
|
* match for that data, it will return the row count in the table
|
|
* otherwise, it will execute the SQL and return the data.
|
|
*
|
|
* @param int $user_id The ID of the user.
|
|
* @param string $sql The SQL query to execute.
|
|
* @param array $sql_params Optional. The parameters for the SQL query. Default is an empty array.
|
|
* @param string $class Optional. The class name for caching purposes. Default is an empty string.
|
|
* @param int $timeout Optional. The cache timeout in seconds. Default is 86400 (24 hours).
|
|
*
|
|
* @return int The total number of rows retrieved by the SQL query.
|
|
*/
|
|
function get_total_row_data(int $user_id, string $sql, array $sql_params = [], string $class = '', int $timeout = 86400) : int {
|
|
$execute = true;
|
|
$now_time = time();
|
|
|
|
if (cacti_sizeof($sql_params)) {
|
|
$nsql = json_encode([$sql, $sql_params]);
|
|
|
|
$hash = md5((string) $nsql);
|
|
} else {
|
|
$hash = md5($sql);
|
|
}
|
|
|
|
$row_data = db_fetch_row_prepared('SELECT total_rows, UNIX_TIMESTAMP(time) AS time
|
|
FROM user_auth_row_cache
|
|
WHERE user_id = ?
|
|
AND class = ?
|
|
AND hash = ?',
|
|
[$user_id, $class, $hash]);
|
|
|
|
$cached = false;
|
|
|
|
if (cacti_sizeof($row_data)) {
|
|
$cached = true;
|
|
$last_time = read_config_option('time_last_change_' . $class);
|
|
|
|
if (!empty($last_time)) {
|
|
if ($row_data['time'] >= $last_time) {
|
|
return $row_data['total_rows'];
|
|
}
|
|
} elseif ($now_time - $row_data['time'] < $timeout) {
|
|
return $row_data['total_rows'];
|
|
}
|
|
}
|
|
|
|
if (cacti_sizeof($sql_params)) {
|
|
$rows = db_fetch_cell_prepared($sql, $sql_params);
|
|
} else {
|
|
$rows = db_fetch_cell($sql);
|
|
}
|
|
|
|
if ($user_id > 0) {
|
|
db_execute_prepared('REPLACE INTO user_auth_row_cache
|
|
(user_id, class, hash, total_rows, time)
|
|
VALUES (?, ?, ?, ?, FROM_UNIXTIME(?))',
|
|
[$user_id, $class, $hash, $rows, $now_time]);
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of hosts taking permissions into account if necessary
|
|
*
|
|
* @return array An array of strings, each containing the description and hostname of a device.
|
|
*/
|
|
function get_host_array() : array {
|
|
$total_rows = -1;
|
|
$return_devices = [];
|
|
|
|
$hosts = get_allowed_devices('', 'description', '', $total_rows);
|
|
|
|
foreach ($hosts as $host) {
|
|
$return_devices[] = strip_domain($host['description']) . ' (' . strip_domain($host['hostname']) . ')';
|
|
}
|
|
|
|
return $return_devices;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of hosts in a way that can be easily read through
|
|
* a callback, in JSON. The 'term' request variable will include an optional search term.
|
|
* The 'noany'/'nonone' request vars, if present, override $include_any/$include_none
|
|
* respectively (used by html_host_filter()'s ajax-backed select2-callback widget).
|
|
*
|
|
* @param bool $include_any Whether to include an "Any" option in the results.
|
|
* @param bool $include_none Whether to include a "None" option in the results.
|
|
* @param string $sql_where Additional SQL WHERE clause to filter the hosts.
|
|
*
|
|
* @return array List of allowed AJAX hosts.
|
|
*/
|
|
function get_allowed_ajax_hosts(bool $include_any = true, bool $include_none = true, string $sql_where = '') : array {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
// html_host_filter()'s select2-callback widget forwards the caller's $noany/$nonone
|
|
// choice as request vars, since it's not the same page context that originally
|
|
// rendered the filter form; only override the defaults when explicitly present.
|
|
if (isrv('noany')) {
|
|
$include_any = !gfrv('noany', FILTER_VALIDATE_BOOLEAN);
|
|
}
|
|
|
|
if (isrv('nonone')) {
|
|
$include_none = !gfrv('nonone', FILTER_VALIDATE_BOOLEAN);
|
|
}
|
|
|
|
$return = [];
|
|
|
|
$term = gfrv('term', FILTER_CALLBACK, ['options' => 'sanitize_search_string']);
|
|
|
|
if ($term != '') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') .
|
|
'(hostname LIKE ' . db_qstr("%$term%") .
|
|
' OR description LIKE ' . db_qstr("%$term%") .
|
|
' OR notes LIKE ' . db_qstr("%$term%") . ')';
|
|
}
|
|
|
|
if (grv('term') == '') {
|
|
if ($include_any) {
|
|
$return[] = ['label' => __('Any'), 'value' => 'Any', 'id' => '-1'];
|
|
}
|
|
|
|
if ($include_none) {
|
|
$return[] = ['label' => __('None'), 'value' => 'None', 'id' => '0'];
|
|
}
|
|
}
|
|
|
|
$total_rows = -1;
|
|
|
|
$hosts = get_allowed_devices($sql_where, 'description', read_config_option('autocomplete_rows'), $total_rows);
|
|
|
|
if (cacti_sizeof($hosts)) {
|
|
foreach ($hosts as $host) {
|
|
$return[] = ['label' => htmle(strip_domain($host['description'])), 'value' => htmle($host['description']), 'id' => $host['id']];
|
|
}
|
|
}
|
|
|
|
print json_encode($return);
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of graph_template in a way that can be easily
|
|
* read through a callback, in JSON. The 'term' request variable will include an optional search term.
|
|
*
|
|
* @param bool $include_any Whether to include an "Any" option in the results.
|
|
* @param bool $include_none Whether to include a "None" option in the results.
|
|
* @param string $sql_where Additional SQL WHERE clause conditions.
|
|
*
|
|
* @return array Outputs a JSON-encoded array of graph templates.
|
|
*/
|
|
function get_allowed_ajax_graph_templates(bool $include_any = true, bool $include_none = true, string $sql_where = '') : array {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$return = [];
|
|
|
|
$term = gfrv('term', FILTER_CALLBACK, ['options' => 'sanitize_search_string']);
|
|
|
|
if ($term != '') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . 'name LIKE ' . db_qstr("%$term%");
|
|
}
|
|
|
|
if (grv('term') == '') {
|
|
if ($include_any) {
|
|
$return[] = ['label' => __('Any'), 'value' => 'Any', 'id' => '-1'];
|
|
}
|
|
|
|
if ($include_none) {
|
|
$return[] = ['label' => __('None'), 'value' => 'None', 'id' => '0'];
|
|
}
|
|
}
|
|
|
|
$total_rows = -1;
|
|
|
|
$templates = get_allowed_graph_templates($sql_where, 'gt.name', read_config_option('autocomplete_rows'), $total_rows);
|
|
|
|
if (cacti_sizeof($templates)) {
|
|
foreach ($templates as $template) {
|
|
$return[] = ['label' => htmle($template['name']), 'value' => htmle($template['name']), 'id' => $template['id']];
|
|
}
|
|
}
|
|
|
|
print json_encode($return);
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of graph items in a way that can be easily
|
|
* read through a callback, in JSON. The 'term' request variable will include an optional search term.
|
|
*
|
|
* @param bool $include_none Whether to include a "None" option in the results. Default is true.
|
|
* @param string $sql_where Additional SQL WHERE conditions to filter the graph items. Default is an empty string.
|
|
*
|
|
* @return array An array of allowed graph items formatted for AJAX response.
|
|
*/
|
|
function get_allowed_ajax_graph_items(bool $include_none = true, string $sql_where = '') : array {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$return = [];
|
|
|
|
$term = gfrv('term', FILTER_CALLBACK, ['options' => 'sanitize_search_string']);
|
|
|
|
if ($term != '') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') .
|
|
'name_cache LIKE ' . db_qstr("%$term%") .
|
|
' OR data_source_name LIKE ' . db_qstr("%$term%");
|
|
}
|
|
|
|
if (grv('term') == '') {
|
|
if ($include_none) {
|
|
$return[] = ['label' => __('None'), 'value' => 'None', 'id' => '0'];
|
|
}
|
|
}
|
|
|
|
$graph_items = get_allowed_graph_items($sql_where, 'name_cache', read_config_option('autocomplete_rows'));
|
|
|
|
if (cacti_sizeof($graph_items)) {
|
|
foreach ($graph_items as $gi) {
|
|
$return[] = ['label' => htmle($gi['name']), 'value' => htmle($gi['name']), 'id' => $gi['id']];
|
|
}
|
|
}
|
|
|
|
print json_encode($return);
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of allowed graphs in a way that can be easily
|
|
* read through a callback, in JSON. The 'term' request variable will include an optional search term.
|
|
*
|
|
* @param string $sql_where Optional SQL where clause to filter the graphs.
|
|
*
|
|
* @return array Outputs a JSON-encoded array of allowed graphs.
|
|
*/
|
|
function get_allowed_ajax_graphs(string $sql_where = '') : array {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$return = [];
|
|
|
|
$term = gfrv('term', FILTER_CALLBACK, ['options' => 'sanitize_search_string']);
|
|
|
|
if ($term != '') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . 'title_cache LIKE ' . db_qstr("%$term%");
|
|
}
|
|
|
|
$total_rows = -1;
|
|
|
|
$graphs = get_allowed_graphs($sql_where, 'gtg.title_cache', read_config_option('autocomplete_rows'), $total_rows);
|
|
|
|
if (cacti_sizeof($graphs)) {
|
|
foreach ($graphs as $graph) {
|
|
$return[] = ['label' => htmle($graph['title_cache']), 'value' => htmle($graph['title_cache']), 'id' => $graph['local_graph_id']];
|
|
}
|
|
}
|
|
|
|
print json_encode($return);
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Returns a array of allowed graph items in a way that can be easily use in a table or list.
|
|
*
|
|
* @param string $sql_where SQL WHERE clause to filter the graph items.
|
|
* @param string $sql_order SQL ORDER BY clause to sort the graph items. Default is 'name'.
|
|
* @param int|string $sql_limit SQL LIMIT clause to limit the number of graph items. Default is 20.
|
|
* @param int $user_id The ID of the user to check permissions for. Default is 0.
|
|
*
|
|
* @return array An array of allowed graph items, each containing 'id' and 'name'.
|
|
*/
|
|
function get_allowed_graph_items(string $sql_where, string $sql_order = 'name', int|string $sql_limit = 20, int $user_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
$return = [];
|
|
|
|
if ($user_id == 0 && isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = 'WHERE ' . $sql_where;
|
|
}
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = 'LIMIT ' . $sql_limit;
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
$items = db_fetch_assoc("SELECT
|
|
CONCAT_WS('', dtd.name_cache,' (', dtr.data_source_name, ')') as name, dtr.id, dl.host_id
|
|
FROM data_local AS dl
|
|
INNER JOIN data_template_data AS dtd
|
|
ON dtd.local_data_id=dl.id
|
|
INNER JOIN data_template_rrd AS dtr
|
|
ON dtr.local_data_id=dl.id
|
|
LEFT JOIN host AS h
|
|
ON dl.host_id=h.id
|
|
$sql_where
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
if (cacti_sizeof($items)) {
|
|
foreach ($items as $i) {
|
|
if (is_device_allowed($i['host_id'], $user_id)) {
|
|
$return[] = ['id' => $i['id'], 'name' => $i['name']];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Returns the login username for the user attempting to login
|
|
*
|
|
* @return string The sanitized username based on the authentication method or an empty string if not available.
|
|
*/
|
|
function auth_get_username() : string {
|
|
$auth_method = read_config_option('auth_method');
|
|
|
|
if ($auth_method == AUTH_METHOD_BASIC) {
|
|
$username = get_basic_auth_username();
|
|
|
|
// Get the Web Basic Auth username and set action so we login right away
|
|
srv('action', 'login');
|
|
} elseif (gnrv('action') == 'login') {
|
|
$username = gnrv('login_username');
|
|
} else {
|
|
$username = '';
|
|
}
|
|
|
|
$username = sanitize_search_string($username);
|
|
|
|
return $username;
|
|
}
|
|
|
|
/**
|
|
* Checks the lockout status of a user and unlocks if necessary
|
|
*
|
|
* @param string $username The username of the account to check.
|
|
* @param int $realm The realm associated with the user account.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_checkclear_lockout(string $username, int $realm) : void {
|
|
// Unlock the user account if timing permits
|
|
$secPassLockFailed = read_config_option('secpass_lockfailed');
|
|
|
|
if ($secPassLockFailed > 0) {
|
|
$max = intval($secPassLockFailed);
|
|
|
|
if ($max > 0) {
|
|
$user = db_fetch_row_prepared("SELECT id, username, lastfail, failed_attempts, `locked`, password
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?
|
|
AND enabled = 'on'",
|
|
[$username, $realm]);
|
|
|
|
if (cacti_sizeof($user)) {
|
|
$unlock = intval(read_config_option('secpass_unlocktime'));
|
|
|
|
if ($unlock > 1440) {
|
|
$unlock = 1440;
|
|
}
|
|
|
|
$secs_unlock = $unlock * 60;
|
|
$secs_fail = time() - $user['lastfail'];
|
|
|
|
cacti_log('DEBUG: User \'' . $username . '\' secs_fail = ' . $secs_fail . ', secs_unlock = ' . $secs_unlock, false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
|
|
if ($unlock > 0 && ($secs_fail > $secs_unlock)) {
|
|
db_execute_prepared("UPDATE user_auth
|
|
SET lastfail = 0, failed_attempts = 0, `locked` = ''
|
|
WHERE username = ?
|
|
AND realm = ?
|
|
AND enabled = 'on'",
|
|
[$username, $realm]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks to see if the user is locked out of their account
|
|
* if there is an error, the globals error and error_msg will be set to notify the caller
|
|
* that a lockout is present and not to proceed with login.
|
|
*
|
|
* @param string $username The username of the account to check.
|
|
* @param int $realm The realm of the account to check.
|
|
*
|
|
* @return bool Returns true if the account is locked, false otherwise.
|
|
*/
|
|
function auth_process_lockout_check(string $username, int $realm) : bool {
|
|
global $error, $error_msg;
|
|
|
|
// Mark failed login attempts
|
|
$secPassLockFailed = read_config_option('secpass_lockfailed');
|
|
|
|
if ($secPassLockFailed > 0) {
|
|
$max = intval($secPassLockFailed);
|
|
|
|
if ($max > 0) {
|
|
$user = db_fetch_row_prepared("SELECT id, username, lastfail, failed_attempts, `locked`, password
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?
|
|
AND enabled = 'on'",
|
|
[$username, $realm]);
|
|
|
|
if (cacti_sizeof($user)) {
|
|
if ($user['locked'] == 'on') {
|
|
$error = true;
|
|
$error_msg = __('Your account has been locked. Please contact your Administrator.');
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Emit a stable, machine-parseable authentication-failure line for external tools
|
|
* (e.g. fail2ban) alongside the existing human-readable AUTH log entries. The
|
|
* grammar is fixed so a fail2ban failregex can key on it reliably across releases:
|
|
*
|
|
* AUTH FAILURE user="<user>" realm="<realm>" ip="<ip>" reason="<reason>"
|
|
*
|
|
* The IP comes from get_client_addr(), which is already validated and honours the
|
|
* trusted-proxy configuration. The username is attacker-influenced, so quotes and
|
|
* control characters are stripped to keep the grammar intact and prevent a crafted
|
|
* username from forging log lines or extra fields.
|
|
*
|
|
* @param string $username Username supplied by the client.
|
|
* @param string $realm Realm token: local|ldap|domain.
|
|
* @param string $reason Reason token: bad_password|no_such_user|2fa.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_log_failure(string $username, string $realm, string $reason) : void {
|
|
$clean = static function (string $value) : string {
|
|
$stripped = preg_replace('/[\x00-\x1f\x7f"]/', '', $value);
|
|
|
|
return $stripped ?? '';
|
|
};
|
|
|
|
cacti_log(
|
|
sprintf(
|
|
'AUTH FAILURE user="%s" realm="%s" ip="%s" reason="%s"',
|
|
$clean($username), $clean($realm), get_client_addr(), $clean($reason)
|
|
),
|
|
false,
|
|
'AUTH'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Map a numeric authentication realm to the token used in auth_log_failure().
|
|
*
|
|
* @param int $realm The numeric realm id.
|
|
*
|
|
* @return string local|ldap|domain
|
|
*/
|
|
function auth_realm_token(int $realm) : string {
|
|
if ($realm <= 1) {
|
|
return 'local';
|
|
}
|
|
|
|
return $realm == 2 ? 'ldap' : 'domain';
|
|
}
|
|
|
|
/**
|
|
* Called when a user login attempt fails to increment or lockout the user
|
|
* if there is an error, the globals error and error_msg will be set to notify the caller
|
|
* that a lockout is present and not to proceed with login.
|
|
*
|
|
* @param string $username The username of the user attempting to log in.
|
|
* @param int $realm The authentication realm.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_process_lockout(string $username, int $realm) : void {
|
|
global $error, $error_msg;
|
|
|
|
// Mark failed login attempts
|
|
$secPassLockFailed = read_config_option('secpass_lockfailed');
|
|
|
|
if ($secPassLockFailed > 0) {
|
|
$max = intval($secPassLockFailed);
|
|
|
|
if ($max > 0) {
|
|
$user = db_fetch_row_prepared('SELECT id, username, enabled, lastfail, failed_attempts, `locked`, password
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$username, $realm]);
|
|
|
|
if (cacti_sizeof($user)) {
|
|
if ($user['enabled'] == '') {
|
|
cacti_log(sprintf("LOGIN FAILED: Login failed for user '%s' from IP address '%s'. User account disabled.", $username, get_client_addr()), false, 'AUTH');
|
|
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Login Disabled.');
|
|
}
|
|
|
|
// Increment the counter in the database rather than reading it,
|
|
// adding one in PHP and writing it back. Concurrent failed logins
|
|
// would otherwise each read the same value and write the same +1,
|
|
// undercounting and letting an attacker exceed secpass_lockfailed
|
|
// before the account locks.
|
|
db_execute_prepared("UPDATE user_auth
|
|
SET lastfail = ?, failed_attempts = failed_attempts + 1
|
|
WHERE username = ?
|
|
AND realm = ?
|
|
AND enabled = 'on'",
|
|
[time(), $username, $realm]);
|
|
|
|
$failed = (int) db_fetch_cell_prepared('SELECT failed_attempts
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$username, $realm]);
|
|
|
|
cacti_log(sprintf('LOGIN FAILED: User \'%s\' failed authentication, incrementing lockout (%d of %d)',$username, $failed, $max), false, 'AUTH', POLLER_VERBOSITY_LOW);
|
|
|
|
if ($failed >= $max) {
|
|
db_execute_prepared("UPDATE user_auth
|
|
SET `locked` = 'on'
|
|
WHERE username = ?
|
|
AND realm = ?
|
|
AND enabled = 'on'",
|
|
[$username, $realm]);
|
|
|
|
$user['locked'] = 'on';
|
|
}
|
|
|
|
// Log the invalid password attempt
|
|
db_execute_prepared('INSERT IGNORE INTO user_log
|
|
(username, user_id, result, ip, time)
|
|
VALUES (?, ?, 0, ?, NOW())',
|
|
[$username, $user['id'] ?? 0, get_client_addr()]);
|
|
|
|
if ($user['locked'] == 'on') {
|
|
cacti_log(sprintf("LOGIN FAILED: Login failed for user '%s' from IP address '%s'. Account is locked out.", $username, get_client_addr()), false, 'AUTH');
|
|
|
|
$error = true;
|
|
$error_msg = __('Your account has been locked. Please contact your Administrator.');
|
|
} else {
|
|
cacti_log(sprintf("LOGIN FAILED: Login failed for user '%s' from IP address '%s'.", $username, get_client_addr()), false, 'AUTH');
|
|
|
|
// error
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Login Failed.');
|
|
}
|
|
} else {
|
|
cacti_log(sprintf("LOGIN FAILED: Login failed to find user '%s' from IP address '%s'.", $username, get_client_addr()), false, 'AUTH');
|
|
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Login Failed.');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Login a basic auth account or generate an error
|
|
* if there is an error, the globals error and error_msg will be set to notify the caller
|
|
* that a lockout is present and not to proceed with login. This function will also
|
|
* exit and return html to the display to notify the user of critical errors.
|
|
*
|
|
* @param string $username The username provided by the web server for authentication.
|
|
*
|
|
* @return array The user data from the database if authentication is successful.
|
|
*/
|
|
function basic_auth_login_process(string $username) : array {
|
|
global $error, $error_msg;
|
|
|
|
if (empty($username)) {
|
|
cacti_log('ERROR: No username passed with Web Basic Authentication enabled. From IP Address ' . get_client_addr(), false, 'AUTH');
|
|
auth_display_custom_error_message(__('Web Basic Authentication configured, but no username was passed from the web server. Please make sure you have authentication enabled on the web server.'));
|
|
|
|
exit;
|
|
}
|
|
|
|
// Locate user in database
|
|
$user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = 2',
|
|
[$username]);
|
|
|
|
if (!is_array($user) && get_template_account($username) == 0 && get_guest_account() === 0) {
|
|
$error = true;
|
|
$error_msg = __esc('%s authenticated by Web Server, but both Template and Guest Users are not defined in Cacti.', $username);
|
|
|
|
cacti_log(sprintf("LOGIN FAILED: User '%s' from IP address %s authenticated by Web Server, but both Template and Guest Users are not defined in Cacti. Exiting.", $username, get_client_addr()), false, 'AUTH');
|
|
|
|
auth_display_custom_error_message($error_msg);
|
|
|
|
exit;
|
|
}
|
|
|
|
return is_array($user) ? $user : [];
|
|
}
|
|
|
|
/**
|
|
* Login through a Login Provider (LDAP, Active Directory, SAML2, or OpenID).
|
|
*
|
|
* SAML2 and OpenID are redirect-only flows and cannot authenticate from a
|
|
* posted username/password, so a realm resolving to one of those types is
|
|
* rejected here; those providers are driven from login_sso.php instead.
|
|
*
|
|
* @param string $username The username of the user attempting to log in.
|
|
*
|
|
* @return array The user information if the login was successful, otherwise an empty array.
|
|
*/
|
|
function login_providers_login_process(string $username) : array {
|
|
global $realm, $error, $error_msg;
|
|
|
|
$realm = gfrv('realm');
|
|
$password = gnrv('login_password');
|
|
|
|
if ($username == '') {
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Login Failed.');
|
|
|
|
cacti_log('LOGIN FAILED: Empty Provider Username provided, from IP address ' . get_client_addr(), false, 'AUTH');
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* get_auth_realms() builds the realm dropdown, so a realm it does not list was
|
|
* never on offer and must not reach the branches below. The check runs before
|
|
* the lockout bookkeeping so a forged realm cannot scatter an account's failure
|
|
* counters across realms that do not exist.
|
|
*/
|
|
if (!array_key_exists($realm, get_auth_realms(true))) {
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Login Failed.');
|
|
|
|
cacti_log(sprintf("LOGIN FAILED: Unknown Login Realm '%s' provided for user '%s' from IP address %s", $realm, $username, get_client_addr()), false, 'AUTH');
|
|
|
|
return [];
|
|
}
|
|
|
|
auth_checkclear_lockout($username, $realm);
|
|
|
|
if (auth_process_lockout_check($username, $realm)) {
|
|
return [];
|
|
}
|
|
|
|
if ($realm < 1000 || $password == '') {
|
|
/**
|
|
* Realms below 1000 are the local and legacy realms, so there is no
|
|
* login_providers row to authenticate against. Without this arm the call
|
|
* returned an empty user with $error still false, which auth_login.php
|
|
* treats as an authenticated user with no account yet and provisions from
|
|
* the template.
|
|
*/
|
|
$error = true;
|
|
$error_msg = $password == '' ? __('Access Denied! No password provided by user.') : __('Access Denied! Login Failed.');
|
|
|
|
cacti_log(sprintf("LOGIN FAILED: Login Realm '%s' is not a credential-based Login Provider for user '%s' from IP address %s", $realm, $username, get_client_addr()), false, 'AUTH');
|
|
|
|
auth_process_lockout($username, $realm);
|
|
|
|
return [];
|
|
}
|
|
|
|
$provider = LoginProviderFactory::fromRealm((int) $realm);
|
|
|
|
if (!$provider instanceof CredentialLoginProviderInterface) {
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Login Failed.');
|
|
|
|
cacti_log(sprintf("LOGIN FAILED: Login Realm '%s' is not a credential-based Login Provider for user '%s' from IP address %s", $realm, $username, get_client_addr()), false, 'AUTH');
|
|
|
|
return [];
|
|
}
|
|
|
|
$result = $provider->authenticate($username, $password);
|
|
|
|
if (!$result->success) {
|
|
$error = true;
|
|
$error_msg = $result->error !== '' ? $result->error : __('Access Denied! Login Failed.');
|
|
|
|
cacti_log(sprintf("LOGIN FAILED: Provider '%s' Error for user '%s' from IP address %s", $provider->getName(), $username, get_client_addr()), false, 'AUTH');
|
|
|
|
// Only an actual wrong username/password counts toward the lockout
|
|
// counter - an LDAP outage or a group-membership denial is not
|
|
// evidence of a credential-guessing attempt against this account.
|
|
if ($result->isCredentialFailure) {
|
|
auth_process_lockout($username, $realm);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
cacti_log(sprintf("LOGIN: User '%s' Authenticated via Provider '%s' from IP address %s", $result->username, $provider->getName(), get_client_addr()), false, 'AUTH');
|
|
|
|
$user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$result->username, $realm]);
|
|
|
|
$user = is_array($user) ? $user : [];
|
|
|
|
$templateUserId = $provider->getTemplateUserId();
|
|
|
|
if (!cacti_sizeof($user) && $templateUserId > 0 && $result->username != '') {
|
|
$template = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$templateUserId]);
|
|
|
|
$template = is_array($template) ? $template : [];
|
|
|
|
if (!cacti_sizeof($template)) {
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Template user id %s does not exist. Please contact your Administrator.', $templateUserId);
|
|
|
|
cacti_log("LOGIN FAILED: Template user id '" . $templateUserId . "' does not exist.", false, 'AUTH');
|
|
|
|
return [];
|
|
}
|
|
|
|
cacti_log("NOTE: User '" . $result->username . "' does not exist, copying template user", false, 'AUTH');
|
|
|
|
user_copy($template['username'], $result->username, 0, $realm, false, [
|
|
'full_name' => $result->claims['full_name'] ?? '',
|
|
'email_address' => $result->claims['email'] ?? '',
|
|
]);
|
|
|
|
$user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$result->username, $realm]);
|
|
|
|
$user = is_array($user) ? $user : [];
|
|
}
|
|
|
|
if (!cacti_sizeof($user)) {
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Provider template is not configured. Please contact your Administrator.');
|
|
|
|
cacti_log("LOGIN FAILED: user '" . $result->username . "' authenticated but the provider has no template and no existing account.", false, 'AUTH');
|
|
|
|
return [];
|
|
}
|
|
|
|
// Reconcile automatic User Group assignments so group changes in the
|
|
// directory/IdP take effect on this login.
|
|
if (method_exists($provider, 'applyAutoAssignments')) {
|
|
$provider->applyAutoAssignments((int) $user['id'], $result->username, $result->claims['groups'] ?? []);
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Validate a given password for various password rules
|
|
*
|
|
* @param string $password The password to be checked.
|
|
*
|
|
* @return string Returns 'ok' if the password meets all criteria, otherwise returns an error message.
|
|
*/
|
|
function secpass_check_pass(string $password) : string {
|
|
$minlen = read_config_option('secpass_minlen');
|
|
|
|
if (strlen($password) < $minlen) {
|
|
return __('Password must be at least %d characters!', $minlen);
|
|
}
|
|
|
|
if (read_config_option('secpass_reqnum') == 'on' &&
|
|
str_replace(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], '', $password) == $password
|
|
) {
|
|
return __('Your password must contain at least 1 numerical character!');
|
|
}
|
|
|
|
if (read_config_option('secpass_reqmixcase') == 'on' && cacti_strtolower($password) == $password) {
|
|
return __('Your password must contain a mix of lower case and upper case characters!');
|
|
}
|
|
|
|
if (read_config_option('secpass_reqspec') == 'on' &&
|
|
str_replace(['~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '+', '=', '[', '{', ']', '}', ';', ':', '<', ',', '.', '>', '?', '|', '/', '\\'], '', $password) == $password
|
|
) {
|
|
return __('Your password must contain at least 1 special character!');
|
|
}
|
|
|
|
if (read_config_option('secpass_pwnedcheck') == 'on') {
|
|
$sha1 = cacti_strtoupper(sha1($password));
|
|
$suffix = substr($sha1,5);
|
|
$options = [
|
|
CURLOPT_RETURNTRANSFER => true, // return web page
|
|
CURLOPT_HEADER => false, // don't return headers
|
|
CURLOPT_FOLLOWLOCATION => true, // follow redirects
|
|
CURLOPT_MAXREDIRS => 10, // stop after 10 redirects
|
|
CURLOPT_ENCODING => '', // handle compressed
|
|
CURLOPT_USERAGENT => 'test', // name of client
|
|
CURLOPT_AUTOREFERER => true, // set referrer on redirect
|
|
CURLOPT_CONNECTTIMEOUT => 120, // time-out on connect
|
|
CURLOPT_TIMEOUT => 120, // time-out on response
|
|
];
|
|
|
|
$ch = curl_init('https://api.pwnedpasswords.com/range/' . substr($sha1,0,5));
|
|
curl_setopt_array($ch, $options);
|
|
|
|
$content = curl_exec($ch);
|
|
|
|
$lines = is_string($content) ? explode("\r\n", $content) : [];
|
|
$count = 0;
|
|
|
|
foreach ($lines as $line) {
|
|
$result = explode(':', $line);
|
|
|
|
if ($result[0] == $suffix) {
|
|
$count = $result[1];
|
|
}
|
|
}
|
|
|
|
if ($count >= read_config_option('secpass_pwnedcount')) {
|
|
return __('This password appears to be a well known password, please use a different one');
|
|
}
|
|
}
|
|
|
|
return 'ok';
|
|
}
|
|
|
|
/**
|
|
* Checks for password reuse for local accounts
|
|
*
|
|
* @param int $id The user ID.
|
|
* @param string $password The password to check.
|
|
*
|
|
* @return bool Returns true if the password is not in the history, false otherwise.
|
|
*/
|
|
function secpass_check_history(int $id, string $password) : bool {
|
|
$history = intval(read_config_option('secpass_history'));
|
|
|
|
if ($history > 0) {
|
|
$user = db_fetch_row_prepared("SELECT password, password_history
|
|
FROM user_auth
|
|
WHERE id = ?
|
|
AND realm = 0
|
|
AND enabled = 'on'",
|
|
[$id]);
|
|
|
|
if (!cacti_sizeof($user)) {
|
|
return true;
|
|
}
|
|
|
|
if (compat_password_verify($password, $user['password'])) {
|
|
return false;
|
|
}
|
|
|
|
$passes = explode('|', is_string($user['password_history']) ? $user['password_history'] : '');
|
|
|
|
// Double check this in case the password history setting was changed
|
|
while (cacti_count($passes) > $history) {
|
|
array_shift($passes);
|
|
}
|
|
|
|
if (!empty($passes)) {
|
|
foreach ($passes as $hash) {
|
|
if (compat_password_verify($password, $hash)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks that Cacti ras_public_key is present. If not
|
|
* it will insert the information into the Cacti database.
|
|
*
|
|
* @return void
|
|
*/
|
|
function rsa_check_keypair() : void {
|
|
$public_key = read_config_option('rsa_public_key');
|
|
|
|
if (!$public_key) {
|
|
$private = RSA::createKey(2048);
|
|
$public = $private->getPublicKey();
|
|
$fingerprint = $public->getFingerprint();
|
|
|
|
db_execute_prepared("INSERT INTO settings
|
|
(`name`, `value`) VALUES ('rsa_public_key', ?), ('rsa_private_key', ?), ('rsa_fingerprint', ?)",
|
|
[$public, $private, $fingerprint]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the raw per-installation symmetric key used by
|
|
* cacti_encrypt_secret()/cacti_decrypt_secret(), generating and persisting
|
|
* a new one on first use. Modeled on plugin_servcheck's
|
|
* servcheck_encrypt_credential() key handling, but Cacti-wide (a `settings`
|
|
* row, not a per-user setting) since callers like Login Providers store
|
|
* secrets that are shared server-wide configuration, not owned by a user.
|
|
*
|
|
* @return string The raw (binary) 256-bit key.
|
|
*
|
|
* @throws \RuntimeException If the persisted key is present but corrupt
|
|
* (not valid base64, or not exactly 32 bytes
|
|
* once decoded). Silently substituting a fresh,
|
|
* unpersisted key here would make every existing
|
|
* ciphertext permanently undecryptable and every
|
|
* newly encrypted value unreadable on the very
|
|
* next request, without any indication why.
|
|
*/
|
|
function cacti_secret_key() : string {
|
|
$key = read_config_option('secret_encryption_key');
|
|
|
|
if (empty($key)) {
|
|
$key = base64_encode(random_bytes(32));
|
|
|
|
// INSERT IGNORE: if a concurrent request already generated and stored
|
|
// a key, keep that one rather than clobbering it with a second,
|
|
// mutually-incompatible key that would strand the loser's ciphertext.
|
|
db_execute_prepared('INSERT IGNORE INTO settings (`name`, `value`) VALUES (?, ?)', ['secret_encryption_key', $key]);
|
|
|
|
$key = (string) read_config_option('secret_encryption_key', true);
|
|
}
|
|
|
|
$decoded = base64_decode($key, true);
|
|
|
|
if ($decoded === false || strlen($decoded) !== 32) {
|
|
throw new \RuntimeException('The secret_encryption_key setting is missing or corrupt; encrypted secrets cannot be read or written until it is restored.');
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
|
|
/**
|
|
* Encrypts an arbitrary secret (e.g. a pasted SAML private key) for storage,
|
|
* using Cacti's per-installation AES-256-GCM key. Modeled on plugin_servcheck's
|
|
* servcheck_encrypt_credential(): a fresh random IV per call, prefixed onto
|
|
* the ciphertext and base64-encoded as a single stored string. GCM's
|
|
* authentication tag is stored alongside so decryption can detect a
|
|
* tampered/corrupted database value instead of silently accepting it.
|
|
*
|
|
* @param string $plaintext The secret to encrypt.
|
|
*
|
|
* @return string The base64-encoded IV + tag + ciphertext, or '' for an empty input.
|
|
*
|
|
* @throws \RuntimeException If the per-installation key is missing/corrupt; see cacti_secret_key().
|
|
*/
|
|
function cacti_encrypt_secret(string $plaintext) : string {
|
|
return cacti_encrypt_secret_with_key($plaintext, cacti_secret_key());
|
|
}
|
|
|
|
/**
|
|
* Decrypts a secret produced by cacti_encrypt_secret(). Degrades to false
|
|
* (rather than throwing) when the per-installation key itself is
|
|
* missing/corrupt, since a read failure must not crash the whole page -
|
|
* callers already treat false as "cannot use this secret".
|
|
*
|
|
* @param string $ciphertext The base64-encoded IV + tag + ciphertext.
|
|
*
|
|
* @return string|false The decrypted secret, '' for an empty input, or false
|
|
* if the stored data or the encryption key is invalid.
|
|
*/
|
|
function cacti_decrypt_secret(string $ciphertext) : string|false {
|
|
try {
|
|
$key = cacti_secret_key();
|
|
} catch (\RuntimeException $e) {
|
|
cacti_log('ERROR: ' . $e->getMessage(), false, 'AUTH');
|
|
|
|
return false;
|
|
}
|
|
|
|
return cacti_decrypt_secret_with_key($ciphertext, $key);
|
|
}
|
|
|
|
/**
|
|
* Core encryption logic behind cacti_encrypt_secret(), split out so it can
|
|
* be exercised against an explicit key (e.g. in unit tests) without the
|
|
* `settings` table key-persistence cacti_secret_key() requires.
|
|
*
|
|
* @param string $plaintext The secret to encrypt.
|
|
* @param string $key The raw (binary) 256-bit AES key.
|
|
*
|
|
* @return string The base64-encoded IV + tag + ciphertext, or '' for an empty input.
|
|
*/
|
|
function cacti_encrypt_secret_with_key(string $plaintext, string $key) : string {
|
|
if ($plaintext === '') {
|
|
return '';
|
|
}
|
|
|
|
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(CACTI_SECRET_CIPHER));
|
|
$tag = '';
|
|
|
|
$encrypted = openssl_encrypt($plaintext, CACTI_SECRET_CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag, '', CACTI_SECRET_TAG_LENGTH);
|
|
|
|
if ($encrypted === false) {
|
|
throw new \RuntimeException('Failed to encrypt secret.');
|
|
}
|
|
|
|
return base64_encode($iv . $tag . $encrypted);
|
|
}
|
|
|
|
/**
|
|
* Core decryption logic behind cacti_decrypt_secret(); see
|
|
* cacti_encrypt_secret_with_key(). openssl_decrypt() itself returns false
|
|
* whenever the GCM tag fails to verify, so a tampered/corrupted database
|
|
* value is rejected rather than being silently accepted as valid plaintext.
|
|
*
|
|
* @param string $ciphertext The base64-encoded IV + tag + ciphertext.
|
|
* @param string $key The raw (binary) 256-bit AES key.
|
|
*
|
|
* @return string|false The decrypted secret, '' for an empty input, or false
|
|
* if the stored data is malformed, tampered, or cannot be decrypted.
|
|
*/
|
|
function cacti_decrypt_secret_with_key(string $ciphertext, string $key) : string|false {
|
|
if ($ciphertext === '') {
|
|
return '';
|
|
}
|
|
|
|
$raw = base64_decode($ciphertext, true);
|
|
|
|
$iv_length = openssl_cipher_iv_length(CACTI_SECRET_CIPHER);
|
|
$tag_length = CACTI_SECRET_TAG_LENGTH;
|
|
|
|
if ($raw === false || strlen($raw) <= $iv_length + $tag_length) {
|
|
return false;
|
|
}
|
|
|
|
$iv = substr($raw, 0, $iv_length);
|
|
$tag = substr($raw, $iv_length, $tag_length);
|
|
$ciphertext = substr($raw, $iv_length + $tag_length);
|
|
|
|
return openssl_decrypt($ciphertext, CACTI_SECRET_CIPHER, $key, OPENSSL_RAW_DATA, $iv, $tag, '');
|
|
}
|
|
|
|
/**
|
|
* Expires persistent authentication tokens for all group members and sets a
|
|
* flag so logged-in members reload their permissions from the database.
|
|
*
|
|
* @param int $group_id The ID of the group whose users' permissions need to be reset.
|
|
*
|
|
* @return void
|
|
*/
|
|
function reset_group_perms(int $group_id) : void {
|
|
$users = array_rekey(db_fetch_assoc_prepared('SELECT user_id
|
|
FROM user_auth_group_members
|
|
WHERE group_id = ?',
|
|
[$group_id]), 'user_id', 'user_id');
|
|
|
|
if (cacti_sizeof($users)) {
|
|
$user_ids = array_values($users);
|
|
$placeholders = implode(',', array_fill(0, count($user_ids), '?'));
|
|
|
|
db_execute_prepared("DELETE FROM user_auth_cache
|
|
WHERE user_id IN ($placeholders)",
|
|
$user_ids);
|
|
|
|
db_execute_prepared("UPDATE user_auth
|
|
SET reset_perms=FLOOR(RAND() * 4294967295) + 1
|
|
WHERE id IN ($placeholders)",
|
|
$user_ids);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expires persistent authentication tokens and sets a flag for all users logged
|
|
* in as this user that their permissions need to be reloaded from the database.
|
|
*
|
|
* @param int $user_id The ID of the user whose permissions are to be reset.
|
|
*
|
|
* @return void
|
|
*/
|
|
function reset_user_perms(int $user_id) : void {
|
|
db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_id]);
|
|
|
|
db_execute_prepared('UPDATE user_auth
|
|
SET reset_perms=FLOOR(RAND() * 4294967295) + 1
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
if ($user_id == $_SESSION[SESS_USER_ID]) {
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks to see if the admin has changed users permissions
|
|
*
|
|
* @param int $user_id The ID of the user whose permissions are being checked.
|
|
*
|
|
* @return bool Returns true if the user's permissions are valid, false otherwise.
|
|
*/
|
|
function is_user_perms_valid(int $user_id) : bool {
|
|
global $config;
|
|
|
|
static $valid = null;
|
|
static $key = null;
|
|
|
|
if ($valid === null && cacti_version_compare($config['cacti_db_version'], '1.0.0', '>=')) {
|
|
$key = db_fetch_cell_prepared('SELECT reset_perms
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
}
|
|
|
|
if (isset($_SESSION[SESS_USER_PERMS_KEY])) {
|
|
if ($key != $_SESSION[SESS_USER_PERMS_KEY]) {
|
|
$valid = false;
|
|
} else {
|
|
$valid = true;
|
|
}
|
|
} else {
|
|
$valid = true;
|
|
}
|
|
|
|
$_SESSION[SESS_USER_PERMS_KEY] = $key;
|
|
|
|
return $valid;
|
|
}
|
|
|
|
/**
|
|
* If the secure function exists, verify against that first.
|
|
* If that checks fails or does not exist, check against older md5 version.
|
|
* If the md5 function is missing, we will go with sha256.
|
|
*
|
|
* @param string $password The password to verify.
|
|
* @param string $hash The hash to verify against.
|
|
*
|
|
* @return bool Returns true if the password matches the hash, false otherwise.
|
|
*/
|
|
function compat_password_verify(string $password, string $hash) : bool {
|
|
if (password_verify($password, $hash)) {
|
|
return true;
|
|
}
|
|
|
|
if (function_exists('md5')) {
|
|
$md5 = md5($password);
|
|
|
|
if ($md5 === $hash) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$sha256 = hash('sha256', $password);
|
|
|
|
return ($sha256 === $hash);
|
|
}
|
|
|
|
/**
|
|
* If the secure function exists, hash using that.
|
|
* If that does not exist, hash older md5 function instead
|
|
*
|
|
* @param string $password The password to be hashed.
|
|
* @param string|int $algo The algorithm to use for hashing. Refer to the `password_hash` documentation for supported algorithms.
|
|
* @param array $options Optional. An associative array of options. Refer to the `password_hash` documentation for supported options.
|
|
*
|
|
* @return string The hashed password.
|
|
*/
|
|
function compat_password_hash(string $password, string|int $algo, array $options = []) : string {
|
|
// Check if options array has anything, only pass when required
|
|
return (cacti_sizeof($options) > 0) ?
|
|
password_hash($password, $algo, $options) :
|
|
password_hash($password, $algo);
|
|
}
|
|
|
|
/**
|
|
* If the secure function exists, check hash using that.
|
|
* If that does not exist, return false as md5 doesn't need a rehash
|
|
*
|
|
* @param string $password The hashed password to check.
|
|
* @param string|int $algo algorithm to use (PASSWORD_DEFAULT)
|
|
* @param array $options (optional) An associative array of options.
|
|
*
|
|
* @return bool Returns true if the password needs to be rehashed, false otherwise.
|
|
*/
|
|
function compat_password_needs_rehash(string $password, string|int $algo, array $options = []) : bool {
|
|
// Check if options array has anything, only pass when required
|
|
return (cacti_sizeof($options) > 0) ?
|
|
password_needs_rehash($password, $algo, $options) :
|
|
password_needs_rehash($password, $algo);
|
|
}
|
|
|
|
/**
|
|
* Verify that the user account has some access to cacti
|
|
*
|
|
* @param array $user The user data array containing user information.
|
|
*
|
|
* @return bool True if the user has access, false otherwise.
|
|
*/
|
|
function auth_user_has_access(array $user) : bool {
|
|
// See if they have access to any realms
|
|
$has_access = db_fetch_cell_prepared('SELECT EXISTS(
|
|
SELECT 1
|
|
FROM user_auth_realm
|
|
WHERE user_id = ?)',
|
|
[$user['id']]);
|
|
|
|
if ($has_access) {
|
|
return true;
|
|
}
|
|
|
|
// See if they have general graph access as a guest account
|
|
$guest_access = read_config_option('guest_user') > 0;
|
|
|
|
if ($guest_access) {
|
|
if ($user['show_tree'] == 'on' || $user['show_list'] == 'on' || $user['show_preview'] == 'on') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Resolve every group membership in one indexed existence query.
|
|
return (bool) db_fetch_cell_prepared("SELECT EXISTS(
|
|
SELECT 1
|
|
FROM user_auth_group_members AS uagm
|
|
INNER JOIN user_auth_group AS uag
|
|
ON uag.id = uagm.group_id
|
|
LEFT JOIN user_auth_group_realm AS uagr
|
|
ON uagr.group_id = uagm.group_id
|
|
WHERE uagm.user_id = ?
|
|
AND uag.enabled = 'on'
|
|
AND (
|
|
uagr.realm_id IS NOT NULL
|
|
OR (? = 1 AND (uag.show_tree = 'on' OR uag.show_list = 'on' OR uag.show_preview = 'on'))
|
|
))",
|
|
[$user['id'], $guest_access ? 1 : 0]);
|
|
}
|
|
|
|
/**
|
|
* Displays a custom error message to the browser that looks like the pre-defined error messages
|
|
*
|
|
* @param string $message The error message to be displayed.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_display_custom_error_message(string $message) : void {
|
|
$auth_method = read_config_option('auth_method');
|
|
|
|
if ($auth_method == AUTH_METHOD_BASIC) {
|
|
$custom_message = read_config_option('basic_auth_fail_message');
|
|
} else {
|
|
$custom_message = '';
|
|
}
|
|
|
|
// kill the session
|
|
cacti_cookie_logout();
|
|
|
|
// print error
|
|
print '<!DOCTYPE html>';
|
|
print '<html>';
|
|
print '<head>';
|
|
|
|
html_common_header(__('Cacti Login Failure'));
|
|
|
|
print '</head>';
|
|
print '<body><center>';
|
|
print '<div class="ui-state-error ui-corner-all" style="width:50%;margin-left:auto;margin-right:auto;margin-top:200px;padding:20px"><p>' . $message . '</p><p>' . $custom_message . '</p></div>';
|
|
|
|
if ($auth_method != AUTH_METHOD_BASIC) {
|
|
print '<div class="ui-corner-all" style="width:50%;margin:auto;padding:20px"><a href="index.php">' . __('Login Again') . '</a></div><script type="text/javascript" ' . CactiSecureHeaders::getNonceAttribute() . '>$(function() { $("a").button(); });</script>';
|
|
}
|
|
|
|
print '</center></body></html>';
|
|
}
|
|
|
|
/**
|
|
* Overrides a freshly authenticated user's login_opts with the highest
|
|
* (MAX) login_opts configured across their groups, when any group sets one.
|
|
* Shared by every login path (password and SSO) so a group-derived landing
|
|
* page preference applies consistently regardless of how the user signed in.
|
|
*
|
|
* @param array $user The user_auth row (must include 'id' and 'login_opts').
|
|
*
|
|
* @return array The same row, with 'login_opts' overridden if applicable.
|
|
*/
|
|
function auth_apply_group_login_opts(array $user) : array {
|
|
if (db_table_exists('user_auth_group')) {
|
|
$group_options = db_fetch_cell_prepared('SELECT MAX(login_opts)
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id=uagm.group_id
|
|
WHERE user_id = ?
|
|
AND login_opts != 4',
|
|
[$user['id']]
|
|
);
|
|
|
|
if (!empty($group_options)) {
|
|
$user['login_opts'] = $group_options;
|
|
}
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Provide default page re-direction when a user first logs in.
|
|
*
|
|
* @param string $login_opts The login options for the user. If not provided, it will be fetched from the database.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_login_redirect(string $login_opts = '') : void {
|
|
if ($login_opts == '') {
|
|
$login_opts = db_fetch_cell_prepared('SELECT login_opts
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$_SESSION[SESS_USER_ID]]);
|
|
}
|
|
|
|
$newtheme = false;
|
|
|
|
if (user_setting_exists('selected_theme', $_SESSION[SESS_USER_ID]) && read_config_option('selected_theme') != read_user_setting('selected_theme')) {
|
|
unset($_SESSION['selected_theme']);
|
|
$newtheme = true;
|
|
}
|
|
|
|
// Decide what to do with an authenticated user
|
|
switch ($login_opts) {
|
|
case '1': // referer
|
|
/**
|
|
* because we use plugins, we can't redirect back to graph_view.php if they don't
|
|
* have console access
|
|
*/
|
|
if (isset($_SERVER['REDIRECT_URL'])) {
|
|
$referer = sanitize_uri($_SERVER['REDIRECT_URL']);
|
|
|
|
if (isset($_SERVER['REDIRECT_QUERY_STRING'])) {
|
|
$referer .= '?' . $_SERVER['REDIRECT_QUERY_STRING'];
|
|
}
|
|
|
|
cacti_log(sprintf("DEBUG: Referer from REDIRECT_URL with Value: '%s', Effective: '%s'", $_SERVER['REDIRECT_URL'], $referer), false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
} elseif (isset($_SERVER['HTTP_REFERER'])) {
|
|
$referer = $_SERVER['HTTP_REFERER'];
|
|
|
|
if (auth_basename($referer) == 'logout.php') {
|
|
$referer = CACTI_PATH_URL . 'index.php';
|
|
} elseif (!str_contains($referer, CACTI_PATH_URL)) {
|
|
if (!is_realm_allowed(8)) {
|
|
$referer = CACTI_PATH_URL . 'graph_view.php' . ($newtheme ? '?newtheme=1' : '');
|
|
} else {
|
|
$referer = CACTI_PATH_URL . 'index.php' . ($newtheme ? '?newtheme=1' : '');
|
|
}
|
|
}
|
|
|
|
cacti_log(sprintf("DEBUG: Referer from HTTP_REFERER with Value: '%s', Effective: '%s'", $_SERVER['HTTP_REFERER'], $referer), false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
} elseif (isset($_SERVER['REQUEST_URI'])) {
|
|
$referer = sanitize_uri($_SERVER['REQUEST_URI']);
|
|
|
|
if (auth_basename($referer) == 'logout.php') {
|
|
$referer = CACTI_PATH_URL . 'index.php';
|
|
}
|
|
|
|
cacti_log(sprintf("DEBUG: Referer from REQUEST_URI with Value: '%s', Effective: '%s'", $_SERVER['REQUEST_URI'], $referer), false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
} else {
|
|
$referer = CACTI_PATH_URL . 'index.php';
|
|
|
|
cacti_log(sprintf("DEBUG: Referer Short Circuit to '%s'", 'index.php'), false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
}
|
|
|
|
$referer .= ($newtheme ? (!str_contains($referer, '?') ? '?' : '&') . 'newtheme=1' : '');
|
|
|
|
// Strip out the login from the referer if present
|
|
$referer = str_replace('?action=login', '', $referer);
|
|
|
|
// Never emit an attacker-controlled Location. The HTTP_REFERER branch
|
|
// only checked str_contains(CACTI_PATH_URL), which an absolute URL such
|
|
// as https://evil.example/cacti/index.php satisfies. validate_redirect_url()
|
|
// rejects an off-host target and returns a safe local path instead.
|
|
$referer = validate_redirect_url($referer);
|
|
|
|
if (api_user_realm_auth(auth_basename($referer))) {
|
|
header('Location: ' . $referer);
|
|
} elseif (!is_realm_allowed(8)) {
|
|
cacti_log(sprintf("DEBUG: Referer Overridden Due to Permissions to '%s'", 'graph_view.php'), false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
|
|
header('Location: graph_view.php');
|
|
} else {
|
|
cacti_log(sprintf("DEBUG: Referer Overridden Due to Permissions to '%s'", 'index.php'), false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
|
|
header('Location: index.php');
|
|
}
|
|
|
|
break;
|
|
case '2': // default console page
|
|
if (!is_realm_allowed(8)) {
|
|
header('Location: ' . CACTI_PATH_URL . 'graph_view.php' . ($newtheme ? '?newtheme=1' : ''));
|
|
} else {
|
|
header('Location: ' . CACTI_PATH_URL . 'index.php' . ($newtheme ? '?newtheme=1' : ''));
|
|
}
|
|
|
|
break;
|
|
case '3': // default graph page
|
|
header('Location: ' . CACTI_PATH_URL . 'graph_view.php' . ($newtheme ? '?newtheme=1' : ''));
|
|
|
|
break;
|
|
default:
|
|
api_plugin_hook_function('login_options_navigate', $login_opts);
|
|
}
|
|
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Provides a URL-knowledgeable basename function
|
|
*
|
|
* @param string $referer The referer URL or file path to extract the base name from.
|
|
*
|
|
* @return string The base name of the referer URL or file path.
|
|
*/
|
|
function auth_basename(string $referer) : string {
|
|
$parts = explode('?', $referer);
|
|
|
|
return basename($parts[0]);
|
|
}
|
|
|
|
/**
|
|
* Creates a new user account from a template account
|
|
* if there is an error that would block login, the function set's the globals
|
|
* error and error_msg to inform the caller not to proceed with the login.
|
|
* in special cases, such as basic auth, the function will print out a custom
|
|
* error message and exit.
|
|
*
|
|
* @param string $username The username to use for the copy
|
|
* @param int $realm The login realm to use for the copy
|
|
*
|
|
* @return array The copied new user account details
|
|
*/
|
|
function auth_login_create_user_from_template(string $username, int $realm) : array {
|
|
global $error, $error_msg;
|
|
|
|
cacti_log("NOTE: User '" . $username . "' does not exist, copying template user", false, 'AUTH');
|
|
|
|
$user = [];
|
|
|
|
$user_template = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[get_template_account($username)]);
|
|
|
|
// check that template user exists
|
|
if (is_array($user_template) && !empty($user_template)) {
|
|
if ($realm == 3) { // This is an ldap login
|
|
// get user CN
|
|
$cn_full_name = read_config_option('cn_full_name');
|
|
$cn_email = read_config_option('cn_email');
|
|
|
|
if ($cn_full_name != '' || $cn_email != '') {
|
|
$ldap_cn_search_response = cacti_ldap_search_cn($username, [$cn_full_name, $cn_email]);
|
|
|
|
if (isset($ldap_cn_search_response['cn'])) {
|
|
$data_override = [];
|
|
|
|
if (array_key_exists($cn_full_name, $ldap_cn_search_response['cn'])) {
|
|
$data_override['full_name'] = $ldap_cn_search_response['cn'][$cn_full_name];
|
|
} else {
|
|
$data_override['full_name'] = '';
|
|
}
|
|
|
|
if (array_key_exists($cn_email, $ldap_cn_search_response['cn'])) {
|
|
$data_override['email_address'] = $ldap_cn_search_response['cn'][$cn_email];
|
|
} else {
|
|
$data_override['email_address'] = '';
|
|
}
|
|
|
|
user_copy($user_template['username'], $username, $user_template['realm'], $realm, false, $data_override);
|
|
} else {
|
|
$ldap_response = ($ldap_cn_search_response[0] ?? '(no response given)');
|
|
$ldap_code = ($ldap_cn_search_response['error_num'] ?? '(no code given)');
|
|
cacti_log('LOGIN: Email Address and Full Name fields not found, reason: ' . $ldap_response . 'code: ' . $ldap_code, false, 'AUTH');
|
|
user_copy($user_template['username'], $username, $user_template['realm'], $realm);
|
|
}
|
|
} else {
|
|
user_copy($user_template['username'], $username, $user_template['realm'], $realm);
|
|
}
|
|
} else {
|
|
user_copy($user_template['username'], $username, $user_template['realm'], $realm);
|
|
}
|
|
|
|
// requery newly created user
|
|
$user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$username, $realm]);
|
|
} else {
|
|
// error
|
|
$error = true;
|
|
$error_msg = __('Access Denied! Template user id %s does not exist. Please contact your Administrator.', read_config_option('user_template'));
|
|
|
|
cacti_log("LOGIN FAILED: Template user id '" . read_config_option('user_template') . "' does not exist.", false, 'AUTH');
|
|
|
|
if (read_config_option('auth_method') == AUTH_METHOD_BASIC) {
|
|
auth_display_custom_error_message($error_msg);
|
|
|
|
exit;
|
|
}
|
|
}
|
|
|
|
return is_array($user) ? $user : [];
|
|
}
|
|
|
|
/**
|
|
* Refuses the deprecated No Authentication mode instead of recovering from it
|
|
* automatically. When auth_method is None this fails closed with an error, so an
|
|
* unauthenticated request can no longer be auto-granted a session (GHSA-69vw).
|
|
*
|
|
* @param int $auth_method The current authentication method.
|
|
*
|
|
* @return bool Always returns false when authentication is configured; does not return when auth_method is None.
|
|
*/
|
|
function check_reset_no_authentication(int $auth_method) : bool {
|
|
global $error, $error_msg;
|
|
|
|
if ($auth_method == AUTH_METHOD_NONE) {
|
|
// GHSA-69vw-qxxv-p8w2: this recovery path formerly auto-migrated the
|
|
// deprecated No Authentication mode to Local auth - wiping the admin
|
|
// password, flipping auth_method, and handing the triggering, still
|
|
// unauthenticated, request a full admin session. On a legacy
|
|
// auth_method=0 instance (written by upgrades from <= 0.8.7) the first
|
|
// visitor therefore became admin and locked the real admin out. Fail
|
|
// closed: never mutate credentials or establish a session from an
|
|
// unauthenticated request; an administrator must set a supported
|
|
// auth_method value in the settings table before login can proceed.
|
|
$error = true;
|
|
$error_msg = __('Authentication is set to None, which is no longer supported. A system administrator must set a supported \'auth_method\' value in the Cacti settings table before login can proceed.');
|
|
|
|
cacti_log('LOGIN FAILED: auth_method is set to None (0); refusing to auto-provision an administrative session from IP address ' . get_client_addr(), false, 'AUTH');
|
|
|
|
auth_display_custom_error_message($error_msg);
|
|
|
|
exit;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Disables two-factor authentication (2FA) for a specified user.
|
|
*
|
|
* @param int $user_id The ID of the user for whom 2FA should be disabled.
|
|
*
|
|
* @return string JSON-encoded result with status and text message.
|
|
*/
|
|
function disable_2fa(int $user_id) : string {
|
|
$current_user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]
|
|
);
|
|
|
|
$result = ['status' => 500, 'text' => __('Unknown error')];
|
|
|
|
if (!cacti_sizeof($current_user)) {
|
|
$result['status'] = 404;
|
|
$result['text'] = __('ERROR: Unable to find user');
|
|
} else {
|
|
db_execute_prepared('UPDATE user_auth SET tfa_enabled = \'\', tfa_secret = \'\' WHERE id = ?', [$user_id]);
|
|
|
|
$current_user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$_SESSION[SESS_USER_ID]]
|
|
);
|
|
|
|
if (cacti_sizeof($current_user) && $current_user['tfa_enabled'] != '') {
|
|
$result['status'] = '501';
|
|
$result['text'] = __('2FA failed to be disabled');
|
|
} else {
|
|
$result['status'] = 200;
|
|
$result['text'] = __('2FA is now disabled');
|
|
}
|
|
}
|
|
|
|
return json_encode($result) ?: '';
|
|
}
|
|
|
|
/**
|
|
* Enables 2FA (Two-Factor Authentication) for a user.
|
|
*
|
|
* @param int $user_id The ID of the user for whom 2FA is being enabled.
|
|
*
|
|
* @return string JSON-encoded result containing the status and message of the operation.
|
|
*/
|
|
function enable_2fa(int $user_id) : string {
|
|
$current_user = db_fetch_row_prepared(
|
|
'SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]
|
|
);
|
|
|
|
$result = ['status' => 500, 'text' => __('Unknown error')];
|
|
|
|
if (!cacti_sizeof($current_user)) {
|
|
$result['status'] = 404;
|
|
$result['text'] = __('ERROR: Unable to find user');
|
|
} else {
|
|
$g = new \Sonata\GoogleAuthenticator\GoogleAuthenticator();
|
|
$secret = $g->generateSecret();
|
|
db_execute_prepared('UPDATE user_auth SET tfa_secret = ? WHERE id = ?', [$secret, $user_id]);
|
|
|
|
$current_user = db_fetch_row_prepared(
|
|
'SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$_SESSION[SESS_USER_ID]]
|
|
);
|
|
|
|
if (!cacti_sizeof($current_user)
|
|
|| !isset($current_user['tfa_secret'])
|
|
|| !isset($current_user['username'])
|
|
|| $current_user['tfa_secret'] != $secret) {
|
|
$result['status'] = '501';
|
|
$result['text'] = __('2FA secret failed to be generated/updated');
|
|
} else {
|
|
$result['status'] = 200;
|
|
$result['text'] = __('2FA secret has needs verification');
|
|
$result['link'] = \Sonata\GoogleAuthenticator\GoogleQrUrl::generate($current_user['username'] . '@' . $_SERVER['HTTP_HOST'], $current_user['tfa_secret'], 'Cacti');
|
|
}
|
|
}
|
|
|
|
return json_encode($result) ?: '';
|
|
}
|
|
|
|
/**
|
|
* Verifies the 2FA code for a given user and updates the user's 2FA status if the code is valid.
|
|
*
|
|
* @param int $user_id The ID of the user to verify.
|
|
* @param string $code The 2FA code to verify.
|
|
*
|
|
* @return string JSON encoded array containing the status and message of the verification process.
|
|
*/
|
|
function verify_2fa(int $user_id, string $code) : string {
|
|
$current_user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]
|
|
);
|
|
|
|
$result = ['status' => 500, 'text' => __('Unknown error')];
|
|
|
|
if (!cacti_sizeof($current_user)) {
|
|
$result['status'] = 404;
|
|
$result['text'] = __('ERROR: Unable to find user');
|
|
} else {
|
|
$g = new \Sonata\GoogleAuthenticator\GoogleAuthenticator();
|
|
$isValid = $g->checkCode($current_user['tfa_secret'], $code);
|
|
|
|
if (!$isValid) {
|
|
$result['status'] = 301;
|
|
$result['text'] = __('ERROR: Code was not verified, please try again');
|
|
} else {
|
|
db_execute_prepared('UPDATE user_auth SET tfa_enabled = ? WHERE id = ?', ['on', $user_id]);
|
|
|
|
$result['status'] = 200;
|
|
$result['text'] = __('2FA has been enabled and verified');
|
|
}
|
|
}
|
|
|
|
return json_encode($result) ?: '';
|
|
}
|
|
|
|
/**
|
|
* Checks if two-factor authentication (2FA) is enabled for a given user.
|
|
*
|
|
* @param int $user_id The ID of the user to check for 2FA status.
|
|
*
|
|
* @return bool Returns true if 2FA is enabled for the user, false otherwise.
|
|
*/
|
|
function is_2fa_enabled(int $user_id) : bool {
|
|
if (read_config_option('secpass_2fa_enabled') == 'on') {
|
|
$current_user = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]
|
|
);
|
|
|
|
return isset($current_user['tfa_enabled']) && ($current_user['tfa_enabled'] != '');
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** remote_agent_fcrdns_confirmed - forward-confirm a reverse DNS (PTR) result.
|
|
*
|
|
* Given the PTR-resolved name for a client address and the address' forward
|
|
* (A/AAAA) records, return true only when one forward record resolves back to
|
|
* the same address. This wraps the forward-confirmation loop that previously
|
|
* lived inline in remote_agent.php so the matching logic has a single home.
|
|
*
|
|
* @param string $client_addr the source address being authorized
|
|
* @param array $forward_records dns_get_record() output for the PTR name
|
|
*
|
|
* @return bool true when forward confirmation matches the source address */
|
|
function remote_agent_fcrdns_confirmed(string $client_addr, array $forward_records) : bool {
|
|
$client_binary = filter_var($client_addr, FILTER_VALIDATE_IP) !== false ? inet_pton($client_addr) : false;
|
|
|
|
if ($client_binary === false) {
|
|
return false;
|
|
}
|
|
|
|
foreach ($forward_records as $record) {
|
|
$ip = isset($record['ip']) ? $record['ip'] : (isset($record['ipv6']) ? $record['ipv6'] : '');
|
|
|
|
if ($ip !== '' && filter_var($ip, FILTER_VALIDATE_IP) !== false && inet_pton($ip) === $client_binary) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Validate an effective user delegated by a trusted Remote Agent peer.
|
|
*
|
|
* @param mixed $value Candidate user identifier from the request
|
|
* @param callable|null $lookup Optional lookup seam returning a user row
|
|
*
|
|
* @return int|false Enabled user identifier, or false when delegation is invalid
|
|
*/
|
|
function remote_agent_validate_effective_user(mixed $value, ?callable $lookup = null) : int|false {
|
|
if (is_int($value)) {
|
|
$user_id = $value;
|
|
} elseif (is_string($value) && ctype_digit($value)) {
|
|
$user_id = (int) $value;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
if ($user_id < 1) {
|
|
return false;
|
|
}
|
|
|
|
$lookup ??= static fn (int $id) : array => db_fetch_row_prepared(
|
|
'SELECT id, enabled
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$id]
|
|
);
|
|
$user = $lookup($user_id);
|
|
|
|
if (!is_array($user) || (int) ($user['id'] ?? 0) !== $user_id || ($user['enabled'] ?? '') !== 'on') {
|
|
return false;
|
|
}
|
|
|
|
return $user_id;
|
|
}
|