mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-24 20:17:28 +09:00
5166 lines
161 KiB
PHP
5166 lines
161 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
require_once(__DIR__ . '/client_address.php');
|
|
require_once(__DIR__ . '/ldap.php');
|
|
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/FixedBitNotation.php');
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/GoogleAuthenticatorInterface.php');
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/GoogleAuthenticator.php');
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/GoogleQrUrl.php');
|
|
include(__DIR__ . '/../include/vendor/GoogleAuthenticator/RuntimeException.php');
|
|
|
|
use Cacti\Auth\CredentialLoginProviderInterface;
|
|
use Cacti\Auth\LoginProviderFactory;
|
|
use phpseclib4\Crypt\RSA;
|
|
|
|
if (!defined('CACTI_SECRET_CIPHER')) {
|
|
define('CACTI_SECRET_CIPHER', 'aes-256-gcm');
|
|
}
|
|
|
|
if (!defined('CACTI_SECRET_TAG_LENGTH')) {
|
|
define('CACTI_SECRET_TAG_LENGTH', 16);
|
|
}
|
|
|
|
/**
|
|
* Clears a users security token
|
|
*
|
|
* @return void
|
|
*/
|
|
function clear_auth_cookie() : void {
|
|
if (isset($_COOKIE['cacti_remembers']) && read_config_option('auth_cache_enabled') == 'on') {
|
|
if (!is_string($_COOKIE['cacti_remembers'])) {
|
|
cacti_cookie_session_logout();
|
|
|
|
return;
|
|
}
|
|
|
|
$parts = explode(',', $_COOKIE['cacti_remembers']);
|
|
|
|
if (cacti_sizeof($parts) == 2) {
|
|
$user_id = $parts[0];
|
|
$realm_id = -1;
|
|
$token = $parts[1];
|
|
} elseif (cacti_sizeof($parts) == 3) {
|
|
$user_id = $parts[0];
|
|
$realm_id = $parts[1];
|
|
$token = $parts[2];
|
|
} else {
|
|
cacti_cookie_session_logout();
|
|
|
|
return;
|
|
}
|
|
|
|
// Legacy support which leaked usernames
|
|
if (!is_numeric($user_id)) {
|
|
$user_id = db_fetch_cell_prepared('SELECT id
|
|
FROM user_auth
|
|
WHERE username = ?',
|
|
[$user_id]);
|
|
}
|
|
|
|
if ($user_id > 0) {
|
|
$secret = hash('sha512', $token, false);
|
|
|
|
db_execute_prepared('DELETE FROM user_auth_cache
|
|
WHERE user_id = ?
|
|
AND token = ?',
|
|
[$user_id, $secret]);
|
|
}
|
|
|
|
// Revoke the server-side credential before clearing browser state.
|
|
cacti_cookie_session_logout();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* set_auth_cookie - sets a users security token
|
|
*
|
|
* @param array $user Is the user_auth row for the user
|
|
*
|
|
* @return void
|
|
*/
|
|
function set_auth_cookie(array $user) : void {
|
|
if (db_table_exists('user_auth_cache')) {
|
|
clear_auth_cookie();
|
|
|
|
$nssecret = bin2hex(random_bytes(32));
|
|
|
|
$secret = hash('sha512', $nssecret, false);
|
|
|
|
db_execute_prepared('REPLACE INTO user_auth_cache
|
|
(user_id, hostname, last_update, token)
|
|
VALUES
|
|
(?, ?, NOW(), ?);',
|
|
[$user['id'], get_client_addr(), $secret]);
|
|
|
|
cacti_cookie_session_set($user['id'], $user['realm'], $nssecret);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the authentication cookie and removes the user session from the cache.
|
|
*
|
|
* @return int|false The user of the session cookie, otherwise false
|
|
*/
|
|
function check_auth_cookie() : int|false {
|
|
if (isset($_COOKIE['cacti_remembers']) &&
|
|
read_config_option('auth_cache_enabled') == 'on' &&
|
|
db_table_exists('user_auth_cache')) {
|
|
if (!is_string($_COOKIE['cacti_remembers'])) {
|
|
return false;
|
|
}
|
|
|
|
$parts = explode(',', $_COOKIE['cacti_remembers']);
|
|
|
|
if (cacti_sizeof($parts) == 2) {
|
|
$user_id = $parts[0];
|
|
$realm_id = -1;
|
|
$token = $parts[1];
|
|
} elseif (cacti_sizeof($parts) == 3) {
|
|
$user_id = $parts[0];
|
|
$realm_id = $parts[1];
|
|
$token = $parts[2];
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
if (!is_numeric($user_id)) {
|
|
$user_id = db_fetch_cell_prepared('SELECT id
|
|
FROM user_auth
|
|
WHERE username = ?',
|
|
[$user_id]);
|
|
}
|
|
|
|
if ($user_id > 0 && $user_id !== get_guest_account()) {
|
|
if ($realm_id == -1) {
|
|
$user_info = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
} else {
|
|
$user_info = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE id = ?
|
|
AND realm = ?',
|
|
[$user_id, $realm_id]);
|
|
}
|
|
|
|
if (cacti_sizeof($user_info)) {
|
|
$secret = hash('sha512', $token, false);
|
|
|
|
// GHSA-xq26-f282-f998: reject remember-me tokens past the 90-day retention window instead of trusting the cron purge alone.
|
|
$found = db_fetch_cell_prepared('SELECT user_id
|
|
FROM user_auth_cache
|
|
WHERE user_id = ?
|
|
AND token = ?
|
|
AND last_update >= DATE_SUB(NOW(), INTERVAL 90 DAY)',
|
|
[$user_info['id'], $secret]
|
|
);
|
|
|
|
if (empty($found)) {
|
|
return false;
|
|
}
|
|
|
|
// a disabled account must not authenticate from a cookie issued before it was disabled
|
|
if ($user_info['enabled'] != 'on') {
|
|
db_execute_prepared('DELETE FROM user_auth_cache
|
|
WHERE user_id = ?',
|
|
[$user_info['id']]);
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!auth_cookie_user_currently_allowed($user_info)) {
|
|
db_execute_prepared('DELETE FROM user_auth_cache
|
|
WHERE user_id = ?',
|
|
[$user_info['id']]);
|
|
|
|
return false;
|
|
}
|
|
|
|
set_auth_cookie($user_info);
|
|
|
|
cacti_log(sprintf('LOGIN: User %s Authenticated via Authentication Cookie from IP Address %s', $user_info['username'], get_client_addr()), false, 'AUTH');
|
|
|
|
db_execute_prepared('INSERT IGNORE INTO user_log
|
|
(username, user_id, result, ip, time)
|
|
VALUES
|
|
(?, ?, 2, ?, NOW())',
|
|
[$user_info['username'], $user_info['id'], get_client_addr()]
|
|
);
|
|
|
|
return $user_info['id'];
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Confirms that a remember-me cookie still belongs to an enabled account
|
|
* with current access before the cookie is allowed to create a new session.
|
|
*
|
|
* @param array $user_info The user_auth row loaded from the cookie identity.
|
|
*
|
|
* @return bool True when the account can still authenticate with the cookie.
|
|
*/
|
|
function auth_cookie_user_currently_allowed(array $user_info) : bool {
|
|
if (($user_info['enabled'] ?? '') != 'on') {
|
|
return false;
|
|
}
|
|
|
|
if (($user_info['locked'] ?? '') == 'on') {
|
|
return false;
|
|
}
|
|
|
|
if (!auth_realm_allows_cookies((int) ($user_info['realm'] ?? 0))) {
|
|
return false;
|
|
}
|
|
|
|
return auth_user_has_access($user_info);
|
|
}
|
|
|
|
/**
|
|
* cacti_auth_transition - move a session from unauthenticated to authenticated.
|
|
*
|
|
* Issues a new session id so a session identifier planted by an attacker before
|
|
* login cannot be reused afterwards, and drops the permission caches so the new identity is
|
|
* evaluated from scratch rather than inheriting the previous one.
|
|
*
|
|
* Call this at every point that first sets the session user id, apart from the
|
|
* guest account, which is not a privilege gain.
|
|
*
|
|
* @param int $user_id The account the session is becoming.
|
|
* @param string $reason Where the transition came from, for the log.
|
|
*
|
|
* @return bool False when the account is locked and must not be let in.
|
|
*/
|
|
function cacti_auth_transition(int $user_id, string $reason = 'login') : bool {
|
|
$locked = db_fetch_cell_prepared('SELECT locked
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
if ($locked === false || $locked == 'on') {
|
|
cacti_log(sprintf('SECURITY: auth transition blocked for unavailable or locked user %d, reason %s', $user_id, $reason), false, 'AUTH');
|
|
|
|
return false;
|
|
}
|
|
|
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
|
if (!session_regenerate_id(true)) {
|
|
cacti_log(sprintf('SECURITY: auth transition blocked because session regeneration failed for user %d, reason %s', $user_id, $reason), false, 'AUTH');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
kill_session_var(SESS_TREE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS);
|
|
kill_session_var(SESS_USER_PERMS_KEY);
|
|
kill_session_var(SESS_USER_2FA);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
|
|
cacti_log(sprintf('NOTE: auth transition completed for user %d, reason %s', $user_id, $reason), false, 'AUTH', POLLER_VERBOSITY_MEDIUM);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Given a username or user_id test if this is a template account
|
|
* Template accounts could be accounts used for the administrative email, for both
|
|
* the guest and template accounts, or a user that is specified by a plugin as a
|
|
* template account.
|
|
*
|
|
* @param null|int|string $user_id The user ID or username to check.
|
|
*
|
|
* @return bool True if the user is a template account, false otherwise.
|
|
*/
|
|
function is_template_account(null|int|string $user_id) : bool {
|
|
if (is_string($user_id)) {
|
|
$user_id = db_fetch_cell_prepared('SELECT id FROM user_auth WHERE username = ?', [$user_id]);
|
|
}
|
|
|
|
if (empty($user_id)) {
|
|
return false;
|
|
}
|
|
|
|
if (read_config_option('admin_user') == $user_id) {
|
|
return true;
|
|
}
|
|
|
|
if (read_config_option('guest_user') == $user_id) {
|
|
return true;
|
|
}
|
|
|
|
if (read_config_option('user_template') == $user_id) {
|
|
return true;
|
|
} else {
|
|
$domain_template = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM login_providers
|
|
WHERE user_id = ?',
|
|
[$user_id]);
|
|
|
|
if ($domain_template > 0) {
|
|
return true;
|
|
} else {
|
|
$plugin_template = get_template_account($user_id);
|
|
|
|
if ($plugin_template == $user_id) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Whether the current request originates from a reverse proxy that config.php
|
|
* lists in $trusted_proxies. Only such proxies may assert a pre-authenticated
|
|
* user via forwarded HTTP headers. Matching is IP-normalized, so equivalent
|
|
* spellings (for example ::1 and 0:0:0:0:0:0:0:1) match regardless of text
|
|
* representation, while an IPv4-mapped entry never silently matches its bare
|
|
* IPv4 form. CIDR entries such as 10.0.0.0/8 are accepted. A malformed or
|
|
* non-IP entry falls back to exact string equality. An empty or unset list
|
|
* trusts no proxy.
|
|
*
|
|
* @return bool true when REMOTE_ADDR matches a trusted proxy entry.
|
|
*/
|
|
function is_trusted_proxy() : bool {
|
|
global $config;
|
|
|
|
if (!isset($_SERVER['REMOTE_ADDR'])) {
|
|
return false;
|
|
}
|
|
|
|
$trusted = $config['trusted_proxies'] ?? [];
|
|
|
|
if (!is_array($trusted) || !cacti_sizeof($trusted)) {
|
|
return false;
|
|
}
|
|
|
|
return cacti_trusted_proxy_match((string) $_SERVER['REMOTE_ADDR'], $trusted);
|
|
}
|
|
|
|
/**
|
|
* If basic auth is used, return the valid username
|
|
*
|
|
* @return string|false The username if found and processed, or false if no username is found.
|
|
*/
|
|
function get_basic_auth_username() : string|false {
|
|
/* Headers arriving with an HTTP_ prefix are client-supplied and spoofable.
|
|
* Only honor a forwarded identity when the request comes from a trusted
|
|
* reverse proxy. The unprefixed REMOTE_USER / REDIRECT_REMOTE_USER vars are
|
|
* set by the web server's own auth module and are always honored. */
|
|
$trusted = is_trusted_proxy();
|
|
|
|
if (isset($_SERVER['PHP_AUTH_USER'])) {
|
|
$raw = is_array($_SERVER['PHP_AUTH_USER']) ? ($_SERVER['PHP_AUTH_USER'][0] ?? '') : $_SERVER['PHP_AUTH_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif (isset($_SERVER['REMOTE_USER'])) {
|
|
$raw = is_array($_SERVER['REMOTE_USER']) ? ($_SERVER['REMOTE_USER'][0] ?? '') : $_SERVER['REMOTE_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif (isset($_SERVER['REDIRECT_REMOTE_USER'])) {
|
|
$raw = is_array($_SERVER['REDIRECT_REMOTE_USER']) ? ($_SERVER['REDIRECT_REMOTE_USER'][0] ?? '') : $_SERVER['REDIRECT_REMOTE_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif ($trusted && isset($_SERVER['HTTP_X_FORWARDED_USER'])) {
|
|
$raw = is_array($_SERVER['HTTP_X_FORWARDED_USER']) ? ($_SERVER['HTTP_X_FORWARDED_USER'][0] ?? '') : $_SERVER['HTTP_X_FORWARDED_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif ($trusted && isset($_SERVER['HTTP_PHP_AUTH_USER'])) {
|
|
$raw = is_array($_SERVER['HTTP_PHP_AUTH_USER']) ? ($_SERVER['HTTP_PHP_AUTH_USER'][0] ?? '') : $_SERVER['HTTP_PHP_AUTH_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif ($trusted && isset($_SERVER['HTTP_REMOTE_USER'])) {
|
|
$raw = is_array($_SERVER['HTTP_REMOTE_USER']) ? ($_SERVER['HTTP_REMOTE_USER'][0] ?? '') : $_SERVER['HTTP_REMOTE_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} elseif ($trusted && isset($_SERVER['HTTP_REDIRECT_REMOTE_USER'])) {
|
|
$raw = is_array($_SERVER['HTTP_REDIRECT_REMOTE_USER']) ? ($_SERVER['HTTP_REDIRECT_REMOTE_USER'][0] ?? '') : $_SERVER['HTTP_REDIRECT_REMOTE_USER'];
|
|
$username = str_replace('\\', '\\\\', $raw);
|
|
} else {
|
|
$username = false;
|
|
}
|
|
|
|
if ($username !== false && is_string($username)) {
|
|
if (str_contains($username, '@')) {
|
|
$upart = explode('@', $username);
|
|
$username = $upart[0];
|
|
}
|
|
|
|
/* Handle mapping basic accounts to shortform accounts.
|
|
* Format of map file is CSV: basic,shortform */
|
|
$mapfile = read_config_option('path_basic_mapfile');
|
|
|
|
if ($mapfile != '' && file_exists($mapfile) && is_readable($mapfile)) {
|
|
$records = file($mapfile);
|
|
$found = false;
|
|
|
|
if (cacti_sizeof($records)) {
|
|
foreach (($records ?: []) as $r) {
|
|
if (trim($r) === '') {
|
|
continue;
|
|
}
|
|
|
|
[$basic, $shortform] = str_getcsv($r);
|
|
|
|
if ($basic === null || trim($basic) === '') {
|
|
continue;
|
|
}
|
|
|
|
if (trim($basic) == $username) {
|
|
$username = trim($shortform ?? '');
|
|
$found = true;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$found) {
|
|
cacti_log("WARNING: Username $username not found in basic mapfile.", false, 'AUTH');
|
|
}
|
|
}
|
|
}
|
|
|
|
return is_string($username) ? $username : false;
|
|
}
|
|
|
|
/**
|
|
* Copies a user from a template user to a new user, optionally overwriting an existing user.
|
|
*
|
|
* @param string $template_user The username of the template user to copy from.
|
|
* @param string $new_user The username of the new user to create.
|
|
* @param int $template_realm The realm of the template user. Default is 0.
|
|
* @param int $new_realm The realm of the new user. Default is 0.
|
|
* @param bool $overwrite Whether to overwrite an existing user with the same username and realm. Default is false.
|
|
* @param array $data_override An associative array of fields to override in the new user's data. Default is an empty array.
|
|
*
|
|
* @return mixed - The ID of the new user if successful, or false if the operation failed.
|
|
*/
|
|
function user_copy(string $template_user, string $new_user, int $template_realm = 0, int $new_realm = 0, bool $overwrite = false, array $data_override = []) : mixed {
|
|
// ================= input validation =================
|
|
input_validate_input_number($template_realm, 'template_realm');
|
|
input_validate_input_number($new_realm, 'new_realm');
|
|
// ====================================================
|
|
|
|
// Check get template users array
|
|
$user_auth = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$template_user, $template_realm]);
|
|
|
|
if (!cacti_sizeof($user_auth)) {
|
|
return false;
|
|
}
|
|
|
|
$template_id = $user_auth['id'];
|
|
|
|
// Create update/insert for new/existing user
|
|
$user_exist = db_fetch_row_prepared('SELECT *
|
|
FROM user_auth
|
|
WHERE username = ?
|
|
AND realm = ?',
|
|
[$new_user, $new_realm]);
|
|
|
|
if (cacti_sizeof($user_exist)) {
|
|
if ($overwrite === true) {
|
|
// Overwrite existing user
|
|
$user_auth['id'] = $user_exist['id'];
|
|
$user_auth['username'] = $user_exist['username'];
|
|
$user_auth['password'] = $user_exist['password'];
|
|
$user_auth['realm'] = $user_exist['realm'];
|
|
$user_auth['full_name'] = $user_exist['full_name'];
|
|
$user_auth['email_address'] = $user_exist['email_address'];
|
|
$user_auth['must_change_password'] = $user_exist['must_change_password'];
|
|
$user_auth['enabled'] = $user_exist['enabled'];
|
|
} else {
|
|
// User already exists, duplicate users are bad
|
|
raise_message(19);
|
|
|
|
return false;
|
|
}
|
|
} else {
|
|
// new user
|
|
$user_auth['id'] = 0;
|
|
$user_auth['username'] = $new_user;
|
|
$user_auth['enabled'] = 'on';
|
|
$user_auth['password'] = mt_rand(100000, 10000000);
|
|
$user_auth['email_address'] = '';
|
|
$user_auth['realm'] = $new_realm;
|
|
}
|
|
|
|
// Update data_override fields
|
|
if (cacti_sizeof($data_override)) {
|
|
foreach ($data_override as $field => $value) {
|
|
if (isset($user_auth[$field]) && $field != 'id' && $field != 'username') {
|
|
$user_auth[$field] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the user
|
|
$new_id = sql_save($user_auth, 'user_auth');
|
|
|
|
// Create/Update permissions and settings
|
|
if (cacti_sizeof($user_exist) && $overwrite === true) {
|
|
db_execute_prepared('DELETE FROM user_auth_perms WHERE user_id = ?', [$user_exist['id']]);
|
|
db_execute_prepared('DELETE FROM user_auth_realm WHERE user_id = ?', [$user_exist['id']]);
|
|
db_execute_prepared('DELETE FROM settings_user WHERE user_id = ?', [$user_exist['id']]);
|
|
db_execute_prepared('DELETE FROM settings_tree WHERE user_id = ?', [$user_exist['id']]);
|
|
}
|
|
|
|
$user_auth_perms = db_fetch_assoc_prepared('SELECT *
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($user_auth_perms)) {
|
|
foreach ($user_auth_perms as $row) {
|
|
$row['user_id'] = $new_id;
|
|
sql_save($row, 'user_auth_perms', ['user_id', 'item_id', 'type'], false);
|
|
}
|
|
}
|
|
|
|
$user_auth_realm = db_fetch_assoc_prepared('SELECT *
|
|
FROM user_auth_realm
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($user_auth_realm)) {
|
|
foreach ($user_auth_realm as $row) {
|
|
$row['user_id'] = $new_id;
|
|
sql_save($row, 'user_auth_realm', ['realm_id', 'user_id'], false);
|
|
}
|
|
}
|
|
|
|
$settings_user = db_fetch_assoc_prepared('SELECT *
|
|
FROM settings_user
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($settings_user)) {
|
|
foreach ($settings_user as $row) {
|
|
$row['user_id'] = $new_id;
|
|
sql_save($row, 'settings_user', ['user_id', 'name'], false);
|
|
}
|
|
}
|
|
|
|
$settings_tree = db_fetch_assoc_prepared('SELECT *
|
|
FROM settings_tree
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($settings_tree)) {
|
|
foreach ($settings_tree as $row) {
|
|
$row['user_id'] = $new_id;
|
|
sql_save($row, 'settings_tree', ['user_id', 'graph_tree_item_id'], false);
|
|
}
|
|
}
|
|
|
|
// apply group permissions for the user
|
|
$groups = db_fetch_assoc_prepared('SELECT group_id
|
|
FROM user_auth_group_members
|
|
WHERE user_id = ?',
|
|
[$template_id]);
|
|
|
|
if (cacti_sizeof($groups)) {
|
|
$sql = [];
|
|
|
|
foreach ($groups as $g) {
|
|
$sql[] = '(' . $new_id . ', ' . $g['group_id'] . ')';
|
|
}
|
|
|
|
db_execute('INSERT IGNORE INTO user_auth_group_members
|
|
(user_id, group_id) VALUES ' . implode(',', $sql));
|
|
}
|
|
|
|
api_plugin_hook_function('copy_user', ['template_id' => $template_id, 'new_id' => $new_id]);
|
|
|
|
return $new_id;
|
|
}
|
|
|
|
/**
|
|
* Removes a user and all associated data from the database.
|
|
*
|
|
* @param int $user_id The ID of the user to be removed.
|
|
*
|
|
* @return void
|
|
*/
|
|
function user_remove(int $user_id) : void {
|
|
// ================= input validation =================
|
|
input_validate_input_number($user_id, 'user_id');
|
|
// ====================================================
|
|
|
|
// check for guest or template user
|
|
$username = db_fetch_cell_prepared('SELECT username
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
if ($username != gnrv('username')) {
|
|
if (is_template_account($user_id)) {
|
|
raise_message(21);
|
|
|
|
return;
|
|
}
|
|
|
|
if ($user_id === get_guest_account()) {
|
|
raise_message(21);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
db_execute_prepared('DELETE FROM user_auth WHERE id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_realm WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_perms WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_row_cache WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_group_members WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM settings_user WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM settings_tree WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_layouts WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM sessions WHERE user_id = ?', [$user_id]);
|
|
|
|
api_plugin_hook_function('user_remove', $user_id);
|
|
}
|
|
|
|
/**
|
|
* Disable a user account
|
|
*
|
|
* @param int $user_id The ID of the user to disable.
|
|
*
|
|
* @return void
|
|
*/
|
|
function user_disable(int $user_id) : void {
|
|
// ================= input validation =================
|
|
input_validate_input_number($user_id, 'user_id');
|
|
// ====================================================
|
|
|
|
db_execute_prepared("UPDATE user_auth SET enabled = '' WHERE id = ?", [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_cache WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM user_auth_row_cache WHERE user_id = ?', [$user_id]);
|
|
db_execute_prepared('DELETE FROM sessions WHERE user_id = ?', [$user_id]);
|
|
|
|
reset_user_perms($user_id);
|
|
}
|
|
|
|
/**
|
|
* Enable a user account
|
|
*
|
|
* @param int $user_id The ID of the user to enable.
|
|
*
|
|
* @return void
|
|
*/
|
|
function user_enable(int $user_id) : void {
|
|
// ================= input validation =================
|
|
input_validate_input_number($user_id, 'user_id');
|
|
// ====================================================
|
|
|
|
db_execute_prepared("UPDATE user_auth SET enabled = 'on' WHERE id = ?", [$user_id]);
|
|
|
|
reset_user_perms($user_id);
|
|
}
|
|
|
|
/**
|
|
* Replace every outstanding password-reset token for a local user.
|
|
*
|
|
* Locking the user row serializes the self-service and administrator issuance
|
|
* paths, so concurrent requests cannot leave multiple valid reset links.
|
|
*
|
|
* @param int $user_id The local user receiving the token
|
|
* @param string $hash Cryptographically random reset-token hash
|
|
* @param int $timeout_minutes Token lifetime in minutes
|
|
*
|
|
* @return bool True when the replacement token was committed
|
|
*/
|
|
function auth_reset_token_replace(int $user_id, string $hash, int $timeout_minutes) : bool {
|
|
if ($user_id <= 0 || $hash === '' || $timeout_minutes <= 0 || !db_begin_transaction()) {
|
|
return false;
|
|
}
|
|
|
|
$locked_user = db_fetch_cell_prepared('SELECT id
|
|
FROM user_auth
|
|
WHERE id = ?
|
|
AND realm = 0
|
|
AND enabled = "on"
|
|
FOR UPDATE',
|
|
[$user_id]);
|
|
|
|
$success = (int) $locked_user === $user_id &&
|
|
db_execute_prepared('DELETE FROM user_auth_reset_hashes WHERE user_id = ?', [$user_id]) !== false &&
|
|
db_execute_prepared('INSERT INTO user_auth_reset_hashes
|
|
(user_id, hash, expiry)
|
|
VALUES (?, ?, date_add(now(), interval ? minute))',
|
|
[$user_id, $hash, $timeout_minutes]) !== false;
|
|
|
|
if ($success && db_commit_transaction()) {
|
|
return true;
|
|
}
|
|
|
|
db_rollback_transaction();
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the authentication realms based on the configured authentication method.
|
|
*
|
|
* @param bool $login Whether the function is being called during a login process. Default is false.
|
|
*
|
|
* @return array An array of authentication realms
|
|
*/
|
|
function get_auth_realms(bool $login = false) : array {
|
|
if (read_config_option('auth_method') == AUTH_METHOD_PROVIDERS) {
|
|
// SAML2/OpenID are redirect-only and cannot be driven from this username
|
|
// + password realm dropdown; they get their own login page buttons instead.
|
|
$drealms = db_fetch_assoc('SELECT id, name
|
|
FROM login_providers
|
|
WHERE enabled="on"
|
|
AND type IN (' . PROVIDER_TYPE_LDAP . ', ' . PROVIDER_TYPE_AD . ')
|
|
ORDER BY name');
|
|
|
|
$drealms = is_array($drealms) ? $drealms : [];
|
|
|
|
if (cacti_sizeof($drealms)) {
|
|
if ($login) {
|
|
$new_realms['0'] = [
|
|
'name' => __('Local'),
|
|
'selected' => false
|
|
];
|
|
|
|
foreach ($drealms as $realm) {
|
|
$new_realms[1000 + $realm['id']] = [
|
|
'name' => $realm['name'],
|
|
'selected' => false
|
|
];
|
|
}
|
|
|
|
$default_realm = db_fetch_cell('SELECT id
|
|
FROM login_providers
|
|
WHERE is_default=1
|
|
AND enabled="on"
|
|
AND type IN (' . PROVIDER_TYPE_LDAP . ', ' . PROVIDER_TYPE_AD . ')');
|
|
|
|
if (!empty($default_realm)) {
|
|
$new_realms[1000 + $default_realm]['selected'] = true;
|
|
} else {
|
|
$new_realms['0']['selected'] = true;
|
|
}
|
|
} else {
|
|
$new_realms['0'] = __('Local');
|
|
|
|
foreach ($drealms as $realm) {
|
|
$new_realms[1000 + $realm['id']] = $realm['name'];
|
|
}
|
|
}
|
|
|
|
return $new_realms;
|
|
} else {
|
|
$new_realms['0'] = [
|
|
'name' => __('Local'),
|
|
'selected' => false
|
|
];
|
|
|
|
return $new_realms;
|
|
}
|
|
}
|
|
|
|
// TODO: Verify this array
|
|
return [
|
|
'0' => ['name' => __('Local')],
|
|
'3' => ['name' => __('LDAP')],
|
|
'2' => ['name' => __('Web Basic')],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Returns the enabled SAML2/OpenID Login Providers, for rendering as
|
|
* "Login with <button label>" buttons on the login page. Unlike
|
|
* get_auth_realms(), these are never part of the username/password realm
|
|
* dropdown since authenticating with them always starts with a browser
|
|
* redirect to the external IdP, handled by login_sso.php.
|
|
*
|
|
* @return array<int, array{realm: int, label: string}>
|
|
*/
|
|
function get_sso_login_providers() : array {
|
|
if (read_config_option('auth_method') != AUTH_METHOD_PROVIDERS) {
|
|
return [];
|
|
}
|
|
|
|
$providers = db_fetch_assoc('SELECT id, name, button_label
|
|
FROM login_providers
|
|
WHERE enabled = "on"
|
|
AND type IN (' . PROVIDER_TYPE_SAML2 . ', ' . PROVIDER_TYPE_OPENID . ')
|
|
ORDER BY name');
|
|
|
|
$buttons = [];
|
|
|
|
foreach ($providers as $provider) {
|
|
$buttons[] = [
|
|
'realm' => 1000 + $provider['id'],
|
|
'label' => $provider['button_label'] !== '' ? $provider['button_label'] : $provider['name'],
|
|
];
|
|
}
|
|
|
|
return $buttons;
|
|
}
|
|
|
|
/**
|
|
* Whether a login realm's provider allows "remember me" auth cookies.
|
|
* Realms below 1000 (local/basic/single-LDAP) always defer to the global
|
|
* auth_cache_enabled setting only, since they have no per-provider row.
|
|
*
|
|
* @param int $realm The login realm (0/2/3, or 1000 + login_providers.id).
|
|
*
|
|
* @return bool
|
|
*/
|
|
function auth_realm_allows_cookies(int $realm) : bool {
|
|
if ($realm < 1000) {
|
|
return true;
|
|
}
|
|
|
|
// Disabling or deleting a provider must revoke the ability for its
|
|
// existing remember-me cookies to keep restoring a session; a missing
|
|
// or disabled row is treated as "deny", not "allow".
|
|
$allowed = db_fetch_cell_prepared('SELECT allow_auth_cookies
|
|
FROM login_providers
|
|
WHERE id = ?
|
|
AND enabled = "on"',
|
|
[$realm - 1000]);
|
|
|
|
return $allowed === 'on';
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current user is allowed to view a certain graph
|
|
*
|
|
* @param int $local_graph_id The ID of the local graph to check.
|
|
* @param int $user_id The ID of the user to check permissions for.
|
|
*
|
|
* @return bool True if the graph is allowed for the user, false otherwise.
|
|
*/
|
|
function is_graph_allowed(int $local_graph_id, int $user_id = 0) : bool {
|
|
$rows = 0;
|
|
|
|
get_allowed_graphs('', '', '', $rows, $user_id, $local_graph_id);
|
|
|
|
return ($rows > 0);
|
|
}
|
|
|
|
/**
|
|
* A helper function to checking Tree permissions
|
|
*
|
|
* @param array $objects An array of objects to check permissions against.
|
|
* @param int $policy The policy to apply
|
|
*
|
|
* @return bool Returns true or false based on the policy and the presence of objects
|
|
*/
|
|
function auth_check_perms(array $objects, int $policy) : bool {
|
|
$objectSize = cacti_sizeof($objects);
|
|
|
|
// policy == allow AND matches = DENY
|
|
if ($objectSize && $policy == 1) {
|
|
return false;
|
|
// policy == deny AND matches = ALLOW
|
|
}
|
|
|
|
if ($objectSize && $policy == 2) {
|
|
return true;
|
|
// policy == allow AND no matches = ALLOW
|
|
}
|
|
|
|
if (!$objectSize && $policy == 1) {
|
|
return true;
|
|
// policy == deny AND no matches = DENY
|
|
}
|
|
|
|
if (!$objectSize && $policy == 2) {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* A helper function to extend Cacti roles with additional realms or to add a new role.
|
|
*
|
|
* @param string $role_name The name of the role to augment.
|
|
* @param array $files An array of file paths to check and associate with realm IDs.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_augment_roles(string $role_name, array $files) : void {
|
|
global $user_auth_roles, $user_auth_realm_filenames;
|
|
|
|
foreach ($files as $file) {
|
|
if (array_search($file, $user_auth_realm_filenames, true) !== false) {
|
|
if (array_search($user_auth_realm_filenames[$file], $user_auth_roles[$role_name], true) === false) {
|
|
$user_auth_roles[$role_name][] = $user_auth_realm_filenames[$file];
|
|
}
|
|
} else {
|
|
if (isset($_SESSION[SESS_AUTH_NAMES][$file])) {
|
|
$realm_id = $_SESSION[SESS_AUTH_NAMES][$file];
|
|
} else {
|
|
$realm_id = db_fetch_cell_prepared('SELECT id+100 AS realm
|
|
FROM plugin_realms
|
|
WHERE file = ?
|
|
OR file LIKE ?
|
|
OR file LIKE ?
|
|
OR file LIKE ?',
|
|
[
|
|
$file,
|
|
$file . ',%',
|
|
'%,' . $file . ',%',
|
|
'%,' . $file
|
|
]
|
|
);
|
|
|
|
if ($realm_id > 0) {
|
|
$_SESSION[SESS_AUTH_NAMES][$file] = $realm_id;
|
|
}
|
|
}
|
|
|
|
if (!empty($realm_id)) {
|
|
if (!isset($user_auth_roles[$role_name])) {
|
|
$user_auth_roles[$role_name][] = $realm_id;
|
|
} elseif (array_search($realm_id, $user_auth_roles[$role_name], true) === false) {
|
|
$user_auth_roles[$role_name][] = $realm_id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A helper function to extend Cacti roles with additional realms or to add a new role.
|
|
*
|
|
* @param string $role_name The name of the role to augment.
|
|
* @param string $auth_name The authentication name to look up the realm ID.
|
|
*
|
|
* @return void
|
|
*/
|
|
function auth_augment_roles_byname(string $role_name, string $auth_name) : void {
|
|
global $user_auth_roles, $user_auth_realm_filenames;
|
|
|
|
if (isset($_SESSION[SESS_AUTH_NAMES][$auth_name])) {
|
|
$realm_id = $_SESSION[SESS_AUTH_NAMES][$auth_name];
|
|
} else {
|
|
$realm_id = db_fetch_cell_prepared('SELECT id+100 AS realm
|
|
FROM plugin_realms
|
|
WHERE display = ?',
|
|
[$auth_name]);
|
|
|
|
if ($realm_id > 0) {
|
|
$_SESSION[SESS_AUTH_NAMES][$auth_name] = $realm_id;
|
|
}
|
|
}
|
|
|
|
if (!empty($realm_id)) {
|
|
if (!isset($user_auth_roles[$role_name])) {
|
|
$user_auth_roles[$role_name][] = $realm_id;
|
|
} elseif (array_search($realm_id, $user_auth_roles[$role_name], true) === false) {
|
|
$user_auth_roles[$role_name][] = $realm_id;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current user is allowed to view a certain graph tree
|
|
*
|
|
* @param int $tree_id The ID of the tree to check access for.
|
|
* @param int $user_id The ID of the user to check access for.
|
|
*
|
|
* @return bool True if the user is allowed to access the tree, false otherwise.
|
|
*/
|
|
function is_tree_allowed(int $tree_id, int $user_id = 0) : bool {
|
|
if ($user_id == -1) {
|
|
return true;
|
|
}
|
|
|
|
if (isset($_SESSION[SESS_TREE_PERMS][$tree_id])) {
|
|
return $_SESSION[SESS_TREE_PERMS][$tree_id];
|
|
}
|
|
|
|
if ($user_id == 0) {
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = false;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$policy = db_fetch_cell_prepared('SELECT policy_trees
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
$trees = db_fetch_assoc_prepared('SELECT user_id
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?
|
|
AND type = 2
|
|
AND item_id = ?',
|
|
[$user_id, $tree_id]);
|
|
|
|
if (auth_check_perms($trees, $policy)) {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
// check for group perms
|
|
$groups = db_fetch_assoc_prepared("SELECT uag.policy_trees
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?",
|
|
[$user_id]);
|
|
|
|
if (!cacti_sizeof($groups)) {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
foreach ($groups as $g) {
|
|
if (auth_check_perms($trees, $g['policy_trees'])) {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// check for group trees
|
|
$gtrees = db_fetch_assoc_prepared("SELECT uagm.user_id
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
INNER JOIN user_auth_group_perms as uagp
|
|
ON uagp.group_id = uag.id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?
|
|
AND uagp.item_id = ?",
|
|
[$user_id, $tree_id]);
|
|
|
|
foreach ($groups as $g) {
|
|
if (auth_check_perms($gtrees, $g['policy_trees'])) {
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$_SESSION[SESS_TREE_PERMS][$tree_id] = false;
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current user is allowed to view a certain device
|
|
*
|
|
* @param int $device_id The ID of the device to check.
|
|
* @param int $user_id The ID of the user to check.
|
|
*
|
|
* @return bool True if the device is allowed for the user, false otherwise.
|
|
*/
|
|
function is_device_allowed(int $device_id, int $user_id = 0) : bool {
|
|
$total_rows = -2;
|
|
get_allowed_devices('', '', '', $total_rows, $user_id, $device_id);
|
|
|
|
return ($total_rows > 0);
|
|
}
|
|
|
|
/**
|
|
* Determines whether the current user is allowed to view a certain graph template
|
|
*
|
|
* @param int $graph_template_id The ID of the graph template to check.
|
|
* @param int $user The ID of the user to check permissions for. Defaults to 0.
|
|
*
|
|
* @return bool Returns true if the graph template is allowed for the user, false otherwise.
|
|
*/
|
|
function is_graph_template_allowed(int $graph_template_id, int $user = 0) : bool {
|
|
$total_rows = -2;
|
|
get_allowed_graph_templates('', '', '', $total_rows, $user, $graph_template_id);
|
|
|
|
return ($total_rows > 0);
|
|
}
|
|
|
|
/**
|
|
* Returns a true or false as to whether or not a specific view type is allowed
|
|
* View options include 'show_tree', 'show_list', 'show_preview', 'graph_settings'
|
|
*
|
|
* @param string $view The view to check permissions for. Default is 'show_tree'.
|
|
*
|
|
* @return bool Returns true if the view is allowed for the user, false otherwise.
|
|
*/
|
|
function is_view_allowed(string $view = 'show_tree') : bool {
|
|
$allowed_views = ['show_tree', 'show_list', 'show_preview', 'graph_settings'];
|
|
|
|
if (!in_array($view, $allowed_views, true)) {
|
|
cacti_log("WARNING: Invalid view parameter '$view' in is_view_allowed()", false, 'AUTH');
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!isset($_SESSION[SESS_USER_ID])) {
|
|
return false;
|
|
}
|
|
|
|
if (db_table_exists('user_auth_group')) {
|
|
$values = array_rekey(
|
|
db_fetch_assoc_prepared("SELECT DISTINCT `$view`
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?",
|
|
[$_SESSION[SESS_USER_ID]]
|
|
), $view, $view
|
|
);
|
|
|
|
if (isset($values[3])) {
|
|
return false;
|
|
}
|
|
|
|
if (isset($values['on'])) {
|
|
return true;
|
|
}
|
|
|
|
if (isset($values[2])) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$value = db_fetch_cell_prepared("SELECT `$view`
|
|
FROM user_auth
|
|
WHERE id = ?",
|
|
[$_SESSION[SESS_USER_ID]]
|
|
);
|
|
|
|
return ($value == 'on');
|
|
}
|
|
|
|
/**
|
|
* Given a tree id and a branch id, check if it's empty
|
|
*
|
|
* @param int $tree_id The ID of the tree to check.
|
|
* @param int $parent The parent ID of the branch to check. Default is 0.
|
|
*
|
|
* @return bool Returns true if the branch is empty, false otherwise.
|
|
*/
|
|
function is_tree_branch_empty(int $tree_id, int $parent = 0) : bool {
|
|
$graphs = array_rekey(
|
|
db_fetch_assoc_prepared('SELECT local_graph_id
|
|
FROM graph_tree_items
|
|
WHERE graph_tree_id = ?
|
|
AND local_graph_id > 0
|
|
AND parent = ?',
|
|
[$tree_id, $parent]
|
|
), 'local_graph_id', 'local_graph_id'
|
|
);
|
|
|
|
$simple_perms = get_simple_graph_perms($_SESSION[SESS_USER_ID]);
|
|
|
|
if (cacti_sizeof($graphs) && ($simple_perms || cacti_sizeof(get_allowed_graphs('gl.id IN(' . implode(',', $graphs) . ')'))) > 0) {
|
|
return false;
|
|
}
|
|
|
|
$hosts = array_rekey(
|
|
db_fetch_assoc_prepared('SELECT host_id
|
|
FROM graph_tree_items
|
|
WHERE graph_tree_id = ?
|
|
AND host_id > 0
|
|
AND parent = ?',
|
|
[$tree_id, $parent]
|
|
), 'host_id', 'host_id'
|
|
);
|
|
|
|
$sites = array_rekey(
|
|
db_fetch_assoc_prepared('SELECT site_id
|
|
FROM graph_tree_items
|
|
WHERE graph_tree_id = ?
|
|
AND site_id > 0
|
|
AND parent = ?',
|
|
[$tree_id, $parent]
|
|
), 'site_id', 'site_id'
|
|
);
|
|
|
|
if (!cacti_sizeof($sites)) {
|
|
$total_rows = -1; // Adding to fix pass by reference error in get_allowed_devices
|
|
|
|
if (cacti_sizeof($hosts) && cacti_sizeof(get_allowed_devices('h.id IN(' . implode(',', $hosts) . ')', 'description', '', $total_rows)) > 0) {
|
|
return false;
|
|
}
|
|
} else {
|
|
$site_hosts = [];
|
|
|
|
foreach ($sites as $site) {
|
|
$site_hosts += array_rekey(
|
|
db_fetch_assoc_prepared('SELECT id
|
|
FROM host
|
|
WHERE site_id = ?',
|
|
[$site]
|
|
), 'id', 'id'
|
|
);
|
|
}
|
|
$total_rows = -1; // Adding to fix pass by reference error in get_allowed_devices
|
|
|
|
if (cacti_sizeof($site_hosts) && cacti_sizeof(get_allowed_devices('h.id IN(' . implode(',', $site_hosts) . ')', 'description', '', $total_rows)) > 0) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$branches = db_fetch_assoc_prepared('SELECT id, graph_tree_id
|
|
FROM graph_tree_items
|
|
WHERE graph_tree_id = ?
|
|
AND parent = ?
|
|
AND local_graph_id = 0
|
|
AND host_id = 0',
|
|
[$tree_id, $parent]);
|
|
|
|
if (cacti_sizeof($branches)) {
|
|
foreach ($branches as $b) {
|
|
if (!is_tree_branch_empty($b['graph_tree_id'], $b['id'])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Given a realm and a user, check their permissions if the
|
|
* admin changed a users settings, setup the case to redirect by clearing
|
|
* session variables so that when an admin makes a change, the user does not
|
|
* have to login again to receive them.
|
|
*
|
|
* @param int $realm The realm to check permissions for.
|
|
* @param int|bool $check_user Optional. The user ID to check permissions for. If false, checks the current session user.
|
|
*
|
|
* @return bool True if the user has permission to access the realm, false otherwise.
|
|
*/
|
|
function is_realm_allowed(int $realm, int|bool $check_user = false) : bool {
|
|
global $config;
|
|
|
|
// if we are only checking another users permission, don't check cache
|
|
if ($check_user == false) {
|
|
// user is not set, no permissions
|
|
if (!isset($_SESSION[SESS_USER_ID])) {
|
|
return false;
|
|
}
|
|
|
|
// check to see if the admin invalidated a permission
|
|
if (!is_user_perms_valid($_SESSION[SESS_USER_ID])) {
|
|
if (db_table_exists('user_auth_cache')) {
|
|
$enabled = db_fetch_cell_prepared('SELECT enabled
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$_SESSION[SESS_USER_ID]]);
|
|
|
|
if ($enabled == '' && get_guest_account() !== $_SESSION[SESS_USER_ID]) {
|
|
db_execute_prepared('DELETE FROM user_auth_cache
|
|
WHERE user_id = ?',
|
|
[$_SESSION[SESS_USER_ID]]);
|
|
|
|
kill_session_var(SESS_USER_ID);
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
kill_session_var(SESS_TREE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS);
|
|
|
|
print '<span style="display:none;">cactiLoginSuspend</span>';
|
|
|
|
exit;
|
|
} else {
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
kill_session_var(SESS_TREE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS);
|
|
}
|
|
|
|
print '<span style="display:none;">cactiRedirect</span>';
|
|
|
|
exit;
|
|
} else {
|
|
kill_session_var(SESS_USER_REALMS);
|
|
kill_session_var(OPTIONS_USER);
|
|
kill_session_var(OPTIONS_WEB);
|
|
kill_session_var(SESS_AUTH_NAMES);
|
|
kill_session_var(SESS_TREE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_PERMS);
|
|
kill_session_var(SESS_SIMPLE_TEMPLATE_PERMS);
|
|
}
|
|
}
|
|
|
|
// if the permission is already valid, the session variable will be set
|
|
if (isset($_SESSION[SESS_USER_REALMS][$realm])) {
|
|
return $_SESSION[SESS_USER_REALMS][$realm];
|
|
}
|
|
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
$user_id = $check_user;
|
|
}
|
|
|
|
/**
|
|
* check the permissions from the table, should only happen once per login
|
|
* of after a permission change by the administrator.
|
|
*/
|
|
if (cacti_version_compare($config['cacti_db_version'], '1.0.0', '>=')) {
|
|
$user_realm = db_fetch_cell_prepared("SELECT realm_id
|
|
FROM user_auth_realm
|
|
WHERE user_id = ?
|
|
AND realm_id = ?
|
|
UNION
|
|
SELECT realm_id
|
|
FROM user_auth_group_realm AS uagr
|
|
INNER JOIN user_auth_group AS uag
|
|
ON uag.id = uagr.group_id
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagr.realm_id = ?
|
|
AND uagm.user_id = ?",
|
|
[$user_id, $realm, $realm, $user_id]);
|
|
} else {
|
|
$user_realm = db_fetch_cell_prepared('SELECT realm_id
|
|
FROM user_auth_realm
|
|
WHERE user_id = ?
|
|
AND realm_id = ?',
|
|
[$user_id, $realm]);
|
|
}
|
|
|
|
if (!empty($user_realm)) {
|
|
if ($check_user == false) {
|
|
$_SESSION[SESS_USER_REALMS][$realm] = true;
|
|
} else {
|
|
return true;
|
|
}
|
|
} else {
|
|
if ($check_user == false) {
|
|
$_SESSION[SESS_USER_REALMS][$realm] = false;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $_SESSION[SESS_USER_REALMS][$realm];
|
|
}
|
|
|
|
/**
|
|
* Get the permitted tree branch data available to the user
|
|
*
|
|
* @param int $tree_id The ID of the tree.
|
|
* @param int $parent_id The ID of the parent node.
|
|
* @param bool $editing Whether the user is in editing mode (default: false).
|
|
* @param int $user_id The ID of the user (default: 0).
|
|
*
|
|
* @return array The list of allowed tree items.
|
|
*/
|
|
function get_allowed_tree_level(int $tree_id, int $parent_id, bool $editing = false, int $user_id = 0) : array {
|
|
$items = db_fetch_assoc_prepared('SELECT gti.id, gti.title, gti.host_id,
|
|
gti.site_id, gti.local_graph_id, gti.host_grouping_type,
|
|
h.description AS hostname, s.name AS sitename
|
|
FROM graph_tree_items AS gti
|
|
INNER JOIN graph_tree AS gt
|
|
ON gt.id = gti.graph_tree_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gti.host_id
|
|
LEFT JOIN sites AS s
|
|
ON s.id = gti.site_id
|
|
WHERE gti.graph_tree_id = ?
|
|
AND gti.parent = ?
|
|
ORDER BY gti.position ASC',
|
|
[$tree_id, $parent_id]);
|
|
|
|
if (!$editing) {
|
|
$i = 0;
|
|
|
|
if (cacti_sizeof($items)) {
|
|
foreach ($items as $item) {
|
|
if ($item['host_id'] > 0) {
|
|
if (!is_device_allowed($item['host_id'], $user_id)) {
|
|
unset($items[$i]);
|
|
}
|
|
} elseif ($item['local_graph_id'] > 0) {
|
|
if (!is_graph_allowed($item['local_graph_id'], $user_id)) {
|
|
unset($items[$i]);
|
|
}
|
|
}
|
|
|
|
$i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Gathers items and statistics of those items that are permitted on the tree specified
|
|
*
|
|
* @param int $tree_id The ID of the tree to retrieve content for.
|
|
* @param int $parent The parent ID to filter the tree content. Default is 0.
|
|
* @param string $sql_where Additional SQL WHERE conditions. Default is an empty string.
|
|
* @param string $sql_order SQL ORDER BY clause. Default is an empty string.
|
|
* @param string $sql_limit SQL LIMIT clause. Default is an empty string.
|
|
* @param int $total_rows Reference to a variable to store the total number of rows. Default is 0.
|
|
* @param int $user_id The ID of the user to filter the allowed trees. Default is 0.
|
|
*
|
|
* @return array - An array of allowed tree content.
|
|
*/
|
|
function get_allowed_tree_content(int $tree_id, int $parent = 0, string $sql_where = '', string $sql_order = '', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0) : array {
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE gti.local_graph_id=0
|
|
AND gti.parent=$parent
|
|
AND gti.graph_tree_id=$tree_id
|
|
AND (" . $sql_where . ')';
|
|
} else {
|
|
$sql_where = "WHERE gti.local_graph_id=0
|
|
AND gti.parent=$parent
|
|
AND gti.graph_tree_id=$tree_id";
|
|
}
|
|
|
|
$trees = array_rekey(
|
|
get_allowed_trees(false, false, '', '', '', $total_rows, $user_id),
|
|
'id', 'name'
|
|
);
|
|
|
|
$hierarchy = [];
|
|
|
|
if ($tree_id > 0) {
|
|
if (cacti_sizeof($trees)) {
|
|
$sql_where .= ' AND gt.id IN (' . implode(', ', array_keys($trees)) . ')';
|
|
}
|
|
|
|
$hierarchy = db_fetch_assoc("SELECT gti.graph_tree_id AS tree_id, gti.id, gti.title, gti.host_id, gti.site_id,
|
|
gti.local_graph_id, gti.host_grouping_type, h.description AS hostname, s.name AS sitename
|
|
FROM graph_tree_items AS gti
|
|
INNER JOIN graph_tree AS gt
|
|
ON gt.id = gti.graph_tree_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gti.host_id
|
|
LEFT JOIN sites AS s
|
|
ON gti.site_id=s.id
|
|
$sql_where
|
|
ORDER BY gti.position");
|
|
} elseif (cacti_sizeof($trees)) {
|
|
$hierarchy = db_fetch_assoc("SELECT gt.id AS tree_id, '0' AS id, gt.name AS title, '0' AS host_id, '0' AS site_id,
|
|
'0' AS local_graph_id, '1' AS host_grouping_type, '' AS hostname, '' AS sitename
|
|
FROM graph_tree AS gt
|
|
WHERE enabled='on'
|
|
AND gt.id IN (" . implode(', ', array_keys($trees)) . ')
|
|
ORDER BY gt.sequence');
|
|
}
|
|
|
|
$new_hierarchy = [];
|
|
|
|
if (cacti_sizeof($hierarchy)) {
|
|
foreach ($hierarchy as $h) {
|
|
if ($h['host_id'] > 0) {
|
|
if (is_device_allowed($h['host_id'])) {
|
|
$new_hierarchy[] = $h;
|
|
}
|
|
} elseif ($h['id'] == 0) {
|
|
if (!is_tree_branch_empty($h['tree_id'], $h['id'])) {
|
|
if (is_tree_allowed($h['tree_id'])) {
|
|
$new_hierarchy[] = $h;
|
|
}
|
|
}
|
|
} elseif ($h['site_id'] > 0) {
|
|
$new_hierarchy[] = $h;
|
|
} elseif (!is_tree_branch_empty($h['tree_id'], $h['id'])) {
|
|
$new_hierarchy[] = $h;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $new_hierarchy;
|
|
}
|
|
|
|
/**
|
|
* Searches both the users and the users groups and returns a policy
|
|
* array to be used for determining object permissions.
|
|
*
|
|
* @param int $user_id The ID of the user for whom to retrieve policies.
|
|
*
|
|
* @return array - An array of policies
|
|
*/
|
|
function get_policies(int $user_id) : array {
|
|
// get policies for all user groups
|
|
$policies = db_fetch_assoc_prepared("SELECT uag.id, 'group' AS type, uag.name,
|
|
uag.policy_graphs, uag.policy_hosts, uag.policy_graph_templates, uag.policy_trees
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uag.id = uagm.group_id
|
|
WHERE uag.enabled = 'on'
|
|
AND uagm.user_id = ?",
|
|
[$user_id]);
|
|
|
|
// get policies for the user
|
|
$policies[] = db_fetch_row_prepared("SELECT id, 'user' AS type, 'user' AS name,
|
|
policy_graphs, policy_hosts, policy_graph_templates, policy_trees
|
|
FROM user_auth
|
|
WHERE id = ?",
|
|
[$user_id]);
|
|
|
|
return $policies;
|
|
}
|
|
|
|
/**
|
|
* Returns the graphs that are permitted at the branch/leaf id specified
|
|
*
|
|
* @param int $tree_id The ID of the tree.
|
|
* @param int $leaf_id The ID of the leaf (default is 0).
|
|
* @param string $sql_where Additional SQL WHERE conditions (default is '').
|
|
* @param string $sql_order SQL ORDER BY clause (default is 'gti.position').
|
|
* @param string $sql_limit SQL LIMIT clause (default is '').
|
|
* @param int $total_rows Reference to a variable to store the total number of rows (default is 0).
|
|
* @param int $user_id The ID of the user (default is 0).
|
|
*
|
|
* @return array - An array of allowed tree header graphs.
|
|
*/
|
|
function get_allowed_tree_header_graphs(int $tree_id, int $leaf_id = 0, string $sql_where = '', string $sql_order = 'gti.position', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($user_id == -1) {
|
|
$auth_method = 0;
|
|
} else {
|
|
$auth_method = read_config_option('auth_method');
|
|
}
|
|
|
|
if ($auth_method > 0 && $user_id == 0) {
|
|
if (isset($_SESSION['sess_user_id'])) {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = " AND ($sql_where)";
|
|
}
|
|
|
|
$sql_where = "WHERE (gti.graph_tree_id=$tree_id AND gti.parent=$leaf_id)" . $sql_where;
|
|
|
|
if (read_user_setting('hide_disabled', false, false, $user_id) == 'on') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . '(h.disabled = "" OR h.disabled IS NULL)';
|
|
}
|
|
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
|
|
$graphs = db_fetch_assoc("SELECT gti.id, gti.title, gtg.local_graph_id, h.description,
|
|
gt.name AS template_name, gtg.title_cache, gtg.width, gtg.height, gl.host_id, h.disabled,
|
|
gl.snmp_index, gl.snmp_query_id
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id = gtg.local_graph_id
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON gti.local_graph_id = gl.id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
$sql_where
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$sql = "SELECT COUNT(*)
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id = gtg.local_graph_id
|
|
INNER JOIN graph_tree_items AS gti
|
|
ON gti.local_graph_id = gl.id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
$sql_where";
|
|
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'graph');
|
|
|
|
return is_array($graphs) ? $graphs : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the graphs that are permitted by the user. Used for table displays
|
|
* where users will view the graphs that they are permitted to access
|
|
*
|
|
* @param string $sql_where The SQL where when searching for specific content
|
|
* @param mixed $sql_order The SQL Order clause to use for the sorting of graphs, if using order by
|
|
* Data source, the following must be passed in an array
|
|
*
|
|
* array(
|
|
* 'data_source' => 'traffic_in',
|
|
* 'order' => 'asc'|'desc',
|
|
* 'start_time' => unix_timestamp,
|
|
* 'end_time' => unix_timestamp,
|
|
* 'cf' => avg (0) | max (1)
|
|
* 'measure' => average | peak | sum | p25n | p50n | p75n | p90n | p95n
|
|
* )
|
|
*
|
|
* @param string $sql_limit SQL LIMIT clause.
|
|
* @param int &$total_rows Reference to a variable to store the total number of rows.
|
|
* @param int $user_id User ID for which to retrieve the graphs.
|
|
* @param int $graph_id Specific graph ID to retrieve.
|
|
*
|
|
* @return array An array of allowed graphs.
|
|
*/
|
|
function get_allowed_graphs(string $sql_where = '', mixed $sql_order = 'gtg.title_cache', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0, int $graph_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($user_id == -1) {
|
|
$auth_method = 0;
|
|
} else {
|
|
$auth_method = read_config_option('auth_method');
|
|
}
|
|
|
|
if ($auth_method > 0 && $user_id == 0) {
|
|
if (isset($_SESSION['sess_user_id'])) {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if ($sql_limit != '') {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
$sql_order_join = '';
|
|
|
|
if (is_array($sql_order)) {
|
|
require_once(CACTI_PATH_LIBRARY . '/dsstats.php');
|
|
require_once(CACTI_PATH_LIBRARY . '/html_utility.php');
|
|
|
|
$table = dsstats_get_best_partition($sql_order['start_time'], $sql_order['end_time']);
|
|
|
|
if ($table != '' && $sql_order['data_source'] != '') {
|
|
if ($sql_order['cf'] == 'avg') {
|
|
$cf = 0;
|
|
} elseif ($sql_order['cf'] == 'max') {
|
|
$cf = 1;
|
|
} else {
|
|
$cf = 0;
|
|
}
|
|
|
|
$sql_order_join = "INNER JOIN
|
|
(
|
|
SELECT DISTINCT gti.local_graph_id, ot.*
|
|
FROM graph_templates_item AS gti
|
|
INNER JOIN data_template_rrd AS dtr
|
|
ON gti.task_item_id = dtr.id
|
|
INNER JOIN $table AS ot
|
|
ON dtr.local_data_id = ot.local_data_id
|
|
WHERE ot.rrd_name = " . db_qstr($sql_order['data_source']) . "
|
|
AND ot.cf = $cf
|
|
) AS rs
|
|
ON gl.id = rs.local_graph_id";
|
|
|
|
$sql_order = get_dsstats_order_string($sql_order);
|
|
} elseif ($sql_order['data_source'] == '') {
|
|
cacti_log('WARNING: Graph Order missing Data Source name for ordering.', false, 'AUTH');
|
|
cacti_log('ORDER, DETAIL: ' . json_encode($sql_order), false, 'AUTH');
|
|
} else {
|
|
cacti_log('WARNING: Graph Order unable to determine table for ordering.', false, 'AUTH');
|
|
cacti_log('ORDER DETAIL: ' . json_encode($sql_order), false, 'AUTH');
|
|
}
|
|
} elseif ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
$sql_order = is_string($sql_order) ? $sql_order : '';
|
|
|
|
if ($graph_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : ' ') . ' gl.id = ' . $graph_id;
|
|
}
|
|
|
|
if (read_user_setting('hide_disabled', false, false, $user_id) == 'on') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . '(h.disabled = "" OR h.disabled IS NULL)';
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL) AND $sql_where";
|
|
} else {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL)";
|
|
}
|
|
|
|
// see if permissions are simple
|
|
$simple_perms = get_simple_graph_perms($user_id);
|
|
|
|
// in case we need to review get the graph_auth_method
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
if (!$simple_perms) {
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
}
|
|
|
|
$graphs = db_fetch_assoc("SELECT gtg.local_graph_id, h.description, gt.name AS template_name,
|
|
gtg.title_cache, gtg.width, gtg.height, gl.snmp_index, gl.snmp_query_id, gl.host_id, h.disabled,
|
|
IF(gl.graph_template_id = 0, 0, IF(gl.snmp_query_id = 0, 2, 1)) AS graph_source,
|
|
s.id AS site_id, s.name AS site_name, h.location
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id = gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
LEFT JOIN sites AS s
|
|
ON h.site_id = s.id
|
|
$sql_order_join
|
|
$sql_where
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$sql = "SELECT COUNT(*)
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id = gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
LEFT JOIN sites AS s
|
|
ON h.site_id = s.id
|
|
$sql_order_join
|
|
$sql_where";
|
|
|
|
if ($graph_id == 0) {
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'graph');
|
|
} else {
|
|
$total_rows = db_fetch_cell($sql);
|
|
}
|
|
|
|
return is_array($graphs) ? $graphs : [];
|
|
}
|
|
|
|
/**
|
|
* Returns the aggregate graphs that are permitted by the user.
|
|
* Used for table displays where users will view the graphs that they are permitted to access
|
|
*
|
|
* @param string $sql_where Additional SQL WHERE conditions.
|
|
* @param string $sql_order SQL ORDER BY clause.
|
|
* @param string $sql_limit SQL LIMIT clause.
|
|
* @param int &$total_rows Reference to a variable to store the total number of rows.
|
|
* @param int $user_id The ID of the user.
|
|
* @param int $graph_id The ID of the graph.
|
|
*
|
|
* @return array The list of allowed aggregate graphs.
|
|
*/
|
|
function get_allowed_aggregate_graphs(string $sql_where = '', string $sql_order = 'gtg.title_cache', string $sql_limit = '', int &$total_rows = 0, int $user_id = 0, int $graph_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($user_id == -1) {
|
|
$auth_method = 0;
|
|
} else {
|
|
$auth_method = read_config_option('auth_method');
|
|
}
|
|
|
|
if ($auth_method > 0 && $user_id == 0) {
|
|
if (isset($_SESSION['sess_user_id'])) {
|
|
$user_id = $_SESSION['sess_user_id'];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($graph_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : ' ') . ' gl.id = ' . (int) $graph_id;
|
|
}
|
|
|
|
if (read_user_setting('hide_disabled', false, false, $user_id) == 'on') {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : '') . '(h.disabled = "" OR h.disabled IS NULL)';
|
|
}
|
|
|
|
if ($sql_where != '') {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL) AND $sql_where";
|
|
} else {
|
|
$sql_where = "WHERE ((h.id > 0 AND h.deleted = '') OR h.id IS NULL)";
|
|
}
|
|
|
|
// see if permissions are simple
|
|
$simple_perms = get_simple_graph_perms($user_id);
|
|
|
|
// in case we need to review get the graph_auth_method
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
if (!$simple_perms) {
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
}
|
|
|
|
$graphs = db_fetch_assoc("SELECT gtg.local_graph_id, '' AS description, gt.name AS template_name,
|
|
gtg.title_cache, gtg.width, gtg.height, gl.snmp_index, gl.snmp_query_id, gl.host_id, h.disabled
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN (
|
|
SELECT ag.local_graph_id AS id, gl.host_id, gl.graph_template_id,
|
|
gl.snmp_query_id, gl.snmp_query_graph_id, gl.snmp_index
|
|
FROM aggregate_graphs AS ag
|
|
INNER JOIN aggregate_graphs_items AS agi
|
|
ON ag.id=agi.aggregate_graph_id
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id=agi.local_graph_id
|
|
) AS gl
|
|
ON gl.id=gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id=gl.host_id
|
|
$sql_where
|
|
GROUP BY gtg.local_graph_id
|
|
$sql_order
|
|
$sql_limit");
|
|
|
|
$sql = "SELECT COUNT(DISTINCT gl.id)
|
|
FROM graph_templates_graph AS gtg
|
|
INNER JOIN (
|
|
SELECT ag.local_graph_id AS id, gl.host_id, gl.graph_template_id,
|
|
gl.snmp_query_id, gl.snmp_query_graph_id, gl.snmp_index
|
|
FROM aggregate_graphs AS ag
|
|
INNER JOIN aggregate_graphs_items AS agi
|
|
ON ag.id=agi.aggregate_graph_id
|
|
INNER JOIN graph_local AS gl
|
|
ON gl.id=agi.local_graph_id
|
|
) AS gl
|
|
ON gl.id=gtg.local_graph_id
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id=gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id=gl.host_id
|
|
$sql_where";
|
|
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'aggregate_graph');
|
|
|
|
return is_array($graphs) ? $graphs : [];
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean true or false if the user has full access to
|
|
* all devices in the system. This function is used to shortcut complex queries that may
|
|
* take multiple seconds to return an answer.
|
|
*
|
|
* @param int $user The user ID to check permissions for.
|
|
*
|
|
* @return bool True if the user has simple device permissions, false otherwise.
|
|
*/
|
|
function get_simple_device_perms(int $user) : bool {
|
|
$policy_hosts = db_fetch_cell_prepared('SELECT policy_hosts
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user]);
|
|
|
|
$perm_count = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?
|
|
AND type = 2',
|
|
[$user]);
|
|
|
|
if ($policy_hosts == 1 && $perm_count == 0) {
|
|
return true;
|
|
} else {
|
|
$policies = db_fetch_assoc_prepared('SELECT policy_hosts, COUNT(*) AS exceptions
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_perms AS uagp
|
|
ON uag.id = uagp.group_id
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uagm.group_id = uag.id
|
|
WHERE uagp.type = 2
|
|
AND uagm.user_id = ?
|
|
GROUP BY uag.id',
|
|
[$user]);
|
|
|
|
if (cacti_sizeof($policies)) {
|
|
foreach ($policies as $p) {
|
|
if ($p['policy_hosts'] == 1 && $p['exceptions'] == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean true or false if the user has full access to
|
|
* all graphs in the system. This function is used to shortcut complex queries that may
|
|
* take multiple seconds to return an answer.
|
|
*
|
|
* @param int $user_id The ID of the user whose permissions are being retrieved.
|
|
*
|
|
* @return bool True if the user has simple graph permissions, false otherwise.
|
|
*/
|
|
function get_simple_graph_perms(int $user_id) : bool {
|
|
if (isset($_SESSION[SESS_SIMPLE_PERMS])) {
|
|
return $_SESSION[SESS_SIMPLE_PERMS];
|
|
}
|
|
|
|
$policy_graphs = db_fetch_cell_prepared('SELECT policy_graphs
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
$perm_count = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?
|
|
AND type = 1',
|
|
[$user_id]);
|
|
|
|
if ($policy_graphs == 1 && $perm_count == 0) {
|
|
$_SESSION[SESS_SIMPLE_PERMS] = true;
|
|
|
|
return true;
|
|
} else {
|
|
$policies = db_fetch_assoc_prepared('SELECT policy_graphs, COUNT(*) AS exceptions
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_perms AS uagp
|
|
ON uag.id = uagp.group_id
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uagm.group_id = uag.id
|
|
WHERE uagp.type = 1
|
|
AND uagm.user_id = ?
|
|
GROUP BY uag.id',
|
|
[$user_id]);
|
|
|
|
if (cacti_sizeof($policies)) {
|
|
foreach ($policies as $p) {
|
|
if ($p['policy_graphs'] == 1 && $p['exceptions'] == 0) {
|
|
$_SESSION[SESS_SIMPLE_PERMS] = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$_SESSION[SESS_SIMPLE_PERMS] = false;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean true or false if the user has full access to
|
|
* all graphs templates in the system. This function is used to shortcut complex queries that may
|
|
* take multiple seconds to return an answer.
|
|
*
|
|
* @param int $user_id The ID of the user whose permissions are being retrieved.
|
|
*
|
|
* @return bool True if the user has simple graph template permissions, false otherwise.
|
|
*/
|
|
function get_simple_graph_template_perms(int $user_id) : bool {
|
|
if (isset($_SESSION[SESS_SIMPLE_TEMPLATE_PERMS])) {
|
|
return $_SESSION[SESS_SIMPLE_TEMPLATE_PERMS];
|
|
}
|
|
|
|
$policy_graph_templates = db_fetch_cell_prepared('SELECT policy_graph_templates
|
|
FROM user_auth
|
|
WHERE id = ?',
|
|
[$user_id]);
|
|
|
|
$perm_count = db_fetch_cell_prepared('SELECT COUNT(*)
|
|
FROM user_auth_perms
|
|
WHERE user_id = ?
|
|
AND type = 4',
|
|
[$user_id]);
|
|
|
|
if ($policy_graph_templates == 1 && $perm_count == 0) {
|
|
$_SESSION[SESS_SIMPLE_TEMPLATE_PERMS] = true;
|
|
|
|
return true;
|
|
} else {
|
|
$policies = db_fetch_assoc_prepared('SELECT policy_graph_templates, COUNT(*) AS exceptions
|
|
FROM user_auth_group AS uag
|
|
INNER JOIN user_auth_group_perms AS uagp
|
|
ON uag.id = uagp.group_id
|
|
INNER JOIN user_auth_group_members AS uagm
|
|
ON uagm.group_id = uag.id
|
|
WHERE uagp.type = 4
|
|
AND uagm.user_id = ?
|
|
GROUP BY uag.id',
|
|
[$user_id]);
|
|
|
|
if (cacti_sizeof($policies)) {
|
|
foreach ($policies as $p) {
|
|
if ($p['policy_graph_templates'] == 1 && $p['exceptions'] == 0) {
|
|
$_SESSION[SESS_SIMPLE_TEMPLATE_PERMS] = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
$_SESSION[SESS_SIMPLE_TEMPLATE_PERMS] = false;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the list of Graph Templates that the user is allowed to access.
|
|
* This function is generally intended for both listbox and table displays.
|
|
*
|
|
* @param string $sql_where Additional SQL WHERE conditions.
|
|
* @param string $sql_order SQL ORDER BY clause.
|
|
* @param string $sql_limit SQL LIMIT clause.
|
|
* @param int|null &$total_rows Reference to the total number of rows.
|
|
* @param int $user_id User ID to check permissions for.
|
|
* @param int $graph_template_id Specific graph template ID to filter by.
|
|
*
|
|
* @return array List of allowed graph templates.
|
|
*/
|
|
function get_allowed_graph_templates(string $sql_where = '', string $sql_order = 'gt.name', string $sql_limit = '', int|null &$total_rows = 0, int $user_id = 0, int $graph_template_id = 0) : array {
|
|
if (!auth_valid_user($user_id)) {
|
|
return [];
|
|
}
|
|
|
|
if ($user_id == -1) {
|
|
$auth_method = 0;
|
|
} else {
|
|
$auth_method = read_config_option('auth_method');
|
|
}
|
|
|
|
if ($user_id == 0) {
|
|
if (isset($_SESSION[SESS_USER_ID])) {
|
|
$user_id = $_SESSION[SESS_USER_ID];
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// For timing
|
|
$start = microtime(true);
|
|
|
|
$init_rows = $total_rows;
|
|
|
|
$templates = [];
|
|
|
|
if ($sql_limit != '' && $sql_limit != -1) {
|
|
$sql_limit = "LIMIT $sql_limit";
|
|
} else {
|
|
$sql_limit = '';
|
|
}
|
|
|
|
if ($sql_order != '') {
|
|
$sql_order = "ORDER BY $sql_order";
|
|
}
|
|
|
|
if ($graph_template_id > 0) {
|
|
$sql_where .= ($sql_where != '' ? ' AND ' : ' ') . "(gl.graph_template_id = $graph_template_id)";
|
|
}
|
|
|
|
$sql_where = 'WHERE ' . ($sql_where != '' ? $sql_where . ' AND ' : ' ') . '(gt.id IS NOT NULL) ';
|
|
|
|
// see if permissions are simple
|
|
$simple_perms = get_simple_graph_perms($user_id);
|
|
|
|
// in case we need to review get the graph_auth_method
|
|
$graph_auth_method = read_config_option('graph_auth_method');
|
|
|
|
// get policies for all groups and user
|
|
$policies = get_policies($user_id);
|
|
|
|
// short circuit if we don't have a user
|
|
if ($user_id == 0) {
|
|
return [];
|
|
}
|
|
|
|
if (!$simple_perms) {
|
|
$sql_where = get_policy_where($graph_auth_method, $policies, $sql_where);
|
|
}
|
|
|
|
if ($total_rows != -2) {
|
|
$templates = db_fetch_assoc("SELECT gt.id, gt.name, COUNT(*) AS graphs
|
|
FROM graph_local AS gl
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
$sql_where
|
|
GROUP BY gl.graph_template_id
|
|
$sql_order
|
|
$sql_limit");
|
|
}
|
|
|
|
if ($total_rows >= 0 || $total_rows == -2) {
|
|
$sql = "SELECT COUNT(DISTINCT gl.graph_template_id) AS id
|
|
FROM graph_local AS gl
|
|
LEFT JOIN graph_templates AS gt
|
|
ON gt.id = gl.graph_template_id
|
|
LEFT JOIN host AS h
|
|
ON h.id = gl.host_id
|
|
$sql_where";
|
|
|
|
if ($graph_template_id == 0) {
|
|
$total_rows = get_total_row_data($user_id, $sql, [], 'graph');
|
|
} else {
|
|
$total_rows = db_fetch_cell($sql);
|
|
}
|
|
}
|
|
|
|
// For timing
|
|
$end = microtime(true);
|
|
|
|
cacti_log(sprintf('The Get Templates total time was %4.2f', $end - $start), false, 'AUTH', POLLER_VERBOSITY_DEBUG);
|
|
|
|
if (!is_array($templates)) {
|
|
$templates = [];
|
|
}
|
|
|
|
return $templates;
|
|
}
|
|
|
|
/**
|
|
* Parse the policies in order to visually display user permissions
|
|
*
|
|
* @param array $policies The list of user and group policies. Will be reversed to show user permissions first.
|
|
*
|
|
* @return array An associative array containing both $sql_select and $sql_join
|
|
*/
|
|
function get_policy_join_select(array $policies) : array {
|
|
$sql_join = '';
|
|
$sql_select = '';
|
|
|
|
$i = 1;
|
|
$j = 1;
|
|
|
|
foreach ($policies as $p) {
|
|
$sql_join .= 'LEFT JOIN (SELECT * FROM user_auth_' . ($p['type'] == 'user' ? '' : 'group_') . 'perms WHERE ' . $p['type'] . '_id = ' . $p['id'] . ") AS uap$j ON (gl.id = uap$j.item_id AND uap$j.type = 1) ";
|
|
$sql_select .= ($sql_select != '' ? ', ' : '') . "uap$j." . $p['type'] . "_id AS graph$i";
|
|
$j++;
|
|
|
|
$sql_join .= 'LEFT JOIN (SELECT * FROM user_auth_' . ($p['type'] == 'user' ? '' : 'group_') . 'perms WHERE ' . $p['type'] . '_id = ' . $p['id'] . ") AS uap$j ON (gl.host_id = uap$j.item_id AND uap$j.type = 3) ";
|
|
$sql_select .= ($sql_select != '' ? ', ' : '') . "uap$j." . $p['type'] . "_id AS device$i";
|
|
$j++;
|
|
|
|
$sql_join .= 'LEFT JOIN (SELECT * FROM user_auth_' . ($p['type'] == 'user' ? '' : 'group_') . 'perms WHERE ' . $p['type'] . '_id = ' . $p['id'] . ") AS uap$j ON (gl.graph_template_id = uap$j.item_id AND uap$j.type = 4) ";
|
|
$sql_select .= ($sql_select != '' ? ', ' : '') . "uap$j." . $p['type'] . "_id AS template$i";
|
|
$j++;
|
|
|
|
$i++;
|
|
}
|
|
|
|
return ['sql_select' => $sql_select, 'sql_join' => $sql_join];
|
|
}
|
|
|
|
/**
|
|
* Parse the policies in order to downselect matching graphs
|
|
* without the use of the SQL Having clause which is very inefficient
|
|
*
|
|
* @param string|int $graph_auth_method The graph auth method: permissive, restrictive, device, graph_template
|
|
* @param array $policies The list of user and group policies.
|
|
* @param string $sql_where The SQL where filter provided by the calling function.
|
|
*
|
|
* @return string Updated sql_where value
|
|
*/
|
|
function get_policy_where(string|int $graph_auth_method, array $policies, string $sql_where) : string {
|
|
if ($graph_auth_method == 1) {
|
|
// Policy Rows include
|
|
// id, type (group|user), policy_graphs, policy_hosts, policy_graph_templates
|
|
|
|
// Table user_auth_perms has user_id, item_id, type
|
|
// Types can be:
|
|
// 1 - Graph
|
|
// 2 - Trees
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
|
|
// Policies are
|
|
// 1 - Default Allow, all exceptions are denied
|
|
// 2 - Default Deny, all exceptions are allowed
|
|
|
|
// Permissive means access to the graph, or access to either the host or template
|
|
|
|
$sql_where .= ($sql_where != '' ? ' AND (' : 'WHERE (');
|
|
|
|
foreach ($policies as $index => $p) {
|
|
$sql_where .= ($index > 0 ? ' OR (' : '(');
|
|
|
|
if ($p['type'] == 'user') {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ' OR ';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
|
|
$sql_where .= ' OR ';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
} else {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ' OR ';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
|
|
$sql_where .= ' OR ';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
} elseif ($graph_auth_method == 2) {
|
|
// Policy Rows include
|
|
// id, type (group|user), policy_graphs, policy_hosts, policy_graph_templates
|
|
|
|
// Table user_auth_perms has user_id, item_id, type
|
|
// Types can be:
|
|
// 1 - Graph
|
|
// 2 - Trees
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
|
|
// Policies are
|
|
// 1 - Default Allow, all exceptions are denied
|
|
// 2 - Default Deny, all exceptions are allowed
|
|
|
|
// Restrictive means access to the graph, or access to both the host and template
|
|
|
|
$sql_where .= ($sql_where != '' ? ' AND (' : 'WHERE (');
|
|
|
|
foreach ($policies as $index => $p) {
|
|
$sql_where .= ($index == 0 ? '((' : ' OR ((');
|
|
|
|
if ($p['type'] == 'user') {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
|
|
$sql_where .= ' AND ';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= ' gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= ' gl.graph_template_id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
} else {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
|
|
$sql_where .= ' AND ';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
} elseif ($graph_auth_method == 3) {
|
|
// Policy Rows include
|
|
// id, type (group|user), policy_graphs, policy_hosts, policy_graph_templates
|
|
|
|
// Table user_auth_perms has user_id, item_id, type
|
|
// Types can be:
|
|
// 1 - Graph
|
|
// 2 - Trees
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
|
|
// Policies are
|
|
// 1 - Default Allow, all exceptions are denied
|
|
// 2 - Default Deny, all exceptions are allowed
|
|
|
|
$sql_where .= ($sql_where != '' ? ' AND (' : 'WHERE (');
|
|
|
|
foreach ($policies as $index => $p) {
|
|
$sql_where .= ($index == 0 ? '((' : ') OR ((');
|
|
|
|
if ($p['type'] == 'user') {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
} else {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
$sql_where .= 'h.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
} else {
|
|
$sql_where .= 'h.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 3)';
|
|
}
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= '))';
|
|
} elseif ($graph_auth_method == 4) {
|
|
// Policy Rows include
|
|
// id, type (group|user), policy_graphs, policy_hosts, policy_graph_templates
|
|
|
|
// Table user_auth_perms has user_id, item_id, type
|
|
// Types can be:
|
|
// 1 - Graph
|
|
// 2 - Trees
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
|
|
// Policies are
|
|
// 1 - Default Allow, all exceptions are denied
|
|
// 2 - Default Deny, all exceptions are allowed
|
|
|
|
$sql_where .= ($sql_where != '' ? ' AND (' : 'WHERE (');
|
|
|
|
foreach ($policies as $index => $p) {
|
|
$sql_where .= ($index == 0 ? '((' : ') OR ((');
|
|
|
|
if ($p['type'] == 'user') {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_perms WHERE user_id = ' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
} else {
|
|
if ($p['policy_graphs'] == 1) {
|
|
$sql_where .= 'gl.id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
} else {
|
|
$sql_where .= 'gl.id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 1)';
|
|
}
|
|
|
|
$sql_where .= ') OR (';
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
$sql_where .= 'gl.graph_template_id NOT IN (SELECT item_id FROM user_auth_group_perms WHERE group_id = ' . $p['id'] . ' AND type = 4)';
|
|
} else {
|
|
$sql_where .= 'gl.graph_template_id IN (SELECT item_id FROM user_auth_group_perms WHERE group_id =' . $p['id'] . ' AND type = 4)';
|
|
}
|
|
}
|
|
|
|
$sql_where .= ')';
|
|
}
|
|
|
|
$sql_where .= '))';
|
|
}
|
|
|
|
return $sql_where;
|
|
}
|
|
|
|
/**
|
|
* Get the effective permission string for the graph in question. The
|
|
* logic for this is somewhat complex, but understandable. First, the $graph object will include
|
|
* three columns generally graphX, deviceX, and templateX for each of the user or groups in the collection.
|
|
* The way we assign a restrictive or permissive value is based upon the graph permission setting
|
|
* in Cacti, but also whether or not the default access for the object type is either 'Allow' or 'Deny'.
|
|
*
|
|
* - If the 'default access' for the object type is 'Deny', then a numeric value in userX means
|
|
* the user has permission to an object.
|
|
* - If the default access for the object is 'Allow', then a numeric
|
|
* value in userX means that the object is blocked.
|
|
*
|
|
* Again, each of the graphX, deviceX, and templateX will always come in three's. They equate to:
|
|
*
|
|
* - graphX - The user does or does not have permission to the Graph at the Graph Level
|
|
* - deviceX - The user does or does not have permission to the Graph at the Device Level
|
|
* - templateX - The user does or does not have permission to the Graph at the Graph Template Level
|
|
*
|
|
* Then, the effective permission are calculated by the Graph Permission Model in Cacti. They are
|
|
*
|
|
* - Permissive - If the user has access to the Graph, the Device, or Graph Template, then the
|
|
* user will have access to the Graph.
|
|
*
|
|
* - Restrictive - If the user has access to the Graph, or both the Device and Graph Template,
|
|
* then the user will have access to the Graph
|
|
*
|
|
* - Device - If the user has access to the Graph, or the Device, then the user will have
|
|
* access to the Graph
|
|
*
|
|
* - Graph Template - If the user has access to the Graph, or the Graph Template, then the user
|
|
* will have access to the Graph.
|
|
*
|
|
* This function will apply this logic, and then respond to the user a 'Granted' or 'Restricted'
|
|
* column value, and a Tooltip, that shows how the permissions were evaluated. In other words
|
|
* why was the user either permitted to or denied access to the Graph.
|
|
*
|
|
* @param array $graph The graph data.
|
|
* @param array $policies The policies to evaluate.
|
|
*
|
|
* @return string The permission string
|
|
*/
|
|
function get_permission_string(array &$graph, array &$policies) : string {
|
|
$grantStr = '';
|
|
$rejectStr = '';
|
|
$reasonStr = '';
|
|
$drejectStr = '';
|
|
|
|
// Methods:
|
|
// 1 - Permissive
|
|
// 2 - Restrictive
|
|
// 3 - Device
|
|
// 4 - Graph Template
|
|
$method = read_config_option('graph_auth_method');
|
|
|
|
if ($graph['disabled'] == 'on' && read_user_setting('hide_disabled', false, false, grv('user_id'))) {
|
|
$drejectStr .= __esc('Device:(Hide Disabled)');
|
|
}
|
|
|
|
// Policies:
|
|
// 1 - Default Allow All - Numeric means blocked
|
|
// 2 - Default Deny All - Numeric means allowed
|
|
|
|
$i = 1;
|
|
|
|
foreach ($policies as $p) {
|
|
$allowed = 0;
|
|
$rejected = 0;
|
|
|
|
// Perform the Graph Check first
|
|
// If a user has access at the Graph Access, they always get Access
|
|
if ($p['policy_graphs'] == 1) {
|
|
// Default is to allow
|
|
if (empty($graph["graph$i"])) {
|
|
// Allow the access at the level
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Graph:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr .= ($rejectStr != '' ? ', ' : '') . __esc('Graph:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
// Default is to Deny
|
|
if (!empty($graph["graph$i"])) {
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Graph:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejectStr .= ($rejectStr != '' ? ', ' : '') . __esc('Graph:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
$rejected++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Now we check at the Device and Graph Template Level. Here the permission get a bit more dicey, so we will
|
|
* Use case logic to make it read more simply.
|
|
*/
|
|
switch($method) {
|
|
case 1: // Permissive
|
|
if ($p['policy_hosts'] == 1) {
|
|
if (empty($graph["device$i"])) {
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Device:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
if (!empty($graph["device$i"])) {
|
|
$grantStr = $grantStr . ($grantStr != '' ? ', ' : '') . __esc('Device:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
}
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
if (empty($graph["template$i"])) {
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
if (!empty($graph["template$i"])) {
|
|
$grantStr .= ($grantStr != '' ? ', ' : '') . __esc('Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
}
|
|
|
|
if ($rejected == 3) {
|
|
$rejectStr .= ($rejectStr != '' ? ', ' : '') . __esc('Graph+Device+Template:(%s%s)', ucfirst($p['type']), ($p['type'] != 'user' ? '/' . $p['name'] : ''));
|
|
}
|
|
|
|
break;
|
|
case 2: // Restrictive
|
|
$allowed = 0;
|
|
$rejected = 0;
|
|
|
|
if ($p['policy_hosts'] == 1) {
|
|
if (empty($graph["device$i"])) {
|
|
$allowed++;
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
if (!empty($graph["device$i"])) {
|
|
$allowed++;
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
}
|
|
|
|
if ($p['policy_graph_templates'] == 1) {
|
|
if (empty($graph["template$i"])) {
|
|
$allowed++;
|
|
} else {
|
|
$rejected++;
|
|
}
|
|
} else {
|
|
if (!empty($graph["template$i"])) {
|
|
$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;
|
|
}
|