calcom/cal.diy · error · ForbiddenException
User with ID=${userId} does not own schedule with ID=${sched
Error message
User with ID=${userId} does not own schedule with ID=${schedule.id} What it means
Thrown by checkUserOwnsSchedule (a NestJS ForbiddenException → HTTP 403) when the resolved userId does not equal schedule.userId. It runs only after the schedule is confirmed to exist, so a 403 here means the resource belongs to a different user.
Source
Thrown at apps/api/v2/src/platform/schedules/schedules_2024_06_11/services/schedules.service.ts:179
return this.outputSchedulesService.getResponseSchedule(updatedSchedule);
}
async deleteUserSchedule(userId: number, scheduleId: number): Promise<Schedule> {
const existingSchedule = await this.schedulesRepository.getScheduleById(scheduleId);
if (!existingSchedule) {
throw new BadRequestException(`Schedule with ID=${scheduleId} does not exist.`);
}
this.checkUserOwnsSchedule(userId, existingSchedule);
return this.schedulesRepository.deleteScheduleById(scheduleId);
}
checkUserOwnsSchedule(userId: number, schedule: Pick<Schedule, "id" | "userId">): void {
if (userId !== schedule.userId) {
throw new ForbiddenException(`User with ID=${userId} does not own schedule with ID=${schedule.id}`);
}
}
}
View on GitHub (pinned to 176037d0af)
Solutions
- Scope the delete request to the current user's own schedules only (fetch the list first, as in 460).
- On the client, clear cached schedule state on logout / account switch to prevent stale IDs leaking across users.
- If legitimate admin deletion is needed, route through an authorized admin endpoint rather than the user-scoped one.
- Surface a 'you do not have permission' UI on HTTP 403 instead of retrying.
Example fix
// before
await schedulesService.deleteUserSchedule(currentUserId, arbitraryScheduleId);
// after
const owned = await schedulesRepository.getScheduleById(scheduleId);
if (owned?.userId !== currentUserId) {
throw new ForbiddenError('not your schedule');
}
await schedulesService.deleteUserSchedule(currentUserId, scheduleId); Defensive patterns
Strategy: validation
Validate before calling
// Verify ownership client-side using the same user context the API checks
const schedule = schedules.find(s => s.id === scheduleId);
if (!schedule || schedule.userId !== session.user.id) {
showError('You can only delete your own schedules.');
return;
}
await api.delete(`/v2/schedules/${scheduleId}`); Type guard
function isOwnedBy(schedule: unknown, userId: number): schedule is { id: number; userId: number } {
return !!schedule && typeof schedule === 'object' &&
(schedule as any).userId === userId;
} Try / catch
try {
await schedulesApi.delete(scheduleId);
} catch (e) {
if (e instanceof HttpError && e.statusCode === 403) {
notify('You do not have permission to delete this schedule.');
return;
}
throw e;
} Prevention
- Only show delete controls for schedules where schedule.userId === currentUserId.
- Clear cached selected schedule on account switch / logout.
- Never pass arbitrary user-supplied scheduleIds to the user-scoped delete endpoint.
When it happens
Trigger: Authenticated user A calls DELETE on a schedule owned by user B; a session/token was reused across accounts; an admin-style client passes an arbitrary scheduleId without scoping it to the caller.
Common situations: Cross-tenant access attempts, shared/leaked URL containing another user's scheduleId, front-end bug reusing a cached selected schedule after account switch.
Related errors
- User with ID=${userId} does not own schedule with ID=${sched
- Access denied. Either the team with ID=${teamId} does not ow
- User with ID=${userId} does not own event type with ID=${eve
- ApiKeysService - This endpoint can only be accessed using an
- Event type with id ${eventTypeId} not found
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/be4a9f83eef164c0.
Report an issue: GitHub.