Skip to content

Commit fd390f9

Browse files
authored
fix: add authorization checks to booking reassignment endpoints (calcom#25054)
1 parent eae779b commit fd390f9

5 files changed

Lines changed: 139 additions & 40 deletions

File tree

apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,9 @@ export class BookingsController_2024_08_13 {
405405
})
406406
async reassignBooking(
407407
@Param("bookingUid") bookingUid: string,
408-
@GetUser() user: ApiAuthGuardUser
408+
@GetUser() reassignedByUser: ApiAuthGuardUser
409409
): Promise<ReassignBookingOutput_2024_08_13> {
410-
const booking = await this.bookingsService.reassignBooking(bookingUid, user);
410+
const booking = await this.bookingsService.reassignBooking(bookingUid, reassignedByUser);
411411

412412
return {
413413
status: SUCCESS_STATUS,
@@ -430,13 +430,13 @@ export class BookingsController_2024_08_13 {
430430
async reassignBookingToUser(
431431
@Param("bookingUid") bookingUid: string,
432432
@Param("userId") userId: number,
433-
@GetUser("id") reassignedById: number,
433+
@GetUser() reassignedByUser: ApiAuthGuardUser,
434434
@Body() body: ReassignToUserBookingInput_2024_08_13
435435
): Promise<ReassignBookingOutput_2024_08_13> {
436436
const booking = await this.bookingsService.reassignBookingToUser(
437437
bookingUid,
438438
userId,
439-
reassignedById,
439+
reassignedByUser,
440440
body
441441
);
442442

apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/emails/team-emails.e2e-spec.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { INestApplication } from "@nestjs/common";
1212
import { NestExpressApplication } from "@nestjs/platform-express";
1313
import { Test } from "@nestjs/testing";
1414
import * as request from "supertest";
15+
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
1516
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
1617
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
1718
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
@@ -20,7 +21,6 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
2021
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
2122
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
2223
import { randomString } from "test/utils/randomString";
23-
import { withApiAuth } from "test/utils/withApiAuth";
2424

2525
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
2626
import {
@@ -72,6 +72,7 @@ type EmailSetup = {
7272
team: Team;
7373
member1: User;
7474
member2: User;
75+
member1ApiKey: string;
7576
collectiveEventType: { id: number };
7677
roundRobinEventType: { id: number };
7778
};
@@ -89,25 +90,21 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
8990
let profileRepositoryFixture: ProfileRepositoryFixture;
9091
let membershipsRepositoryFixture: MembershipRepositoryFixture;
9192
let hostsRepositoryFixture: HostsRepositoryFixture;
93+
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
9294

9395
// Setup data for tests
9496
let emailsEnabledSetup: EmailSetup;
9597
let emailsDisabledSetup: EmailSetup;
9698

97-
const authEmail = "team-emails-2024-08-13-user-admin@example.com";
98-
9999
// Utility function to check response data type
100-
const responseDataIsBooking = (data: any): data is BookingOutput_2024_08_13 => {
101-
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
100+
const responseDataIsBooking = (data: unknown): data is BookingOutput_2024_08_13 => {
101+
return !Array.isArray(data) && data !== null && typeof data === "object" && data && "id" in data;
102102
};
103103

104104
beforeAll(async () => {
105-
const moduleRef = await withApiAuth(
106-
authEmail,
107-
Test.createTestingModule({
108-
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
109-
})
110-
)
105+
const moduleRef = await Test.createTestingModule({
106+
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
107+
})
111108
.overrideGuard(PermissionsGuard)
112109
.useValue({
113110
canActivate: () => true,
@@ -122,6 +119,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
122119
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
123120
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
124121
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
122+
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
125123
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
126124

127125
// Create a base organization for all tests
@@ -131,11 +129,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
131129
emailsEnabledSetup = await setupTestEnvironment(true);
132130
emailsDisabledSetup = await setupTestEnvironment(false);
133131

134-
await userRepositoryFixture.create({
135-
email: authEmail,
136-
organization: { connect: { id: organization.id } },
137-
});
138-
139132
app = moduleRef.createNestApplication();
140133
bootstrap(app as NestExpressApplication);
141134
await app.init();
@@ -173,10 +166,15 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
173166
const collectiveEvent = await createEventType("COLLECTIVE", team.id, [member1.id, member2.id]);
174167
const roundRobinEvent = await createEventType("ROUND_ROBIN", team.id, [member1.id, member2.id]);
175168

169+
// Create API key for member1 to use in authorized tests
170+
const { keyString } = await apiKeysRepositoryFixture.createApiKey(member1.id, null);
171+
const member1ApiKey = `cal_test_${keyString}`;
172+
176173
return {
177174
team,
178175
member1,
179176
member2,
177+
member1ApiKey,
180178
collectiveEventType: { id: collectiveEvent.id },
181179
roundRobinEventType: { id: roundRobinEvent.id },
182180
};
@@ -351,6 +349,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
351349
: emailsDisabledSetup.member1.id;
352350
const manualReassignResponse = await request(app.getHttpServer())
353351
.post(`/v2/bookings/${rescheduledBookingUid}/reassign/${reassignToId}`)
352+
.set("Authorization", `Bearer ${emailsDisabledSetup.member1ApiKey}`)
354353
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
355354

356355
expect(manualReassignResponse.status).toBe(200);
@@ -359,6 +358,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
359358
// --- 4. Automatic Reassign ---
360359
const autoReassignResponse = await request(app.getHttpServer())
361360
.post(`/v2/bookings/${rescheduledBookingUid}/reassign`)
361+
.set("Authorization", `Bearer ${emailsDisabledSetup.member1ApiKey}`)
362362
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
363363

364364
expect(autoReassignResponse.status).toBe(200);
@@ -484,6 +484,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
484484

485485
const manualReassignResponse = await request(app.getHttpServer())
486486
.post(`/v2/bookings/${rescheduledBookingUid}/reassign/${reassignToId}`)
487+
.set("Authorization", `Bearer ${emailsEnabledSetup.member1ApiKey}`)
487488
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
488489

489490
expect(manualReassignResponse.status).toBe(200);
@@ -499,6 +500,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
499500
jest.clearAllMocks();
500501
const autoReassignResponse = await request(app.getHttpServer())
501502
.post(`/v2/bookings/${rescheduledBookingUid}/reassign`)
503+
.set("Authorization", `Bearer ${emailsEnabledSetup.member1ApiKey}`)
502504
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
503505

504506
expect(autoReassignResponse.status).toBe(200);
@@ -523,7 +525,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
523525
afterAll(async () => {
524526
// Clean up database records
525527
await teamRepositoryFixture.delete(organization.id);
526-
await userRepositoryFixture.deleteByEmail(authEmail);
527528
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member1.email);
528529
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member2.email);
529530
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member1.email);

apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/reassign-bookings.e2e-spec.ts

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { bootstrap } from "@/app";
22
import { AppModule } from "@/app.module";
33
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
4+
import { BOOKING_REASSIGN_PERMISSION_ERROR } from "@/ee/bookings/2024-08-13/services/bookings.service";
45
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
56
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
67
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
@@ -11,6 +12,7 @@ import { INestApplication } from "@nestjs/common";
1112
import { NestExpressApplication } from "@nestjs/platform-express";
1213
import { Test } from "@nestjs/testing";
1314
import * as request from "supertest";
15+
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
1416
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
1517
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
1618
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
@@ -21,15 +23,11 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
2123
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
2224
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
2325
import { randomString } from "test/utils/randomString";
24-
import { withApiAuth } from "test/utils/withApiAuth";
25-
26-
2726

2827
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
2928
import type { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
3029
import type { Booking, User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
3130

32-
3331
describe("Bookings Endpoints 2024-08-13", () => {
3432
describe("Reassign bookings", () => {
3533
let app: INestApplication;
@@ -47,13 +45,16 @@ describe("Bookings Endpoints 2024-08-13", () => {
4745
let hostsRepositoryFixture: HostsRepositoryFixture;
4846
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
4947
let profileRepositoryFixture: ProfileRepositoryFixture;
48+
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
5049

5150
const teamUserEmail = `reassign-bookings-2024-08-13-user1-${randomString()}@api.com`;
5251
const teamUserEmail2 = `reassign-bookings-2024-08-13-user2-${randomString()}@api.com`;
5352
const teamUserEmail3 = `reassign-bookings-2024-08-13-user3-${randomString()}@api.com`;
5453
let teamUser1: User;
5554
let teamUser2: User;
5655
let teamUser3: User;
56+
let teamUser1ApiKey: string;
57+
let teamUser2ApiKey: string;
5758

5859
let teamRoundRobinEventTypeId: number;
5960
let teamRoundRobinFixedHostEventTypeId: number;
@@ -69,12 +70,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
6970
let rescheduleReasonBookingInitialHostId: number;
7071

7172
beforeAll(async () => {
72-
const moduleRef = await withApiAuth(
73-
teamUserEmail,
74-
Test.createTestingModule({
75-
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
76-
})
77-
)
73+
const moduleRef = await Test.createTestingModule({
74+
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
75+
})
7876
.overrideGuard(PermissionsGuard)
7977
.useValue({
8078
canActivate: () => true,
@@ -90,6 +88,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
9088
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
9189
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
9290
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
91+
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
9392
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
9493

9594
organization = await organizationsRepositoryFixture.create({
@@ -126,6 +125,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
126125
name: `reassign-bookings-2024-08-13-user3-${randomString()}`,
127126
});
128127

128+
const { keyString } = await apiKeysRepositoryFixture.createApiKey(teamUser1.id, null);
129+
teamUser1ApiKey = `cal_test_${keyString}`;
130+
131+
const { keyString: keyString2 } = await apiKeysRepositoryFixture.createApiKey(teamUser2.id, null);
132+
teamUser2ApiKey = `cal_test_${keyString2}`;
133+
129134
const userSchedule: CreateScheduleInput_2024_04_15 = {
130135
name: `reassign-bookings-2024-08-13-schedule-${randomString()}`,
131136
timeZone: "Europe/Rome",
@@ -473,6 +478,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
473478

474479
return request(app.getHttpServer())
475480
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign`)
481+
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
476482
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
477483
.expect(200)
478484
.then(async (response) => {
@@ -512,6 +518,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
512518
return request(app.getHttpServer())
513519
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser1.id}`)
514520
.send(body)
521+
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
515522
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
516523
.expect(200)
517524
.then(async (response) => {
@@ -562,6 +569,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
562569

563570
return request(app.getHttpServer())
564571
.post(`/v2/bookings/${bookingUid}/reassign`)
572+
.set("Authorization", `Bearer ${teamUser2ApiKey}`)
565573
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
566574
.expect(200)
567575
.then(async (response) => {
@@ -611,6 +619,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
611619

612620
return request(app.getHttpServer())
613621
.post(`/v2/bookings/${bookingUid}/reassign/${teamUser3.id}`)
622+
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
614623
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
615624
.expect(200)
616625
.then(async (response) => {
@@ -669,6 +678,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
669678

670679
return request(app.getHttpServer())
671680
.post(`/v2/bookings/${bookingUid}/reassign/${reassignToHostId}`)
681+
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
672682
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
673683
.expect(200)
674684
.then(async (response) => {
@@ -704,6 +714,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
704714

705715
return request(app.getHttpServer())
706716
.post(`/v2/bookings/${bookingUid}/reassign`)
717+
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
707718
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
708719
.expect(200)
709720
.then(async (response) => {
@@ -724,6 +735,51 @@ describe("Bookings Endpoints 2024-08-13", () => {
724735
});
725736
});
726737

738+
it("should return 403 when unauthorized user tries to reassign booking", async () => {
739+
const unauthorizedUserEmail = `fake-user-${randomString()}@api.com`;
740+
const unauthorizedUser = await userRepositoryFixture.create({
741+
email: unauthorizedUserEmail,
742+
locale: "en",
743+
name: `fake-user-${randomString()}`,
744+
});
745+
746+
const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
747+
const unauthorizedApiKeyString = `cal_test_${keyString}`;
748+
749+
const response = await request(app.getHttpServer())
750+
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign`)
751+
.set("Authorization", `Bearer ${unauthorizedApiKeyString}`)
752+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
753+
.expect(403);
754+
755+
expect(response.body.error.message).toBe(BOOKING_REASSIGN_PERMISSION_ERROR);
756+
757+
await userRepositoryFixture.deleteByEmail(unauthorizedUserEmail);
758+
});
759+
760+
it("should return 403 when unauthorized user tries to reassign booking to specific user", async () => {
761+
const unauthorizedUserEmail = `fake-user-${randomString()}@api.com`;
762+
const unauthorizedUser = await userRepositoryFixture.create({
763+
email: unauthorizedUserEmail,
764+
locale: "en",
765+
name: `fake-user-${randomString()}`,
766+
});
767+
768+
const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
769+
const unauthorizedApiKeyString = `cal_test_${keyString}`;
770+
771+
const response = await request(app.getHttpServer())
772+
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser2.id}`)
773+
.send({ reason: "Testing unauthorized access" })
774+
.set("Authorization", `Bearer ${unauthorizedApiKeyString}`)
775+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
776+
.expect(403);
777+
778+
expect(response.body.error.message).toBe(BOOKING_REASSIGN_PERMISSION_ERROR);
779+
780+
await userRepositoryFixture.deleteByEmail(unauthorizedUserEmail);
781+
});
782+
727783
async function createOAuthClient(organizationId: number) {
728784
const data = {
729785
logo: "logo-url",
@@ -749,4 +805,4 @@ describe("Bookings Endpoints 2024-08-13", () => {
749805
await app.close();
750806
});
751807
});
752-
});
808+
});

apps/api/v2/src/ee/bookings/2024-08-13/repositories/bookings.repository.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ export class BookingsRepository_2024_08_13 {
7272
});
7373
}
7474

75+
async getByUidWithEventType(bookingUid: string) {
76+
return this.dbRead.prisma.booking.findUnique({
77+
where: {
78+
uid: bookingUid,
79+
},
80+
include: {
81+
eventType: true,
82+
},
83+
});
84+
}
85+
7586
async getByUidWithUser(bookingUid: string) {
7687
return this.dbRead.prisma.booking.findUnique({
7788
where: {

0 commit comments

Comments
 (0)