Files

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"])) {
$allowed++;
} else {
$rejected++;
}
}
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;
}