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: