feat(server): activate projected authentication safely
This commit is contained in:
+22
-3
@@ -5,6 +5,7 @@ import {
|
||||
type AuthenticationConfigProvider,
|
||||
type AuthMode,
|
||||
} from "./auth/config.js";
|
||||
import { createProjectedAuthenticationConfigProvider } from "./auth/runtime-projection.js";
|
||||
import type { WorkspaceRegistryConfig } from "./workspaces/types.js";
|
||||
|
||||
export interface AppConfig {
|
||||
@@ -176,13 +177,31 @@ function positiveDimension(value: string | undefined, fallback: number): number
|
||||
}
|
||||
|
||||
export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
const authConfigFile = absoluteAuthPath(env.THT_AUTH_CONFIG_FILE ?? "/run/thothii-auth/auth.yaml", "file");
|
||||
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");
|
||||
const hasAuthenticationConfig = authConfigFileExists(authConfigFile);
|
||||
const runtimeProjectionRoot = env.THT_AUTH_RUNTIME_PROJECTION_ROOT;
|
||||
let hasAuthenticationConfig = false;
|
||||
let authentication: AuthenticationConfigProvider | undefined;
|
||||
if (runtimeProjectionRoot !== undefined) {
|
||||
let projectionRoot: string;
|
||||
try {
|
||||
projectionRoot = absoluteAuthPath(runtimeProjectionRoot, "runtime projection root");
|
||||
} catch {
|
||||
throw new Error("authentication configuration is invalid");
|
||||
}
|
||||
if (env.THT_AUTH_CONFIG_FILE !== undefined && env.THT_AUTH_CONFIG_FILE !== defaultAuthConfigFile) {
|
||||
throw new Error("authentication configuration is invalid");
|
||||
}
|
||||
authentication = createProjectedAuthenticationConfigProvider(projectionRoot);
|
||||
hasAuthenticationConfig = true;
|
||||
} else {
|
||||
hasAuthenticationConfig = authConfigFileExists(authConfigFile);
|
||||
authentication = hasAuthenticationConfig ? createAuthenticationConfigProvider(authConfigFile) : undefined;
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { stringify } from "yaml";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { createProjectedAuthenticationConfigProvider } from "../src/auth/runtime-projection.js";
|
||||
import { createCurrentLocalUserRegistryResolver } from "../src/auth/local-registry.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
|
||||
const fsHook = vi.hoisted(() => ({
|
||||
path: undefined as string | undefined,
|
||||
@@ -308,6 +309,35 @@ test("loads a complete OIDC projection without a users snapshot", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("loadConfig selects an immutable projected local provider and its in-memory registry", async () => {
|
||||
const root = projectionRoot();
|
||||
writeReadyProjection(root, localProjectionFixture("projected-user", passwordHash));
|
||||
|
||||
const config = loadConfig({
|
||||
THT_AUTH_RUNTIME_PROJECTION_ROOT: root,
|
||||
THT_AUTH_STATE_ROOT: "/state/auth",
|
||||
});
|
||||
const loaded = config.authentication?.current();
|
||||
expect(loaded).toMatchObject({ value: { mode: "local" }, runtimeProjection: expect.any(Object) });
|
||||
const registry = createCurrentLocalUserRegistryResolver().resolve(loaded!);
|
||||
expect(await registry?.findByUsername("PROJECTED-USER")).toMatchObject({ username: "projected-user" });
|
||||
});
|
||||
|
||||
test("loadConfig selects an immutable projected OIDC provider without direct-file fallback", () => {
|
||||
const root = projectionRoot();
|
||||
writeReadyOidcProjection(root);
|
||||
|
||||
const config = loadConfig({
|
||||
THT_AUTH_RUNTIME_PROJECTION_ROOT: root,
|
||||
THT_AUTH_CONFIG_FILE: "/run/thothii-auth/auth.yaml",
|
||||
THT_AUTH_STATE_ROOT: "/state/auth",
|
||||
});
|
||||
expect(config.authentication?.current()).toMatchObject({
|
||||
value: { mode: "oidc" },
|
||||
runtimeProjection: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects a trailing-slash runtime root", () => {
|
||||
const root = projectionRoot();
|
||||
writeReadyProjection(
|
||||
|
||||
@@ -117,6 +117,31 @@ test("loadConfig makes an existing auth.yaml authoritative and rejects AUTH_MODE
|
||||
}
|
||||
});
|
||||
|
||||
test.each([
|
||||
["relative root", { THT_AUTH_RUNTIME_PROJECTION_ROOT: "relative" }],
|
||||
["conflicting direct file", {
|
||||
THT_AUTH_RUNTIME_PROJECTION_ROOT: "/run/thothii-auth",
|
||||
THT_AUTH_CONFIG_FILE: "/different/auth.yaml",
|
||||
}],
|
||||
])("rejects projected authentication configuration: %s", (_name, env) => {
|
||||
expect(() => loadConfig(env)).toThrow("authentication configuration is invalid");
|
||||
});
|
||||
|
||||
test("keeps the direct auth-file provider when the runtime projection environment is absent", () => {
|
||||
const { directory, file } = authFile(oidcAuthConfig());
|
||||
try {
|
||||
const loaded = loadConfig({ THT_AUTH_CONFIG_FILE: file, THT_AUTH_STATE_ROOT: "/state/auth" });
|
||||
const current = loaded.authentication?.current();
|
||||
expect(current).toMatchObject({
|
||||
sourcePath: file,
|
||||
value: { mode: "oidc" },
|
||||
});
|
||||
expect(current?.runtimeProjection).toBeUndefined();
|
||||
} 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(realpathSync(tmpdir()), "thothii-app-auth-config-directory-"));
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user