392 lines
10 KiB
PHP
392 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* Authentication and RBAC Helper Functions
|
|
*
|
|
* Provides role-based access control with Cloudflare Access integration
|
|
* Admin users are stored in the database (admin_users table)
|
|
*/
|
|
|
|
// Role constants
|
|
define('ROLE_STAFF', 'staff');
|
|
define('ROLE_ADMIN', 'admin');
|
|
|
|
/**
|
|
* Get the currently authenticated user from Cloudflare Access headers
|
|
* Falls back to session-based auth if CF headers not present
|
|
*
|
|
* @return array|null User info array or null if not authenticated
|
|
*/
|
|
function getCurrentUser() {
|
|
// Check for Cloudflare Access headers first
|
|
// Try multiple header variations that Cloudflare might use
|
|
$cfEmail = null;
|
|
|
|
// Standard Cloudflare Access header
|
|
if (!empty($_SERVER['HTTP_CF_ACCESS_AUTHENTICATED_USER_EMAIL'])) {
|
|
$cfEmail = $_SERVER['HTTP_CF_ACCESS_AUTHENTICATED_USER_EMAIL'];
|
|
}
|
|
// Alternative: some proxies might pass it differently
|
|
elseif (!empty($_SERVER['CF_ACCESS_AUTHENTICATED_USER_EMAIL'])) {
|
|
$cfEmail = $_SERVER['CF_ACCESS_AUTHENTICATED_USER_EMAIL'];
|
|
}
|
|
// Check via getallheaders() for non-standard server configs
|
|
elseif (function_exists('getallheaders')) {
|
|
$headers = getallheaders();
|
|
if (!empty($headers['Cf-Access-Authenticated-User-Email'])) {
|
|
$cfEmail = $headers['Cf-Access-Authenticated-User-Email'];
|
|
}
|
|
}
|
|
|
|
if ($cfEmail) {
|
|
// User authenticated via Cloudflare Access
|
|
return [
|
|
'email' => $cfEmail,
|
|
'auth_method' => 'cloudflare_access',
|
|
'role' => getUserRole($cfEmail)
|
|
];
|
|
}
|
|
|
|
// Fall back to session-based auth
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
if (!empty($_SESSION['authenticated']) && !empty($_SESSION['user'])) {
|
|
return [
|
|
'email' => $_SESSION['user'],
|
|
'auth_method' => 'session',
|
|
'role' => getUserRole($_SESSION['user'])
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Ensure admin_users table exists and seed from ADMIN_EMAILS if empty
|
|
*/
|
|
function ensureAdminUsersTable() {
|
|
if (!function_exists('getDB')) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$db = getDB();
|
|
|
|
// Create table if not exists
|
|
$db->exec("
|
|
CREATE TABLE IF NOT EXISTS admin_users (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
email VARCHAR(255) NOT NULL,
|
|
role ENUM('staff', 'admin') NOT NULL DEFAULT 'staff',
|
|
display_name VARCHAR(255) DEFAULT NULL,
|
|
active TINYINT(1) DEFAULT 1,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
created_by VARCHAR(255) DEFAULT NULL,
|
|
UNIQUE KEY unique_email (email),
|
|
INDEX idx_role (role),
|
|
INDEX idx_active (active)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
");
|
|
|
|
// Check if table is empty
|
|
$count = $db->query("SELECT COUNT(*) FROM admin_users")->fetchColumn();
|
|
|
|
if ($count == 0) {
|
|
// Seed from ADMIN_EMAILS environment variable or constant
|
|
$adminEmails = getenv('ADMIN_EMAILS');
|
|
if (empty($adminEmails) && defined('ADMIN_EMAILS')) {
|
|
$adminEmails = ADMIN_EMAILS;
|
|
}
|
|
|
|
if (!empty($adminEmails)) {
|
|
$emails = array_map('trim', explode(',', $adminEmails));
|
|
$stmt = $db->prepare("INSERT INTO admin_users (email, role, created_by) VALUES (?, 'admin', 'system_seed')");
|
|
|
|
foreach ($emails as $email) {
|
|
if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
try {
|
|
$stmt->execute([strtolower($email)]);
|
|
} catch (Exception $e) {
|
|
// Ignore duplicates
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user role from database
|
|
*
|
|
* @param string $email User email
|
|
* @return string Role (admin or staff)
|
|
*/
|
|
function getUserRole($email) {
|
|
if (empty($email)) {
|
|
return ROLE_STAFF;
|
|
}
|
|
|
|
// Normalize email for comparison (lowercase, trimmed)
|
|
$email = strtolower(trim($email));
|
|
|
|
// Ensure table exists and is seeded
|
|
ensureAdminUsersTable();
|
|
|
|
// Try database lookup
|
|
if (function_exists('getDB')) {
|
|
try {
|
|
$db = getDB();
|
|
$stmt = $db->prepare("SELECT role FROM admin_users WHERE LOWER(email) = ? AND active = 1");
|
|
$stmt->execute([$email]);
|
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if ($result && !empty($result['role'])) {
|
|
return $result['role'];
|
|
}
|
|
} catch (Exception $e) {
|
|
// Table might not exist yet, fall through to default
|
|
}
|
|
}
|
|
|
|
// Default to staff if not found in database
|
|
return ROLE_STAFF;
|
|
}
|
|
|
|
/**
|
|
* Check if current user has a specific role
|
|
*
|
|
* @param string $requiredRole Role to check for
|
|
* @return bool True if user has the role
|
|
*/
|
|
function hasRole($requiredRole) {
|
|
$user = getCurrentUser();
|
|
|
|
if (!$user) {
|
|
return false;
|
|
}
|
|
|
|
// Admin has access to everything
|
|
if ($user['role'] === ROLE_ADMIN) {
|
|
return true;
|
|
}
|
|
|
|
return $user['role'] === $requiredRole;
|
|
}
|
|
|
|
/**
|
|
* Check if current user is an admin
|
|
*
|
|
* @return bool True if user is admin
|
|
*/
|
|
function isAdmin() {
|
|
return hasRole(ROLE_ADMIN);
|
|
}
|
|
|
|
/**
|
|
* Check if current user is staff (or admin)
|
|
*
|
|
* @return bool True if user is staff or admin
|
|
*/
|
|
function isStaff() {
|
|
return hasRole(ROLE_STAFF) || hasRole(ROLE_ADMIN);
|
|
}
|
|
|
|
/**
|
|
* Require a specific role to access a page
|
|
* Redirects to appropriate page if not authorized
|
|
*
|
|
* @param string $requiredRole Role required
|
|
* @param string $redirectUrl URL to redirect to if unauthorized
|
|
*/
|
|
function requireRole($requiredRole, $redirectUrl = '/') {
|
|
if (!hasRole($requiredRole)) {
|
|
header('HTTP/1.1 403 Forbidden');
|
|
header('Location: ' . $redirectUrl . '?error=unauthorized');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Require admin role to access a page
|
|
*/
|
|
function requireAdmin() {
|
|
requireRole(ROLE_ADMIN, '/');
|
|
}
|
|
|
|
/**
|
|
* Get user identifier for audit logging
|
|
* Returns email or 'anonymous' if not authenticated
|
|
*
|
|
* @return string User identifier
|
|
*/
|
|
function getAuditUser() {
|
|
$user = getCurrentUser();
|
|
|
|
if ($user) {
|
|
return $user['email'];
|
|
}
|
|
|
|
// Check legacy session
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
return $_SESSION['user'] ?? 'anonymous';
|
|
}
|
|
|
|
/**
|
|
* Get user display info for header
|
|
*
|
|
* @return array User display info
|
|
*/
|
|
function getUserDisplayInfo() {
|
|
$user = getCurrentUser();
|
|
|
|
if (!$user) {
|
|
return [
|
|
'name' => 'Guest',
|
|
'email' => '',
|
|
'role' => '',
|
|
'initials' => 'G',
|
|
'auth_method' => 'none',
|
|
'is_admin' => false
|
|
];
|
|
}
|
|
|
|
$email = $user['email'];
|
|
$name = explode('@', $email)[0];
|
|
$initials = strtoupper(substr($name, 0, 2));
|
|
|
|
// Try to get display name from database
|
|
$displayName = null;
|
|
if (function_exists('getDB')) {
|
|
try {
|
|
$db = getDB();
|
|
$stmt = $db->prepare("SELECT display_name FROM admin_users WHERE LOWER(email) = ?");
|
|
$stmt->execute([strtolower($email)]);
|
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($result && !empty($result['display_name'])) {
|
|
$displayName = $result['display_name'];
|
|
$nameParts = explode(' ', $displayName);
|
|
$initials = strtoupper(substr($nameParts[0], 0, 1) . (isset($nameParts[1]) ? substr($nameParts[1], 0, 1) : substr($nameParts[0], 1, 1)));
|
|
}
|
|
} catch (Exception $e) {
|
|
// Ignore
|
|
}
|
|
}
|
|
|
|
return [
|
|
'name' => $displayName ?: ucfirst($name),
|
|
'email' => $email,
|
|
'role' => $user['role'],
|
|
'initials' => $initials,
|
|
'auth_method' => $user['auth_method'],
|
|
'is_admin' => $user['role'] === ROLE_ADMIN
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get all admin users from database
|
|
*
|
|
* @return array List of admin users
|
|
*/
|
|
function getAdminUsers() {
|
|
if (!function_exists('getDB')) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
$db = getDB();
|
|
$stmt = $db->query("SELECT id, email, role, display_name, active, created_at, created_by FROM admin_users ORDER BY role DESC, email ASC");
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
} catch (Exception $e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add or update an admin user
|
|
*
|
|
* @param string $email User email
|
|
* @param string $role User role (staff or admin)
|
|
* @param string|null $displayName Display name
|
|
* @param string $createdBy Who created this user
|
|
* @return bool Success
|
|
*/
|
|
function saveAdminUser($email, $role, $displayName = null, $createdBy = null) {
|
|
if (!function_exists('getDB')) {
|
|
return false;
|
|
}
|
|
|
|
$email = strtolower(trim($email));
|
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
return false;
|
|
}
|
|
|
|
if (!in_array($role, [ROLE_STAFF, ROLE_ADMIN])) {
|
|
$role = ROLE_STAFF;
|
|
}
|
|
|
|
try {
|
|
$db = getDB();
|
|
$stmt = $db->prepare("
|
|
INSERT INTO admin_users (email, role, display_name, created_by)
|
|
VALUES (?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE role = VALUES(role), display_name = VALUES(display_name), updated_at = CURRENT_TIMESTAMP
|
|
");
|
|
$stmt->execute([$email, $role, $displayName, $createdBy]);
|
|
return true;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete an admin user
|
|
*
|
|
* @param int $id User ID
|
|
* @return bool Success
|
|
*/
|
|
function deleteAdminUser($id) {
|
|
if (!function_exists('getDB')) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$db = getDB();
|
|
$stmt = $db->prepare("DELETE FROM admin_users WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
return $stmt->rowCount() > 0;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle admin user active status
|
|
*
|
|
* @param int $id User ID
|
|
* @return bool Success
|
|
*/
|
|
function toggleAdminUser($id) {
|
|
if (!function_exists('getDB')) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$db = getDB();
|
|
$stmt = $db->prepare("UPDATE admin_users SET active = NOT active, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
return $stmt->rowCount() > 0;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|