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
+66
View File
@@ -1,6 +1,29 @@
import { expect, test } from "vitest";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { stringify } from "yaml";
import { loadConfig } from "../src/config.js";
function authFile(value: unknown): { directory: string; file: string } {
const directory = mkdtempSync(join(tmpdir(), "thothii-app-auth-config-"));
const file = join(directory, "auth.yaml");
writeFileSync(file, stringify(value), "utf8");
return { directory, file };
}
function oidcAuthConfig(): Record<string, unknown> {
return {
version: 1, mode: "oidc", publicUrl: "https://thothii.example.org",
oidc: {
issuer: "https://authentik.example.org/application/o/thothii/", clientId: "thothii",
clientSecretRef: "THT_OIDC_CLIENT_SECRET", scopes: ["openid"], groupsClaim: "groups",
},
groupCatalog: { driver: "authentik", baseUrl: "https://authentik.example.org", apiTokenRef: "THT_AUTHENTIK_API_TOKEN" },
authorization: { groupRoles: { "TOT Users": ["user"], "TOT Admin": ["admin"] } },
};
}
test("loadConfig accepts container listening and runtime paths", () => {
expect(loadConfig({
HOST: "0.0.0.0",
@@ -41,10 +64,53 @@ test("loadConfig keeps local development defaults", () => {
internalEmbeddingUrl: "http://embedding:11434",
internalEmbeddingModel: "qwen3-embedding:0.6b",
internalEmbeddingDimensions: 1024,
authMode: "none",
authConfigFile: "/run/thothii-auth/auth.yaml",
authStateRoot: "/data/auth",
});
expect(loadConfig({}).dataRoot).toBeUndefined();
});
test("loadConfig makes an existing auth.yaml authoritative and rejects AUTH_MODE split-brain", () => {
const { directory, file } = authFile(oidcAuthConfig());
try {
const config = loadConfig({ THT_AUTH_CONFIG_FILE: file, THT_AUTH_STATE_ROOT: "/state/auth" });
expect(config.authMode).toBe("oidc");
expect(config.authStateRoot).toBe("/state/auth");
expect(config.authentication?.current().sourcePath).toBe(file);
expect(() => loadConfig({ THT_AUTH_CONFIG_FILE: file, AUTH_MODE: "upstream" }))
.toThrow("authentication configuration and AUTH_MODE cannot both be set");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test("loadConfig rejects an auth config path that exists but is not a regular file", () => {
const directory = mkdtempSync(join(tmpdir(), "thothii-app-auth-config-directory-"));
try {
expect(() => loadConfig({ THT_AUTH_CONFIG_FILE: directory }))
.toThrow("authentication configuration is invalid");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test("public exposure accepts configured OIDC and the upstream migration mode only", () => {
const { directory, file } = authFile(oidcAuthConfig());
try {
expect(loadConfig({
THOTH_PUBLIC_EXPOSURE: "true", THT_AUTH_CONFIG_FILE: file, THT_SESSION_STORAGE: "postgres",
THT_SESSION_DB_HOST: "db.internal", THT_SESSION_DB_NAME: "thoth", THT_SESSION_RUNTIME_USER: "thoth_sessions_app",
THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session_runtime_password", THT_SESSION_DB_SSLMODE: "verify-full",
THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session_ca.pem",
}).authMode).toBe("oidc");
expect(() => loadConfig({ THOTH_PUBLIC_EXPOSURE: "true", AUTH_MODE: "mock" }))
.toThrow("public exposure requires AUTH_MODE=upstream or configured OIDC");
} finally {
rmSync(directory, { recursive: true, force: true });
}
});
test("loadConfig accepts only the allowed internal semantic runtime hosts", () => {
expect(loadConfig({
THT_INTERNAL_QDRANT_URL: "http://localhost:6333",