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

A repository's egress-grant routes leave api.ts for src/http/api/egress_grants.ts #6877

Merged opened this

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

Merged

mason/forge:mason/6304-a-repository-s-egress-grant-routes-leave → main 1 commit fork 1 thread · 1 unresolved

  • Merged. mason/6304-a-repository-s-egress-grant-routes-leave was merged into main as 74972c2.

Review

Sign in to review this change.
4 files changed +72 −59
src/http/api.ts +3 −55
@@ -303,11 +303,6 @@ import {
303303 listAppDomains,
304304 removeAppDomain,
305305 } from "../actions/domains.ts";
306 import {
307 egressView,
308 grantAppEgress,
309 revokeAppEgress,
310 } from "../actions/egress_grants.ts";
311306 import {
312307 addPromptSource,
313308 listPromptSources,
@@ -457,6 +452,7 @@ import {
457452 import { frontInput, frontJson, setFront } from "../actions/front.ts";
458453 import { COPIES_PER_DAY, makeItYours } from "../actions/make_it_yours.ts";
459454 import { passportRoutes } from "./passport_edge.ts";
455 import { egressGrantRoutes } from "./api/egress_grants.ts";
460456 import type { Router } from "./router.ts";
461457 import { clientAddress, clientBucket } from "./peer.ts";
462458 import { manifestFor } from "../actions/here.ts";
@@ -1188,7 +1184,7 @@ function appLimitResponse(ctx: Ctx, ownerName: string): Response {
11881184 // ---- resolution ----
11891185
11901186 /** The repo a route names, if the viewer may see it. */
1191 function loadRepo(ctx: Ctx): Repo | undefined {
1187 export function loadRepo(ctx: Ctx): Repo | undefined {

Exporting loadRepo makes api.ts and api/egress_grants.ts import each other. That is fine today because it is only called inside handlers. Once a few more families move out, it would be cleaner to move loadRepo into a small http/repo_access.ts that both import. That can be a later pull, not this one.

11921188 const repo = getRepo(ctx.app.db, ctx.params.owner!, ctx.params.name!);
11931189 return repo && canRead(ctx.app.db, repo, ctx.auth.user) ? repo : undefined;
11941190 }
@@ -5875,55 +5871,7 @@ export function apiRoutes(router: Router<Handler>): void {
58755871 },
58765872 );
58775873
5878 // A site admin's grant of one private destination forge.json asks for
5879 // (#3433). Reading takes admin over the repository; changing takes a site
5880 // admin. The destination is the exact `host:port` forge.json names.
5881 router.add("GET", "/api/v1/repos/:owner/:name/egress-grants", async (ctx) => {
5882 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
5883 const denied = requireScope(ctx, "admin");
5884 if (denied) return denied;
5885 const repo = loadRepo(ctx);
5886 if (!repo) return notFound();
5887 const view = await timePhaseAsync(ctx.request, "grants", () =>
5888 egressView(ctx.app, ctx.auth.user, repo),
5889 );
5890 if (!view.ok) return respondAccess(view.error, 422);
5891 return timePhase(ctx.request, "render", () =>
5892 json({ items: view.value.rows, changes: view.value.changes, next_cursor: null }),
5893 );
5894 });
5895
5896 router.add("POST", "/api/v1/repos/:owner/:name/egress-grants", async (ctx) => {
5897 const denied = requireScope(ctx, "admin");
5898 if (denied) return denied;
5899 const repo = loadRepo(ctx);
5900 if (!repo) return notFound();
5901 const input = await body(ctx.request);
5902 const granted = await grantAppEgress(
5903 ctx.app,
5904 ctx.auth.user,
5905 repo,
5906 str(input.destination),
5907 );
5908 if (!granted.ok) return respondAccess(granted.error, 422);
5909 return json(granted.value, 201);
5910 });
5911
5912 router.add("DELETE", "/api/v1/repos/:owner/:name/egress-grants", async (ctx) => {
5913 const denied = requireScope(ctx, "admin");
5914 if (denied) return denied;
5915 const repo = loadRepo(ctx);
5916 if (!repo) return notFound();
5917 const input = await body(ctx.request);
5918 const revoked = revokeAppEgress(
5919 ctx.app,
5920 ctx.auth.user,
5921 repo,
5922 str(input.destination),
5923 );
5924 if (!revoked.ok) return respondAccess(revoked.error, 422);
5925 return json({}, 200);
5926 });
5874 egressGrantRoutes(router);
59275875
59285876 // Where the crew's next tell comes from when nobody is typing
59295877 // (docs/APPS.md, "Prompts from Instagram"). Admin scope and admin over
src/http/api/egress_grants.ts added +64
@@ -0,0 +1,64 @@
1 /**
2 * A repository's egress grants over the API: `GET`, `POST` and `DELETE`
3 * `/api/v1/repos/:owner/:name/egress-grants`. Moved out of `api.ts` (#6304);
4 * the rules live in `actions/egress_grants.ts`.
5 */
6
7 import { egressView, grantAppEgress, revokeAppEgress } from "../../actions/egress_grants.ts";
8 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
9 import { loadRepo } from "../api.ts";
10 import type { Handler } from "../context.ts";
11 import { body, json, notFound, requireScope, respondAccess, str } from "../respond.ts";
12 import type { Router } from "../router.ts";
13
14 export function egressGrantRoutes(router: Router<Handler>): void {
15 // A site admin's grant of one private destination forge.json asks for
16 // (#3433). Reading takes admin over the repository; changing takes a site
17 // admin. The destination is the exact `host:port` forge.json names.
18 router.add("GET", "/api/v1/repos/:owner/:name/egress-grants", async (ctx) => {
19 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
20 const denied = requireScope(ctx, "admin");
21 if (denied) return denied;
22 const repo = loadRepo(ctx);
23 if (!repo) return notFound();
24 const view = await timePhaseAsync(ctx.request, "grants", () =>
25 egressView(ctx.app, ctx.auth.user, repo),
26 );
27 if (!view.ok) return respondAccess(view.error, 422);
28 return timePhase(ctx.request, "render", () =>
29 json({ items: view.value.rows, changes: view.value.changes, next_cursor: null }),
30 );
31 });
32
33 router.add("POST", "/api/v1/repos/:owner/:name/egress-grants", async (ctx) => {
34 const denied = requireScope(ctx, "admin");
35 if (denied) return denied;
36 const repo = loadRepo(ctx);
37 if (!repo) return notFound();
38 const input = await body(ctx.request);
39 const granted = await grantAppEgress(
40 ctx.app,
41 ctx.auth.user,
42 repo,
43 str(input.destination),
44 );
45 if (!granted.ok) return respondAccess(granted.error, 422);
46 return json(granted.value, 201);
47 });
48
49 router.add("DELETE", "/api/v1/repos/:owner/:name/egress-grants", async (ctx) => {
50 const denied = requireScope(ctx, "admin");
51 if (denied) return denied;
52 const repo = loadRepo(ctx);
53 if (!repo) return notFound();
54 const input = await body(ctx.request);
55 const revoked = revokeAppEgress(
56 ctx.app,
57 ctx.auth.user,
58 repo,
59 str(input.destination),
60 );
61 if (!revoked.ok) return respondAccess(revoked.error, 422);
62 return json({}, 200);
63 });
64 }
test/openapi-coverage.test.ts +2 −2
@@ -13,9 +13,9 @@ const NOT_YET_DESCRIBED: readonly string[] = [
1313
1414 const norm = (path: string) => path.replace(/[:*]\w+/g, "{}").replace(/\{\w+\}/g, "{}");
1515
16 /** Every file under src/http that registers routes. */
16 /** Every file under src/http (and its route modules in src/http/api/) that registers routes. */
1717 const files = () =>
18 readdirSync(new URL("../src/http/", import.meta.url))
18 [...readdirSync(new URL("../src/http/", import.meta.url)), ...readdirSync(new URL("../src/http/api/", import.meta.url)).map((f) => `api/${f}`)]
1919 .filter((f) => f.endsWith(".ts"))
2020 .map((f) => ({ f, src: readFileSync(new URL(`../src/http/${f}`, import.meta.url), "utf8") }))
2121 .filter(({ src }) => src.includes("router.add("));
test/openapi.test.ts +3 −2
@@ -1,6 +1,6 @@
11 /** The OpenAPI document names every route the manifest names. */
22 import { expect, test } from "bun:test";
3 import { readFileSync } from "node:fs";
3 import { readdirSync, readFileSync } from "node:fs";
44 import { mkdtemp, rm } from "node:fs/promises";
55 import { tmpdir } from "node:os";
66 import { join } from "node:path";
@@ -30,7 +30,8 @@ test("every http route the manifest names is described", () => {
3030 });
3131
3232 test("every described route is registered in the router", () => {
33 const src = ["src/http/server.ts", "src/http/api.ts", "src/http/mcp.ts", "src/http/passport_edge.ts", "src/http/feed.ts", "src/http/checks_recheck.ts"]
33 const src = ["src/http/server.ts", "src/http/api.ts", "src/http/mcp.ts", "src/http/passport_edge.ts", "src/http/feed.ts", "src/http/checks_recheck.ts",
34 ...readdirSync("src/http/api").map((f) => `src/http/api/${f}`)]
3435 .map((f) => readFileSync(f, "utf8"))
3536 .join("\n")
3637 // A route registered in a loop over issues and pulls is two routes.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs