Skip to content
File

Blob: tests/worker/admin-allowlist.test.ts

typescript83 lines
1import { env, exports } from "cloudflare:workers";
2import { beforeAll, describe, expect, it } from "vitest";
3 
4const SELF = exports.default;
5 
6import { makeAuth } from "@/worker/auth";
7 
8import { ISSUER, signInForCookie, testHeaders } from "./helpers";
9 
10// tessera owns the user-management surface and does not expose the full
11// Better Auth admin plugin. The allowlist middleware in
12// `src/worker/middleware/admin-allowlist.ts` only lets list-users,
13// set-role, and unban-user reach Better Auth; ban-user is a tessera
14// handler. Everything else under /api/auth/admin/* must 404 — this
15// preserves the invite-only invariant (no create-user) and forbids
16// destructive actions tessera's UI never surfaces.
17describe("admin route allowlist", () => {
18 const adminCred = {
19 email: "allowlist-admin@example.com",
20 password: "correct-horse-battery-staple",
21 name: "Allowlist Admin",
22 };
23 let cookie: string;
24 
25 beforeAll(async () => {
26 const auth = makeAuth(env, { baseURL: ISSUER, issuer: ISSUER });
27 await auth.api.signUpEmail({ body: adminCred, asResponse: false });
28 await env.DB.prepare("UPDATE users SET role = ? WHERE email = ?").bind("admin", adminCred.email).run();
29 cookie = await signInForCookie(adminCred.email, adminCred.password, "10.40.0.1");
30 });
31 
32 const blockedRoutes: Array<[string, string, Record<string, unknown>?]> = [
33 ["POST", "/api/auth/admin/create-user", { email: "x@example.com", password: "a-twelve-char", name: "X" }],
34 ["POST", "/api/auth/admin/set-user-password", { userId: "anything", newPassword: "another-twelve" }],
35 ["POST", "/api/auth/admin/impersonate-user", { userId: "anything" }],
36 ["POST", "/api/auth/admin/stop-impersonating", {}],
37 ["POST", "/api/auth/admin/remove-user", { userId: "anything" }],
38 ["POST", "/api/auth/admin/revoke-user-session", { sessionToken: "x" }],
39 ["POST", "/api/auth/admin/revoke-user-sessions", { userId: "anything" }],
40 ];
41 
42 for (const [method, path, body] of blockedRoutes) {
43 it(`blocks ${method} ${path} with 404`, async () => {
44 const res = await SELF.fetch(`${ISSUER}${path}`, {
45 method,
46 headers: testHeaders({ cookie }),
47 body: body ? JSON.stringify(body) : undefined,
48 });
49 expect(res.status).toBe(404);
50 });
51 }
52 
53 it("allows GET /api/auth/admin/list-users (passthrough to Better Auth)", async () => {
54 const res = await SELF.fetch(`${ISSUER}/api/auth/admin/list-users?limit=5`, { headers: { cookie } });
55 expect(res.status).toBe(200);
56 const body = (await res.json()) as { users: unknown[] };
57 expect(Array.isArray(body.users)).toBe(true);
58 });
59 
60 it("allows POST /api/auth/admin/set-role on another user (passthrough)", async () => {
61 const targetCred = {
62 email: "allowlist-target@example.com",
63 password: "correct-horse-battery-staple",
64 name: "Target",
65 };
66 const auth = makeAuth(env, { baseURL: ISSUER, issuer: ISSUER });
67 await auth.api.signUpEmail({ body: targetCred, asResponse: false });
68 const targetId = (
69 await env.DB.prepare("SELECT id FROM users WHERE email = ?").bind(targetCred.email).first<{ id: string }>()
70 )?.id;
71 expect(targetId).toBeTruthy();
72 
73 // `origin` is required by Better Auth's CSRF gate for state-changing
74 // auth endpoints.
75 const res = await SELF.fetch(`${ISSUER}/api/auth/admin/set-role`, {
76 method: "POST",
77 headers: testHeaders({ cookie }),
78 body: JSON.stringify({ userId: targetId, role: "admin" }),
79 });
80 expect(res.status).toBe(200);
81 });
82});