Skip to content

Commit c7b3e2c

Browse files
authored
impr: move permission checks to contracts (@fehmer, @Miodec) (#5848)
!nuf
1 parent 1427753 commit c7b3e2c

File tree

20 files changed

+611
-150
lines changed

20 files changed

+611
-150
lines changed
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import { Response } from "express";
2+
import { verifyPermissions } from "../../src/middlewares/permission";
3+
import { EndpointMetadata } from "@monkeytype/contracts/schemas/api";
4+
import * as Misc from "../../src/utils/misc";
5+
import * as AdminUids from "../../src/dal/admin-uids";
6+
import * as UserDal from "../../src/dal/user";
7+
import MonkeyError from "../../src/utils/error";
8+
9+
const uid = "123456789";
10+
11+
describe("permission middleware", () => {
12+
const handler = verifyPermissions();
13+
const res: Response = {} as any;
14+
const next = vi.fn();
15+
const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
16+
const isAdminMock = vi.spyOn(AdminUids, "isAdmin");
17+
const isDevMock = vi.spyOn(Misc, "isDevEnvironment");
18+
19+
beforeEach(() => {
20+
next.mockReset();
21+
getPartialUserMock.mockReset().mockResolvedValue({} as any);
22+
isDevMock.mockReset().mockReturnValue(false);
23+
isAdminMock.mockReset().mockResolvedValue(false);
24+
});
25+
afterEach(() => {
26+
//next function must only be called once
27+
expect(next).toHaveBeenCalledOnce();
28+
});
29+
30+
it("should bypass without requiredPermission", async () => {
31+
//GIVEN
32+
const req = givenRequest({});
33+
//WHEN
34+
await handler(req, res, next);
35+
36+
//THEN
37+
expect(next).toHaveBeenCalledWith();
38+
});
39+
it("should bypass with empty requiredPermission", async () => {
40+
//GIVEN
41+
const req = givenRequest({ requirePermission: [] });
42+
//WHEN
43+
await handler(req, res, next);
44+
45+
//THE
46+
expect(next).toHaveBeenCalledWith();
47+
});
48+
49+
describe("admin check", () => {
50+
const requireAdminPermission: EndpointMetadata = {
51+
requirePermission: "admin",
52+
};
53+
54+
it("should fail without authentication", async () => {
55+
//GIVEN
56+
const req = givenRequest(requireAdminPermission);
57+
//WHEN
58+
await handler(req, res, next);
59+
60+
//THEN
61+
expect(next).toHaveBeenCalledWith(
62+
new MonkeyError(403, "You don't have permission to do this.")
63+
);
64+
});
65+
it("should pass without authentication if publicOnDev on dev", async () => {
66+
//GIVEN
67+
isDevMock.mockReturnValue(true);
68+
const req = givenRequest(
69+
{
70+
...requireAdminPermission,
71+
authenticationOptions: { isPublicOnDev: true },
72+
},
73+
{ uid }
74+
);
75+
//WHEN
76+
await handler(req, res, next);
77+
78+
//THEN
79+
expect(next).toHaveBeenCalledWith();
80+
});
81+
it("should fail without authentication if publicOnDev on prod ", async () => {
82+
//GIVEN
83+
const req = givenRequest(
84+
{
85+
...requireAdminPermission,
86+
authenticationOptions: { isPublicOnDev: true },
87+
},
88+
{ uid }
89+
);
90+
//WHEN
91+
await handler(req, res, next);
92+
93+
//THEN
94+
expect(next).toHaveBeenCalledWith(
95+
new MonkeyError(403, "You don't have permission to do this.")
96+
);
97+
});
98+
it("should fail without admin permissions", async () => {
99+
//GIVEN
100+
const req = givenRequest(requireAdminPermission, { uid });
101+
102+
//WHEN
103+
await handler(req, res, next);
104+
105+
//THEN
106+
expect(next).toHaveBeenCalledWith(
107+
new MonkeyError(403, "You don't have permission to do this.")
108+
);
109+
expect(isAdminMock).toHaveBeenCalledWith(uid);
110+
});
111+
});
112+
describe("user checks", () => {
113+
it("should fetch user only once", async () => {
114+
//GIVEN
115+
const req = givenRequest(
116+
{
117+
requirePermission: ["canReport", "canManageApeKeys"],
118+
},
119+
{ uid }
120+
);
121+
122+
//WHEN
123+
await handler(req, res, next);
124+
125+
//THEN
126+
expect(getPartialUserMock).toHaveBeenCalledOnce();
127+
expect(getPartialUserMock).toHaveBeenCalledWith(
128+
uid,
129+
"check user permissions",
130+
["canReport", "canManageApeKeys"]
131+
);
132+
});
133+
it("should fail if authentication is missing", async () => {
134+
//GIVEN
135+
const req = givenRequest({
136+
requirePermission: ["canReport", "canManageApeKeys"],
137+
});
138+
139+
//WHEN
140+
await handler(req, res, next);
141+
142+
//THEN
143+
expect(next).toHaveBeenCalledWith(
144+
new MonkeyError(
145+
403,
146+
"Failed to check permissions, authentication required."
147+
)
148+
);
149+
});
150+
});
151+
describe("quoteMod check", () => {
152+
const requireQuoteMod: EndpointMetadata = {
153+
requirePermission: "quoteMod",
154+
};
155+
156+
it("should pass for quoteAdmin", async () => {
157+
//GIVEN
158+
getPartialUserMock.mockResolvedValue({ quoteMod: true } as any);
159+
const req = givenRequest(requireQuoteMod, { uid });
160+
161+
//WHEN
162+
await handler(req, res, next);
163+
164+
//THEN
165+
expect(next).toHaveBeenCalledWith();
166+
expect(getPartialUserMock).toHaveBeenCalledWith(
167+
uid,
168+
"check user permissions",
169+
["quoteMod"]
170+
);
171+
});
172+
it("should pass for specific language", async () => {
173+
//GIVEN
174+
getPartialUserMock.mockResolvedValue({ quoteMod: "english" } as any);
175+
const req = givenRequest(requireQuoteMod, { uid });
176+
177+
//WHEN
178+
await handler(req, res, next);
179+
180+
//THEN
181+
expect(next).toHaveBeenCalledWith();
182+
expect(getPartialUserMock).toHaveBeenCalledWith(
183+
uid,
184+
"check user permissions",
185+
["quoteMod"]
186+
);
187+
});
188+
it("should fail for empty string", async () => {
189+
//GIVEN
190+
getPartialUserMock.mockResolvedValue({ quoteMod: "" } as any);
191+
const req = givenRequest(requireQuoteMod, { uid });
192+
193+
//WHEN
194+
await handler(req, res, next);
195+
196+
//THEN
197+
expect(next).toHaveBeenCalledWith(
198+
new MonkeyError(403, "You don't have permission to do this.")
199+
);
200+
});
201+
it("should fail for missing quoteMod", async () => {
202+
//GIVEN
203+
getPartialUserMock.mockResolvedValue({} as any);
204+
const req = givenRequest(requireQuoteMod, { uid });
205+
206+
//WHEN
207+
await handler(req, res, next);
208+
209+
//THEN
210+
expect(next).toHaveBeenCalledWith(
211+
new MonkeyError(403, "You don't have permission to do this.")
212+
);
213+
});
214+
});
215+
describe("canReport check", () => {
216+
const requireCanReport: EndpointMetadata = {
217+
requirePermission: "canReport",
218+
};
219+
220+
it("should fail if user cannot report", async () => {
221+
//GIVEN
222+
getPartialUserMock.mockResolvedValue({ canReport: false } as any);
223+
const req = givenRequest(requireCanReport, { uid });
224+
225+
//WHEN
226+
await handler(req, res, next);
227+
228+
//THEN
229+
expect(next).toHaveBeenCalledWith(
230+
new MonkeyError(403, "You don't have permission to do this.")
231+
);
232+
expect(getPartialUserMock).toHaveBeenCalledWith(
233+
uid,
234+
"check user permissions",
235+
["canReport"]
236+
);
237+
});
238+
it("should pass if user can report", async () => {
239+
//GIVEN
240+
getPartialUserMock.mockResolvedValue({ canReport: true } as any);
241+
const req = givenRequest(requireCanReport, { uid });
242+
243+
//WHEN
244+
await handler(req, res, next);
245+
246+
//THEN
247+
expect(next).toHaveBeenCalledWith();
248+
});
249+
it("should pass if canReport is not set", async () => {
250+
//GIVEN
251+
getPartialUserMock.mockResolvedValue({} as any);
252+
const req = givenRequest(requireCanReport, { uid });
253+
254+
//WHEN
255+
await handler(req, res, next);
256+
257+
//THEN
258+
expect(next).toHaveBeenCalledWith();
259+
});
260+
});
261+
describe("canManageApeKeys check", () => {
262+
const requireCanReport: EndpointMetadata = {
263+
requirePermission: "canManageApeKeys",
264+
};
265+
266+
it("should fail if user cannot report", async () => {
267+
//GIVEN
268+
getPartialUserMock.mockResolvedValue({ canManageApeKeys: false } as any);
269+
const req = givenRequest(requireCanReport, { uid });
270+
271+
//WHEN
272+
await handler(req, res, next);
273+
274+
//THEN
275+
expect(next).toHaveBeenCalledWith(
276+
new MonkeyError(
277+
403,
278+
"You have lost access to ape keys, please contact support"
279+
)
280+
);
281+
expect(getPartialUserMock).toHaveBeenCalledWith(
282+
uid,
283+
"check user permissions",
284+
["canManageApeKeys"]
285+
);
286+
});
287+
it("should pass if user can report", async () => {
288+
//GIVEN
289+
getPartialUserMock.mockResolvedValue({ canManageApeKeys: true } as any);
290+
const req = givenRequest(requireCanReport, { uid });
291+
292+
//WHEN
293+
await handler(req, res, next);
294+
295+
//THEN
296+
expect(next).toHaveBeenCalledWith();
297+
});
298+
it("should pass if canManageApeKeys is not set", async () => {
299+
//GIVEN
300+
getPartialUserMock.mockResolvedValue({} as any);
301+
const req = givenRequest(requireCanReport, { uid });
302+
303+
//WHEN
304+
await handler(req, res, next);
305+
306+
//THEN
307+
expect(next).toHaveBeenCalledWith();
308+
});
309+
});
310+
});
311+
312+
function givenRequest(
313+
metadata: EndpointMetadata,
314+
decodedToken?: Partial<MonkeyTypes.DecodedToken>
315+
): TsRestRequest {
316+
return { tsRestRoute: { metadata }, ctx: { decodedToken } } as any;
317+
}

backend/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
"dependencies": {
2525
"@date-fns/utc": "1.2.0",
2626
"@monkeytype/contracts": "workspace:*",
27-
"@ts-rest/core": "3.49.3",
28-
"@ts-rest/express": "3.49.3",
29-
"@ts-rest/open-api": "3.49.3",
27+
"@ts-rest/core": "3.51.0",
28+
"@ts-rest/express": "3.51.0",
29+
"@ts-rest/open-api": "3.51.0",
3030
"bcrypt": "5.1.1",
3131
"bullmq": "1.91.1",
3232
"chalk": "4.1.2",
@@ -87,6 +87,7 @@
8787
"eslint": "8.57.0",
8888
"eslint-watch": "8.0.0",
8989
"ioredis-mock": "7.4.0",
90+
"openapi3-ts": "2.0.2",
9091
"readline-sync": "1.4.10",
9192
"supertest": "6.2.3",
9293
"tsx": "4.16.2",

0 commit comments

Comments
 (0)