From ac77fe360d404e9968318722bc5e6ff197aa2936 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 11 Apr 2024 16:15:21 -0300 Subject: [PATCH] fix: Invalid 403 forbidden for event-types GET (#14528) --- .../api/event-types/[id]/_auth-middleware.ts | 18 --- .../v1/pages/api/event-types/[id]/index.ts | 3 - .../_utils/checkTeamEventEditPermission.ts | 2 +- .../v1/test/lib/event-types/[id]/_get.test.ts | 143 ++++++++++++++++++ vitest.workspace.ts | 21 +++ 5 files changed, 165 insertions(+), 22 deletions(-) delete mode 100644 apps/api/v1/pages/api/event-types/[id]/_auth-middleware.ts create mode 100644 apps/api/v1/test/lib/event-types/[id]/_get.test.ts diff --git a/apps/api/v1/pages/api/event-types/[id]/_auth-middleware.ts b/apps/api/v1/pages/api/event-types/[id]/_auth-middleware.ts deleted file mode 100644 index 0aea723640..0000000000 --- a/apps/api/v1/pages/api/event-types/[id]/_auth-middleware.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { NextApiRequest } from "next"; - -import { HttpError } from "@calcom/lib/http-error"; -import prisma from "@calcom/prisma"; - -import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; - -async function authMiddleware(req: NextApiRequest) { - const { userId, isAdmin } = req; - const { id } = schemaQueryIdParseInt.parse(req.query); - if (isAdmin) return; - const eventType = await prisma.eventType.findFirst({ - where: { id, users: { some: { id: userId } } }, - }); - if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" }); -} - -export default authMiddleware; diff --git a/apps/api/v1/pages/api/event-types/[id]/index.ts b/apps/api/v1/pages/api/event-types/[id]/index.ts index 727ad02843..26d7389880 100644 --- a/apps/api/v1/pages/api/event-types/[id]/index.ts +++ b/apps/api/v1/pages/api/event-types/[id]/index.ts @@ -4,11 +4,8 @@ import { defaultHandler, defaultResponder } from "@calcom/lib/server"; import { withMiddleware } from "~/lib/helpers/withMiddleware"; -import authMiddleware from "./_auth-middleware"; - export default withMiddleware()( defaultResponder(async (req: NextApiRequest, res: NextApiResponse) => { - await authMiddleware(req); return defaultHandler({ GET: import("./_get"), PATCH: import("./_patch"), diff --git a/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts b/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts index 71094ba253..1055dc0d32 100644 --- a/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts +++ b/apps/api/v1/pages/api/event-types/_utils/checkTeamEventEditPermission.ts @@ -26,7 +26,7 @@ export default async function checkTeamEventEditPermission( if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) { throw new HttpError({ - statusCode: 401, + statusCode: 403, message: "No permission to operate on event-type for this team", }); } diff --git a/apps/api/v1/test/lib/event-types/[id]/_get.test.ts b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts new file mode 100644 index 0000000000..84c057c222 --- /dev/null +++ b/apps/api/v1/test/lib/event-types/[id]/_get.test.ts @@ -0,0 +1,143 @@ +import prismaMock from "../../../../../../../tests/libs/__mocks__/prismaMock"; + +import type { Request, Response } from "express"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { createMocks } from "node-mocks-http"; +import { describe, expect, test } from "vitest"; + +import { buildEventType } from "@calcom/lib/test/builder"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import handler from "../../../../pages/api/event-types/[id]/_get"; + +type CustomNextApiRequest = NextApiRequest & Request; +type CustomNextApiResponse = NextApiResponse & Response; + +describe("GET /api/event-types/[id]", () => { + describe("Errors", () => { + test("Returns 403 if user not admin/team member/event owner", async () => { + const { req, res } = createMocks({ + method: "GET", + body: {}, + query: { + id: 123456, + }, + }); + + prismaMock.eventType.findUnique.mockResolvedValue( + buildEventType({ + id: 123456, + userId: 444444, + }) + ); + + req.userId = 333333; + await handler(req, res); + + expect(res.statusCode).toBe(403); + }); + }); + + describe("Success", async () => { + test("Returns event type if user is admin", async () => { + const eventTypeId = 123456; + const { req, res } = createMocks({ + method: "GET", + body: {}, + query: { + id: eventTypeId, + }, + }); + + prismaMock.eventType.findUnique.mockResolvedValue( + buildEventType({ + id: eventTypeId, + }) + ); + + req.isAdmin = true; + req.userId = 333333; + await handler(req, res); + + expect(res.statusCode).toBe(200); + expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId); + }); + + test("Returns event type if user is in team associated with event type", async () => { + const eventTypeId = 123456; + const teamId = 9999; + const userId = 333333; + const { req, res } = createMocks({ + method: "GET", + body: {}, + query: { + id: eventTypeId, + }, + }); + + prismaMock.eventType.findUnique.mockResolvedValue( + buildEventType({ + id: eventTypeId, + teamId, + }) + ); + + prismaMock.team.findFirst.mockResolvedValue({ + id: teamId, + members: [ + { + userId, + }, + ], + }); + + req.isAdmin = false; + req.userId = userId; + await handler(req, res); + + expect(res.statusCode).toBe(200); + expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId); + expect(prismaMock.team.findFirst).toHaveBeenCalledWith({ + where: { + id: teamId, + members: { + some: { + userId: req.userId, + role: { + in: [MembershipRole.OWNER, MembershipRole.ADMIN, MembershipRole.MEMBER], + }, + }, + }, + }, + }); + }); + + test("Returns event type if user is the event type owner", async () => { + const eventTypeId = 123456; + const userId = 333333; + const { req, res } = createMocks({ + method: "GET", + body: {}, + query: { + id: eventTypeId, + }, + }); + + prismaMock.eventType.findUnique.mockResolvedValue( + buildEventType({ + id: eventTypeId, + userId, + scheduleId: 1111, + }) + ); + + req.isAdmin = false; + req.userId = userId; + await handler(req, res); + + expect(res.statusCode).toBe(200); + expect(JSON.parse(res._getData()).event_type.id).toEqual(eventTypeId); + expect(prismaMock.team.findFirst).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 7a7fdc7be3..5a49eac472 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -41,12 +41,33 @@ const workspaces = packagedEmbedTestsOnly "packages/embeds/**/*", "packages/lib/hooks/**/*", "packages/platform/**/*", + "apps/api/v1/**/*", "apps/api/v2/**/*", ], name: "@calcom/core", setupFiles: ["setupVitest.ts"], }, }, + { + test: { + include: ["apps/api/v1/**/*.{test,spec}.{ts,js}"], + exclude: [ + "**/node_modules/**/*", + "**/.next/**/*", + "packages/embeds/**/*", + "packages/lib/hooks/**/*", + "packages/platform/**/*", + "apps/api/v2/**/*", + ], + name: "@calcom/api", + setupFiles: ["setupVitest.ts"], + }, + resolve: { + alias: { + "~": new URL("./apps/api/v1", import.meta.url).pathname, + }, + }, + }, { test: { globals: true,