Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion apps/web/app/api/cancel/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ async function handler(req: NextRequest) {
}
const bookingData = bookingCancelWithCsrfSchema.parse(appDirRequestBody);

// Integer IDs are sequential/guessable — only accept high-entropy UIDs on this route
if (!bookingData.uid) {
return NextResponse.json(
{ success: false, message: "uid is required for booking cancellation" },
{ status: 400 }
);
}

const csrfError = await validateCsrfToken(bookingData.csrfToken);
if (csrfError) {
return csrfError;
Expand All @@ -38,8 +46,11 @@ async function handler(req: NextRequest) {
identifier,
});

// Strip integer id to ensure lookup is always by uid
const { id: _id, ...safeBookingData } = bookingData;

const result = await handleCancelBooking({
bookingData,
bookingData: safeBookingData,
userId: session?.user?.id || -1,
userUuid: session?.user?.uuid,
actionSource: "WEBAPP",
Expand Down
1 change: 1 addition & 0 deletions apps/web/playwright/lib/orgMigration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ async function dbRemoveUserFromOrg({

await ProfileRepository.deleteMany({
userIds: [userToRemoveFromOrg.id],
organizationId: userToRemoveFromOrg.organizationId!,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export interface ISyncFormData {
id: string;
name: string;
credentialId?: number;
integration?: AttributeSyncIntegrations;
enabled: boolean;
organizationId: number;
ruleId: string;
Expand Down Expand Up @@ -138,7 +139,7 @@ export interface IIntegrationAttributeSyncUpdateParams {
integrationAttributeSync: Omit<
IntegrationAttributeSync,
"attributeSyncRule" | "syncFieldMappings" | "integration"
>;
> & { integration?: AttributeSyncIntegrations };
attributeSyncRule: AttributeSyncRule;
fieldMappingsToCreate: Omit<AttributeSyncFieldMapping, "id">[];
fieldMappingsToUpdate: AttributeSyncFieldMapping[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,11 @@ export class IntegrationAttributeSyncService {
async getAllByCredentialId(credentialId: number) {
return this.deps.integrationAttributeSyncRepository.getAllByCredentialId(credentialId);
}

async validateCredentialBelongsToOrg(credentialId: number, organizationId: number) {
return this.deps.credentialRepository.findByIdAndTeamId({
id: credentialId,
teamId: organizationId,
});
}
}
20 changes: 15 additions & 5 deletions packages/features/eventtypes/lib/getPublicEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,16 +345,26 @@ export const getPublicEvent = async (
bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }),
restrictionScheduleId: null,
useBookerTimezone: false,
// Clears meta data since we don't want to send this in the public api.
// Only return fields consumed by the booker.
subsetOfUsers: users.map((user) => ({
...user,
metadata: undefined,
name: user.name,
username: user.username,
avatarUrl: user.avatarUrl,
weekStart: user.weekStart,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
profile: user.profile,
bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null),
})),
users: fetchAllUsers
? users.map((user) => ({
...user,
metadata: undefined,
name: user.name,
username: user.username,
avatarUrl: user.avatarUrl,
weekStart: user.weekStart,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
profile: user.profile,
bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null),
}))
: undefined,
Expand Down
4 changes: 2 additions & 2 deletions packages/features/profile/repositories/ProfileRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,10 +400,10 @@ export class ProfileRepository implements IProfileRepository {
});
}

static deleteMany({ userIds }: { userIds: number[] }) {
static deleteMany({ userIds, organizationId }: { userIds: number[]; organizationId: number }) {
// Even though there can be just one profile matching a userId and organizationId, we are using deleteMany as it won't error if the profile doesn't exist
return prisma.profile.deleteMany({
where: { userId: { in: userIds } },
where: { userId: { in: userIds }, organizationId },
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { getIntegrationAttributeSyncService } from "@calcom/ee/integration-attribute-sync/di/IntegrationAttributeSyncService.container";
import {
AttributeSyncIntegrations,
type ISyncFormData,
} from "@calcom/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository";
import {
DuplicateAttributeWithinSyncError,
DuplicateAttributeAcrossSyncsError,
Expand Down Expand Up @@ -37,8 +41,29 @@ const updateAttributeSyncHandler = async ({ ctx, input }: UpdateAttributeSyncOpt
throw new TRPCError({ code: "UNAUTHORIZED" });
}

// Never trust user-supplied organizationId — override with authenticated user's org
const safeInput: ISyncFormData = { ...input, organizationId: org.id };

// Validate credential belongs to the user's organization and derive integration type
if (input.credentialId !== undefined) {
const credential = await integrationAttributeSyncService.validateCredentialBelongsToOrg(
input.credentialId,
org.id
);
if (!credential) {
throw new TRPCError({ code: "NOT_FOUND", message: "Credential not found" });
}

const integrationValue = credential.app?.slug || credential.type;
if (!Object.values(AttributeSyncIntegrations).includes(integrationValue as AttributeSyncIntegrations)) {
throw new TRPCError({ code: "BAD_REQUEST", message: `Unsupported integration type: ${integrationValue}` });
}

safeInput.integration = integrationValue as AttributeSyncIntegrations;
}

try {
await integrationAttributeSyncService.updateIncludeRulesAndMappings(input);
await integrationAttributeSyncService.updateIncludeRulesAndMappings(safeInput);
} catch (error) {
if (error instanceof DuplicateAttributeWithinSyncError) {
throw new TRPCError({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export async function bulkDeleteUsersHandler({ ctx, input }: BulkDeleteUsersHand
id: {
in: input.userIds,
},
organizationId: currentUserOrgId,
},
data: {
organizationId: null,
Expand Down Expand Up @@ -130,6 +131,7 @@ export async function bulkDeleteUsersHandler({ ctx, input }: BulkDeleteUsersHand

const removeProfiles = ProfileRepository.deleteMany({
userIds: input.userIds,
organizationId: currentUserOrgId,
});

// We do this in a transaction to make sure that all memberships are removed before we remove the organization relation from the user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ensureAvailableUsers } from "@calcom/features/bookings/lib/handleNewBoo
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
import type { IsFixedAwareUser } from "@calcom/features/bookings/lib/handleNewBooking/types";
import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { getBookingAccessService } from "@calcom/features/di/containers/BookingAccessService";
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
import { withSelectedCalendars } from "@calcom/features/users/repositories/UserRepository";
import { ErrorCode } from "@calcom/lib/errorCodes";
Expand All @@ -12,6 +13,8 @@ import type { PrismaClient } from "@calcom/prisma";

import type { TrpcSessionUser } from "@calcom/trpc/server/types";

import { TRPCError } from "@trpc/server";

import type { TGetManagedEventUsersToReassignInputSchema } from "./getManagedEventUsersToReassign.schema";

type GetManagedEventUsersToReassignOptions = {
Expand Down Expand Up @@ -74,6 +77,16 @@ export const getManagedEventUsersToReassign = async ({
prefix: ["gettingManagedEventUsersToReassign", `${bookingId}`],
});

const bookingAccessService = getBookingAccessService();
const isAllowed = await bookingAccessService.doesUserIdHaveAccessToBooking({
userId: user.id,
bookingId,
});

if (!isAllowed) {
throw new TRPCError({ code: "FORBIDDEN", message: "You do not have permission" });
}

const bookingRepository = new BookingRepository(prisma);
const eventTypeRepository = new EventTypeRepository(prisma);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { enrichUsersWithDelegationCredentials } from "@calcom/app-store/delegati
import dayjs from "@calcom/dayjs";
import { ensureAvailableUsers } from "@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers";
import type { IsFixedAwareUser } from "@calcom/features/bookings/lib/handleNewBooking/types";
import { getBookingAccessService } from "@calcom/features/di/containers/BookingAccessService";
import { withSelectedCalendars } from "@calcom/features/users/repositories/UserRepository";
import { ErrorCode } from "@calcom/lib/errorCodes";
import logger from "@calcom/lib/logger";
Expand All @@ -10,6 +11,8 @@ import { userSelect } from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";

import { TRPCError } from "@trpc/server";

import type { TGetRoundRobinHostsToReassignInputSchema } from "./getRoundRobinHostsToReasign.schema";

type GetRoundRobinHostsToReassignOptions = {
Expand Down Expand Up @@ -111,6 +114,16 @@ export const getRoundRobinHostsToReassign = async ({ ctx, input }: GetRoundRobin
prefix: ["gettingRoundRobinHostsToReassign", `${bookingId}`],
});

const bookingAccessService = getBookingAccessService();
const isAllowed = await bookingAccessService.doesUserIdHaveAccessToBooking({
userId: user.id,
bookingId,
});

if (!isAllowed) {
throw new TRPCError({ code: "FORBIDDEN", message: "You do not have permission" });
}

const booking = await prisma.booking.findUniqueOrThrow({
where: { id: bookingId },
select: {
Expand Down
Loading