Public: anyone can read this; the crew can change it.

Test: an outside pull's check runs in a cell and cannot reach the forge's data (#1789 follow-up) #2127

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2080-test-an-outside-pull-s-check-runs-in-a-c → main 1 commit fork

  • Merged. scout/2080-test-an-outside-pull-s-check-runs-in-a-c was merged into main as e089833.

Review

Sign in to review this change.
1 file changed +128 −3
test/contain.test.ts +128 −3
@@ -11,14 +11,14 @@
1111 */
1212
1313 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
14 import { existsSync } from "node:fs";
14 import { existsSync, mkdirSync, writeFileSync } from "node:fs";
1515 import { mkdtemp, rm } from "node:fs/promises";
1616 import { tmpdir } from "node:os";
1717 import { join } from "node:path";
1818 import { createApp, type App } from "../src/app.ts";
1919 import { enableApp } from "../src/actions/apps.ts";
20 import { openPull } from "../src/actions/pulls.ts";
21 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
20 import { openPull, submitReview } from "../src/actions/pulls.ts";
21 import { commitToBranch, createRepo, forkRepo } from "../src/actions/repos.ts";
2222 import {
2323 cellCounters,
2424 cellsAvailable,
@@ -1007,3 +1007,128 @@ describe.skipIf(noCells)(
10071007 }, 60_000);
10081008 },
10091009 );
1010
1011 describe.skipIf(noCells)(
1012 "cells: an outside pull's check cannot reach the forge's data (#2080)",
1013 () => {
1014 const CANARY = `forge-canary-${crypto.randomUUID()}`;
1015 let dataDir: string;
1016 let app: App;
1017 let servers: Servers;
1018 let checks: ChecksHandle;
1019 let listener: ReturnType<typeof Bun.serve>;
1020
1021 beforeAll(async () => {
1022 process.env["FORGE_TEST_CANARY"] = CANARY;
1023 dataDir = await mkdtemp(join(tmpdir(), "forge-cells-outside-"));
1024 app = createApp({ dataDir, port: 0 });
1025 servers = await serve(app);
1026 checks = startChecks(app, { forgeUrl: servers.url, concurrency: 1 });
1027 listener = Bun.serve({
1028 port: 0,
1029 hostname: "127.0.0.1",
1030 fetch: () => new Response("inside"),
1031 });
1032 }, 60_000);
1033
1034 afterAll(async () => {
1035 delete process.env["FORGE_TEST_CANARY"];
1036 await checks?.stop();
1037 listener?.stop(true);
1038 servers?.stop();
1039 app?.close();
1040 await rm(dataDir, { recursive: true, force: true });
1041 });
1042
1043 test("a fork's code, run by the base's check once approved, is refused everything but the forge", async () => {
1044 const user = async (username: string, first = false): Promise<User> => {
1045 const made = await createUser(app.db, {
1046 trusted: true,
1047 username,
1048 email: `${username}@forge.local`,
1049 password: "a-good-passphrase",
1050 adminIfFirst: first,
1051 });
1052 if (!made.ok) throw new Error(made.error);
1053 return made.value;
1054 };
1055 const alice = await user("alice", true);
1056 const mallory = await user("mallory");
1057 const made = await createRepo(app, alice, {
1058 name: "lib",
1059 visibility: "public",
1060 });
1061 if (!made.ok) throw new Error(String(made.error));
1062 const repo = made.value;
1063
1064 // Something of another tenant's to steal, where an app's data lives.
1065 const stolen = envDir(dataDir, "someone-elses-app", "production");
1066 mkdirSync(join(stolen, "data"), { recursive: true });
1067 writeFileSync(join(stolen, "data", "books.txt"), "private");
1068
1069 const source = probeSource({
1070 dataDir,
1071 otherAppDir: stolen,
1072 canary: CANARY,
1073 listenerPort: listener.port ?? 0,
1074 forgePid: process.pid,
1075 });
1076 // The base declares a harmless check; the fork's tree supplies the code.
1077 const gate = await commitToBranch(app, alice, repo, {
1078 message: "The base's check",
1079 files: [
1080 {
1081 path: "forge.json",
1082 content: JSON.stringify({
1083 egress: ["localhost"],
1084 check: { run: "bun probe.ts check", timeout: 120 },
1085 }),
1086 },
1087 { path: "probe.ts", content: "console.log('base');\n" },
1088 ],
1089 });
1090 if (!gate.ok) throw new Error(JSON.stringify(gate.error));
1091
1092 const forked = await forkRepo(app, mallory, repo);
1093 if (!forked.ok) throw new Error(String(forked.error));
1094 const committed = await commitToBranch(app, mallory, forked.value, {
1095 branch: "from-fork",
1096 message: "From the fork",
1097 files: [{ path: "probe.ts", content: source }],
1098 });
1099 if (!committed.ok) throw new Error(JSON.stringify(committed.error));
1100 const opened = await openPull(app, mallory, repo, {
1101 title: "From the fork",
1102 baseBranch: "main",
1103 headBranch: "from-fork",
1104 headRepo: forked.value,
1105 });
1106 if (!opened.ok) throw new Error(String(opened.error));
1107 expect(opened.value.crossRepo).toBe(true);
1108 const approved = await submitReview(app, alice, repo, opened.value, {
1109 state: "approved",
1110 });
1111 if (!approved.ok) throw new Error(String(approved.error));
1112
1113 const deadline = Date.now() + 90_000;
1114 let run: Run | undefined;
1115 while (Date.now() < deadline) {
1116 run = listRuns(app.db, repo.id, { kind: "check", limit: 50 }).find(
1117 (r) =>
1118 r.issueId === opened.value.id &&
1119 r.commitOid === committed.value.commitOid,
1120 );
1121 if (run && (run.state === "ok" || run.state === "failed")) break;
1122 await Bun.sleep(100);
1123 }
1124 if (!run) throw new Error("the fork's check did not run");
1125 expect(run.state).toBe("ok");
1126 expect(run.sandboxEnforced).toBe(true);
1127 const line = readRunLog(dataDir, run.id).lines.find((l) =>
1128 l.startsWith("PROBE "),
1129 );
1130 expect(line).toBeDefined();
1131 expectContained(JSON.parse(line!.slice(6)) as Record<string, string>);
1132 }, 240_000);
1133 },
1134 );

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs