feat(auth): define strict installation authentication config

This commit is contained in:
2026-08-16 17:26:31 +02:00
parent d464f3a982
commit a66ef58766
6 changed files with 533 additions and 11 deletions
+52 -10
View File
@@ -1,9 +1,18 @@
import path from "node:path";
import { statSync } from "node:fs";
import {
createAuthenticationConfigProvider,
type AuthenticationConfigProvider,
type AuthMode,
} from "./auth/config.js";
import type { WorkspaceRegistryConfig } from "./workspaces/types.js";
export interface AppConfig {
host: string; port: number; harnessDir: string; thtBin: string; piBin: string;
authMode: "none" | "mock" | "upstream";
authMode: AuthMode;
authConfigFile: string;
authStateRoot: string;
authentication?: AuthenticationConfigProvider;
publicExposure: boolean;
sessionStorage: {
mode: "local" | "postgres";
@@ -55,6 +64,23 @@ function absoluteRegistryPath(value: string, label: string): string {
return pathValue;
}
function absoluteAuthPath(value: string, label: string): string {
if (value.length === 0 || value.trim() !== value || value.includes("\0") || !path.isAbsolute(value)) {
throw new Error(`authentication ${label} configuration is invalid`);
}
return value;
}
function authConfigFileExists(file: string): boolean {
try {
if (!statSync(file).isFile()) throw new Error("authentication configuration is invalid");
return true;
} catch (error: any) {
if (error?.code === "ENOENT") return false;
throw new Error("authentication configuration is invalid");
}
}
function refSafeGitBranch(value: string): string {
const branch = requiredRegistryValue(value, "branch");
if (
@@ -149,13 +175,26 @@ function positiveDimension(value: string | undefined, fallback: number): number
}
export function loadConfig(env: Record<string, string | undefined>): AppConfig {
const authMode = env.AUTH_MODE ?? "none";
if (!(["none", "mock", "upstream"] as const).includes(authMode as AppConfig["authMode"])) {
throw new Error(`unsupported AUTH_MODE=${authMode}; use none, mock, or upstream`);
const authConfigFile = absoluteAuthPath(env.THT_AUTH_CONFIG_FILE ?? "/run/thothii-auth/auth.yaml", "file");
const authStateRoot = absoluteAuthPath(env.THT_AUTH_STATE_ROOT ?? "/data/auth", "state root");
const hasAuthenticationConfig = authConfigFileExists(authConfigFile);
if (hasAuthenticationConfig && env.AUTH_MODE !== undefined) {
throw new Error("authentication configuration and AUTH_MODE cannot both be set");
}
const authentication = hasAuthenticationConfig ? createAuthenticationConfigProvider(authConfigFile) : undefined;
let authMode: AuthMode;
if (authentication) {
authMode = authentication.current().value.mode;
} else {
const requestedMode = env.AUTH_MODE ?? "none";
if (!(["none", "mock", "upstream"] as const).includes(requestedMode as "none" | "mock" | "upstream")) {
throw new Error(`unsupported AUTH_MODE=${requestedMode}; use none, mock, or upstream`);
}
authMode = requestedMode as "none" | "mock" | "upstream";
}
const publicExposure = env.THOTH_PUBLIC_EXPOSURE === "true";
if (publicExposure && authMode !== "upstream") {
throw new Error("public exposure requires AUTH_MODE=upstream behind a trusted proxy");
if (publicExposure && authMode !== "oidc" && authMode !== "upstream") {
throw new Error("public exposure requires AUTH_MODE=upstream or configured OIDC behind a trusted proxy");
}
const sessionStorageMode = env.THT_SESSION_STORAGE ?? "local";
if (sessionStorageMode !== "local" && sessionStorageMode !== "postgres") {
@@ -181,15 +220,15 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
const sslrootcert = env.THT_SESSION_DB_SSLROOTCERT;
const port = Number(env.THT_SESSION_DB_PORT ?? 5432);
if (
authMode !== "upstream"
(authMode !== "upstream" && authMode !== "oidc")
|| !host || !database || !runtimeUser
|| !runtimePasswordFile || !path.isAbsolute(runtimePasswordFile)
|| (sslmode !== "verify-ca" && sslmode !== "verify-full")
|| !sslrootcert || !path.isAbsolute(sslrootcert)
|| !Number.isInteger(port) || port < 1 || port > 65535
) {
if (authMode !== "upstream") {
throw new Error("server session storage requires AUTH_MODE=upstream");
if (authMode !== "upstream" && authMode !== "oidc") {
throw new Error("server session storage requires AUTH_MODE=upstream or configured OIDC");
}
throw new Error("server session storage configuration is invalid");
}
@@ -277,7 +316,10 @@ export function loadConfig(env: Record<string, string | undefined>): AppConfig {
harnessDir: env.THT_HARNESS_DIR ?? "../harness",
thtBin: env.THT_BIN ?? "tht",
piBin: env.PI_BIN ?? "pi",
authMode: authMode as AppConfig["authMode"],
authMode,
authConfigFile,
authStateRoot,
authentication,
publicExposure,
sessionStorage,
defaults: { provider: env.PI_PROVIDER, model: env.PI_MODEL, thinking: env.PI_THINKING },