Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

impr: move permission checks to contracts (@fehmer) #5848

Merged
merged 12 commits into from
Sep 10, 2024
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
Loading