test: isolate QA trusted certificates
This commit is contained in:
1 parent
28a433386e
commit
4799dd0251
3 files changed
+53
-1
No files matched your search
@@ -123,6 +123,7 @@ interface RuntimeManifest {
|
||||
runRoot: string;
|
||||
slotDir: string;
|
||||
dbPath: string;
|
||||
trustedCertificatePath: string;
|
||||
fixtureManifestPath: string;
|
||||
nextDistDir: string;
|
||||
url: string;
|
||||
@@ -140,6 +141,16 @@ interface RuntimeManifest {
|
||||
docker: { redisContainer: string };
|
||||
}
|
||||
|
||||
export const createSlotTrustedCertificateDirectory = async (runRoot: string, slotDir: string) => {
|
||||
await assertSafeContainedPath(runRoot, slotDir, "QA slot directory");
|
||||
const trustedCertificatePath = path.join(slotDir, "trusted-certificates");
|
||||
await assertSafeContainedPath(slotDir, trustedCertificatePath, "QA trusted certificate directory");
|
||||
await mkdir(trustedCertificatePath, { mode: 0o700 });
|
||||
await assertSafeContainedPath(slotDir, trustedCertificatePath, "QA trusted certificate directory");
|
||||
await chmod(trustedCertificatePath, 0o700);
|
||||
return trustedCertificatePath;
|
||||
};
|
||||
|
||||
const usage = () => {
|
||||
console.log(
|
||||
"Usage: runtime.mts <start|status|stop> --slot <1|2|3> [--profile <main-writable|main-readonly|onboarding-fresh|degraded>] [--run-root <path>] [--candidate <ref>] [--reset]",
|
||||
@@ -664,6 +675,7 @@ const startWithLease = async (options: Options) => {
|
||||
const runId = `release-v2-${candidateSha.slice(0, 8)}-s${options.slot}-${Date.now()}`;
|
||||
const redisContainer = `homarr-release-v2-qa-redis-s${options.slot}-${candidateSha.slice(0, 8)}-${Date.now().toString(36)}`;
|
||||
const dbPath = path.join(slotDir, "db.sqlite");
|
||||
const trustedCertificatePath = await createSlotTrustedCertificateDirectory(options.runRoot, slotDir);
|
||||
const fixtureReadyPath = path.join(slotDir, "fixture-ready.json");
|
||||
const fixtureManifestPath = path.join(slotDir, "fixture-manifest.json");
|
||||
const fixtureLogPath = path.join(slotDir, "fixture.log");
|
||||
@@ -725,6 +737,7 @@ const startWithLease = async (options: Options) => {
|
||||
AUTH_COOKIE_PREFIX: `homarr_qa_s${options.slot}`,
|
||||
DB_DRIVER: "better-sqlite3",
|
||||
DB_URL: dbPath,
|
||||
LOCAL_CERTIFICATE_PATH: trustedCertificatePath,
|
||||
DEMO_MODE: String(flags.demoMode),
|
||||
DEMO_READ_ONLY: String(flags.demoReadOnly),
|
||||
UNSAFE_ENABLE_MOCK_INTEGRATION: String(flags.unsafeMockIntegration),
|
||||
@@ -807,6 +820,7 @@ const startWithLease = async (options: Options) => {
|
||||
runRoot: options.runRoot,
|
||||
slotDir,
|
||||
dbPath,
|
||||
trustedCertificatePath,
|
||||
fixtureManifestPath,
|
||||
nextDistDir: candidateBuild.buildDir,
|
||||
url,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { mkdtemp, mkdir, rm, stat } from "node:fs/promises";
|
||||
import { createServer } from "node:net";
|
||||
import type { Server } from "node:net";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
@@ -9,12 +12,14 @@ import {
|
||||
createAppLaunchCommand,
|
||||
createCandidateBuildPaths,
|
||||
createRuntimeExecutionContract,
|
||||
createSlotTrustedCertificateDirectory,
|
||||
launchWithPortRetry,
|
||||
reserveLoopbackPort,
|
||||
validateRuntimeExecutionContract,
|
||||
} from "./runtime.mts";
|
||||
|
||||
const openServers: Server[] = [];
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
const listen = async (server: Server, port: number) =>
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -36,6 +41,31 @@ afterEach(async () => {
|
||||
if (server.listening) await close(server);
|
||||
}),
|
||||
);
|
||||
await Promise.all(temporaryDirectories.splice(0).map(async (directory) => await rm(directory, { recursive: true })));
|
||||
});
|
||||
|
||||
describe("release-v2 QA slot storage", () => {
|
||||
test("creates a restrictive slot-local trusted certificate directory", async () => {
|
||||
const runRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-spec-"));
|
||||
temporaryDirectories.push(runRoot);
|
||||
const slotDir = path.join(runRoot, "slots", "1");
|
||||
await mkdir(slotDir, { recursive: true, mode: 0o700 });
|
||||
|
||||
const trustedCertificatePath = await createSlotTrustedCertificateDirectory(runRoot, slotDir);
|
||||
const directoryStats = await stat(trustedCertificatePath);
|
||||
|
||||
expect(trustedCertificatePath).toBe(path.join(slotDir, "trusted-certificates"));
|
||||
expect(directoryStats.isDirectory()).toBe(true);
|
||||
expect(directoryStats.mode & 0o777).toBe(0o700);
|
||||
});
|
||||
|
||||
test("refuses a trusted certificate directory outside the approved run root", async () => {
|
||||
const runRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-spec-"));
|
||||
const outsideRoot = await mkdtemp(path.join(tmpdir(), "homarr-release-v2-qa-runtime-outside-"));
|
||||
temporaryDirectories.push(runRoot, outsideRoot);
|
||||
|
||||
await expect(createSlotTrustedCertificateDirectory(runRoot, outsideRoot)).rejects.toThrow(/escapes/u);
|
||||
});
|
||||
});
|
||||
|
||||
describe("release-v2 QA runtime port startup", () => {
|
||||
|
||||
@@ -112,6 +112,7 @@ interface RuntimeManifest {
|
||||
runRoot: string;
|
||||
slotDir: string;
|
||||
dbPath: string;
|
||||
trustedCertificatePath: string;
|
||||
fixtureManifestPath: string;
|
||||
nextDistDir: string;
|
||||
url: string;
|
||||
@@ -646,6 +647,7 @@ const validateRuntimeManifest = async (
|
||||
const expectedSlotDir = join(runtime.runRoot, "slots", String(runtime.slot));
|
||||
const expectedManifestPath = join(expectedSlotDir, "runtime-manifest.json");
|
||||
const expectedDbPath = join(expectedSlotDir, "db.sqlite");
|
||||
const expectedTrustedCertificatePath = join(expectedSlotDir, "trusted-certificates");
|
||||
const expectedFixtureManifestPath = join(expectedSlotDir, "fixture-manifest.json");
|
||||
const expectedBuild = createCandidateBuildPaths(campaignCandidateSha);
|
||||
if (resolve(runtime.slotDir) !== resolve(expectedSlotDir))
|
||||
@@ -653,6 +655,9 @@ const validateRuntimeManifest = async (
|
||||
if (resolve(manifestPath) !== resolve(expectedManifestPath))
|
||||
errors.push(`${label}: runtime manifest is outside the selected slot`);
|
||||
if (resolve(runtime.dbPath) !== resolve(expectedDbPath)) errors.push(`${label}: dbPath is outside the selected slot`);
|
||||
if (resolve(runtime.trustedCertificatePath ?? "") !== resolve(expectedTrustedCertificatePath)) {
|
||||
errors.push(`${label}: trusted certificate path is outside the selected slot`);
|
||||
}
|
||||
if (resolve(runtime.fixtureManifestPath) !== resolve(expectedFixtureManifestPath)) {
|
||||
errors.push(`${label}: fixture manifest path is outside the selected slot`);
|
||||
}
|
||||
@@ -675,8 +680,11 @@ const validateRuntimeManifest = async (
|
||||
errors.push(`${label}: runRoot must not be a symlink`);
|
||||
if ((await realpath(runtime.slotDir)) !== resolve(runtime.slotDir))
|
||||
errors.push(`${label}: slotDir must not be a symlink`);
|
||||
if ((await realpath(runtime.trustedCertificatePath)) !== resolve(runtime.trustedCertificatePath)) {
|
||||
errors.push(`${label}: trusted certificate path must exist inside the selected slot and must not be a symlink`);
|
||||
}
|
||||
} catch {
|
||||
errors.push(`${label}: runRoot or slotDir is missing`);
|
||||
errors.push(`${label}: runRoot, slotDir, or trusted certificate path is missing`);
|
||||
}
|
||||
|
||||
let appUrl: URL | undefined;
|
||||
|
||||
Reference in new issue
Block a user