From fd878b8c3e678dd2dc2e1aad8c0fb3cdac0f4dc5 Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 25 Aug 2026 18:37:39 +0200 Subject: [PATCH] fix(workspace): isolate maintenance authentication surface --- backend/src/config.ts | 10 +++++++--- backend/src/workspace-maintenance.ts | 2 +- backend/test/config.test.ts | 11 +++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/backend/src/config.ts b/backend/src/config.ts index 4a4946e4..1b7e680a 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -176,7 +176,11 @@ function positiveDimension(value: string | undefined, fallback: number): number return parsed; } -export function loadConfig(env: Record): AppConfig { +export function loadConfig( + env: Record, + options: { surface?: "application" | "workspace-maintenance" } = {}, +): AppConfig { + const applicationSurface = options.surface !== "workspace-maintenance"; const defaultAuthConfigFile = "/run/thothii-auth/auth.yaml"; const authConfigFile = absoluteAuthPath(env.THT_AUTH_CONFIG_FILE ?? defaultAuthConfigFile, "file"); const authStateRoot = absoluteAuthPath(env.THT_AUTH_STATE_ROOT ?? "/data/auth", "state root"); @@ -211,13 +215,13 @@ export function loadConfig(env: Record): AppConfig { throw new Error(`unsupported AUTH_MODE=${requestedMode}; use none, mock, or upstream`); } const nodeEnvironment = env.NODE_ENV ?? process.env.NODE_ENV; - if ((requestedMode === "none" || requestedMode === "mock") + if (applicationSurface && (requestedMode === "none" || requestedMode === "mock") && nodeEnvironment !== "development" && nodeEnvironment !== "test") { throw new Error("production requires auth.yaml or AUTH_MODE=upstream"); } authMode = requestedMode as "none" | "mock" | "upstream"; } - const publicExposure = env.THOTH_PUBLIC_EXPOSURE === "true"; + const publicExposure = applicationSurface && env.THOTH_PUBLIC_EXPOSURE === "true"; if (publicExposure && authMode !== "oidc" && authMode !== "upstream") { throw new Error("public exposure requires AUTH_MODE=upstream or configured OIDC behind a trusted proxy"); } diff --git a/backend/src/workspace-maintenance.ts b/backend/src/workspace-maintenance.ts index 89ee0067..8a466eae 100644 --- a/backend/src/workspace-maintenance.ts +++ b/backend/src/workspace-maintenance.ts @@ -212,7 +212,7 @@ async function readSessionInventory(dataRoot: string, workspaceId: string): Prom } function createProductionService(): WorkspacePreprocessingService { - const config = loadConfig(process.env); + const config = loadConfig(process.env, { surface: "workspace-maintenance" }); const registry = new WorkspaceRegistry(config.workspaceRegistry); const workspaceSecretStore = new WorkspaceSecretStore({ root: config.workspaceSecretStoreRoot, diff --git a/backend/test/config.test.ts b/backend/test/config.test.ts index 68ba2742..17d8ab6f 100644 --- a/backend/test/config.test.ts +++ b/backend/test/config.test.ts @@ -97,6 +97,17 @@ test("loadConfig allows none and mock only outside production when auth.yaml is .toThrow("production requires auth.yaml or AUTH_MODE=upstream"); }); +test("workspace maintenance loads production configuration without an authentication surface", () => { + expect(loadConfig( + { NODE_ENV: "production", THOTH_PUBLIC_EXPOSURE: "true" }, + { surface: "workspace-maintenance" }, + )).toMatchObject({ + authMode: "none", + authentication: undefined, + publicExposure: false, + }); +}); + test("local Compose profiles explicitly select the development auth environment", () => { for (const profile of ["../../deploy/compose.local.yaml", "../../docker-compose.dev.yml"]) { expect(readFileSync(new URL(profile, import.meta.url), "utf8")).toMatch(/NODE_ENV:\s*development/);