Files
ThothII/backend/src/workspaces/secret-requirements.ts
T

200 lines
6.6 KiB
TypeScript

import { dirname } from "node:path";
import {
buildInstallationContract,
type InstallationRole,
type InstallationSuffix,
type InstallationVariable,
} from "./contracts.js";
import { resolveRuntimeBindings, type RuntimeBindings } from "./bindings.js";
import {
DWH_TRANSPORTS,
validateWorkspaceDescriptor,
type DwhTransport,
type WorkspaceDescriptor,
} from "./schema.js";
import {
WorkspaceSecretStore,
type WorkspaceSecretMaterialization,
} from "./secret-store.js";
export type WorkspaceSecretInput = "password" | "textarea";
export interface WorkspaceSecretRequirement {
id: string;
variable: string;
connector: "dwh" | "evidence";
label: string;
description: string;
input: WorkspaceSecretInput;
required: boolean;
}
export interface WorkspaceRuntimeBindingLease {
bindings: RuntimeBindings;
release(): void;
}
interface RequirementDefinition {
id: string;
label: string;
description: string;
input: WorkspaceSecretInput;
}
const DEFINITIONS: Readonly<Partial<Record<`${InstallationRole}.${InstallationSuffix}`, RequirementDefinition>>> = {
"DWH.PASSWORD_FILE": {
id: "dwh.password",
label: "Data warehouse password",
description: "Password used by the selected data warehouse connection.",
input: "password",
},
"DWH.API_KEY_FILE": {
id: "dwh.api_key",
label: "Data warehouse API key",
description: "API key sent to the configured data warehouse REST endpoint.",
input: "password",
},
"DWH.SSH_PRIVATE_KEY_FILE": {
id: "dwh.ssh_private_key",
label: "SSH private key",
description: "Private key used to open the configured SSH tunnel to the data warehouse.",
input: "textarea",
},
"EVIDENCE.SIGNED_URLS_FILE": {
id: "evidence.signed_urls",
label: "Evidence signed URLs",
description: "Signed URLs that authorize ThothII to retrieve the workspace Evidence sources.",
input: "textarea",
},
"EVIDENCE.ACCESS_KEY_FILE": {
id: "evidence.access_key",
label: "Evidence access key",
description: "Access-key identifier used for the configured S3 Evidence source.",
input: "password",
},
"EVIDENCE.SECRET_KEY_FILE": {
id: "evidence.secret_key",
label: "Evidence secret key",
description: "Secret access key used for the configured S3 Evidence source.",
input: "password",
},
"EVIDENCE.SESSION_TOKEN_FILE": {
id: "evidence.session_token",
label: "Evidence session token",
description: "Optional temporary session token used with the S3 Evidence credentials.",
input: "password",
},
};
const REQUIRED_DWH_SECRETS: Readonly<Record<DwhTransport, readonly InstallationSuffix[]>> = {
postgres_direct: ["PASSWORD_FILE"],
rest_api: ["API_KEY_FILE"],
ssh_tunnel: ["PASSWORD_FILE", "SSH_PRIVATE_KEY_FILE"],
};
function isTransport(value: string | undefined): value is DwhTransport {
return value !== undefined && (DWH_TRANSPORTS as readonly string[]).includes(value);
}
function selectedTransport(
descriptor: WorkspaceDescriptor,
variables: readonly InstallationVariable[],
env: NodeJS.ProcessEnv,
): DwhTransport {
const transportVariable = variables.find(({ role, suffix }) => role === "DWH" && suffix === "TRANSPORT");
const value = transportVariable === undefined ? undefined : env[transportVariable.name];
return isTransport(value) && descriptor.dwh.supported_transports.includes(value)
? value
: descriptor.dwh.supported_transports[0];
}
function requirementFor(
variable: InstallationVariable,
required: boolean,
): WorkspaceSecretRequirement | undefined {
const definition = DEFINITIONS[`${variable.role}.${variable.suffix}`];
if (definition === undefined) return undefined;
return {
...definition,
variable: variable.name,
connector: variable.role === "DWH" ? "dwh" : "evidence",
required,
};
}
/**
* Discover the credential fields for the connector and authentication mechanisms selected by
* this installation. Paths, hostnames and trust files remain installation configuration rather
* than user-entered secrets.
*/
export function discoverWorkspaceSecretRequirements(
workspace: WorkspaceDescriptor,
env: NodeJS.ProcessEnv,
): WorkspaceSecretRequirement[] {
const descriptor = validateWorkspaceDescriptor(workspace);
const variables = buildInstallationContract(descriptor).variables;
const transport = selectedTransport(descriptor, variables, env);
const restHasNoAuthentication = transport === "rest_api" && descriptor.diagnostics?.dwh_rest?.auth === "none";
const requiredDwh = new Set(restHasNoAuthentication ? [] : REQUIRED_DWH_SECRETS[transport]);
const requirements: WorkspaceSecretRequirement[] = [];
for (const variable of variables) {
if (variable.role === "DWH") {
if (variable.transports !== undefined && !variable.transports.includes(transport)) continue;
const requirement = requirementFor(variable, requiredDwh.has(variable.suffix));
if (requirement !== undefined && requirement.required) requirements.push(requirement);
continue;
}
const requirement = requirementFor(variable, variable.suffix !== "SESSION_TOKEN_FILE");
if (requirement !== undefined) requirements.push(requirement);
}
return requirements;
}
/**
* Materialize only the selected workspace credentials, translate them to the existing file-based
* harness contract, and bind cleanup to the returned lease.
*/
export function resolveRuntimeBindingsWithWorkspaceSecrets(
workspace: WorkspaceDescriptor,
env: NodeJS.ProcessEnv,
secretRoots: readonly string[],
store: WorkspaceSecretStore,
): WorkspaceRuntimeBindingLease {
const descriptor = validateWorkspaceDescriptor(workspace);
const requirements = discoverWorkspaceSecretRequirements(descriptor, env);
let materialization: WorkspaceSecretMaterialization | undefined;
try {
materialization = store.materialize(
descriptor.workspace.id,
requirements.map(({ id }) => id),
);
const effectiveEnvironment: NodeJS.ProcessEnv = { ...env };
const materializedRoots = new Set<string>();
for (const requirement of requirements) {
const path = materialization.files.get(requirement.id);
if (path === undefined) continue;
effectiveEnvironment[requirement.variable] = path;
materializedRoots.add(dirname(path));
}
const bindings = resolveRuntimeBindings(
descriptor,
effectiveEnvironment,
[...secretRoots, ...materializedRoots],
);
let released = false;
return {
bindings,
release: () => {
if (released) return;
released = true;
materialization?.release();
},
};
} catch (error) {
materialization?.release();
throw error;
}
}