diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts index 77f3933dcd..7c36d4b6ee 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/confirm-emails.e2e-spec.ts @@ -16,9 +16,9 @@ import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.rep import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture"; import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { randomString } from "test/utils/randomString"; -import { withApiAuth } from "test/utils/withApiAuth"; import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; import { @@ -73,6 +73,7 @@ type EmailSetup = { eventTypeId: number; createdBookingUid: string; rescheduledBookingUid: string; + accessToken: string; }; describe("Bookings Endpoints 2024-08-13 confirm emails", () => { @@ -85,6 +86,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { let eventTypesRepositoryFixture: EventTypesRepositoryFixture; let oauthClientRepositoryFixture: OAuthClientRepositoryFixture; let teamRepositoryFixture: TeamRepositoryFixture; + let tokensRepositoryFixture: TokensRepositoryFixture; let emailsEnabledSetup: EmailSetup; let emailsDisabledSetup: EmailSetup; @@ -94,12 +96,9 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { const userEmailsDisabled = `confirm-emails-2024-08-13-user-${randomString()}@api.com`; beforeAll(async () => { - const moduleRef = await withApiAuth( - authEmail, - Test.createTestingModule({ - imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], - }) - ) + const moduleRef = await Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15], + }) .overrideGuard(PermissionsGuard) .useValue({ canActivate: () => true, @@ -112,6 +111,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef); teamRepositoryFixture = new TeamRepositoryFixture(moduleRef); schedulesService = moduleRef.get(SchedulesService_2024_04_15); + tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef); organization = await teamRepositoryFixture.create({ name: `confirm-emails-2024-08-13-organization-${randomString()}`, @@ -152,6 +152,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { }, }); + const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClientEmailsEnabled.id); + const userSchedule: CreateScheduleInput_2024_04_15 = { name: `confirm-emails-2024-08-13-schedule-${randomString()}`, timeZone: "Europe/Rome", @@ -174,6 +176,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { eventTypeId: event.id, createdBookingUid: "", rescheduledBookingUid: "", + accessToken: tokens.accessToken, }; } @@ -188,6 +191,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { }, }, }); + const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClientEmailsDisabled.id); + const userSchedule: CreateScheduleInput_2024_04_15 = { name: `confirm-emails-2024-08-13-schedule-${randomString()}`, timeZone: "Europe/Rome", @@ -209,6 +214,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { eventTypeId: event.id, createdBookingUid: "", rescheduledBookingUid: "", + accessToken: tokens.accessToken, }; } @@ -277,6 +283,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { return request(app.getHttpServer()) .post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/confirm`) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${emailsDisabledSetup.accessToken}`) .expect(200) .then(async (response) => { const responseBody: GetBookingOutput_2024_08_13 = response.body; @@ -331,6 +338,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { return request(app.getHttpServer()) .post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/decline`) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${emailsDisabledSetup.accessToken}`) .expect(200) .then(async (response) => { const responseBody: GetBookingOutput_2024_08_13 = response.body; @@ -391,6 +399,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { return request(app.getHttpServer()) .post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${emailsEnabledSetup.accessToken}`) .expect(200) .then(async (response) => { const responseBody: GetBookingOutput_2024_08_13 = response.body; @@ -508,6 +517,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => { return request(app.getHttpServer()) .post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`) .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set("Authorization", `Bearer ${emailsEnabledSetup.accessToken}`) .expect(200) .then(async (response) => { const responseBody: GetBookingOutput_2024_08_13 = response.body; diff --git a/packages/features/handleMarkNoShow.ts b/packages/features/handleMarkNoShow.ts index e40380745d..09634b07fa 100644 --- a/packages/features/handleMarkNoShow.ts +++ b/packages/features/handleMarkNoShow.ts @@ -1,6 +1,7 @@ import { type TFunction } from "i18next"; import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; +import { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService"; import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows"; @@ -423,9 +424,14 @@ const assertCanAccessBooking = async (bookingUid: string, userId?: number) => { if (!userId) throw new HttpError({ statusCode: 401 }); const bookingRepo = new BookingRepository(prisma); - const booking = await bookingRepo.findBookingByUidAndUserId({ bookingUid, userId }); + const booking = await bookingRepo.findByUidIncludeEventTypeAndReferences({ bookingUid }); + const bookingAccessService = new BookingAccessService(prisma); + const isAuthorized = await bookingAccessService.doesUserIdHaveAccessToBooking({ + userId, + bookingUid, + }); - if (!booking) + if (!isAuthorized) throw new HttpError({ statusCode: 403, message: "You are not allowed to access this booking" }); const isUpcoming = new Date(booking.endTime) >= new Date(); diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index af6c1d79c8..c4850356ef 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -7,12 +7,12 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund"; +import { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService"; import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows"; import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; -import { PrismaOrgMembershipRepository } from "@calcom/features/membership/repositories/PrismaOrgMembershipRepository"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; @@ -24,13 +24,7 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import type { TraceContext } from "@calcom/lib/tracing"; import { prisma } from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; -import { - BookingStatus, - MembershipRole, - WebhookTriggerEvents, - WorkflowTriggerEvents, - UserPermissionRole, -} from "@calcom/prisma/enums"; +import { BookingStatus, WebhookTriggerEvents, WorkflowTriggerEvents } from "@calcom/prisma/enums"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -149,14 +143,19 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { throw new TRPCError({ code: "BAD_REQUEST", message: "Booking must have an organizer" }); } - await checkIfUserIsAuthorizedToConfirmBooking({ - eventTypeId: booking.eventTypeId, - loggedInUserId: ctx.user.id, - teamId: booking.eventType?.teamId || booking.eventType?.parent?.teamId, - bookingUserId: booking.userId, - userRole: ctx.user.role, + const bookingAccessService = new BookingAccessService(prisma); + const isUserAuthorizedToConfirmBooking = await bookingAccessService.doesUserIdHaveAccessToBooking({ + userId: ctx.user.id, + bookingId: bookingId, }); + if (!isUserAuthorizedToConfirmBooking) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "User is not authorized to confirm this booking", + }); + } + // Do not move this before authorization check. // This is done to avoid exposing extra information to the requester. if (booking.status === BookingStatus.ACCEPTED) { @@ -443,68 +442,3 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { return { message, status }; }; - -const checkIfUserIsAuthorizedToConfirmBooking = async ({ - eventTypeId, - loggedInUserId, - teamId, - bookingUserId, - userRole, -}: { - eventTypeId: number | null; - loggedInUserId: number; - teamId?: number | null; - bookingUserId: number | null; - userRole: string; -}): Promise => { - // check system wide admin - if (userRole === UserPermissionRole.ADMIN) return; - - // Check if the user is the owner of the event type - if (bookingUserId === loggedInUserId) return; - - // Check if user is associated with the event type - if (eventTypeId) { - const [loggedInUserAsHostOfEventType, loggedInUserAsUserOfEventType] = await Promise.all([ - prisma.eventType.findUnique({ - where: { - id: eventTypeId, - hosts: { some: { userId: loggedInUserId } }, - }, - select: { id: true }, - }), - prisma.eventType.findUnique({ - where: { - id: eventTypeId, - users: { some: { id: loggedInUserId } }, - }, - select: { id: true }, - }), - ]); - - if (loggedInUserAsHostOfEventType || loggedInUserAsUserOfEventType) return; - } - - // Check if the user is an admin/owner of the team the booking belongs to - if (teamId) { - const membership = await prisma.membership.findFirst({ - where: { - userId: loggedInUserId, - teamId: teamId, - role: { - in: [MembershipRole.OWNER, MembershipRole.ADMIN], - }, - }, - }); - if (membership) return; - } - - if ( - bookingUserId && - (await PrismaOrgMembershipRepository.isLoggedInUserOrgAdminOfBookingHost(loggedInUserId, bookingUserId)) - ) { - return; - } - - throw new TRPCError({ code: "UNAUTHORIZED", message: "User is not authorized to confirm this booking" }); -};