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

A test pins removing a secret from Settings: a forge admin only, same-origin only, and the value never echoed #7035

Merged opened this

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

Merged

scout/forge:scout/7019-a-test-pins-removing-a-secret-from-setti → main 1 commit fork

  • Merged. scout/7019-a-test-pins-removing-a-secret-from-setti was merged into main as d4a3b06.

Review

Sign in to review this change.
1 file changed +94 −0
test/settings-secret-delete.test.ts added +94
@@ -0,0 +1,94 @@
1 /** Removing a secret from Settings: forge admin only, same-origin only, value never echoed (#7019). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createSession } from "../src/core/sessions.ts";
8 import { listEvents } from "../src/core/events.ts";
9 import { resolveSecrets, setSecret } from "../src/core/secret_store.ts";
10 import { createUser } from "../src/core/users.ts";
11 import { serve, type Servers } from "../src/http/server.ts";
12 import { adminCookie } from "./helpers/admin.ts";
13
14 const VALUE = "s3cr3t-value-do-not-echo-7019";
15 const NAME = "CLOUDFLARE_API_TOKEN";
16
17 let dataDir = "";
18 let app: App, servers: Servers;
19 let admin = "", member = "";
20
21 beforeAll(async () => {
22 dataDir = await mkdtemp(join(tmpdir(), "forge-secret-del-"));
23 app = createApp({ dataDir, port: 0 });
24 servers = await serve(app);
25 admin = await adminCookie(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
26 const bob = await createUser(app.db, {
27 origin: "seed", trusted: true, username: "bob", email: "[email protected]", password: "correct-horse-battery",
28 });
29 if (!bob.ok) throw new Error(bob.error);
30 member = `forge_session=${createSession(app.db, bob.value.id).token}`;
31 });
32
33 afterAll(async () => {
34 servers.stop();
35 app.close();
36 await rm(dataDir, { recursive: true, force: true });
37 });
38
39 const seal = () => {
40 const set = setSecret(app.db, dataDir, "forge", "", NAME, VALUE);
41 if (!set.ok) throw new Error(set.error);
42 };
43 const present = () => resolveSecrets(app.db, dataDir, app.bus, [NAME])[NAME] === VALUE;
44
45 function post(cookie: string | undefined, origin: string | null) {
46 const headers: Record<string, string> = { "content-type": "application/x-www-form-urlencoded" };
47 if (cookie) headers.cookie = cookie;
48 if (origin !== null) headers.origin = origin;
49 return fetch(`${servers.url}/settings/secrets/delete`, {
50 method: "POST",
51 headers,
52 body: new URLSearchParams({ scope: "forge", ref: "", name: NAME }),
53 redirect: "manual",
54 });
55 }
56
57 async function noValueAnywhere(res: Response) {
58 expect(await res.text()).not.toContain(VALUE);
59 expect([...res.headers.values()].join("\n")).not.toContain(VALUE);
60 expect(decodeURIComponent(JSON.stringify(listEvents(app.db, {})))).not.toContain(VALUE);
61 }
62
63 test("an admin removes a named secret; the flash names it and it resolves no more", async () => {
64 seal();
65 expect(present()).toBe(true);
66 const res = await post(admin, servers.url);
67 expect(res.status).toBe(303);
68 expect(res.headers.get("location")).toBe("/settings/secrets");
69 expect(decodeURIComponent(res.headers.get("set-cookie") ?? "")).toContain(`${NAME} is removed.`);
70 expect(resolveSecrets(app.db, dataDir, app.bus, [NAME])).toEqual({});
71 const gone = listEvents(app.db, { kind: "secret.deleted" });
72 expect(gone.length).toBe(1);
73 expect(JSON.stringify(gone[0]!.payload)).toContain(NAME);
74 await noValueAnywhere(res);
75 });
76
77 test("a non-admin, a signed-out caller and a cross-origin post are refused; the secret stays", async () => {
78 seal();
79 const before = listEvents(app.db, { kind: "secret.deleted" }).length;
80 const refused = [
81 await post(member, servers.url),
82 await post(undefined, servers.url),
83 await post(admin, "https://evil.example"),
84 ];
85 expect(refused[0]!.status).toBe(404);
86 expect(refused[1]!.headers.get("location")).toStartWith("/signin");
87 expect(refused[2]!.status).toBe(403);
88 for (const res of refused) {
89 expect(decodeURIComponent(res.headers.get("set-cookie") ?? "")).not.toContain("is removed");
90 await noValueAnywhere(res);
91 }
92 expect(present()).toBe(true);
93 expect(listEvents(app.db, { kind: "secret.deleted" }).length).toBe(before);
94 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs