test(auth): gate local and OIDC authentication release

This commit is contained in:
2026-08-18 06:02:25 +02:00
parent 7cf7d9db6b
commit 8a3fa5031d
23 changed files with 1711 additions and 168 deletions
+521
View File
@@ -0,0 +1,521 @@
import { spawn } from "node:child_process";
import { argon2 } from "node:crypto";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { createServer as createHttpServer } from "node:http";
import { request as httpsRequest } from "node:https";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { startFakeOidcProvider } from "../../../backend/test/fixtures/oidc-provider.mjs";
const __dir = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(__dir, "../../..");
const frontendRoot = join(repositoryRoot, "frontend");
const backendRoot = join(repositoryRoot, "backend");
const harnessRoot = join(repositoryRoot, "harness");
const thtRoot = join(repositoryRoot, "tools", "tht");
const fakePi = join(frontendRoot, "e2e", "fixtures", "fake-pi.mjs");
const fakeTht = join(frontendRoot, "e2e", "fixtures", "fake-tht.mjs");
function safeError(code) {
return new Error(code);
}
function buildAuthenticationStorageBridge(output) {
const result = spawn("go", ["build", "-o", output, "./cmd/tht"], {
cwd: thtRoot,
stdio: "ignore",
});
return new Promise((resolveBuild, rejectBuild) => {
result.once("error", () => rejectBuild(safeError("e2e_auth_storage_build_failed")));
result.once("exit", (code) => code === 0 ? resolveBuild() : rejectBuild(safeError("e2e_auth_storage_build_failed")));
});
}
function secureDirectory(path) {
mkdirSync(path, { recursive: true, mode: 0o700 });
chmodSync(path, 0o700);
}
function runFixtureCommand(command, args, cwd) {
const child = spawn(command, args, { cwd, stdio: "ignore" });
return new Promise((resolveCommand, rejectCommand) => {
child.once("error", () => rejectCommand(safeError("e2e_workspace_fixture_command_failed")));
child.once("exit", (code) => code === 0
? resolveCommand()
: rejectCommand(safeError("e2e_workspace_fixture_command_failed")));
});
}
const F1_WORKSPACE_ID = "fixture-workspace";
const F1_WORKSPACE_DESCRIPTOR = `workspace:
schema_version: 3
id: fixture-workspace
name: Fixture workspace
language: en
dwh:
engine: postgres
database: fixture
schema: fixture
supported_transports: [postgres_direct]
semantic_index:
vector_store:
engine: qdrant
collection: fixture-workspace
dimensions: 1024
distance: cosine
embedding:
provider: ollama_internal
model: qwen3-embedding:0.6b
dimensions: 1024
llm_policy:
allowed: [zai/glm-5.2]
default: zai/glm-5.2
`;
async function prepareF1Workspace(root) {
const source = join(root, "workspace-source");
const remote = join(root, "workspace-remote.git");
secureDirectory(source);
secureDirectory(join(source, F1_WORKSPACE_ID));
writeSecure(join(source, "thoth-workspaces.yaml"), [
"schema_version: 1",
"workspaces:",
` - id: ${F1_WORKSPACE_ID}`,
" name: Fixture workspace",
"",
].join("\n"));
writeSecure(join(source, F1_WORKSPACE_ID, "workspace.yaml"), F1_WORKSPACE_DESCRIPTOR);
await runFixtureCommand("git", ["init", "--bare", "--initial-branch=main", remote], root);
chmodSync(remote, 0o700);
await runFixtureCommand("git", ["init", "--initial-branch=main"], source);
await runFixtureCommand("git", ["config", "user.name", "ThothII E2E Fixture"], source);
await runFixtureCommand("git", ["config", "user.email", "thothii-e2e@example.invalid"], source);
await runFixtureCommand("git", ["add", "-A"], source);
await runFixtureCommand("git", ["commit", "-m", "Create deterministic fixture workspace"], source);
await runFixtureCommand("git", ["remote", "add", "origin", remote], source);
await runFixtureCommand("git", ["push", "origin", "main"], source);
return { id: F1_WORKSPACE_ID, remote };
}
function writeSecure(path, value) {
writeFileSync(path, value, { encoding: "utf8", mode: 0o600 });
chmodSync(path, 0o600);
}
async function testPasswordHash(password) {
const salt = Buffer.from("thothii-e2e-salt");
const message = Buffer.from(password, "utf8");
let digest;
try {
digest = await new Promise((resolveDigest, rejectDigest) => {
argon2("argon2id", {
message,
nonce: salt,
memory: 65_536,
passes: 3,
parallelism: 1,
tagLength: 32,
}, (error, derived) => error || !derived ? rejectDigest(error ?? safeError("e2e_password_hash_failed")) : resolveDigest(derived));
});
return `$argon2id$v=19$m=65536,t=3,p=1$${salt.toString("base64").replaceAll("=", "")}$${digest.toString("base64").replaceAll("=", "")}`;
} finally {
message.fill(0);
salt.fill(0);
digest?.fill(0);
}
}
function pause(milliseconds) {
return new Promise((resolvePause) => setTimeout(resolvePause, milliseconds));
}
function providerJson(url, caFile, options = {}) {
return new Promise((resolveResponse, rejectResponse) => {
const body = options.body ?? "";
const request = httpsRequest(url, {
method: options.method ?? "GET",
ca: readFileSync(caFile),
headers: {
accept: "application/json",
...(body.length === 0 ? {} : {
"content-length": String(Buffer.byteLength(body)),
"content-type": "application/x-www-form-urlencoded",
}),
...options.headers,
},
}, (response) => {
const chunks = [];
let size = 0;
response.on("data", (chunk) => {
size += chunk.length;
if (size > 64 * 1024) request.destroy(safeError("e2e_provider_response_too_large"));
else chunks.push(chunk);
});
response.once("error", () => rejectResponse(safeError("e2e_provider_response_failed")));
response.once("end", () => {
try {
resolveResponse({ status: response.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) });
} catch {
rejectResponse(safeError("e2e_provider_response_invalid"));
}
});
});
request.once("error", () => rejectResponse(safeError("e2e_provider_request_failed")));
request.end(body);
});
}
async function freeLoopbackPort() {
const server = createServer();
await new Promise((resolveListen, rejectListen) => {
server.once("error", rejectListen);
server.listen({ host: "127.0.0.1", port: 0 }, resolveListen);
});
const address = server.address();
await new Promise((resolveClose) => server.close(resolveClose));
if (!address || typeof address === "string") throw safeError("e2e_loopback_port_unavailable");
return address.port;
}
const F1_QDRANT_INDEXES = Object.freeze([
"content_hash", "document_id", "kind", "record_key",
"record_kind", "vector_generation", "workspace_id", "workspace_revision",
]);
async function startFakeQdrant() {
const payloadSchema = Object.fromEntries(F1_QDRANT_INDEXES.map((field) => [field, { data_type: "keyword" }]));
const server = createHttpServer((request, response) => {
const path = new URL(request.url ?? "/", "http://loopback.invalid").pathname;
if (request.method !== "GET" || path !== `/collections/${F1_WORKSPACE_ID}`) {
response.writeHead(404).end();
return;
}
response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
result: {
config: { params: { vectors: { size: 1024, distance: "Cosine" } } },
payload_schema: payloadSchema,
},
}));
});
await new Promise((resolveListen, rejectListen) => {
server.once("error", rejectListen);
server.listen({ host: "127.0.0.1", port: 0 }, resolveListen);
});
const address = server.address();
if (!address || typeof address === "string") {
await new Promise((resolveClose) => server.close(resolveClose));
throw safeError("e2e_qdrant_loopback_port_unavailable");
}
return {
baseUrl: `http://127.0.0.1:${address.port}/`,
close: () => new Promise((resolveClose) => server.close(resolveClose)),
};
}
function managedProcess(command, args, options) {
const child = spawn(command, args, {
cwd: options.cwd,
env: options.env,
stdio: ["ignore", "pipe", "pipe"],
});
let exited = false;
let diagnostic = "";
const captureDiagnostic = (chunk) => {
const sanitized = String(chunk)
.replace(/https?:\/\/[^\s)]+/g, "[url]")
.replace(/(?:THT_[A-Z_]+|PI_[A-Z_]+|AUTH_MODE)=\S+/g, "$1=[redacted]")
.replace(/[A-Za-z0-9_-]{43,}/g, "[redacted]");
diagnostic = `${diagnostic}${sanitized}`.slice(-800);
};
child.once("exit", () => { exited = true; });
child.stdout?.on("data", captureDiagnostic);
child.stderr?.on("data", captureDiagnostic);
return {
child,
exited: () => exited,
diagnostic: () => diagnostic.replace(/\s+/g, " ").trim(),
async close() {
if (exited) return;
child.kill("SIGTERM");
for (let attempt = 0; attempt < 50 && !exited; attempt += 1) await pause(20);
if (!exited) child.kill("SIGKILL");
for (let attempt = 0; attempt < 50 && !exited; attempt += 1) await pause(20);
},
};
}
async function waitForOk(url, processHandle) {
for (let attempt = 0; attempt < 300; attempt += 1) {
if (processHandle.exited()) {
const detail = processHandle.diagnostic();
throw safeError(detail ? `e2e_service_stopped_during_startup:${detail}` : "e2e_service_stopped_during_startup");
}
try {
const response = await fetch(url, { redirect: "error" });
if (response.ok) return;
} catch {
// The process is expected to race its listener setup.
}
await pause(100);
}
throw safeError("e2e_service_startup_timeout");
}
function cleanBackendEnvironment(overrides) {
const env = { ...process.env };
for (const name of [
"AUTH_MODE", "THT_AUTH_CONFIG_FILE", "THT_AUTH_STATE_ROOT", "THT_SECRETS_FILE", "NODE_EXTRA_CA_CERTS",
"SETTINGS_FILE", "THT_MAINTENANCE_FILE", "THT_WORKSPACE_REGISTRY_ROOT", "THT_WORKSPACE_SECRET_STORE_ROOT",
"THT_WORKSPACE_SECRET_RUNTIME_ROOT", "THT_WORKSPACE_GIT_REMOTE", "THT_WORKSPACE_GIT_BRANCH",
"THT_WORKSPACE_SECRET_ROOTS", "THT_WORKSPACE_INSTALLATION_ID", "THT_LEGACY_WORKSPACE_MODE",
"THT_DWH_PRECHECK", "THT_INTERNAL_QDRANT_URL", "THT_CONFIG",
]) delete env[name];
for (const name of Object.keys(env)) {
if (name.startsWith("THT_WS_")) delete env[name];
}
return { ...env, ...overrides };
}
export async function createAuthenticationStack({ withF1Workspace = false } = {}) {
const root = mkdtempSync(join(realpathSync(tmpdir()), "thothii-auth-e2e-"));
secureDirectory(root);
const stateRoot = join(root, "auth-state");
const registryRoot = join(root, "workspace-registry");
const workspaceSecretRoot = join(root, "workspace-secrets");
const workspaceRuntimeRoot = join(root, "workspace-runtime");
const fixtureSecretRoot = join(root, "fixture-runtime-secrets");
const providerRoot = join(root, "provider");
const authConfigFile = join(root, "auth.yaml");
const usersFile = join(root, "users.yaml");
const secretsFile = join(root, "test.secrets");
const settingsFile = join(root, "settings.json");
const maintenanceFile = join(root, "maintenance.json");
const authStorageBinary = join(root, "tht-auth-storage");
const fixtureDwhPasswordFile = join(fixtureSecretRoot, "fixture-dwh-password");
const fixtureDwhCaFile = join(fixtureSecretRoot, "fixture-dwh-ca.pem");
for (const path of [stateRoot, registryRoot, workspaceSecretRoot, workspaceRuntimeRoot, fixtureSecretRoot, providerRoot]) secureDirectory(path);
for (const child of ["sessions", "oidc"]) secureDirectory(join(stateRoot, child));
const [frontendPort, backendPort] = await Promise.all([freeLoopbackPort(), freeLoopbackPort()]);
const publicUrl = `http://127.0.0.1:${frontendPort}`;
const backendUrl = `http://127.0.0.1:${backendPort}`;
const workspace = withF1Workspace ? await prepareF1Workspace(root) : undefined;
const provider = await startFakeOidcProvider({ directory: providerRoot });
await buildAuthenticationStorageBridge(authStorageBinary);
const localPassword = "e2e-local-password";
const passwordHash = await testPasswordHash(localPassword);
const accounts = Object.freeze({
ordinary: Object.freeze({ username: "ordinary", password: localPassword }),
admin: Object.freeze({ username: "administrator", password: localPassword }),
});
writeSecure(usersFile, JSON.stringify({
version: 1,
users: [
{
id: "11111111-1111-4111-8111-111111111111",
username: accounts.ordinary.username,
displayName: "Fixture ordinary",
passwordHash,
roles: ["user"],
enabled: true,
authRevision: 1,
},
{
id: "22222222-2222-4222-8222-222222222222",
username: accounts.admin.username,
displayName: "Fixture administrator",
passwordHash,
roles: ["admin"],
enabled: true,
authRevision: 1,
},
],
}));
writeSecure(secretsFile, [
"THT_OIDC_CLIENT_SECRET=e2e-client-secret-not-a-production-secret",
"THT_AUTHENTIK_API_TOKEN=e2e-group-catalog-token-not-a-production-secret",
"",
].join("\n"));
if (workspace) {
writeSecure(fixtureDwhPasswordFile, "fixture-password-not-a-secret\n");
writeSecure(fixtureDwhCaFile, "fixture-ca-not-a-production-certificate\n");
writeSecure(settingsFile, JSON.stringify({
workspace: workspace.id,
provider: "zai",
model: "glm-5.2",
thinking: "medium",
}));
}
let mode = undefined;
let backend = undefined;
let qdrant = undefined;
const frontend = managedProcess(join(frontendRoot, "node_modules", ".bin", "vite"), [
"--host", "127.0.0.1", "--port", String(frontendPort), "--strictPort",
], {
cwd: frontendRoot,
env: {
...process.env,
THT_FRONTEND_API_UPSTREAM: backendUrl,
},
});
try {
await waitForOk(publicUrl, frontend);
qdrant = workspace ? await startFakeQdrant() : undefined;
const localConfig = () => ({
version: 1,
mode: "local",
publicUrl,
session: {
regularTtlSeconds: 600,
regularIdleSeconds: 600,
rememberTtlSeconds: 2_592_000,
rememberIdleSeconds: 604_800,
},
local: { usersFile: "users.yaml" },
});
const oidcConfig = () => ({
version: 1,
mode: "oidc",
publicUrl,
session: {
regularTtlSeconds: 600,
regularIdleSeconds: 600,
oidcTtlSeconds: 60,
},
oidc: {
issuer: provider.issuer,
clientId: "thothii-e2e-client",
clientSecretRef: "THT_OIDC_CLIENT_SECRET",
scopes: ["openid", "profile", "groups"],
groupsClaim: "groups",
},
groupCatalog: {
driver: "authentik",
baseUrl: provider.baseUrl,
apiTokenRef: "THT_AUTHENTIK_API_TOKEN",
},
authorization: {
groupRoles: {
"fixture-users": ["user"],
"fixture-admin": ["admin"],
},
},
});
async function startBackend() {
if (mode === undefined) throw safeError("e2e_auth_mode_not_configured");
backend = managedProcess(join(backendRoot, "node_modules", ".bin", "tsx"), ["src/server.ts"], {
cwd: backendRoot,
env: cleanBackendEnvironment({
NODE_ENV: "test",
HOST: "127.0.0.1",
PORT: String(backendPort),
PI_BIN: fakePi,
THT_BIN: fakeTht,
THT_AUTH_STORAGE_BIN: authStorageBinary,
THT_HARNESS_DIR: harnessRoot,
THT_AUTH_CONFIG_FILE: authConfigFile,
THT_AUTH_STATE_ROOT: stateRoot,
THT_SECRETS_FILE: secretsFile,
NODE_EXTRA_CA_CERTS: provider.caFile,
SETTINGS_FILE: settingsFile,
THT_MAINTENANCE_FILE: maintenanceFile,
THT_WORKSPACE_REGISTRY_ROOT: registryRoot,
THT_WORKSPACE_SECRET_STORE_ROOT: workspaceSecretRoot,
THT_WORKSPACE_SECRET_RUNTIME_ROOT: workspaceRuntimeRoot,
THT_WORKSPACE_INSTALLATION_ID: "e2e",
THT_DATA_ROOT: join(root, "data"),
...(qdrant ? { THT_INTERNAL_QDRANT_URL: qdrant.baseUrl } : {}),
...(workspace ? {
THT_WORKSPACE_GIT_REMOTE: workspace.remote,
THT_WORKSPACE_GIT_BRANCH: "main",
THT_WORKSPACE_SECRET_ROOTS: fixtureSecretRoot,
THT_WS_FIXTURE_WORKSPACE_DWH_TRANSPORT: "postgres_direct",
THT_WS_FIXTURE_WORKSPACE_DWH_HOST: "127.0.0.1",
THT_WS_FIXTURE_WORKSPACE_DWH_PORT: "5432",
THT_WS_FIXTURE_WORKSPACE_DWH_USER: "fixture",
THT_WS_FIXTURE_WORKSPACE_DWH_PASSWORD_FILE: fixtureDwhPasswordFile,
THT_WS_FIXTURE_WORKSPACE_DWH_TLS_CA_FILE: fixtureDwhCaFile,
} : {}),
}),
});
await waitForOk(`${backendUrl}/health`, backend);
}
async function restartBackend() {
await backend?.close();
backend = undefined;
await startBackend();
}
return {
publicUrl,
localAccount(account) {
const found = accounts[account];
if (!found) throw safeError("e2e_local_account_unknown");
return found;
},
lastAuthorization() {
return provider.lastAuthorization();
},
async providerSurface() {
const discovery = await providerJson(`${provider.issuer}.well-known/openid-configuration`, provider.caFile);
const jwks = await providerJson(`${provider.issuer}jwks`, provider.caFile);
const device = await providerJson(`${provider.issuer}device_authorization`, provider.caFile, {
method: "POST",
body: "client_id=thothii-e2e-client",
});
const deviceCode = typeof device.body?.device_code === "string" ? device.body.device_code : "";
const deviceToken = deviceCode.length === 0 ? { status: 0, body: {} } : await providerJson(`${provider.issuer}token`, provider.caFile, {
method: "POST",
body: `grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&device_code=${encodeURIComponent(deviceCode)}`,
});
const groups = await providerJson(`${provider.baseUrl}/api/v3/core/groups/?name=fixture-users`, provider.caFile, {
headers: { authorization: "Bearer e2e-fixture" },
});
return {
discovery: discovery.status === 200 && discovery.body?.issuer === provider.issuer,
jwks: jwks.status === 200 && jwks.body?.keys?.[0]?.alg === "RS256" && jwks.body?.keys?.[0]?.use === "sig",
deviceAuthorization: device.status === 200 && typeof device.body?.device_code === "string"
&& typeof device.body?.verification_uri === "string",
deviceToken: deviceToken.status === 200 && typeof deviceToken.body?.id_token === "string",
groupList: groups.status === 200 && groups.body?.results?.[0]?.name === "fixture-users",
};
},
setOidcIdentity(identity) {
provider.setIdentity(identity);
},
async useLocalMode() {
writeSecure(authConfigFile, JSON.stringify(localConfig()));
mode = "local";
await restartBackend();
},
async useOidcMode(identity) {
provider.setIdentity(identity);
writeSecure(authConfigFile, JSON.stringify(oidcConfig()));
mode = "oidc";
await restartBackend();
},
restartBackend,
async close() {
await backend?.close();
await frontend.close();
await provider.close();
await qdrant?.close();
rmSync(root, { recursive: true, force: true });
},
};
} catch (error) {
await backend?.close();
await frontend.close();
await provider.close();
await qdrant?.close();
rmSync(root, { recursive: true, force: true });
throw error;
}
}