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

  1. Scope the delete request to the current user's own schedules only (fetch the list first, as in 460).
  2. On the client, clear cached schedule state on logout / account switch to prevent stale IDs leaking across users.
  3. If legitimate admin deletion is needed, route through an authorized admin endpoint rather than the user-scoped one.
  4. 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

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


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/be4a9f83eef164c0. Report an issue: GitHub.