diff --git a/pages/api/attendees/[id]/_patch.ts b/pages/api/attendees/[id]/_patch.ts index 46db6f285f..73ef195ff8 100644 --- a/pages/api/attendees/[id]/_patch.ts +++ b/pages/api/attendees/[id]/_patch.ts @@ -1,5 +1,7 @@ import type { NextApiRequest } from "next"; +import { z } from "zod"; +import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { schemaAttendeeEditBodyParams, schemaAttendeeReadPublic } from "~/lib/validations/attendee"; @@ -50,8 +52,21 @@ export async function patchHandler(req: NextApiRequest) { const { prisma, query, body } = req; const { id } = schemaQueryIdParseInt.parse(query); const data = schemaAttendeeEditBodyParams.parse(body); + await checkPermissions(req, data); const attendee = await prisma.attendee.update({ where: { id }, data }); return { attendee: schemaAttendeeReadPublic.parse(attendee) }; } +async function checkPermissions(req: NextApiRequest, body: z.infer) { + const { isAdmin, prisma } = req; + if (isAdmin) return; + const { userId } = req; + const { bookingId } = body; + if (bookingId) { + // Ensure that the booking the attendee is being added to belongs to the user + const booking = await prisma.booking.findFirst({ where: { id: bookingId, userId } }); + if (!booking) throw new HttpError({ statusCode: 403, message: "You don't have access to the booking" }); + } +} + export default defaultResponder(patchHandler); diff --git a/pages/api/bookings/[id]/_patch.ts b/pages/api/bookings/[id]/_patch.ts index a1378e16b4..fc20c14709 100644 --- a/pages/api/bookings/[id]/_patch.ts +++ b/pages/api/bookings/[id]/_patch.ts @@ -1,5 +1,7 @@ import type { NextApiRequest } from "next"; +import { z } from "zod"; +import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { schemaBookingEditBodyParams, schemaBookingReadPublic } from "~/lib/validations/booking"; @@ -49,8 +51,20 @@ export async function patchHandler(req: NextApiRequest) { const { prisma, query, body } = req; const { id } = schemaQueryIdParseInt.parse(query); const data = schemaBookingEditBodyParams.parse(body); + await checkPermissions(req, data); const booking = await prisma.booking.update({ where: { id }, data }); return { booking: schemaBookingReadPublic.parse(booking) }; } +async function checkPermissions(req: NextApiRequest, body: z.infer) { + const { isAdmin } = req; + if (body.userId && !isAdmin) { + // Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name + throw new HttpError({ + statusCode: 403, + message: "Only admin can change the organizer of a booking", + }); + } +} + export default defaultResponder(patchHandler); diff --git a/pages/api/event-types/[id]/_patch.ts b/pages/api/event-types/[id]/_patch.ts index 2ce0886029..a1c0bef77e 100644 --- a/pages/api/event-types/[id]/_patch.ts +++ b/pages/api/event-types/[id]/_patch.ts @@ -1,11 +1,18 @@ import type { NextApiRequest } from "next"; +import { z } from "zod"; import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; -import { schemaEventTypeEditBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type"; +import { + schemaEventTypeEditBodyParams, + schemaEventTypeBaseBodyParams, + schemaEventTypeReadPublic, +} from "~/lib/validations/event-type"; import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransformParseInt"; +import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission"; + /** * @swagger * /event-types/{id}: @@ -37,18 +44,19 @@ export async function patchHandler(req: NextApiRequest) { const { prisma, query, body } = req; const { id } = schemaQueryIdParseInt.parse(query); const data = schemaEventTypeEditBodyParams.parse(body); - await checkPermissions(req); + await checkPermissions(req, body); const event_type = await prisma.eventType.update({ where: { id }, data }); return { event_type: schemaEventTypeReadPublic.parse(event_type) }; } -async function checkPermissions(req: NextApiRequest) { +async function checkPermissions(req: NextApiRequest, body: z.infer) { const { userId, prisma, isAdmin } = req; const { id } = schemaQueryIdParseInt.parse(req.query); if (isAdmin) return; /** Only event type owners can modify it */ const eventType = await prisma.eventType.findFirst({ where: { id, userId } }); if (!eventType) throw new HttpError({ statusCode: 403, message: "Forbidden" }); + await checkTeamEventEditPermission(req, body); } export default defaultResponder(patchHandler); diff --git a/pages/api/event-types/_post.ts b/pages/api/event-types/_post.ts index 1868a35529..faefb4cc74 100644 --- a/pages/api/event-types/_post.ts +++ b/pages/api/event-types/_post.ts @@ -6,6 +6,8 @@ import { defaultResponder } from "@calcom/lib/server"; import { schemaEventTypeCreateBodyParams, schemaEventTypeReadPublic } from "~/lib/validations/event-type"; +import checkTeamEventEditPermission from "./_utils/checkTeamEventEditPermission"; + /** * @swagger * /event-types: @@ -61,22 +63,7 @@ async function postHandler(req: NextApiRequest) { if (isAdmin && body.userId) args = { data: { ...body, users: { connect: { id: body.userId } } } }; - if (body.teamId) { - const hasMembership = await prisma.membership.findFirst({ - where: { - userId, - teamId: body.teamId, - accepted: true, - }, - }); - - if (!hasMembership?.role || !["ADMIN", "OWNER"].includes(hasMembership.role)) { - throw new HttpError({ - statusCode: 401, - message: "No permission to create an event-type for this team`", - }); - } - } + await checkTeamEventEditPermission(req, body); const data = await prisma.eventType.create(args); diff --git a/pages/api/event-types/_utils/checkTeamEventEditPermission.ts b/pages/api/event-types/_utils/checkTeamEventEditPermission.ts new file mode 100644 index 0000000000..b2392ba7b4 --- /dev/null +++ b/pages/api/event-types/_utils/checkTeamEventEditPermission.ts @@ -0,0 +1,29 @@ +import type { NextApiRequest } from "next"; +import { z } from "zod"; + +import { HttpError } from "@calcom/lib/http-error"; + +import { schemaEventTypeBaseBodyParams } from "~/lib/validations/event-type"; + +export default async function checkTeamEventEditPermission( + req: NextApiRequest, + body: Pick, "teamId"> +) { + const { prisma, userId } = req; + if (body.teamId) { + const membership = await prisma.membership.findFirst({ + where: { + userId, + teamId: body.teamId, + accepted: true, + }, + }); + + if (!membership?.role || !["ADMIN", "OWNER"].includes(membership.role)) { + throw new HttpError({ + statusCode: 401, + message: "No permission to operate on event-type for this team", + }); + } + } +} diff --git a/pages/api/memberships/[id]/_delete.ts b/pages/api/memberships/[id]/_delete.ts index 160db80653..06acefb3bb 100644 --- a/pages/api/memberships/[id]/_delete.ts +++ b/pages/api/memberships/[id]/_delete.ts @@ -1,5 +1,6 @@ import type { NextApiRequest } from "next"; +import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { membershipIdSchema } from "~/lib/validations/membership"; @@ -35,8 +36,67 @@ import { membershipIdSchema } from "~/lib/validations/membership"; export async function deleteHandler(req: NextApiRequest) { const { prisma, query } = req; const userId_teamId = membershipIdSchema.parse(query); + await checkPermissions(req); await prisma.membership.delete({ where: { userId_teamId } }); return { message: `Membership with id: ${query.id} deleted successfully` }; } +async function checkPermissions(req: NextApiRequest) { + const { prisma, isAdmin, userId, query } = req; + const userId_teamId = membershipIdSchema.parse(query); + // Admin User can do anything including deletion of Admin Team Member in any team + if (isAdmin) { + return; + } + + // Owner can delete Admin and Member + // Admin Team Member can delete Member + // Member can't delete anyone + const PRIVILEGE_ORDER = ["OWNER", "ADMIN", "MEMBER"]; + + const memberShipToBeDeleted = await prisma.membership.findUnique({ + where: { userId_teamId }, + }); + + if (!memberShipToBeDeleted) { + throw new HttpError({ statusCode: 404, message: "Membership not found" }); + } + + // If a user is deleting their own membership, then they can do it + if (userId === memberShipToBeDeleted.userId) { + return; + } + + const currentUserMembership = await prisma.membership.findUnique({ + where: { + userId_teamId: { + userId, + teamId: memberShipToBeDeleted.teamId, + }, + }, + }); + + if (!currentUserMembership) { + // Current User isn't a member of the team + throw new HttpError({ statusCode: 403, message: "You are not a member of the team" }); + } + + if ( + PRIVILEGE_ORDER.indexOf(memberShipToBeDeleted.role) === -1 || + PRIVILEGE_ORDER.indexOf(currentUserMembership.role) === -1 + ) { + throw new HttpError({ statusCode: 400, message: "Invalid role" }); + } + + // If Role that is being deleted comes before the current User's Role, or it's the same ROLE, throw error + if ( + PRIVILEGE_ORDER.indexOf(memberShipToBeDeleted.role) <= PRIVILEGE_ORDER.indexOf(currentUserMembership.role) + ) { + throw new HttpError({ + statusCode: 403, + message: "You don't have the appropriate role to delete this membership", + }); + } +} + export default defaultResponder(deleteHandler); diff --git a/pages/api/schedules/[id]/_patch.ts b/pages/api/schedules/[id]/_patch.ts index 4846bd173f..e8a27ee105 100644 --- a/pages/api/schedules/[id]/_patch.ts +++ b/pages/api/schedules/[id]/_patch.ts @@ -1,5 +1,7 @@ import type { NextApiRequest } from "next"; +import { z } from "zod"; +import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server"; import { schemaSchedulePublic, schemaSingleScheduleBodyParams } from "~/lib/validations/schedule"; @@ -32,8 +34,18 @@ export async function patchHandler(req: NextApiRequest) { const { prisma, query } = req; const { id } = schemaQueryIdParseInt.parse(query); const data = schemaSingleScheduleBodyParams.parse(req.body); + await checkPermissions(req, data); const result = await prisma.schedule.update({ where: { id }, data, include: { availability: true } }); return { schedule: schemaSchedulePublic.parse(result) }; } +async function checkPermissions(req: NextApiRequest, body: z.infer) { + const { isAdmin } = req; + if (isAdmin) return; + if (body.userId) { + throw new HttpError({ statusCode: 403, message: "Non admin cannot change the owner of a schedule" }); + } + //_auth-middleware takes care of verifying the ownership of schedule. +} + export default defaultResponder(patchHandler);