a bunch of stuff happened. I'll do better next commit...

This commit is contained in:
2026-10-09 08:24:28 -04:00
parent bf8ad6a858
commit 4bceed7cb9
27 changed files with 540 additions and 112 deletions
+147
View File
@@ -0,0 +1,147 @@
<?php
class Applications extends Base {
public function authorizeAdministration(): void {
$this->requireAdministrator();
}
private function requireAdministrator(): void {
$statement = $this->connect()->prepare('SELECT user_name, user_role, user_status FROM users WHERE user_serial = :user');
$statement->execute([':user' => (int) ($_SESSION[self::USER_SERIAL] ?? 0)]);
$user = $statement->fetch(PDO::FETCH_ASSOC);
$allowed = $user && $user['user_status'] === 'active' && in_array($user['user_role'], ['Administrator', 'System Administrator'], true);
if ($user && $user['user_status'] === 'active') {
foreach ($this->getSettings()->xpath('//SystemAdministrator') as $name) {
$allowed = $allowed || (string) $name === $user['user_name'];
}
}
if (!$allowed) {
http_response_code(403);
exit('Administrator access is required.');
}
}
public function dashboard(): void {
try {
$statement = $this->connect()->prepare('SELECT a.* FROM applications a JOIN user_applications ua ON ua.application_serial = a.application_serial WHERE ua.user_serial = :user AND a.application_active = 1 ORDER BY a.application_name');
$statement->execute([':user' => (int) $_SESSION[self::USER_SERIAL]]);
$xml = $this->getTableXML('applications', $statement);
} catch (PDOException $e) {
if ((int) ($e->errorInfo[1] ?? 0) !== 1146) { throw $e; }
$xml = new SimpleXMLElement('<applications/>');
$xml->addAttribute('setup_required', 'true');
}
$this->displayContent($this->applyXSL($xml, $this->getXSL('home')));
}
public function manageAccess(): void {
$this->requireAdministrator();
$connection = $this->connect();
$userSerial = (int) (filter_input(INPUT_GET, 'user_serial', FILTER_VALIDATE_INT) ?: 0);
$xml = new SimpleXMLElement('<access/>');
$xml->addAttribute('selected_user', (string) $userSerial);
$xml->addAttribute('saved', filter_input(INPUT_GET, 'saved') === '1' ? 'true' : 'false');
$xml = $this->mergeXML($this->getTableXML('users', $connection->query('SELECT user_serial, user_name, user_status FROM users ORDER BY user_name')), $xml);
try {
$statement = $connection->prepare('SELECT a.*, CASE WHEN ua.user_serial IS NULL THEN 0 ELSE 1 END AS assigned FROM applications a LEFT JOIN user_applications ua ON ua.application_serial = a.application_serial AND ua.user_serial = :user ORDER BY a.application_name');
$statement->execute([':user' => $userSerial]);
} catch (PDOException $e) {
if ((int) ($e->errorInfo[1] ?? 0) !== 1146) { throw $e; }
$this->displayContent($this->applyXSL('<access setup_required="true"/>', $this->getXSL('applicationAccess')));
return;
}
$xml = $this->mergeXML($this->getTableXML('applications', $statement), $xml);
try {
$links = $connection->prepare('SELECT application_serial, local_user_serial FROM user_application_accounts WHERE user_serial = :user');
$links->execute([':user' => $userSerial]);
$xml = $this->mergeXML($this->getTableXML('account_links', $links), $xml);
} catch (PDOException $e) {
if ((int) ($e->errorInfo[1] ?? 0) !== 1146) { throw $e; }
}
$this->displayContent($this->applyXSL($xml, $this->getXSL('applicationAccess')));
}
public function saveAccess(): void {
$this->requireAdministrator();
if (!$this->validAuthRequest()) { http_response_code(403); exit('Invalid request. Reload the application access page.'); }
$userSerial = filter_input(INPUT_POST, 'user_serial', FILTER_VALIDATE_INT) ?: 0;
$selected = filter_input(INPUT_POST, 'applications', FILTER_VALIDATE_INT, FILTER_REQUIRE_ARRAY);
$selected = $selected === null ? [] : $selected;
if (!is_array($selected) || in_array(false, $selected, true) || in_array(null, $selected, true)) {
http_response_code(422); exit('Invalid application selection.');
}
$selected = array_unique($selected);
$links = filter_input(INPUT_POST, 'local_users', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
if ($links !== null && !is_array($links)) { http_response_code(422); exit('Invalid account links.'); }
$linkedAccounts = [];
foreach ($links ?? [] as $application => $localUser) {
if (!ctype_digit((string) $application) || !is_string($localUser)) { http_response_code(422); exit('Invalid account link.'); }
if (trim($localUser) === '') { continue; }
$localUser = filter_var(trim($localUser), FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if ($localUser === false) { http_response_code(422); exit('Account IDs must be positive whole numbers.'); }
$linkedAccounts[(int) $application] = $localUser;
}
$connection = $this->connect();
$connection->beginTransaction();
try {
$statement = $connection->prepare('SELECT user_serial FROM users WHERE user_serial = :user FOR UPDATE');
$statement->execute([':user' => $userSerial]);
if (!$statement->fetchColumn()) { $connection->rollBack(); http_response_code(422); exit('Select an existing user.'); }
$valid = array_map('intval', $connection->query('SELECT application_serial FROM applications')->fetchAll(PDO::FETCH_COLUMN));
if (array_diff($selected, $valid)) { $connection->rollBack(); http_response_code(422); exit('Invalid application selection.'); }
if (array_diff(array_keys($links ?? []), $valid)) { $connection->rollBack(); http_response_code(422); exit('Invalid application account link.'); }
if ($links !== null) {
$delete = $connection->prepare('DELETE FROM user_application_accounts WHERE user_serial = :user');
$delete->execute([':user' => $userSerial]);
$insert = $connection->prepare('INSERT INTO user_application_accounts (user_serial, application_serial, local_user_serial, linked_by) VALUES (:user, :application, :local, :actor)');
foreach ($linkedAccounts as $application => $localUser) {
$insert->execute([':user' => $userSerial, ':application' => $application, ':local' => $localUser, ':actor' => $_SESSION[self::USER_NAME]]);
}
}
$statement = $connection->prepare('DELETE FROM user_applications WHERE user_serial = :user');
$statement->execute([':user' => $userSerial]);
$statement = $connection->prepare('INSERT INTO user_applications (user_serial, application_serial, assigned_by) VALUES (:user, :application, :actor)');
foreach ($selected as $application) {
$statement->execute([':user' => $userSerial, ':application' => $application, ':actor' => $_SESSION[self::USER_NAME]]);
}
$journal = $connection->prepare('INSERT INTO journal (journal_origin, journal_ip, journal_entry) VALUES (:actor, :ip, :entry)');
$journal->execute([':actor' => $_SESSION[self::USER_NAME], ':ip' => $_SERVER['REMOTE_ADDR'] ?? '', ':entry' => 'Updated application access for user ' . $userSerial . ': ' . implode(', ', $selected) . ($links !== null ? '; account links: ' . json_encode($linkedAccounts) : '')]);
$connection->commit();
} catch (Throwable $e) {
if ($connection->inTransaction()) { $connection->rollBack(); }
if ($e instanceof PDOException && (int) ($e->errorInfo[1] ?? 0) === 1062) {
http_response_code(409);
exit('That application account is already linked to another DEC user. No changes were saved.');
}
throw $e;
}
header('Location: main.php?Xaction=Applications.manageAccess&user_serial=' . $userSerial . '&saved=1', true, 303);
exit();
}
public function setup(): void {
$this->requireAdministrator();
if (!$this->validAuthRequest()) { http_response_code(403); exit('Invalid setup request. Reload the page.'); }
$connection = $this->connect();
foreach (['001_application_access.sql', '002_application_account_links.sql'] as $migration) {
$sql = file_get_contents(__DIR__ . '/../migrations/' . $migration);
foreach (explode(';', $sql) as $statement) {
if (trim($statement) !== '') { $connection->exec($statement); }
}
}
header('Location: main.php?Xaction=Applications.manageAccess', true, 303);
exit();
}
public function launch(): void {
$application = filter_input(INPUT_GET, 'application_serial', FILTER_VALIDATE_INT) ?: 0;
$statement = $this->connect()->prepare('SELECT a.application_url FROM applications a JOIN user_applications ua ON ua.application_serial = a.application_serial WHERE a.application_serial = :application AND ua.user_serial = :user AND a.application_active = 1');
$statement->execute([':application' => $application, ':user' => (int) $_SESSION[self::USER_SERIAL]]);
$url = $statement->fetchColumn();
if (!$url) { http_response_code(403); exit('This application is not assigned to your account.'); }
if (!filter_var($url, FILTER_VALIDATE_URL) || !in_array(strtolower(parse_url($url, PHP_URL_SCHEME) ?: ''), ['http', 'https'], true) || preg_match('/[\r\n]/', $url)) {
http_response_code(500); exit('The application URL is invalid. Contact your administrator.');
}
header('Location: ' . $url, true, 303);
exit();
}
}
+3 -4
View File
@@ -47,7 +47,6 @@ class Authentication {
$host = 'localhost';
} else {
$host = '192.168.1.190';
$host = '100.67.215.67';
}
try {
@@ -78,7 +77,7 @@ class Authentication {
$password = '';
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$index = random_int(0, strlen($characters) - 1);
$password .= $characters[$index];
}
@@ -186,14 +185,14 @@ class Authentication {
$user_temp_password = password_hash($user_random_password, PASSWORD_DEFAULT);
$SQL = "update users
set user_password = :user_password,
set user_password_hash = :user_password_hash,
user_change_password = :user_change_password
where user_serial = :user_serial";
$statement = $this->ghrconnection->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_password", $user_temp_password);
$statement->bindValue(":user_password_hash", $user_temp_password);
$statement->bindValue(":user_change_password", 1);
$statement->execute();
+60 -29
View File
@@ -13,8 +13,8 @@ class Base {
// Constants
// =========
const APPLICATION_NAME = "BaseProject";
const APPLICATION_MNEMONIC = "BASEPROJECT";
const APPLICATION_NAME = "DEC Authentication Service";
const APPLICATION_MNEMONIC = "DECAUTHY";
const VERSION = "1.00";
const TIMEZONE = "America/New_York";
@@ -22,22 +22,22 @@ class Base {
// Session Variables
// =================
const ENVIRONMENT = "BASEPROJECT_ENVIRONMENT";
const ACTIVE_SESSION = "BASEPROJECT_ACTIVE_SESSION";
const USER_SERIAL = "BASEPROJECT_USER_SERIAL";
const USER_NAME = "BASEPROJECT_USER_NAME";
const USER_FULL_NAME = "BASEPROJECT_USER_FULL_NAME";
const USER_EMAIL = "BASEPROJECT_USER_EMAIL";
const USER_ROLE = "BASEPROJECT_USER_ROLE";
const USER_AUTHENTICATED = "BASEPROJECT_USER_AUTHENTICATED";
const REPORTS_GET = "BASEPROJECT_REPORTS_GET";
const REPORTS_POST = "BASEPROJECT_REPORTS_POST";
const THEME = "BASEPROJECT_THEME";
const AJAX = "BASEPROJECT_AJAX";
const TODAY = "BASEPROJECT_TODAY";
const COPYRIGHT = "BASEPROJECT_COPYRIGHT";
const PAGE = "BASEPROJECT_PAGE";
const PAGINATION_PAGE = "BASEPROJECT_PAGINATION_PAGE";
const ENVIRONMENT = "DECAUTHY_ENVIRONMENT";
const ACTIVE_SESSION = "DECAUTHY_ACTIVE_SESSION";
const USER_SERIAL = "DECAUTHY_USER_SERIAL";
const USER_NAME = "DECAUTHY_USER_NAME";
const USER_FULL_NAME = "DECAUTHY_USER_FULL_NAME";
const USER_EMAIL = "DECAUTHY_USER_EMAIL";
const USER_ROLE = "DECAUTHY_USER_ROLE";
const USER_AUTHENTICATED = "DECAUTHY_USER_AUTHENTICATED";
const REPORTS_GET = "DECAUTHY_REPORTS_GET";
const REPORTS_POST = "DECAUTHY_REPORTS_POST";
const THEME = "DECAUTHY_THEME";
const AJAX = "DECAUTHY_AJAX";
const TODAY = "DECAUTHY_TODAY";
const COPYRIGHT = "DECAUTHY_COPYRIGHT";
const PAGE = "DECAUTHY_PAGE";
const PAGINATION_PAGE = "DECAUTHY_PAGINATION_PAGE";
// =================
// Class Constructor
@@ -93,6 +93,8 @@ class Base {
$XSLParms["TODAY_MMDDYYYY"] = date("m/d/Y");
$XSLParms["TODAY_HHMM"] = date("Hi");
$XSLParms["COPYRIGHT"] = date("Y");
$_SESSION['DECAUTHY_CSRF'] ??= bin2hex(random_bytes(32));
$XSLParms['CSRF_TOKEN'] = $_SESSION['DECAUTHY_CSRF'];
$XML = new DOMDocument();
$XSL = new DOMDocument();
@@ -257,8 +259,7 @@ class Base {
public function displayHome() {
$content = $this->applyXSL("", $this->getXSL("home"));
$this->displayContent($content);
(new Applications())->dashboard();
}
// =======================
@@ -276,6 +277,14 @@ class Base {
$action = (empty($Xaction) ? $action : $Xaction);
if (strtolower(trim($action)) === 'signout') {
$_SESSION = [];
session_regenerate_id(true);
$this->initializeSession();
header('Location: main.php');
exit();
}
if ($Xaction == 'resetpassword') {
$this->resetpassword();
}
@@ -289,7 +298,7 @@ class Base {
// Determine if user needs a password change
if ($this->userChangePassword() === true) {
(new Users())->changePassword();
(new Users())->changePassword($action !== 'Users.changePassword');
exit();
}
@@ -304,16 +313,19 @@ class Base {
$action = (empty($Xaction) ? $action : $Xaction);
// Sign Out if requested.
if (strtolower(trim($action)) === "signout") {
$this->initializeSession();
}
// Process the requested Action.
list($class, $method) = array_pad(explode('.', $action), 2, "");
if (strcasecmp($class, 'Applications') === 0 && !in_array(strtolower($method), ['dashboard', 'manageaccess', 'saveaccess', 'launch', 'setup'], true)) {
http_response_code(404);
exit('Unknown application action.');
}
// Account management can change roles, so enforce its administrator boundary here as well.
if (strcasecmp($class, 'Users') === 0 && strcasecmp($method, 'changePassword') !== 0) {
(new Applications())->authorizeAdministration();
}
if (empty($method)) {
$method = $class;
@@ -745,7 +757,7 @@ class Base {
$password = '';
for ($i = 0; $i < $length; $i++) {
$index = rand(0, strlen($characters) - 1);
$index = random_int(0, strlen($characters) - 1);
$password .= $characters[$index];
}
@@ -760,6 +772,13 @@ class Base {
$user = (new Users())->getUserByUserName($_SESSION[self::USER_NAME]);
if ($user->count() === 0 || (string) $user->record->user_status !== 'active') {
$_SESSION = [];
session_regenerate_id(true);
header('Location: main.php');
exit();
}
if ($user->record->user_change_password == 1) {
return true;
@@ -802,16 +821,28 @@ class Base {
public function resetPassword() {
if (!$this->validAuthRequest()) {
http_response_code(403);
exit('Invalid authentication request. Reload the sign-in page.');
}
require_once 'classes/Authentication.php';
$XSLParms = [
"BTN_SUBMIT" => "main.php?action=Users.signIn&step=authenticate",
"BTN_SUBMIT" => "main.php",
"RESET_PASSWORD_SUCCESSFUL" => (new Authentication())->resetPassword() ? "TRUE" : "FALSE"
];
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
exit;
}
public function validAuthRequest(): bool {
$token = filter_input(INPUT_POST, 'csrf_token') ?: '';
return ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST'
&& isset($_SESSION['DECAUTHY_CSRF'])
&& hash_equals($_SESSION['DECAUTHY_CSRF'], $token);
}
}
?>
+9 -4
View File
@@ -40,7 +40,7 @@ class SignIn extends Base {
case "prompt":
$XSLParms["BTN_SUBMIT"] = "main.php?action=SignIn.login&step=authenticate";
$XSLParms["BTN_SUBMIT"] = "main.php";
$XSLParms["INVALID_SIGNIN"] = "FALSE";
echo $this->applyXSL("", $this->getXSL("signIn"), $XSLParms);
@@ -50,10 +50,15 @@ class SignIn extends Base {
case "authenticate":
if (!$this->validAuthRequest()) {
http_response_code(403);
exit('Invalid authentication request. Reload the sign-in page.');
}
$user_name = trim(filter_input(INPUT_POST, "user_name") ?: "");
$user_password = filter_input(INPUT_POST, "user_password") ?: "";
$XSLParms["BTN_SUBMIT"] = "main.php?action=SignIn.login&step=authenticate";
$XSLParms["BTN_SUBMIT"] = "main.php";
$XSLParms["INVALID_SIGNIN"] = "TRUE";
$user = (new Users())->getUserByUserName($user_name);
@@ -102,14 +107,14 @@ class SignIn extends Base {
(new Users())->updateLogin();
header("Location: ./");
header("Location: main.php");
exit();
break;
default:
// User NOT Validated.
header("Location: ./");
header("Location: main.php");
exit();
}
}
+33 -26
View File
@@ -8,7 +8,6 @@ class Users extends Base {
// Variables
private $current_user_serial = "";
private $current_user_name = "";
private $current_user_email = "";
@@ -25,6 +24,8 @@ class Users extends Base {
date_default_timezone_set(self::TIMEZONE);
$this->current_user_serial = (int) ($_SESSION[self::USER_SERIAL] ?? 0);
$this->current_user_name = $_SESSION[self::USER_NAME] ?? "";
$this->current_user_email = $_SESSION[self::USER_EMAIL] ?? "";
$this->pagination_page = $_SESSION[self::PAGINATION_PAGE] ?? 1;
@@ -120,18 +121,18 @@ class Users extends Base {
( user_name,
user_email,
user_client_name,
user_password,
user_first_name,
user_password_hash,
user_role,
user_change_password,
user_active,
user_status,
user_creator,
user_created )
values ( :user_name,
:user_email,
:user_client_name,
:user_password,
:user_password_hash,
:user_role,
:user_change_password,
:user_active,
@@ -143,10 +144,10 @@ class Users extends Base {
$statement->bindValue(":user_name", $user_name);
$statement->bindValue(":user_email", $user_email);
$statement->bindValue(":user_client_name", $user_client_name);
$statement->bindValue(":user_password", $user_temp_password);
$statement->bindValue(":user_password_hash", $user_temp_password);
$statement->bindValue(":user_role", $user_role);
$statement->bindValue(":user_change_password", $user_change_password, PDO::PARAM_BOOL);
$statement->bindValue(":user_active", $user_active, PDO::PARAM_BOOL);
$statement->bindValue(":user_active", $user_active ? 'active' : 'inactive');
$statement->bindValue(":user_creator", $this->current_user_name);
$statement->bindValue(":user_created", $this->getTimestamp());
@@ -241,9 +242,9 @@ class Users extends Base {
$SQL = "update users
set user_name = :user_name,
user_email = :user_email,
user_client_name = :user_client_name,
user_first_name = :user_client_name,
user_role = :user_role,
user_active = :user_active,
user_status = :user_active,
user_changer = :user_changer,
user_changed = :user_changed
where user_serial = :user_serial";
@@ -255,7 +256,7 @@ class Users extends Base {
$statement->bindValue(":user_email", $user_email);
$statement->bindValue(":user_client_name", $user_client_name);
$statement->bindValue(":user_role", $user_role);
$statement->bindValue(":user_active", $user_active, PDO::PARAM_BOOL);
$statement->bindValue(":user_active", $user_active ? 'active' : 'inactive');
$statement->bindValue(":user_changer", $this->current_user_name);
$statement->bindValue(":user_changed", $this->getTimestamp());
@@ -288,11 +289,9 @@ class Users extends Base {
public function getUserByUserName($user_name) {
$SQL = "select view_users.*
from view_users
where user_name = '{$user_name}'";
return $this->getTable("user", $SQL);
$statement = $this->connect()->prepare('select * from view_users where user_name = :user_name');
$statement->execute([':user_name' => $user_name]);
return $this->getTableXML('user', $statement);
}
// =====================
@@ -484,9 +483,9 @@ class Users extends Base {
// Change Password
// ===============
public function changePassword() {
public function changePassword($forcePrompt = false) {
$step = filter_input(INPUT_POST, "step") ?: "prompt";
$step = $forcePrompt ? 'prompt' : (filter_input(INPUT_POST, "step") ?: "prompt");
switch ($step) {
@@ -502,12 +501,19 @@ class Users extends Base {
case "change":
if (!$this->validAuthRequest()) {
http_response_code(403);
return;
}
$user_serial = $_SESSION[self::USER_SERIAL];
$user_new_password = filter_input(INPUT_POST, "user_new_password") ?: "";
$user_verify_password = filter_input(INPUT_POST, "user_verify_password") ?: "";
if (!$user_new_password == $user_verify_password) {
$this->initializeSession();
if ($user_serial <= 0 || $user_new_password === '' || $user_new_password !== $user_verify_password) {
http_response_code(422);
echo 'Enter a nonempty password and matching confirmation.';
return;
}
$user_new_hashed_password = password_hash($user_new_password, PASSWORD_DEFAULT);
@@ -517,7 +523,7 @@ class Users extends Base {
$connection = $this->connect();
$SQL = "update users
set user_password = :user_password,
set user_password_hash = :user_password_hash,
user_change_password = :user_change_password,
user_changer = :user_changer,
user_changed = :user_changed
@@ -526,7 +532,7 @@ class Users extends Base {
$statement = $connection->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_password", $user_new_hashed_password);
$statement->bindValue(":user_password_hash", $user_new_hashed_password);
$statement->bindValue(":user_change_password", 0);
$statement->bindValue(":user_changer", $this->current_user_name);
$statement->bindValue(":user_changed", $this->getTimestamp());
@@ -536,6 +542,7 @@ class Users extends Base {
}
(new Journal())->journalEntry("{$_SESSION[self::USER_NAME]} changed their password.");
session_regenerate_id(true);
break;
@@ -567,14 +574,14 @@ class Users extends Base {
$connection = $this->connect();
$SQL = "update users
set user_password = :user_password,
set user_password_hash = :user_password_hash,
user_change_password = :user_change_password
where user_serial = :user_serial";
$statement = $connection->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_password", $user_temp_password);
$statement->bindValue(":user_password_hash", $user_temp_password);
$statement->bindValue(":user_change_password", 1);
if (!$statement->execute()) {
@@ -599,7 +606,7 @@ class Users extends Base {
public function updateLogin() {
$SQL = "update users
set user_login = :user_login
set user_last_login = :user_login
where user_serial = :user_serial";
$statement = $this->connect()->prepare($SQL);
@@ -620,7 +627,7 @@ class Users extends Base {
$user_active = filter_input(INPUT_POST, "user_active", FILTER_SANITIZE_NUMBER_INT) ?: 0;
$SQL = "update users
set user_active = :user_active,
set user_status = :user_active,
user_changed = :user_changed,
user_changer = :user_changer
where user_serial = :user_serial";
@@ -628,7 +635,7 @@ class Users extends Base {
$statement = $this->connect()->prepare($SQL);
$statement->bindValue(":user_serial", $user_serial);
$statement->bindValue(":user_active", $user_active);
$statement->bindValue(":user_active", $user_active ? 'active' : 'inactive');
$statement->bindValue(":user_changed", $this->getTimestamp());
$statement->bindValue(":user_changer", $this->current_user_name);