Skip to content

Commit 717a26f

Browse files
fix: prevent bulk update of locked locations in child managed event types (#24978)
* fix: prevent bulk update of locked locations in child managed event types - Filter out child managed event types with locked locations in getBulkUserEventTypes - Add validation in bulkUpdateEventsToDefaultLocation to prevent updating locked fields - Implements defense in depth with validation at multiple layers Co-Authored-By: [email protected] <[email protected]> * Abstract filtering logic * test: add comprehensive tests for bulk location update filtering - Add unit tests for filterEventTypesWhereLocationUpdateIsAllowed - Add unit tests for bulkUpdateEventsToDefaultLocation - Add integration tests for getBulkUserEventTypes - Fix bug: change unlockedFields?.locations check from !== undefined to === true This ensures that locations: false is properly treated as locked, addressing the security issue identified in PR review comments Co-Authored-By: [email protected] <[email protected]> * fix: filter locked managed event types on app installation page - Add parentId to eventTypeSelect in getEventTypes function - Apply filterEventTypesWhereLocationUpdateIsAllowed to both team and user event types - Only filter when isConferencing is true to avoid affecting other app types - Fixes issue where locked managed event types were showing in the event type selection list on /apps/installation/event-types page Co-Authored-By: [email protected] <[email protected]> * fix(embed-react): remove obsolete availabilityLoaded event listener The availabilityLoaded event does not exist in the EventDataMap type system in embed-core. This code was causing 5 TypeScript errors in CI: - Type 'availabilityLoaded' does not satisfy constraint 'keyof EventDataMap' - 'data' is of type 'unknown' (2 occurrences) - Type 'availabilityLoaded' is not assignable to action union (2 occurrences) Since this is an example file and the event is not defined in the type system, removing this obsolete code resolves the type errors. Co-Authored-By: [email protected] <[email protected]> * fix: correct Prisma type for metadata in test helper function Co-Authored-By: [email protected] <[email protected]> * fix: use flexible PrismaLike type for better test compatibility Co-Authored-By: [email protected] <[email protected]> * fix: properly type mock Prisma objects in test files Co-Authored-By: [email protected] <[email protected]> * fix: properly mock Prisma methods in test file Co-Authored-By: [email protected] <[email protected]> * Filter out metadata * Undo change in embed file * Address feedback --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 6818806 commit 717a26f

File tree

6 files changed

+818
-27
lines changed

6 files changed

+818
-27
lines changed

apps/web/lib/apps/installation/[[...step]]/getServerSideProps.ts

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { GetServerSidePropsContext } from "next";
22
import { z } from "zod";
33

4+
import { filterEventTypesWhereLocationUpdateIsAllowed } from "@calcom/app-store/_utils/getBulkEventTypes";
45
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
56
import type { LocationObject } from "@calcom/app-store/locations";
67
import { isConferencing as isConferencingApp } from "@calcom/app-store/utils";
@@ -73,7 +74,15 @@ const getAppBySlug = async (appSlug: string) => {
7374
return app;
7475
};
7576

76-
const getEventTypes = async (userId: number, teamIds?: number[]) => {
77+
const getEventTypes = async ({
78+
userId,
79+
teamIds,
80+
isConferencing = false,
81+
}: {
82+
userId: number;
83+
teamIds?: number[];
84+
isConferencing?: boolean;
85+
}) => {
7786
const eventTypeSelect = {
7887
id: true,
7988
description: true,
@@ -96,6 +105,7 @@ const getEventTypes = async (userId: number, teamIds?: number[]) => {
96105
destinationCalendar: true,
97106
bookingFields: true,
98107
calVideoSettings: true,
108+
parentId: true,
99109
} satisfies Prisma.EventTypeSelect;
100110

101111
let eventTypeGroups: TEventTypeGroup[] | null = [];
@@ -119,22 +129,28 @@ const getEventTypes = async (userId: number, teamIds?: number[]) => {
119129
},
120130
},
121131
});
122-
eventTypeGroups = teams.map((team) => ({
123-
teamId: team.id,
124-
slug: team.slug,
125-
name: team.name,
126-
isOrganisation: team.isOrganization,
127-
image: getPlaceholderAvatar(team.logoUrl, team.name),
128-
eventTypes: team.eventTypes
129-
.map((item) => ({
130-
...item,
131-
URL: `${CAL_URL}/${item.team ? `team/${item.team.slug}` : item?.users?.[0]?.username}/${item.slug}`,
132-
selected: false,
133-
locations: item.locations as unknown as LocationObject[],
134-
bookingFields: eventTypeBookingFields.parse(item.bookingFields || []),
135-
}))
136-
.sort((eventTypeA, eventTypeB) => eventTypeB.position - eventTypeA.position),
137-
}));
132+
eventTypeGroups = teams.map((team) => {
133+
const filteredEventTypes = isConferencing
134+
? filterEventTypesWhereLocationUpdateIsAllowed(team.eventTypes)
135+
: team.eventTypes;
136+
137+
return {
138+
teamId: team.id,
139+
slug: team.slug,
140+
name: team.name,
141+
isOrganisation: team.isOrganization,
142+
image: getPlaceholderAvatar(team.logoUrl, team.name),
143+
eventTypes: filteredEventTypes
144+
.map((item) => ({
145+
...item,
146+
URL: `${CAL_URL}/${item.team ? `team/${item.team.slug}` : item?.users?.[0]?.username}/${item.slug}`,
147+
selected: false,
148+
locations: item.locations as unknown as LocationObject[],
149+
bookingFields: eventTypeBookingFields.parse(item.bookingFields || []),
150+
}))
151+
.sort((eventTypeA, eventTypeB) => eventTypeB.position - eventTypeA.position),
152+
};
153+
});
138154
} else {
139155
const user = await prisma.user.findUnique({
140156
where: {
@@ -155,12 +171,16 @@ const getEventTypes = async (userId: number, teamIds?: number[]) => {
155171
});
156172

157173
if (user) {
174+
const filteredEventTypes = isConferencing
175+
? filterEventTypesWhereLocationUpdateIsAllowed(user.eventTypes)
176+
: user.eventTypes;
177+
158178
eventTypeGroups.push({
159179
userId: user.id,
160180
slug: user.username,
161181
name: user.name,
162182
image: getPlaceholderAvatar(user.avatarUrl, user.name),
163-
eventTypes: user.eventTypes
183+
eventTypes: filteredEventTypes
164184
.map((item) => ({
165185
...item,
166186
URL: `${CAL_URL}/${item.team ? `team/${item.team.slug}` : item?.users?.[0]?.username}/${
@@ -208,7 +228,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
208228
const _ = stepsEnum.parse(parsedStepParam);
209229
const session = await getServerSession({ req });
210230
if (!session?.user?.id) return { redirect: { permanent: false, destination: "/auth/login" } };
211-
const locale = await getLocale(context.req);
231+
const _locale = await getLocale(context.req);
212232
const app = await getAppBySlug(parsedAppSlug);
213233
if (!app) return { redirect: { permanent: false, destination: "/apps" } };
214234
const appMetadata = appStoreMetadata[app.dirName as keyof typeof appStoreMetadata];
@@ -246,11 +266,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
246266
}
247267
if (isOrg) {
248268
const teamIds = userTeams.map((item) => item.id);
249-
eventTypeGroups = await getEventTypes(user.id, teamIds);
269+
eventTypeGroups = await getEventTypes({ userId: user.id, teamIds, isConferencing });
250270
} else if (parsedTeamIdParam) {
251-
eventTypeGroups = await getEventTypes(user.id, [parsedTeamIdParam]);
271+
eventTypeGroups = await getEventTypes({ userId: user.id, teamIds: [parsedTeamIdParam], isConferencing });
252272
} else {
253-
eventTypeGroups = await getEventTypes(user.id);
273+
eventTypeGroups = await getEventTypes({ userId: user.id, isConferencing });
254274
}
255275
if (isConferencing && eventTypeGroups) {
256276
const destinationCalendar = await prisma.destinationCalendar.findFirst({

0 commit comments

Comments
 (0)