Skip to content

Commit 829edec

Browse files
refactor: v2 api event-types/:eventTypeId access (calcom#24969)
* refactor: EventTypeAccess service * feat: event-types/:id system admin access and team event access * fix: implement cubic feedback * fix: e2e * fix: e2e * fix: oasdiff ignore non-breaking change --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
1 parent 81a99ea commit 829edec

10 files changed

Lines changed: 1414 additions & 1177 deletions

File tree

.github/oasdiff-err-ignore.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GET /v2/event-types/{eventTypeId} added to the 'data' response property 'oneOf' list for the response status '200'
2+
GET /v2/event-types/{eventTypeId} added '#/components/schemas/EventTypeOutput_2024_06_14, #/components/schemas/TeamEventTypeOutput_2024_06_14' to the 'data' response property 'oneOf' list for the response status '200'

apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
21
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
2+
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
33
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
44
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
55
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
@@ -13,9 +13,8 @@ import { AuthOptionalUser } from "@/modules/auth/decorators/get-optional-user/ge
1313
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
1414
import { BillingService } from "@/modules/billing/services/billing.service";
1515
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
16+
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
1617
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
17-
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
18-
import { MembershipsService } from "@/modules/memberships/services/memberships.service";
1918
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
2019
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
2120
import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository";
@@ -109,12 +108,11 @@ export class BookingsService_2024_08_13 {
109108
private readonly organizationsRepository: OrganizationsRepository,
110109
private readonly teamsRepository: TeamsRepository,
111110
private readonly teamsEventTypesRepository: TeamsEventTypesRepository,
112-
private readonly membershipsRepository: MembershipsRepository,
113-
private readonly membershipsService: MembershipsService,
114111
private readonly errorsBookingsService: ErrorsBookingsService_2024_08_13,
115112
private readonly regularBookingService: RegularBookingService,
116113
private readonly recurringBookingService: RecurringBookingService,
117-
private readonly instantBookingCreateService: InstantBookingCreateService
114+
private readonly instantBookingCreateService: InstantBookingCreateService,
115+
private readonly eventTypeAccessService: EventTypeAccessService
118116
) {}
119117

120118
async createBooking(request: Request, body: CreateBookingInput, authUser: AuthOptionalUser) {
@@ -128,7 +126,7 @@ export class BookingsService_2024_08_13 {
128126
this.errorsBookingsService.handleEventTypeToBeBookedNotFound(body);
129127
}
130128
const userIsEventTypeAdminOrOwner = authUser
131-
? await this.userIsEventTypeAdminOrOwner(authUser, eventType)
129+
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, eventType)
132130
: false;
133131
await this.checkBookingRequiresAuthenticationSetting(eventType, authUser, userIsEventTypeAdminOrOwner);
134132

@@ -197,44 +195,6 @@ export class BookingsService_2024_08_13 {
197195
}
198196
}
199197

200-
async userIsEventTypeAdminOrOwner(authUser: ApiAuthGuardUser, eventType: EventType) {
201-
const authUserId = authUser.id;
202-
const authUserRole = authUser.role;
203-
const eventTypeId = eventType.id;
204-
const teamId = eventType.teamId;
205-
const eventTypeOwnerId = eventType.userId || null;
206-
207-
if (authUserRole === "ADMIN") return true;
208-
209-
if (eventTypeOwnerId === authUserId) return true;
210-
211-
if (eventTypeId) {
212-
const [isUserHost, isUserAssigned] = await Promise.all([
213-
this.eventTypesRepository.isUserHostOfEventType(authUserId, eventTypeId),
214-
this.eventTypesRepository.isUserAssignedToEventType(authUserId, eventTypeId),
215-
]);
216-
217-
if (isUserHost || isUserAssigned) return true;
218-
}
219-
220-
if (teamId) {
221-
const membership = await this.membershipsRepository.getUserAdminOrOwnerTeamMembership(
222-
authUserId,
223-
teamId
224-
);
225-
if (membership) return true;
226-
}
227-
228-
if (
229-
eventTypeOwnerId &&
230-
(await this.membershipsService.isUserOrgAdminOrOwnerOfAnotherUser(authUserId, eventTypeOwnerId))
231-
) {
232-
return true;
233-
}
234-
235-
return false;
236-
}
237-
238198
async getBookedEventType(body: CreateBookingInput) {
239199
if (body.eventTypeId) {
240200
return await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(body.eventTypeId);
@@ -616,7 +576,7 @@ export class BookingsService_2024_08_13 {
616576
const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid);
617577
const userIsEventTypeAdminOrOwner =
618578
authUser && booking?.eventType
619-
? await this.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
579+
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, booking.eventType)
620580
: false;
621581

622582
if (booking) {
@@ -805,7 +765,7 @@ export class BookingsService_2024_08_13 {
805765

806766
const userIsEventTypeAdminOrOwner =
807767
authUser && databaseBooking.eventType
808-
? await this.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
768+
? await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(authUser, databaseBooking.eventType)
809769
: false;
810770
const isRecurring = !!databaseBooking.recurringEventId;
811771
const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot;

apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts

Lines changed: 128 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,31 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
2222
import { randomString } from "test/utils/randomString";
2323
import { withApiAuth } from "test/utils/withApiAuth";
2424

25-
26-
2725
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
28-
import { BookingWindowPeriodInputTypeEnum_2024_06_14, BookerLayoutsInputEnum_2024_06_14, ConfirmationPolicyEnum, NoticeThresholdUnitEnum, FrequencyInput } from "@calcom/platform-enums";
26+
import {
27+
BookingWindowPeriodInputTypeEnum_2024_06_14,
28+
BookerLayoutsInputEnum_2024_06_14,
29+
ConfirmationPolicyEnum,
30+
NoticeThresholdUnitEnum,
31+
FrequencyInput,
32+
} from "@calcom/platform-enums";
2933
import { SchedulingType } from "@calcom/platform-libraries";
30-
import { type ApiSuccessResponse, type CreateEventTypeInput_2024_06_14, type EventTypeOutput_2024_06_14, type GuestsDefaultFieldOutput_2024_06_14, type NameDefaultFieldInput_2024_06_14, type NotesDefaultFieldInput_2024_06_14, type SplitNameDefaultFieldOutput_2024_06_14, type UpdateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
34+
import {
35+
BaseConfirmationPolicy_2024_06_14,
36+
TeamEventTypeOutput_2024_06_14,
37+
type ApiSuccessResponse,
38+
type CreateEventTypeInput_2024_06_14,
39+
type EventTypeOutput_2024_06_14,
40+
type GuestsDefaultFieldOutput_2024_06_14,
41+
type NameDefaultFieldInput_2024_06_14,
42+
type NotesDefaultFieldInput_2024_06_14,
43+
type SplitNameDefaultFieldOutput_2024_06_14,
44+
type UpdateEventTypeInput_2024_06_14,
45+
} from "@calcom/platform-types";
3146
import { FAILED_RECURRING_EVENT_TYPE_WITH_BOOKER_LIMITS_ERROR_MESSAGE } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/validators/CantHaveRecurrenceAndBookerActiveBookingsLimit";
3247
import { REQUIRES_AT_LEAST_ONE_PROPERTY_ERROR } from "@calcom/platform-types/utils/RequiresOneOfPropertiesWhenNotDisabled";
3348
import type { PlatformOAuthClient, Team, User, Schedule, EventType } from "@calcom/prisma/client";
3449

35-
3650
const orderBySlug = (a: { slug: string }, b: { slug: string }) => {
3751
if (a.slug < b.slug) return -1;
3852
if (a.slug > b.slug) return 1;
@@ -83,6 +97,8 @@ describe("Event types Endpoints", () => {
8397
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
8498
let apiKeyString: string;
8599
let apiKeyOrgUser: string;
100+
let systemAdminUser: User;
101+
let systemAdminApiKeyString: string;
86102

87103
const userEmail = `event-types-2024-06-14-user-${randomString()}@api.com`;
88104
const falseTestEmail = `event-types-2024-06-14-false-user-${randomString()}@api.com`;
@@ -196,6 +212,17 @@ describe("Event types Endpoints", () => {
196212
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
197213
apiKeyString = `cal_test_${keyString}`;
198214

215+
systemAdminUser = await userRepositoryFixture.create({
216+
email: `event-types-2024-06-14-system-admin-${randomString()}@api.com`,
217+
username: `event-types-2024-06-14-system-admin-${randomString()}`,
218+
role: "ADMIN",
219+
});
220+
const { keyString: adminKeyString } = await apiKeysRepositoryFixture.createApiKey(
221+
systemAdminUser.id,
222+
null
223+
);
224+
systemAdminApiKeyString = `cal_test_${adminKeyString}`;
225+
199226
orgUser = await userRepositoryFixture.create({
200227
email: `event-types-2024-06-14-org-user-${randomString()}@example.com`,
201228
name: `event-types-2024-06-14-org-user-${randomString()}`,
@@ -1375,6 +1402,99 @@ describe("Event types Endpoints", () => {
13751402
expect(fetchedEventType.color).toEqual(eventType.color);
13761403
});
13771404

1405+
it("system admin can access another user's event type by id", async () => {
1406+
return request(app.getHttpServer())
1407+
.get(`/api/v2/event-types/${orgUserEventType1.id}`)
1408+
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
1409+
.set("Authorization", `Bearer ${systemAdminApiKeyString}`)
1410+
.expect(200)
1411+
.then((response) => {
1412+
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
1413+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
1414+
expect(responseBody.data.id).toEqual(orgUserEventType1.id);
1415+
expect(responseBody.data.ownerId).toEqual(orgUser.id);
1416+
});
1417+
});
1418+
1419+
it("user can access a team event type as team member (using user's API key)", async () => {
1420+
const team = await teamRepositoryFixture.create({
1421+
name: `event-types-2024-06-14-team-${randomString()}`,
1422+
isOrganization: false,
1423+
});
1424+
1425+
await membershipsRepositoryFixture.create({
1426+
role: "ADMIN",
1427+
user: { connect: { id: user.id } },
1428+
team: { connect: { id: team.id } },
1429+
accepted: true,
1430+
});
1431+
1432+
const teamEventType = await eventTypesRepositoryFixture.createTeamEventType({
1433+
title: `event-types-2024-06-14-team-event-${randomString()}`,
1434+
slug: `event-types-2024-06-14-team-event-${randomString()}`,
1435+
length: 60,
1436+
locations: [],
1437+
schedulingType: "COLLECTIVE",
1438+
team: { connect: { id: team.id } },
1439+
});
1440+
1441+
return request(app.getHttpServer())
1442+
.get(`/api/v2/event-types/${teamEventType.id}`)
1443+
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
1444+
.set("Authorization", `Bearer ${apiKeyString}`)
1445+
.expect(200)
1446+
.then(async (response) => {
1447+
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
1448+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
1449+
expect(responseBody.data.id).toEqual(teamEventType.id);
1450+
expect(responseBody.data.teamId).toEqual(team.id);
1451+
await teamRepositoryFixture.delete(team.id);
1452+
});
1453+
});
1454+
1455+
it("user can access a team event type as HOST even if membership role is MEMBER (using user's API key)", async () => {
1456+
const team = await teamRepositoryFixture.create({
1457+
name: `event-types-2024-06-14-host-team-${randomString()}`,
1458+
isOrganization: false,
1459+
});
1460+
1461+
await membershipsRepositoryFixture.create({
1462+
role: "MEMBER",
1463+
user: { connect: { id: user.id } },
1464+
team: { connect: { id: team.id } },
1465+
accepted: true,
1466+
});
1467+
1468+
const teamEventType = await eventTypesRepositoryFixture.createTeamEventType({
1469+
title: `event-types-2024-06-14-host-event-${randomString()}`,
1470+
slug: `event-types-2024-06-14-host-event-${randomString()}`,
1471+
length: 60,
1472+
locations: [],
1473+
schedulingType: "COLLECTIVE",
1474+
team: { connect: { id: team.id } },
1475+
hosts: {
1476+
create: [
1477+
{
1478+
user: { connect: { id: user.id } },
1479+
},
1480+
],
1481+
},
1482+
});
1483+
1484+
return request(app.getHttpServer())
1485+
.get(`/api/v2/event-types/${teamEventType.id}`)
1486+
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
1487+
.set("Authorization", `Bearer ${apiKeyString}`)
1488+
.expect(200)
1489+
.then(async (response) => {
1490+
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
1491+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
1492+
expect(responseBody.data.id).toEqual(teamEventType.id);
1493+
expect(responseBody.data.teamId).toEqual(team.id);
1494+
await teamRepositoryFixture.delete(team.id);
1495+
});
1496+
});
1497+
13781498
it(`/GET/event-types by username and eventSlug`, async () => {
13791499
const response = await request(app.getHttpServer())
13801500
.get(`/api/v2/event-types?username=${username}&eventSlug=${eventType.slug}`)
@@ -1780,7 +1900,7 @@ describe("Event types Endpoints", () => {
17801900
.then(async (response) => {
17811901
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
17821902
const updatedEventType = responseBody.data;
1783-
const policy = updatedEventType.confirmationPolicy as any;
1903+
const policy = updatedEventType.confirmationPolicy as BaseConfirmationPolicy_2024_06_14;
17841904
expect(policy?.type).toEqual(ConfirmationPolicyEnum.ALWAYS);
17851905
expect(policy?.blockUnconfirmedBookingsInBooker).toEqual(false);
17861906
expect(policy?.noticeThreshold).toBeUndefined();
@@ -1808,7 +1928,7 @@ describe("Event types Endpoints", () => {
18081928
.then(async (response) => {
18091929
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
18101930
const updatedEventType = responseBody.data;
1811-
const policy = updatedEventType.confirmationPolicy as any;
1931+
const policy = updatedEventType.confirmationPolicy as BaseConfirmationPolicy_2024_06_14;
18121932
expect(policy?.type).toEqual(ConfirmationPolicyEnum.TIME);
18131933
expect(policy?.noticeThreshold).toEqual({
18141934
unit: NoticeThresholdUnitEnum.MINUTES,
@@ -2793,4 +2913,4 @@ describe("Event types Endpoints", () => {
27932913
await app.close();
27942914
});
27952915
});
2796-
});
2916+
});

apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { Permissions } from "@/modules/auth/decorators/permissions/permissions.d
2323
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
2424
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
2525
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
26+
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
27+
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
2628
import { UserWithProfile } from "@/modules/users/users.repository";
2729
import {
2830
Controller,
@@ -73,7 +75,8 @@ export class EventTypesController_2024_06_14 {
7375
private readonly eventTypesService: EventTypesService_2024_06_14,
7476
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
7577
private readonly eventTypeResponseTransformPipe: EventTypeResponseTransformPipe,
76-
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14
78+
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
79+
private readonly outputTeamEventTypesResponsePipe: OutputTeamEventTypesResponsePipe
7780
) {}
7881

7982
@Post("/")
@@ -107,21 +110,36 @@ export class EventTypesController_2024_06_14 {
107110
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
108111
@ApiOperation({
109112
summary: "Get an event type",
110-
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>`,
113+
description: `<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
114+
115+
Access control: This endpoint fetches an event type by ID and returns it only if the authenticated user is authorized. Authorization is granted to:
116+
- System admins
117+
- The event type owner
118+
- Hosts of the event type or users assigned to the event type
119+
- Team admins/owners of the team that owns the team event type
120+
- Organization admins/owners of the event type owner's organization
121+
- Organization admins/owners of the team's parent organization
122+
123+
Note: Update and delete endpoints remain restricted to the event type owner only.`,
111124
})
112125
async getEventTypeById(
113126
@Param("eventTypeId") eventTypeId: string,
114-
@GetUser() user: UserWithProfile
127+
@GetUser() user: ApiAuthGuardUser
115128
): Promise<GetEventTypeOutput_2024_06_14> {
116-
const eventType = await this.eventTypesService.getUserEventType(user.id, Number(eventTypeId));
129+
const eventType = await this.eventTypesService.getEventTypeByIdIfAuthorized(user, Number(eventTypeId));
117130

118131
if (!eventType) {
119132
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
120133
}
121134

135+
const responseEventType =
136+
"hosts" in eventType
137+
? await this.outputTeamEventTypesResponsePipe.transform(eventType)
138+
: this.eventTypeResponseTransformPipe.transform(eventType);
139+
122140
return {
123141
status: SUCCESS_STATUS,
124-
data: this.eventTypeResponseTransformPipe.transform(eventType),
142+
data: responseEventType,
125143
};
126144
}
127145

apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.module.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types
1010
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
1111
import { AppsRepository } from "@/modules/apps/apps.repository";
1212
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
13+
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
1314
import { MembershipsModule } from "@/modules/memberships/memberships.module";
15+
import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer";
16+
import { OutputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/output.service";
1417
import { PrismaModule } from "@/modules/prisma/prisma.module";
1518
import { RedisModule } from "@/modules/redis/redis.module";
1619
import { SelectedCalendarsModule } from "@/modules/selected-calendars/selected-calendars.module";
20+
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
21+
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
1722
import { TokensModule } from "@/modules/tokens/tokens.module";
1823
import { UsersService } from "@/modules/users/services/users.service";
1924
import { UsersRepository } from "@/modules/users/users.repository";
@@ -26,6 +31,8 @@ import { Module } from "@nestjs/common";
2631
EventTypesService_2024_06_14,
2732
InputEventTypesService_2024_06_14,
2833
OutputEventTypesService_2024_06_14,
34+
EventTypeAccessService,
35+
TeamsRepository,
2936
UsersRepository,
3037
UsersService,
3138
SchedulesRepository_2024_06_11,
@@ -35,13 +42,17 @@ import { Module } from "@nestjs/common";
3542
CredentialsRepository,
3643
AppsRepository,
3744
CalendarsRepository,
45+
OutputTeamEventTypesResponsePipe,
46+
OutputOrganizationsEventTypesService,
47+
TeamsEventTypesRepository,
3848
],
3949
controllers: [EventTypesController_2024_06_14],
4050
exports: [
4151
EventTypesService_2024_06_14,
4252
EventTypesRepository_2024_06_14,
4353
InputEventTypesService_2024_06_14,
4454
OutputEventTypesService_2024_06_14,
55+
EventTypeAccessService,
4556
],
4657
})
4758
export class EventTypesModule_2024_06_14 {}

0 commit comments

Comments
 (0)