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

An app host's /__forge/ door shows the exact ssh command for the app's terminal to its writers, or says the terminal is off #5969

Merged opened this

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

Merged

scout/forge:scout/5957-an-app-host-s-forge-door-shows-the-exact → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/5957-an-app-host-s-forge-door-shows-the-exact was merged into main as c77475d.

Review

Sign in to review this change.
3 files changed +75 −2
src/apps/door.ts +13 −2
@@ -14,10 +14,15 @@ export type DoorFacts = {
1414 tell?: string;
1515 /** This host's own sign-in door, offered when nobody is signed in. */
1616 signin?: string;
17 /**
18 * The terminal, only for someone who may write the repository: the exact
19 * `ssh …` command, or null when the terminal is off. Unset shows nothing.
20 */
21 ssh?: string | null;
1722 };
1823
1924 const STYLE =
20 "body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}li{margin:.4rem 0}";
25 "body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:3rem auto;padding:0 1rem}li{margin:.4rem 0}pre{overflow-x:auto;padding:.6rem;background:#eee;color:#111;user-select:all}";
2126 const STYLE_HASH = createHash("sha256").update(STYLE).digest("base64");
2227
2328 /** The same words as the pill's accessible name. */
@@ -42,9 +47,15 @@ export function doorPage(facts: DoorFacts, head: boolean): Response {
4247 if (facts.tell !== undefined) links.push(link(facts.tell, "Tell the crew"));
4348 else if (facts.signin !== undefined)
4449 links.push(link(facts.signin, "Sign in to tell the crew"));
50 const terminal =
51 facts.ssh === undefined
52 ? ""
53 : facts.ssh === null
54 ? "<p>The terminal is off.</p>"
55 : `<p>Terminal:</p><pre><code>${escapeAttr(facts.ssh)}</code></pre>`;

Add tabindex="0" to this pre: with overflow-x:auto it is a scroll container, and a keyboard user cannot reach it otherwise.

4556 const html = `<!doctype html>
4657 <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>${escapeAttr(where.app)}, ${escapeAttr(doorWhere(where))}</title><style>${STYLE}</style></head>
47 <body><main><h1>${escapeAttr(where.app)}</h1><p>${escapeAttr(doorWhere(where))}</p><ul>${links.join("")}</ul></main></body></html>
58 <body><main><h1>${escapeAttr(where.app)}</h1><p>${escapeAttr(doorWhere(where))}</p><ul>${links.join("")}</ul>${terminal}</main></body></html>
4859 `;
4960 return new Response(head ? null : html, {
5061 headers: {
src/apps/proxy.ts +23
@@ -26,6 +26,7 @@ import { getAppById, getAppBySlug, type AppRecord } from "../core/apps.ts";
2626 import { getVerifiedDomain } from "../core/domains.ts";
2727 import {
2828 canEnterApp,
29 canWrite,
2930 canRead,
3031 getRepoById,
3132 ownerInStanding,
@@ -414,6 +415,26 @@ export function pillWhere(
414415 return where;
415416 }
416417
418 /**
419 * What the door says of the terminal: for a viewer who may write the
420 * repository, the exact command (the preview's login on a preview), or null
421 * when the terminal is off; for anyone else, nothing.
422 */
423 function doorSsh(
outdated

A never-live app has no status, so this falls through to sshFor and may show a command for an app with no terminal. Prefer the manifest tty declaration here, and test it.

424 runtime: AppsRuntime,
425 target: AppTarget,
426 who: GrantWho | undefined,
427 ): string | null | undefined {
428 if (who === undefined) return undefined;
429 const db = runtime.app.db;
430 const viewer = getUserById(db, who.id);
431 const repo = getRepoById(db, target.app.repoId);
432 if (viewer === undefined || !isActive(viewer) || repo === undefined) return undefined;
433 if (!canWrite(db, repo, viewer)) return undefined;
434 if (runtime.envStatus(target.app, target.env)?.tty === null) return null;
435 return runtime.sshFor(target.app, target.env) ?? null;
436 }
437
417438 /** Whether the person a grant names may enter this app now. */
418439 function stillEntitled(
419440 runtime: AppsRuntime,
@@ -656,6 +677,7 @@ export async function proxyApp(
656677 signed?.who?.id,
657678 ),
658679 );
680 const ssh = doorSsh(runtime, target, signed?.who);
659681 const where = timePhase(request, "pull", () =>
660682 pillWhere(runtime, target, signed?.who),
661683 );
@@ -664,6 +686,7 @@ export async function proxyApp(
664686 {
665687 where,
666688 forgeUrl: runtime.options.forgeUrl,
689 ...(ssh === undefined ? {} : { ssh }),
667690 ...(tell === undefined ? { signin: "/__forge/signin" } : { tell }),
668691 },
669692 request.method === "HEAD",
test/apps-door.test.ts +39
@@ -178,6 +178,45 @@ test("HEAD answers the headers with no body; POST is a 405 that says what is all
178178 expect(post.headers.get("x-content-type-options")).toBe("nosniff");
179179 });
180180
181 test("the terminal: a writer sees the exact command, a reader and a visitor see nothing", async () => {
182 const cfg = app.config as { sshPort?: number | undefined };
183 const was = cfg.sshPort;
184 const b = await createUser(app.db, {
185 origin: "seed",
186 trusted: true,
187 username: "carol",
188 email: "[email protected]",
189 password: "a-good-passphrase",
190 });
191 if (!b.ok) throw new Error(b.error);
192 const cookieFor = (env: string, id: string, name: string, role: "admin" | "user") =>
193 `forge_grant_${record.id.slice(-8)}=${signGrant(dataDir, { app: record, env } as AppTarget, 60_000, Date.now(), { id, name, role })}`;
194 try {
195 cfg.sshPort = 2222;
196 const prod = await (
197 await door(`notes.${DOMAIN}`, cookieFor("production", alice, "alice", "admin"))
198 ).text();
199 expect(prod).toContain("<code>ssh -p 2222 [email protected]</code>");
200 const prev = await (
201 await door(`notes--${pull}.${DOMAIN}`, cookieFor(`pull-${pull}`, alice, "alice", "admin"))
202 ).text();
203 expect(prev).toContain(`<code>ssh -p 2222 notes--${pull}@forge.test</code>`);
204 for (const cookie of [undefined, cookieFor("production", b.value.id, "carol", "user")]) {
205 const html = await (await door(`notes.${DOMAIN}`, cookie)).text();
206 expect(html).not.toContain("ssh");
207 expect(html).not.toContain("terminal is off");
208 }
209 cfg.sshPort = undefined;
210 const off = await (
211 await door(`notes.${DOMAIN}`, cookieFor("production", alice, "alice", "admin"))
212 ).text();
213 expect(off).toContain("The terminal is off.");
214 expect(off).not.toContain("ssh");
215 } finally {
216 cfg.sshPort = was;
217 }
218 });
219
181220 test("every name the door draws is escaped, for production and a preview", () => {
182221 const evil = `<script>alert("x")</script>'`;
183222 const out = (where: PillWhere) =>

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs