pr_01m47d15m3e54sn21z27rpy5n9/apps/sudo/workers/app.ts

68 lines2,592 bytesCodeBlame
1import { RouterContextProvider, createRequestHandler } from "react-router";
2
3import { authorize, isSameOrigin, readSettings } from "../app/lib/access";
4import { denied, secure } from "../app/lib/guard";
5import { staffContext } from "../app/lib/staff";
6
7const requestHandler = createRequestHandler(
8 () => import("virtual:react-router/server-build"),
9 import.meta.env.MODE,
10);
11
12/** Files the build emits for the pages; still behind the same check. */
13const ASSET = /^\/(?:assets\/[\w.-]+|favicon\.svg)$/;
14
15/**
16 * Every request, assets included, passes the same gate before anything
17 * is served:
18 *
19 * 1. sudo is configured, or nothing is served at all;
20 * 2. Cloudflare Access's token verifies (signature, audience, issuer, time);
21 * 3. its email is on the staff list;
22 * 4. a change is a POST from sudo's own pages.
23 */
24async function handle(request: Request, env: Env): Promise<Response> {
25 const settings = readSettings(env);
26 if (!settings) {
27 return denied(
28 403,
29 "sudo is not configured",
30 "ACCESS_TEAM_DOMAIN, ACCESS_AUD and STAFF_EMAILS must all be set before sudo will answer. See apps/sudo/README.md.",
31 );
32 }
33
34 const auth = await authorize(request, settings);
35 if (!auth.ok) {
36 console.warn(JSON.stringify({ event: "sudo.denied", reason: auth.reason, email: auth.email ?? null, path: new URL(request.url).pathname }));
37 return auth.reason === "not staff"
38 ? denied(403, "Not staff", `${auth.email} is signed in, but is not on sudo's staff list.`)
39 : denied(403, "Not allowed", "sudo is for g1t staff, signed in through Cloudflare Access.");
40 }
41
42 const { method } = request;
43 if (method !== "GET" && method !== "HEAD" && method !== "POST") {
44 return denied(405, "Method not allowed", "sudo takes GET and POST only.");
45 }
46 if (method === "POST" && !isSameOrigin(request)) {
47 console.warn(JSON.stringify({ event: "sudo.cross_site", email: auth.email, origin: request.headers.get("origin") }));
48 return denied(403, "Refused", "Changes are only accepted from sudo's own pages.");
49 }
50
51 const { pathname } = new URL(request.url);
52 if (method !== "POST" && ASSET.test(pathname)) {
53 return env.ASSETS.fetch(request);
54 }
55
56 if (method === "POST") {
57 console.log(JSON.stringify({ event: "sudo.change", email: auth.email, path: pathname }));
58 }
59 const context = new RouterContextProvider();
60 context.set(staffContext, { email: auth.email });
61 return requestHandler(request, context);
62}
63
64export default {
65 async fetch(request, env) {
66 return secure(await handle(request, env));
67 },
68} satisfies ExportedHandler<Env>;