fix(workspace): isolate maintenance authentication surface

This commit is contained in:
2026-08-25 18:37:39 +02:00
parent db375298d0
commit fd878b8c3e
3 changed files with 19 additions and 4 deletions
+7 -3
View File
@@ -176,7 +176,11 @@ function positiveDimension(value: string | undefined, fallback: number): number
return parsed; return parsed;
} }
export function loadConfig(env: Record<string, string | undefined>): AppConfig { export function loadConfig(
env: Record<string, string | undefined>,
options: { surface?: "application" | "workspace-maintenance" } = {},
): AppConfig {
const applicationSurface = options.surface !== "workspace-maintenance";
const defaultAuthConfigFile = "/run/thothii-auth/auth.yaml"; const defaultAuthConfigFile = "/run/thothii-auth/auth.yaml";
const authConfigFile = absoluteAuthPath(env.THT_AUTH_CONFIG_FILE ?? defaultAuthConfigFile, "file"); const authConfigFile = absoluteAuthPath(env.THT_AUTH_CONFIG_FILE ?? defaultAuthConfigFile, "file");
const authStateRoot = absoluteAuthPath(env.THT_AUTH_STATE_ROOT ?? "/data/auth", "state root"); const authStateRoot = absoluteAuthPath(env.THT_AUTH_STATE_ROOT ?? "/data/auth", "state root");
@@ -211,13 +215,13 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
throw new Error(`unsupported AUTH_MODE=${requestedMode}; use none, mock, or upstream`); throw new Error(`unsupported AUTH_MODE=${requestedMode}; use none, mock, or upstream`);
} }
const nodeEnvironment = env.NODE_ENV ?? process.env.NODE_ENV; const nodeEnvironment = env.NODE_ENV ?? process.env.NODE_ENV;
if ((requestedMode === "none" || requestedMode === "mock") if (applicationSurface && (requestedMode === "none" || requestedMode === "mock")
&& nodeEnvironment !== "development" && nodeEnvironment !== "test") { && nodeEnvironment !== "development" && nodeEnvironment !== "test") {
throw new Error("production requires auth.yaml or AUTH_MODE=upstream"); throw new Error("production requires auth.yaml or AUTH_MODE=upstream");
} }
authMode = requestedMode as "none" | "mock" | "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") { if (publicExposure && authMode !== "oidc" && authMode !== "upstream") {
throw new Error("public exposure requires AUTH_MODE=upstream or configured OIDC behind a trusted proxy"); throw new Error("public exposure requires AUTH_MODE=upstream or configured OIDC behind a trusted proxy");
} }
+1 -1
View File
@@ -212,7 +212,7 @@ async function readSessionInventory(dataRoot: string, workspaceId: string): Prom
} }
function createProductionService(): WorkspacePreprocessingService { function createProductionService(): WorkspacePreprocessingService {
const config = loadConfig(process.env); const config = loadConfig(process.env, { surface: "workspace-maintenance" });
const registry = new WorkspaceRegistry(config.workspaceRegistry); const registry = new WorkspaceRegistry(config.workspaceRegistry);
const workspaceSecretStore = new WorkspaceSecretStore({ const workspaceSecretStore = new WorkspaceSecretStore({
root: config.workspaceSecretStoreRoot, root: config.workspaceSecretStoreRoot,
+11
View File
@@ -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"); .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", () => { test("local Compose profiles explicitly select the development auth environment", () => {
for (const profile of ["../../deploy/compose.local.yaml", "../../docker-compose.dev.yml"]) { 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/); expect(readFileSync(new URL(profile, import.meta.url), "utf8")).toMatch(/NODE_ENV:\s*development/);