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

GET /api/v1/admin/capacity, /admin/limits and /admin/quotas carry an ETag and answer a repeat with 304 until a limit or the capacity reading changes #9266

Merged opened this

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

Merged

mason/forge:mason/9248-get-api-v1-admin-capacity-admin-limits-a → main 2 commits fork

  • Merged. mason/9248-get-api-v1-admin-capacity-admin-limits-a was merged into main as e2def76.

Review

Sign in to review this change.
2 files changed +127 −4
src/http/api/admin.ts +5 −4
@@ -40,7 +40,8 @@ export const accountAdmin = (ctx: Ctx, scope: "ro" | "admin"): Response | undefi
4040 ctx.auth.user?.isAdmin === true ? requireScope(ctx, scope) : notFound();
4141
4242 /**
43 * Who can reach the forge, polled often and changed rarely (#9224): the tag
43 * Who can reach the forge, and its capacity, limits and quotas (#9248):
44 * polled often and changed rarely (#9224). The tag
4445 * is of the exact bytes sent, so a person, invite or page change moves it and
4546 * two viewers shown different bodies never share one. Called only past the
4647 * access and query checks, which carry no tag.
@@ -142,7 +143,7 @@ export function adminRoutes(router: Router<Handler>): void {
142143 const denied = accountAdmin(ctx, "ro");
143144 if (denied) return denied;
144145 const listed = timePhase(ctx.request, "capacity", () => listCapacity(ctx.app, ctx.auth.user));
145 return timePhase(ctx.request, "render", () => (listed.ok ? json({ items: listed.value.settings, changes: listed.value.changes, next_cursor: null }) : notFound()));
146 return timePhase(ctx.request, "render", () => (listed.ok ? taggedJson(ctx, { items: listed.value.settings, changes: listed.value.changes, next_cursor: null }) : notFound()));
146147 });
147148
148149 router.add("PUT", "/api/v1/admin/capacity/:key", async (ctx) => {
@@ -168,7 +169,7 @@ export function adminRoutes(router: Router<Handler>): void {
168169 const denied = accountAdmin(ctx, "ro");
169170 if (denied) return denied;
170171 const listed = timePhase(ctx.request, "limits", () => listLimits(ctx.app, ctx.auth.user));
171 return timePhase(ctx.request, "render", () => (listed.ok ? json({ items: listed.value.limits, changes: listed.value.changes, next_cursor: null }) : notFound()));
172 return timePhase(ctx.request, "render", () => (listed.ok ? taggedJson(ctx, { items: listed.value.limits, changes: listed.value.changes, next_cursor: null }) : notFound()));
172173 });
173174
174175 router.add("PUT", "/api/v1/admin/limits/:key", async (ctx) => {
@@ -199,7 +200,7 @@ export function adminRoutes(router: Router<Handler>): void {
199200 const denied = accountAdmin(ctx, "ro");
200201 if (denied) return denied;
201202 const read = readQuotas(ctx.app, ctx.auth.user);
202 return read.ok ? json({ quotas: read.value.quotas, changes: read.value.changes }) : notFound();
203 return read.ok ? taggedJson(ctx, { quotas: read.value.quotas, changes: read.value.changes }) : notFound();
203204 });
204205
205206 router.add("PUT", "/api/v1/admin/quotas/:key", async (ctx) => {
test/admin-limits-etag.test.ts added +122
@@ -0,0 +1,122 @@
1 /**
2 * GET /api/v1/admin/capacity, /admin/limits and /admin/quotas carry a weak
3 * ETag and answer a repeat with 304 until a limit, quota or capacity reading
4 * changes (#9248).
5 */
6 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { mkdtemp, rm } from "node:fs/promises";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
10 import { createApp, type App } from "../src/app.ts";
11 import { createToken } from "../src/core/tokens.ts";
12 import { createUser } from "../src/core/users.ts";
13 import { weakEtag } from "../src/http/etag.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15
16 let dataDir = "";
17 let app: App, servers: Servers;
18 let admin = "", adminRo = "", admin2 = "", member = "", agentAdmin = "";
19
20 const ROUTES = [
21 { path: "/admin/capacity", keys: ["items", "changes", "next_cursor"], set: ["/admin/capacity/checks.concurrency", "9"] },
22 { path: "/admin/limits", keys: ["items", "changes", "next_cursor"], set: ["/admin/limits/share.daily_max", "9"] },
23 { path: "/admin/quotas", keys: ["quotas", "changes"], set: ["/admin/quotas/previews", 4] },
24 ] as const;
25
26 const get = (path: string, token?: string, headers: Record<string, string> = {}) =>
27 fetch(`${servers.url}/api/v1${path}`, {
28 headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), ...headers },
29 });
30 const put = (path: string, value: unknown) =>
31 fetch(`${servers.url}/api/v1${path}`, {
32 method: "PUT",
33 headers: { authorization: `Bearer ${admin}`, "content-type": "application/json" },
34 body: JSON.stringify({ value }),
35 });
36
37 beforeAll(async () => {
38 dataDir = await mkdtemp(join(tmpdir(), "forge-admin-limits-etag-"));
39 app = createApp({ dataDir, port: 0, url: "https://forge.example" });
40 servers = await serve(app);
41 const mk = async (username: string, extra: Record<string, unknown> = {}) => {
42 const u = await createUser(app.db, {
43 origin: "seed", trusted: true, username,
44 email: `${username}@forge.local`, password: "a-good-passphrase", ...extra,
45 });
46 if (!u.ok) throw new Error(u.error);
47 return u.value;
48 };
49 const alice = await mk("alice", { adminIfFirst: true });
50 const carol = await mk("carol", { isAdmin: true });
51 const bob = await mk("bob");
52 const scribe = await mk("scribe", { agent: true, isAdmin: true });
53 admin = createToken(app.db, alice.id, "a", "admin").token;
54 adminRo = createToken(app.db, alice.id, "r", "ro").token;
55 admin2 = createToken(app.db, carol.id, "c", "admin").token;
56 member = createToken(app.db, bob.id, "b", "admin").token;
57 agentAdmin = createToken(app.db, scribe.id, "s", "admin").token;
58 }, 30_000);
59 afterAll(async () => {
60 await servers.stop();
61 app.close();
62 await rm(dataDir, { recursive: true, force: true });
63 });
64
65 for (const route of ROUTES) {
66 test(`${route.path}: 200 carries a weak tag over the body; a repeat is a bodiless 304 with the floor`, async () => {
67 const res = await get(route.path, adminRo);
68 expect(res.status).toBe(200);
69 expect(res.headers.get("content-type")).toBe("application/json");
70 const text = await res.text();
71 expect(Object.keys(JSON.parse(text))).toEqual([...route.keys]);
72 const tag = res.headers.get("etag")!;
73 expect(tag).toBe(weakEtag(text));
74 expect(res.headers.get("cache-control")).toBe("private, no-cache");
75 expect(res.headers.get("vary")).toContain("authorization, cookie");
76 const again = await get(route.path, adminRo, { "if-none-match": tag });
77 expect(again.status).toBe(304);
78 expect(again.headers.get("etag")).toBe(tag);
79 expect(again.headers.get("cache-control")).toBe("private, no-cache");
80 expect(again.headers.get("vary")).toContain("authorization, cookie");
81 expect(again.headers.get("x-content-type-options")).toBe("nosniff");
82 expect(again.headers.get("content-security-policy")).not.toBeNull();
83 expect(again.headers.get("referrer-policy")).not.toBeNull();
84 expect(await again.text()).toBe("");
85 });
86
87 test(`${route.path}: a change moves the tag and a stale tag gets the new body`, async () => {
88 const first = await get(route.path, admin);
89 const before = first.headers.get("etag")!;
90 const old = await first.text();
91 const [path, value] = route.set;
92 expect((await put(path, value)).status).toBe(200);
93 const res = await get(route.path, admin, { "if-none-match": before });
94 expect(res.status).toBe(200);
95 const tag = res.headers.get("etag")!;
96 expect(tag).not.toBe(before);
97 expect(await res.text()).not.toBe(old);
98 expect((await put(path, null)).status).toBe(200);
99 const third = await get(route.path, admin, { "if-none-match": tag });
100 expect(third.status).toBe(200);
101 expect(third.headers.get("etag")).not.toBe(tag);
102 });
103
104 test(`${route.path}: another admin shown the same bytes shares the tag; the tag is of the body, not the viewer`, async () => {
105 const a = await get(route.path, admin);
106 const b = await get(route.path, admin2, { "if-none-match": a.headers.get("etag")! });
107 expect(b.status).toBe(304);
108 expect(b.headers.get("etag")).toBe(weakEtag(await a.text()));
109 });
110
111 test(`${route.path}: no session, a member and an agent admin answer as before with no ETag, even holding *`, async () => {
112 for (const t of [undefined, member, agentAdmin]) {
113 const plain = await get(route.path, t);
114 const held = await get(route.path, t, { "if-none-match": "*" });
115 expect(plain.status).toBeGreaterThanOrEqual(400);
116 expect(held.status).toBe(plain.status);
117 expect(await held.text()).toBe(await plain.text());
118 expect(held.headers.get("etag")).toBeNull();
119 expect(plain.headers.get("etag")).toBeNull();
120 }
121 });
122 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs