Skip to content

Commit

Permalink
impr: move permission checks to contracts (@fehmer, @Miodec) (#5848)
Browse files Browse the repository at this point in the history
!nuf
  • Loading branch information
fehmer authored Sep 10, 2024
1 parent 1427753 commit c7b3e2c
Show file tree
Hide file tree
Showing 20 changed files with 611 additions and 150 deletions.
317 changes: 317 additions & 0 deletions backend/__tests__/middlewares/permission.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
import { Response } from "express";
import { verifyPermissions } from "../../src/middlewares/permission";
import { EndpointMetadata } from "@monkeytype/contracts/schemas/api";
import * as Misc from "../../src/utils/misc";
import * as AdminUids from "../../src/dal/admin-uids";
import * as UserDal from "../../src/dal/user";
import MonkeyError from "../../src/utils/error";

const uid = "123456789";

describe("permission middleware", () => {
const handler = verifyPermissions();
const res: Response = {} as any;
const next = vi.fn();
const getPartialUserMock = vi.spyOn(UserDal, "getPartialUser");
const isAdminMock = vi.spyOn(AdminUids, "isAdmin");
const isDevMock = vi.spyOn(Misc, "isDevEnvironment");

beforeEach(() => {
next.mockReset();
getPartialUserMock.mockReset().mockResolvedValue({} as any);
isDevMock.mockReset().mockReturnValue(false);
isAdminMock.mockReset().mockResolvedValue(false);
});
afterEach(() => {
//next function must only be called once
expect(next).toHaveBeenCalledOnce();
});

it("should bypass without requiredPermission", async () => {
//GIVEN
const req = givenRequest({});
//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith();
});
it("should bypass with empty requiredPermission", async () => {
//GIVEN
const req = givenRequest({ requirePermission: [] });
//WHEN
await handler(req, res, next);

//THE
expect(next).toHaveBeenCalledWith();
});

describe("admin check", () => {
const requireAdminPermission: EndpointMetadata = {
requirePermission: "admin",
};

it("should fail without authentication", async () => {
//GIVEN
const req = givenRequest(requireAdminPermission);
//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith(
new MonkeyError(403, "You don't have permission to do this.")
);
});
it("should pass without authentication if publicOnDev on dev", async () => {
//GIVEN
isDevMock.mockReturnValue(true);
const req = givenRequest(
{
...requireAdminPermission,
authenticationOptions: { isPublicOnDev: true },
},
{ uid }
);
//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith();
});
it("should fail without authentication if publicOnDev on prod ", async () => {
//GIVEN
const req = givenRequest(
{
...requireAdminPermission,
authenticationOptions: { isPublicOnDev: true },
},
{ uid }
);
//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith(
new MonkeyError(403, "You don't have permission to do this.")
);
});
it("should fail without admin permissions", async () => {
//GIVEN
const req = givenRequest(requireAdminPermission, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith(
new MonkeyError(403, "You don't have permission to do this.")
);
expect(isAdminMock).toHaveBeenCalledWith(uid);
});
});
describe("user checks", () => {
it("should fetch user only once", async () => {
//GIVEN
const req = givenRequest(
{
requirePermission: ["canReport", "canManageApeKeys"],
},
{ uid }
);

//WHEN
await handler(req, res, next);

//THEN
expect(getPartialUserMock).toHaveBeenCalledOnce();
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["canReport", "canManageApeKeys"]
);
});
it("should fail if authentication is missing", async () => {
//GIVEN
const req = givenRequest({
requirePermission: ["canReport", "canManageApeKeys"],
});

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith(
new MonkeyError(
403,
"Failed to check permissions, authentication required."
)
);
});
});
describe("quoteMod check", () => {
const requireQuoteMod: EndpointMetadata = {
requirePermission: "quoteMod",
};

it("should pass for quoteAdmin", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ quoteMod: true } as any);
const req = givenRequest(requireQuoteMod, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith();
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["quoteMod"]
);
});
it("should pass for specific language", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ quoteMod: "english" } as any);
const req = givenRequest(requireQuoteMod, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith();
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["quoteMod"]
);
});
it("should fail for empty string", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ quoteMod: "" } as any);
const req = givenRequest(requireQuoteMod, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith(
new MonkeyError(403, "You don't have permission to do this.")
);
});
it("should fail for missing quoteMod", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({} as any);
const req = givenRequest(requireQuoteMod, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith(
new MonkeyError(403, "You don't have permission to do this.")
);
});
});
describe("canReport check", () => {
const requireCanReport: EndpointMetadata = {
requirePermission: "canReport",
};

it("should fail if user cannot report", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ canReport: false } as any);
const req = givenRequest(requireCanReport, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith(
new MonkeyError(403, "You don't have permission to do this.")
);
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["canReport"]
);
});
it("should pass if user can report", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ canReport: true } as any);
const req = givenRequest(requireCanReport, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith();
});
it("should pass if canReport is not set", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({} as any);
const req = givenRequest(requireCanReport, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith();
});
});
describe("canManageApeKeys check", () => {
const requireCanReport: EndpointMetadata = {
requirePermission: "canManageApeKeys",
};

it("should fail if user cannot report", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ canManageApeKeys: false } as any);
const req = givenRequest(requireCanReport, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith(
new MonkeyError(
403,
"You have lost access to ape keys, please contact support"
)
);
expect(getPartialUserMock).toHaveBeenCalledWith(
uid,
"check user permissions",
["canManageApeKeys"]
);
});
it("should pass if user can report", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({ canManageApeKeys: true } as any);
const req = givenRequest(requireCanReport, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith();
});
it("should pass if canManageApeKeys is not set", async () => {
//GIVEN
getPartialUserMock.mockResolvedValue({} as any);
const req = givenRequest(requireCanReport, { uid });

//WHEN
await handler(req, res, next);

//THEN
expect(next).toHaveBeenCalledWith();
});
});
});

function givenRequest(
metadata: EndpointMetadata,
decodedToken?: Partial<MonkeyTypes.DecodedToken>
): TsRestRequest {
return { tsRestRoute: { metadata }, ctx: { decodedToken } } as any;
}
7 changes: 4 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
"dependencies": {
"@date-fns/utc": "1.2.0",
"@monkeytype/contracts": "workspace:*",
"@ts-rest/core": "3.49.3",
"@ts-rest/express": "3.49.3",
"@ts-rest/open-api": "3.49.3",
"@ts-rest/core": "3.51.0",
"@ts-rest/express": "3.51.0",
"@ts-rest/open-api": "3.51.0",
"bcrypt": "5.1.1",
"bullmq": "1.91.1",
"chalk": "4.1.2",
Expand Down Expand Up @@ -87,6 +87,7 @@
"eslint": "8.57.0",
"eslint-watch": "8.0.0",
"ioredis-mock": "7.4.0",
"openapi3-ts": "2.0.2",
"readline-sync": "1.4.10",
"supertest": "6.2.3",
"tsx": "4.16.2",
Expand Down
Loading

0 comments on commit c7b3e2c

Please sign in to comment.