Skip to content
File

Blob: test/auth.worker.test.ts

typescript168 lines
1import { it, expect } from "vitest";
2import { env, exports as workerExports } from "cloudflare:workers";
3import { pktLine, flushPkt, concatChunks } from "@/worker/git";
4import { asBufferSource, deflate } from "@/worker/common";
5import { setupRepoForTests } from "./util/repoSeed";
6 
7function encodeObjHeader(type: number, size: number): Uint8Array {
8 let first = (type << 4) | (size & 0x0f);
9 size >>= 4;
10 const bytes: number[] = [];
11 if (size > 0) first |= 0x80;
12 bytes.push(first);
13 while (size > 0) {
14 let b = size & 0x7f;
15 size >>= 7;
16 if (size > 0) b |= 0x80;
17 bytes.push(b);
18 }
19 return new Uint8Array(bytes);
20}
21 
22async function buildPack(
23 objects: { type: "commit" | "tree" | "blob" | "tag"; payload: Uint8Array }[]
24): Promise<Uint8Array> {
25 const hdr = new Uint8Array(12);
26 hdr.set(new TextEncoder().encode("PACK"), 0);
27 const dv = new DataView(hdr.buffer);
28 dv.setUint32(4, 2);
29 dv.setUint32(8, objects.length);
30 const parts: Uint8Array[] = [hdr];
31 for (const o of objects) {
32 const typeCode = o.type === "commit" ? 1 : o.type === "tree" ? 2 : o.type === "blob" ? 3 : 4;
33 parts.push(encodeObjHeader(typeCode, o.payload.byteLength));
34 parts.push(await deflate(o.payload));
35 }
36 const body = concatChunks(parts);
37 const sha = new Uint8Array(await crypto.subtle.digest("SHA-1", asBufferSource(body)));
38 const out = new Uint8Array(body.byteLength + 20);
39 out.set(body, 0);
40 out.set(sha, body.byteLength);
41 return out;
42}
43 
44function zero40() {
45 return "0".repeat(40);
46}
47 
48function basicAuth(user: string, pass: string) {
49 const pair = `${user}:${pass}`;
50 const b64 = btoa(pair);
51 return `Basic ${b64}`;
52}
53 
54it("auth: owner token management API is absent", async () => {
55 const res = await workerExports.default.fetch("https://example.com/auth/api/users", {
56 headers: {
57 Authorization: "Bearer admin",
58 },
59 } as any);
60 expect(res.status).toBe(404);
61});
62 
63it("auth: receive-pack requires a push PAT and rejects non-PAT Basic credentials", async () => {
64 const owner = "alice";
65 const repo = "auth-repo";
66 const token = "alicesecret";
67 await setupRepoForTests(env, owner, repo);
68 
69 const treePayload = new Uint8Array(0);
70 const treeHeader = new TextEncoder().encode(`tree ${treePayload.byteLength}\0`);
71 const treeRaw = new Uint8Array(treeHeader.length + treePayload.length);
72 treeRaw.set(treeHeader, 0);
73 treeRaw.set(treePayload, treeHeader.length);
74 const treeOid = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-1", treeRaw)))
75 .map((b) => b.toString(16).padStart(2, "0"))
76 .join("");
77 const author = `You <you@example.com> 0 +0000`;
78 const commitPayload = new TextEncoder().encode(
79 `tree ${treeOid}\n` + `author ${author}\n` + `committer ${author}\n\nmsg\n`
80 );
81 const pack = await buildPack([
82 { type: "tree", payload: treePayload },
83 { type: "commit", payload: commitPayload },
84 ]);
85 const commitOid = await (async () => {
86 const head = new TextEncoder().encode(`commit ${commitPayload.byteLength}\0`);
87 const raw = new Uint8Array(head.length + commitPayload.length);
88 raw.set(head, 0);
89 raw.set(commitPayload, head.length);
90 const hash = await crypto.subtle.digest("SHA-1", raw);
91 return Array.from(new Uint8Array(hash))
92 .map((b) => b.toString(16).padStart(2, "0"))
93 .join("");
94 })();
95 
96 const cmd = `${zero40()} ${commitOid} refs/heads/main\0 report-status\n`;
97 const body = concatChunks([pktLine(cmd), flushPkt(), pack]);
98 const url = `https://example.com/${owner}/${repo}/git-receive-pack`;
99 
100 // No auth โ†’ 401
101 const r1 = await workerExports.default.fetch(url, {
102 method: "POST",
103 headers: { "Content-Type": "application/x-git-receive-pack-request" },
104 body,
105 } as any);
106 expect(r1.status).toBe(401);
107 
108 // Wrong username โ†’ 401
109 const r2 = await workerExports.default.fetch(url, {
110 method: "POST",
111 headers: {
112 "Content-Type": "application/x-git-receive-pack-request",
113 Authorization: basicAuth("bob", token),
114 },
115 body,
116 } as any);
117 expect(r2.status).toBe(401);
118 
119 // Correct username but wrong token โ†’ 401 (malformed PAT shape)
120 const r3 = await workerExports.default.fetch(url, {
121 method: "POST",
122 headers: {
123 "Content-Type": "application/x-git-receive-pack-request",
124 Authorization: basicAuth(owner, "wrong"),
125 },
126 body,
127 } as any);
128 expect(r3.status).toBe(401);
129 
130 // Correct username + non-PAT password -> 401.
131 const r4 = await workerExports.default.fetch(url, {
132 method: "POST",
133 headers: {
134 "Content-Type": "application/x-git-receive-pack-request",
135 Authorization: basicAuth(owner, token),
136 },
137 body,
138 } as any);
139 expect(r4.status).toBe(401);
140});
141 
142it("auth: per-repo admin endpoints reject Basic credentials and require session membership", async () => {
143 const owner = "alice2";
144 const repo = "auth-repo2";
145 const token = "s3cr3t";
146 const seededRepo = await setupRepoForTests(env, owner, repo);
147 
148 const refsUrl = `https://example.com/${owner}/${repo}/admin/refs`;
149 
150 // No auth -> 401
151 const a1 = await workerExports.default.fetch(refsUrl);
152 expect(a1.status).toBe(401);
153 
154 // Git Basic credentials do not authorize repo admin.
155 const a2 = await workerExports.default.fetch(refsUrl, {
156 headers: { Authorization: basicAuth(owner, token) },
157 } as any);
158 expect(a2.status).toBe(401);
159 
160 // Session cookie for a member -> 200.
161 const a3 = await workerExports.default.fetch(refsUrl, {
162 headers: { Cookie: seededRepo.cookieHeader },
163 } as any);
164 expect(a3.status).toBe(200);
165 const refs = await a3.json();
166 expect(Array.isArray(refs)).toBe(true);
167});