feat(auth): define strict installation authentication config
This commit is contained in:
+52
-10
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user