swadhinbiswas Enforce path permissions for PR rewrite operations · 2/19/2026
677e3c0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { beforeEach, describe, expect, it, vi } from "vitest";
const {
canWriteRepoMock,
checkPathPermissionsMock,
getRewriteOperationFilesMock,
rewriteBranchHistoryMock,
fakeSchema,
} = vi.hoisted(() => ({
canWriteRepoMock: vi.fn(async () => true),
checkPathPermissionsMock: vi.fn(async () => ({ allowed: true, deniedPaths: [] })),
getRewriteOperationFilesMock: vi.fn(async () => ["src/app.ts"]),
rewriteBranchHistoryMock: vi.fn(async () => undefined),
fakeSchema: {
repositories: { name: {}, ownerId: {}, id: {} },
users: { username: {} },
pullRequests: { repositoryId: {}, number: {} },
} as any,
}));
let mockDb: any;
vi.mock("@/db", () => ({
getDatabase: () => mockDb,
schema: fakeSchema,
}));
vi.mock("@/lib/permissions", () => ({
canWriteRepo: canWriteRepoMock,
}));
vi.mock("@/lib/path-scoping", () => ({
checkPathPermissions: checkPathPermissionsMock,
}));
vi.mock("@/lib/git-rewrite", () => ({
getRewriteOperationFiles: getRewriteOperationFilesMock,
rewriteBranchHistory: rewriteBranchHistoryMock,
}));
import { POST as rewritePost } from "@/pages/api/repos/[owner]/[repo]/pulls/[number]/rewrite";
function makeDb() {
return {
query: {
users: {
findFirst: vi.fn(async () => ({ id: "owner-1" })),
},
repositories: {
findFirst: vi.fn(async () => ({
id: "repo-1",
name: "demo",
ownerId: "owner-1",
owner: { username: "owner-1" },
})),
},
pullRequests: {
findFirst: vi.fn(async () => ({
id: "pr-1",
number: 42,
repositoryId: "repo-1",
baseBranch: "main",
headBranch: "feature",
})),
},
},
};
}
describe("pull rewrite path permissions", () => {
beforeEach(() => {
vi.clearAllMocks();
mockDb = makeDb();
canWriteRepoMock.mockResolvedValue(true);
checkPathPermissionsMock.mockResolvedValue({ allowed: true, deniedPaths: [] });
getRewriteOperationFilesMock.mockResolvedValue(["src/app.ts"]);
});
it("blocks rewrite when touched paths are denied", async () => {
checkPathPermissionsMock.mockResolvedValue({
allowed: false,
deniedPaths: ["src/secret.ts"],
reason: "Insufficient permissions for paths: src/secret.ts",
});
const response = await rewritePost({
params: { owner: "owner-1", repo: "demo", number: "42" },
locals: { user: { id: "user-1" } },
request: new Request("http://localhost/api/repos/owner-1/demo/pulls/42/rewrite", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
operations: [{ type: "pick", hash: "abc123" }],
}),
}),
} as any);
expect(response.status).toBe(403);
expect(rewriteBranchHistoryMock).not.toHaveBeenCalled();
});
it("executes rewrite when touched paths are allowed", async () => {
const response = await rewritePost({
params: { owner: "owner-1", repo: "demo", number: "42" },
locals: { user: { id: "user-1" } },
request: new Request("http://localhost/api/repos/owner-1/demo/pulls/42/rewrite", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
operations: [{ type: "pick", hash: "abc123" }],
}),
}),
} as any);
expect(response.status).toBe(200);
expect(getRewriteOperationFilesMock).toHaveBeenCalled();
expect(checkPathPermissionsMock).toHaveBeenCalledWith("user-1", "repo-1", ["src/app.ts"], "write");
expect(rewriteBranchHistoryMock).toHaveBeenCalled();
});
});