Skip to content

Commit 045163b

Browse files
author
sbsb
committed
fix: restrict forceConfirm flag to event owner or admin only
1 parent b2275f7 commit 045163b

2 files changed

Lines changed: 76 additions & 3 deletions

File tree

packages/features/bookings/lib/handleNewBooking/test/booking-flags.test.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { expectBookingToBeInDatabase } from "@calcom/web/test/utils/bookingScena
1515
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
1616
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
1717

18-
import { describe, expect, test } from "vitest";
18+
import { describe, expect, test, vi } from "vitest";
1919

2020
import { BookingStatus } from "@calcom/prisma/enums";
2121

@@ -465,6 +465,7 @@ describe("handleNewBooking - Booking Flags", () => {
465465

466466
const createdBooking = await handleNewBooking({
467467
bookingData: mockBookingData,
468+
userId: 101, // Owner
468469
});
469470

470471
expect(createdBooking.responses).toEqual(
@@ -474,7 +475,7 @@ describe("handleNewBooking - Booking Flags", () => {
474475
})
475476
);
476477

477-
// Even though requiresConfirmation is true, forceConfirm=true should produce ACCEPTED
478+
// Even though requiresConfirmation is true, forceConfirm=true from owner should produce ACCEPTED
478479
expect(createdBooking.status).toBe(BookingStatus.ACCEPTED);
479480

480481
await expectBookingToBeInDatabase({
@@ -577,5 +578,66 @@ describe("handleNewBooking - Booking Flags", () => {
577578
status: BookingStatus.PENDING,
578579
});
579580
});
581+
582+
test("should NOT honor forceConfirm if caller is NOT owner/admin", async () => {
583+
const handleNewBooking = getNewBookingHandler();
584+
const booker = getBooker({
585+
email: "booker@example.com",
586+
name: "Booker",
587+
});
588+
589+
const organizer = getOrganizer({
590+
name: "Organizer",
591+
email: "organizer@example.com",
592+
id: 101,
593+
schedules: [TestData.schedules.IstWorkHours],
594+
credentials: [getGoogleCalendarCredential()],
595+
selectedCalendars: [TestData.selectedCalendars.google],
596+
});
597+
598+
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
599+
600+
await createBookingScenario(
601+
getScenarioData({
602+
eventTypes: [
603+
{
604+
id: 1,
605+
slotInterval: 45,
606+
length: 45,
607+
requiresConfirmation: true,
608+
users: [{ id: 101 }],
609+
userId: 101, // eventType owner
610+
},
611+
],
612+
organizer,
613+
})
614+
);
615+
616+
vi.spyOn(prismaMock.user, "findUnique").mockResolvedValueOnce({
617+
id: 999,
618+
role: "USER", // Not an admin
619+
} as Awaited<ReturnType<typeof prismaMock.user.findUnique>>);
620+
621+
const mockBookingData = getMockRequestDataForBooking({
622+
data: {
623+
eventTypeId: 1,
624+
responses: {
625+
email: booker.email,
626+
name: booker.name,
627+
},
628+
start: `${plus1DateString}T05:00:00.000Z`,
629+
end: `${plus1DateString}T05:45:00.000Z`,
630+
forceConfirm: true,
631+
},
632+
});
633+
634+
const createdBooking = await handleNewBooking({
635+
bookingData: mockBookingData,
636+
userId: 999, // Unauthorized caller
637+
});
638+
639+
// forceConfirm should be ignored, status should be PENDING
640+
expect(createdBooking.status).toBe(BookingStatus.PENDING);
641+
});
580642
});
581643
});

packages/features/bookings/lib/service/RegularBookingService.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ import {
8686
WebhookTriggerEvents,
8787
WorkflowTriggerEvents,
8888
CreationSource,
89+
UserPermissionRole,
8990
} from "@calcom/prisma/enums";
9091
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
9192
import type {
@@ -752,7 +753,17 @@ async function handler(
752753

753754
// Allow callers to force-confirm a booking even when the event type requires confirmation.
754755
// When forceConfirm is true, the booking is created as ACCEPTED regardless of requiresConfirmation.
755-
const isConfirmedByDefault = isConfirmedByDefaultFromFlags || !!forceConfirm;
756+
// Security Fix: Only the event type owner or an admin can use the forceConfirm flag.
757+
const isOwner = !!(userId && (eventType.userId === userId || eventType.users?.some((u) => u.id === userId)));
758+
let callerIsOwnerOrAdmin = isOwner;
759+
if (!callerIsOwnerOrAdmin && userId) {
760+
const caller = await deps.prismaClient.user.findUnique({
761+
where: { id: userId },
762+
select: { role: true },
763+
});
764+
callerIsOwnerOrAdmin = caller?.role === UserPermissionRole.ADMIN;
765+
}
766+
const isConfirmedByDefault = isConfirmedByDefaultFromFlags || (!!forceConfirm && callerIsOwnerOrAdmin);
756767

757768
// For unconfirmed bookings or round robin bookings with the same attendee and timeslot, return the original booking
758769
if (

0 commit comments

Comments
 (0)