« Back to History
page_acl.php
|
20260723_000646.php
Initial Domain Snapshot
Copy Code
<?php /* ============================================================ /erp/modules/auth/page_acl.php PURPOSE: Central Page ACL (SAFE, ERP-STABLE) FIX: Permission stale session auto-refresh ============================================================ */ error_reporting(E_ALL); ini_set('display_errors', 1); /* ---------- SESSION (unchanged behavior) ---------- */ $__sess_dir = dirname(__DIR__, 2) . '/tmp_sessions'; if (!is_dir($__sess_dir)) { @mkdir($__sess_dir, 0775, true); } @ini_set('session.save_path', $__sess_dir); if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } /* ---------- DB + AUTH ---------- */ require_once dirname(__DIR__, 2) . '/core/db.php'; require_once __DIR__ . '/auth.php'; $STRICT_ACL = true; function get_acl_map(PDO $pdo, int $company_id, bool $force = false): array { $cache_key = 'acl_map_' . $company_id; if (!$force && !empty($_SESSION[$cache_key]) && is_array($_SESSION[$cache_key])) { return $_SESSION[$cache_key]; } try { if ($company_id > 0) { $st = $pdo->prepare( "SELECT slug, roles_json, company_id FROM page_acl_master WHERE is_enabled = 1 AND (company_id = 0 OR company_id = :cid) ORDER BY company_id DESC" ); $st->execute([':cid' => $company_id]); } else { $st = $pdo->prepare( "SELECT slug, roles_json, company_id FROM page_acl_master WHERE is_enabled = 1 AND company_id = 0 ORDER BY company_id DESC" ); $st->execute(); } $map = []; while ($row = $st->fetch(PDO::FETCH_ASSOC)) { $slug = strtolower(trim((string)($row['slug'] ?? ''))); if ($slug === '') { continue; } $roles = json_decode((string)($row['roles_json'] ?? '[]'), true); if (!is_array($roles)) { $roles = []; } $clean_roles = []; foreach ($roles as $role) { if (!is_scalar($role)) { continue; } $role_norm = strtolower(trim((string)$role)); if ($role_norm !== '') { $clean_roles[] = $role_norm; } } $map[$slug] = array_values(array_unique($clean_roles)); } $_SESSION[$cache_key] = $map; return $map; } catch (Throwable $e) { return []; } } function page_acl_fallback(): array { global $pdo; try { if (!($pdo instanceof PDO)) { return []; } $u = auth_user(); $company_id = (int)($u['company_id'] ?? 0); return get_acl_map($pdo, max(0, $company_id)); } catch (Throwable $e) { return []; } } function page_acl_block(array $u, string $slug_norm, string $message = 'Forbidden: ACL denied.'): void { http_response_code(403); error_log("ACL BLOCK: user {$u['id']} tried to access {$slug_norm}"); exit($message); } function page_acl_functional_role_access(PDO $pdo, int $company_id, array $user, string $slug_norm): bool { static $cache = []; $userId = (int)($user['id'] ?? 0); if ($company_id <= 0 || $userId <= 0 || $slug_norm === '') { return false; } $cacheKey = $company_id . ':' . $userId; if (!isset($cache[$cacheKey])) { $cache[$cacheKey] = []; try { $allowedStmt = $pdo->prepare("\n SELECT DISTINCT nmi.url\n FROM user_functional_roles ufr\n INNER JOIN functional_role_menu_items frmi\n ON frmi.functional_role_id = ufr.functional_role_id\n AND frmi.company_id = ufr.company_id\n INNER JOIN nav_menu_items nmi\n ON nmi.id = frmi.menu_item_id\n WHERE ufr.company_id = ?\n AND ufr.user_id = ?\n AND nmi.is_enabled = 1\n "); $allowedStmt->execute([$company_id, $userId]); foreach ($allowedStmt->fetchAll(PDO::FETCH_COLUMN) as $url) { $path = (string)parse_url((string)$url, PHP_URL_PATH); $allowedSlug = strtolower(basename($path !== '' ? $path : (string)$url, '.php')); if ($allowedSlug !== '') { $cache[$cacheKey][$allowedSlug] = true; } } $blockedStmt = $pdo->prepare("\n SELECT permission_key\n FROM user_permission_blocks\n WHERE company_id = ?\n AND user_id = ?\n "); $blockedStmt->execute([$company_id, $userId]); foreach ($blockedStmt->fetchAll(PDO::FETCH_COLUMN) as $blockedSlug) { $blockedSlug = strtolower(trim((string)$blockedSlug)); if ($blockedSlug !== '') { unset($cache[$cacheKey][$blockedSlug]); } } } catch (Throwable $e) { $cache[$cacheKey] = []; } } return isset($cache[$cacheKey][$slug_norm]); } /* ---------- UTILS ---------- */ function mm_table_exists(PDO $pdo, string $table): bool { try { $st = $pdo->prepare( "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?" ); $st->execute([$table]); return (bool)$st->fetchColumn(); } catch (Throwable $e) { return false; } } /* ---------- DB ACL CHECK ---------- */ function can_access_via_db(PDO $pdo, int $company_id, string $slug_norm, array $user): ?bool { try { $slug_php = $slug_norm . '.php'; if ($company_id > 0) { $st = $pdo->prepare( "SELECT slug, allow_roles, allow_user_ids, is_enabled, company_id FROM page_permissions WHERE (company_id = 0 OR company_id = ?) AND slug IN (?, ?) ORDER BY company_id DESC, slug LIMIT 1" ); $st->execute([$company_id, $slug_norm, $slug_php]); } else { $st = $pdo->prepare( "SELECT slug, allow_roles, allow_user_ids, is_enabled, company_id FROM page_permissions WHERE company_id = 0 AND slug IN (?, ?) ORDER BY slug LIMIT 1" ); $st->execute([$slug_norm, $slug_php]); } $r = $st->fetch(PDO::FETCH_ASSOC); if (!$r) return null; if ((int)$r['is_enabled'] !== 1) return false; $uid = (int)$user['id']; $role = strtolower((string)$user['role']); if (!empty($r['allow_user_ids'])) { $uids = json_decode($r['allow_user_ids'], true); if (is_array($uids) && in_array($uid, $uids, true)) { return true; } } if (!empty($r['allow_roles'])) { $roles = json_decode($r['allow_roles'], true); if (is_array($roles)) { $roles = array_map('strtolower', $roles); if (in_array($role, $roles, true)) { return true; } } } return false; } catch (Throwable $e) { return false; } } /* ---------- HEADER AUTO-INCLUDE (INTENTIONALLY DISABLED) ---------- */ function mm_auto_header(): void { // intentionally blank (no behavior change) } /* ============================================================ MAIN GUARD ============================================================ */ function page_require_access(?string $slug = null, array $opts = []): array { global $pdo, $STRICT_ACL; /* ---------- AUTH ---------- */ $u = auth_user(); if (!$u) { $next = $_SERVER['REQUEST_URI'] ?? '/erp/public/index.php'; header('Location: /erp/public/login.php?next=' . rawurlencode($next)); exit; } /* ====================================================== 🔴 CRITICAL FIX: PERMISSION STALE SESSION ====================================================== */ $dbUpdated = strtotime($u['permissions_updated_at'] ?? '1970-01-01'); $sessLoaded = $_SESSION['perm_loaded_at'] ?? 0; if ($sessLoaded < $dbUpdated) { // Force fresh reload ONCE $u = auth_user(true); // auth.php must support force reload $_SESSION['perm_loaded_at'] = time(); } /* ---------- COMPANY ---------- */ $company_id = (int)($u['company_id'] ?? 0); $acl_company_id = max(0, $company_id); /* ---------- SLUG ---------- */ if (!$slug) { $slug = basename($_SERVER['SCRIPT_FILENAME'] ?? 'unknown'); } $slug_norm = strtolower(preg_replace('/\.php$/i', '', $slug)); $role_ci = strtolower((string)$u['role']); $ctx = [ 'user' => $u, 'company_id' => $company_id, 'role' => $u['role'], 'slug' => $slug_norm, 'pdo' => $pdo, ]; /* ---------- OWNER / ADMIN / DEVELOPER = FULL ACCESS ---------- */ if (in_array($role_ci, ['owner', 'admin', 'developer'], true)) { return $ctx; } /* ---------- FUNCTIONAL ROLE ACL ---------- */ if (page_acl_functional_role_access($pdo, $acl_company_id, $u, $slug_norm)) { return $ctx; } /* ---------- DB ACL ---------- */ $acl_has_match = false; if (mm_table_exists($pdo, 'page_permissions')) { $db = can_access_via_db($pdo, $acl_company_id, $slug_norm, $u); if ($db === true) { return $ctx; } if ($db === false) { $acl_has_match = true; page_acl_block($u, $slug_norm, 'Forbidden: ACL(DB) denied.'); } // db === null -> no DB ACL row found, continue to ACL map / fallback } /* ---------- DB ACL MAP ---------- */ $map = get_acl_map($pdo, $acl_company_id); if (!array_key_exists($slug_norm, $map)) { $map = get_acl_map($pdo, $acl_company_id, true); } if (array_key_exists($slug_norm, $map)) { $acl_has_match = true; $allowed = array_map('strtolower', $map[$slug_norm]); if (in_array($role_ci, $allowed, true)) { return $ctx; } page_acl_block($u, $slug_norm, 'Forbidden: ACL(map) denied.'); } /* ====================================================== 🔐 FINAL SAFETY NET Logged-in + valid company + php page = allow Explicit DB deny already handled above ====================================================== */ $script = $_SERVER['SCRIPT_FILENAME'] ?? ''; if ($STRICT_ACL) { page_acl_block($u, $slug_norm, $acl_has_match ? 'Forbidden: ACL denied.' : 'Forbidden: ACL missing.'); } if ($company_id > 0 && str_ends_with($script, '.php')) { return $ctx; } page_acl_block($u, $slug_norm, 'Forbidden: ACL denied.'); return $ctx; }