From 8157673291110505c59905c776d37d7ead5d791a Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sat, 15 Aug 2026 15:27:50 -0400 Subject: [PATCH] Add AUTH_STACK so the Auth tab follows the stack in force Authentik is the likely destination and the page had Authelia and lldap wired in at every level, so the seam goes in now: the panels and every endpoint action route off one conf value, and a stack that cannot be driven yet says so rather than drawing controls with nothing behind. --- Deployment/master.conf.template | 19 ++++- Plugin/unraid/api/auth.php | 37 +++++++++ Plugin/unraid/include/auth.php | 51 +++++++++++++ Plugin/unraid/include/confform.php | 2 +- Plugin/unraid/pages/auth.php | 107 +++++++++++++++++++++------ Plugin/unraid/pages/readme/ui-map.md | 28 ++++++- 6 files changed, 219 insertions(+), 25 deletions(-) diff --git a/Deployment/master.conf.template b/Deployment/master.conf.template index dc3b34f..949a155 100644 --- a/Deployment/master.conf.template +++ b/Deployment/master.conf.template @@ -1439,6 +1439,23 @@ # Defined per host in host*.conf — Emby container names and keys differ per server: # HOST1_TRANSCODE_SERVERS / HOST2_TRANSCODE_SERVERS +# ============================================================================================== +# ── AUTH STACK ──────────────────────────────────────────────────────────────────────────────── +# ============================================================================================== + +# ━━━ Auth Stack ━━━ +# Which identity stack this mesh runs behind its protected hostnames. The Auth tab reads this to +# decide which panels to draw and which endpoints to call — it is a routing switch, not a +# migration. Changing it does not move users, groups or rules between stacks; stand the new one +# up first, then point this at it. +# +# Mesh-wide rather than per host: auth is the one service the partnership treats as shared, owned +# by the mesh owner and consumed by everyone else, so two nodes disagreeing about which stack is +# in force would mean two different answers to "who is this person". +# "authelia_lldap" — Authelia for access rules, lldap for users and groups. Fully implemented. +# "authentik" — single stack for both. NOT IMPLEMENTED YET; the tab says so instead of drawing panels that cannot work. + AUTH_STACK="authelia_lldap" + # ============================================================================================== # ── MONITORS ────────────────────────────────────────────────────────────────────────────────── # ============================================================================================== @@ -1706,12 +1723,12 @@ # earned it — narration for months before anything is allowed near a decision. # ━━━ AI Master Switch ━━━ -# Fail-closed: anything other than the literal "true" means off. # Which node runs the model, the retrieval index and the bug store. Every other node borrows it # over the mesh, so a box without a GPU still gets the assistant — it just does not get the AI tab. # Defaults to host1 when unset or malformed: whoever builds the mesh is host1. AI_OWNER_HOST="host1" +# Fail-closed: anything other than the literal "true" means off. AI_ENABLED=false AI_CONNECT_TIMEOUT=5 # seconds — probe when resolving which node has Ollama AI_REQUEST_TIMEOUT=240 # seconds — must clear a cold model load diff --git a/Plugin/unraid/api/auth.php b/Plugin/unraid/api/auth.php index e754a9c..b73fe43 100644 --- a/Plugin/unraid/api/auth.php +++ b/Plugin/unraid/api/auth.php @@ -93,8 +93,44 @@ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/auth.php'; +// Which panel each action belongs to. AUTH_STACK decides which panels the tab draws, and this is +// the same decision applied to the endpoint — a tab left open from before a switch would otherwise +// keep writing to the stack that is no longer in force, which on this page means editing the +// directory or the rules of a system nobody is authenticating against any more. +// +// Proxies and certs are Nginx Proxy Manager's, not the identity stack's, so they are listed under +// panels every stack carries rather than gated to one. +const VV_AUTH_ACTION_PANEL = [ + // GET + 'npm_proxies' => 'proxies', 'npm_certs' => 'proxies', + 'lldap_users' => 'users', 'lldap_groups' => 'users', 'lldap_avatar' => 'users', + 'authelia_rules' => 'acl', + // POST + 'npm_create' => 'proxies', 'npm_update' => 'proxies', + 'npm_delete' => 'proxies', 'npm_toggle' => 'proxies', + 'lldap_create_user' => 'users', 'lldap_update_user' => 'users', 'lldap_delete_user' => 'users', + 'lldap_set_password' => 'users', 'lldap_set_avatar' => 'users', 'lldap_remove_avatar' => 'users', + 'lldap_create_group' => 'users', 'lldap_delete_group' => 'users', 'lldap_rename_group' => 'users', + 'lldap_add_to_group' => 'users', 'lldap_remove_from_group' => 'users', + 'authelia_save' => 'acl', +]; + +function vv_auth_action_allowed(string $action): bool { + $panel = VV_AUTH_ACTION_PANEL[$action] ?? null; + // Unmapped actions are left to the existing "Unknown action" answer rather than being refused + // here, so a new action is never silently blocked by a table someone forgot to extend. + return $panel === null || vv_auth_panel_on($panel); +} + +function vv_auth_action_refusal(string $action): array { + $d = vv_auth_stack_def(); + return ['ok' => false, 'error' => 'AUTH_STACK is "' . vv_auth_stack() . '" (' . $d['label'] + . '), which does not serve this request. Reload the Auth tab.']; +} + if ($_SERVER['REQUEST_METHOD'] === 'GET') { $action = $_GET['action'] ?? ''; + if (!vv_auth_action_allowed($action)) { echo json_encode(vv_auth_action_refusal($action)); exit; } // The one route here that does not answer in JSON — it streams the stored JPEG so the page can // point an at it, rather than carrying 470 KB of base64 through the user list on every @@ -133,6 +169,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { } $action = trim($_POST['action'] ?? ''); +if (!vv_auth_action_allowed($action)) { echo json_encode(vv_auth_action_refusal($action)); exit; } $result = match ($action) { // NPM diff --git a/Plugin/unraid/include/auth.php b/Plugin/unraid/include/auth.php index 595fcc7..9a861e8 100644 --- a/Plugin/unraid/include/auth.php +++ b/Plugin/unraid/include/auth.php @@ -98,6 +98,57 @@ function vv_auth_conf(): array { ]; } +// ── Which stack ─────────────────────────────────────────────────────────────── + +// The stacks this page knows how to drive, and how far it can drive each one. Declared rather +// than inferred so the tab can offer a stack it cannot yet operate and say exactly that, instead +// of drawing panels that call endpoints with nothing behind them — which is how a switch ends up +// looking like a feature while reading nothing. +const VV_AUTH_STACKS = [ + 'authelia_lldap' => [ + 'label' => 'Authelia + lldap', + 'ready' => true, + 'panels' => ['proxies', 'users', 'acl', 'certs'], + 'summary' => 'Authelia holds the access rules, lldap holds users and groups, ' + . 'Nginx Proxy Manager holds the hostnames.', + ], + 'authentik' => [ + 'label' => 'Authentik', + 'ready' => false, + // Proxies and certs are NPM's, not the identity stack's, so they keep working whichever + // stack is selected. Users and access control are the two this page cannot draw yet. + 'panels' => ['proxies', 'certs'], + 'summary' => 'One stack for identity and access. Varaverk can still manage the proxy ' + . 'hosts and certificates, but not Authentik users, groups or policies yet.', + 'needs' => 'An API token and base URL in host*.conf, then user, group and policy ' + . 'calls against Authentik\'s REST API to sit behind the same page.', + ], +]; + +// Falls back rather than failing: an unrecognised value means someone typed a stack name into the +// conf, and answering with a blank tab helps nobody. The working stack is the safe answer, and +// vv_auth_stack_valid() is what the page uses to say the value was not understood. +function vv_auth_stack(): string { + $v = trim(vv_conf_vars()['AUTH_STACK'] ?? ''); + return isset(VV_AUTH_STACKS[$v]) ? $v : 'authelia_lldap'; +} + +function vv_auth_stack_valid(): bool { + $v = trim(vv_conf_vars()['AUTH_STACK'] ?? ''); + return $v === '' || isset(VV_AUTH_STACKS[$v]); +} + +function vv_auth_stack_def(): array { + return VV_AUTH_STACKS[vv_auth_stack()]; +} + +// One question, asked the same way by the page and by the endpoint. The page uses it to decide +// what to draw; api/auth.php uses it to refuse an action belonging to a stack that is not the one +// in force, so a stale tab left open across a switch cannot write to the wrong directory. +function vv_auth_panel_on(string $panel): bool { + return in_array($panel, vv_auth_stack_def()['panels'], true); +} + // ── Credential state ────────────────────────────────────────────────────────── // Three different failures arrive at a token fetch as the same empty string: the credential was diff --git a/Plugin/unraid/include/confform.php b/Plugin/unraid/include/confform.php index 97b1e61..5242f4a 100644 --- a/Plugin/unraid/include/confform.php +++ b/Plugin/unraid/include/confform.php @@ -128,7 +128,7 @@ const VV_UI_SECTION_SURFACES = [ // is what its Certs panel reports. The credentials in particular belong on the page that fails // without them: a blank NPM_USER surfaces there as a refused login, and the fix is two panels // away rather than in a hundred-and-twenty-field catch-all. - ['match' => 'NginxProxyManager|lldap|Authelia|Certificate Monitor', 'tab' => 'Auth', + ['match' => 'Auth Stack|NginxProxyManager|lldap|Authelia|Certificate Monitor', 'tab' => 'Auth', 'route' => 'Auth tab → Auth settings'], // Everything that is not structurally excluded. The catch-all exists so no ordinary setting diff --git a/Plugin/unraid/pages/auth.php b/Plugin/unraid/pages/auth.php index a2dd468..d68b3b9 100644 --- a/Plugin/unraid/pages/auth.php +++ b/Plugin/unraid/pages/auth.php @@ -37,6 +37,12 @@ // LLDAP user and group management // Authelia access-control rules and default policy // +// STACK SWITCHING +// AUTH_STACK in master.conf decides which panels exist. Proxies and Certs belong to Nginx +// Proxy Manager and are drawn for every stack; Users and Access Control are the identity +// stack's and are drawn only for one that Varaverk can drive. api/auth.php applies the same +// rule to every action, so a tab left open across a switch cannot write to the old stack. +// // DEPENDS ON // include/auth.php required directly for initial render // api/auth.php mutations @@ -60,6 +66,17 @@ require_once dirname(__DIR__) . '/include/ai_chat.php'; .vv-au-btn.green{ border-color:#1a3a1a;background:#0d1f0d;color:#4caf50; } .vv-au-btn:disabled { opacity:.4;cursor:default; } +/* ── Stack indicator ─────────────────────────────────────────────────────── */ +/* Which stack the tab is driving. Always visible, because every panel below it means something + different depending on this, and it is a conf value nothing else on the page reveals. */ +.vv-au-stackchip{ font-size:10px;padding:2px 8px;border-radius:3px;background:#12181f; + border:1px solid #1e2a38;color:#5c9fd4;white-space:nowrap; } +.vv-au-stacknote{ font-size:11px;color:#997;background:#1a1400;border:1px solid #3a2d00; + border-radius:4px;padding:8px 12px;margin-bottom:10px;line-height:1.55; } +.vv-au-stacknote b { color:#ffb74d;display:block;margin-bottom:2px; } +.vv-au-stacknote.bad { background:#1a0d0d;border-color:#3a1a1a;color:#a77; } +.vv-au-stacknote.bad b { color:#ef5350; } + /* ── Panels ──────────────────────────────────────────────────────────────── */ .vv-au-panel { display:none; } .vv-au-panel.active { display:block; } @@ -231,19 +248,45 @@ require_once dirname(__DIR__) . '/include/ai_chat.php'; 'Proxies', 'users' => 'Users & Groups', + 'acl' => 'Access Control', 'certs' => 'Certs']; ?>
Auth Stack - - - - + $label): if (!in_array($id, $panels, true)) continue; ?> + + +
+ + +
+ AUTH_STACK is set to "", which is not a stack this page knows. + Showing instead. Fix it in Auth settings, below. +
+ + +
+ is selected, and Varaverk cannot manage it yet. + + Needed: +
+ + -
+ +
" id="vv-au-panel-proxies">
@@ -259,9 +302,11 @@ $isOwner = vv_is_owner();
+ -
+ +
" id="vv-au-panel-users">
@@ -286,9 +331,11 @@ $isOwner = vv_is_owner();
+ -
+ +
" id="vv-au-panel-acl">
Default policy: