fix: harden demo-mode guard, availability scope, and image secrets
Pull Request Labeler / labeler (pull_request_target) Has been cancelled
Pull Request Labeler / apply-labels-from-issue (pull_request_target) Has been cancelled
PR Welcome Bot / Welcome new contributors (pull_request_target) Has been cancelled
PR Update / Trust Check (pull_request_target) Has been cancelled
Validate PRs / Validate PR title (pull_request_target) Has been cancelled
cleanup caches by a branch / cleanup (pull_request) Has been cancelled
/ cleanup-report (pull_request) Has been cancelled
PR Update / Prepare (pull_request_target) Has been cancelled
PR Update / Type Checks (pull_request_target) Has been cancelled
PR Update / Linters (pull_request_target) Has been cancelled
PR Update / Security Audit (pull_request_target) Has been cancelled
PR Update / Check Prisma Migrations (pull_request_target) Has been cancelled
PR Update / Setup Database (pull_request_target) Has been cancelled
PR Update / Analyze Build (pull_request_target) Has been cancelled
PR Update / required (pull_request_target) Has been cancelled
PR Update / Production builds (pull_request_target) Has been cancelled
PR Update / Tests (pull_request_target) Has been cancelled
Pull Request Labeler / labeler (pull_request_target) Has been cancelled
Pull Request Labeler / apply-labels-from-issue (pull_request_target) Has been cancelled
PR Welcome Bot / Welcome new contributors (pull_request_target) Has been cancelled
PR Update / Trust Check (pull_request_target) Has been cancelled
Validate PRs / Validate PR title (pull_request_target) Has been cancelled
cleanup caches by a branch / cleanup (pull_request) Has been cancelled
/ cleanup-report (pull_request) Has been cancelled
PR Update / Prepare (pull_request_target) Has been cancelled
PR Update / Type Checks (pull_request_target) Has been cancelled
PR Update / Linters (pull_request_target) Has been cancelled
PR Update / Security Audit (pull_request_target) Has been cancelled
PR Update / Check Prisma Migrations (pull_request_target) Has been cancelled
PR Update / Setup Database (pull_request_target) Has been cancelled
PR Update / Analyze Build (pull_request_target) Has been cancelled
PR Update / required (pull_request_target) Has been cancelled
PR Update / Production builds (pull_request_target) Has been cancelled
PR Update / Tests (pull_request_target) Has been cancelled
- Move the demo-mode check into a single shared module so the NODE_ENV!=production guard cannot drift; service.ts previously had an unguarded copy that could serve fixture data / impersonate the organizer in production. - Restrict getAvailability to unlocked team members so an authenticated viewer cannot probe free/busy for arbitrary user ids. - Pass build-time secrets inline on the build RUN instead of persisting them in an image ENV layer.
This commit is contained in:
@@ -10,11 +10,10 @@ ARG CALENDSO_ENCRYPTION_KEY=secret
|
||||
ARG DATABASE_URL=postgresql://calcom:calcom@postgres:5432/calcom
|
||||
ARG MAX_OLD_SPACE_SIZE=4096
|
||||
|
||||
# Only non-secret values persist in the image. The build-time secrets are passed
|
||||
# inline on the build RUN below so they never bake into an image layer; real
|
||||
# runtime values are injected by compose.
|
||||
ENV NODE_ENV=production \
|
||||
NEXTAUTH_SECRET=${NEXTAUTH_SECRET} \
|
||||
CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY} \
|
||||
DATABASE_URL=${DATABASE_URL} \
|
||||
DATABASE_DIRECT_URL=${DATABASE_URL} \
|
||||
NODE_OPTIONS=--max-old-space-size=${MAX_OLD_SPACE_SIZE}
|
||||
|
||||
COPY package.json yarn.lock .yarnrc.yml turbo.json i18n.json ./
|
||||
@@ -24,7 +23,11 @@ COPY packages ./packages
|
||||
|
||||
RUN yarn config set httpTimeout 1200000
|
||||
RUN yarn install
|
||||
RUN yarn workspace @vynte/scheduler build
|
||||
RUN NEXTAUTH_SECRET=${NEXTAUTH_SECRET} \
|
||||
CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY} \
|
||||
DATABASE_URL=${DATABASE_URL} \
|
||||
DATABASE_DIRECT_URL=${DATABASE_URL} \
|
||||
yarn workspace @vynte/scheduler build
|
||||
|
||||
EXPOSE 3040
|
||||
|
||||
|
||||
@@ -3,19 +3,12 @@ import { cookies, headers } from "next/headers";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { isDemoMode } from "./demo-mode";
|
||||
import { buildLegacyRequest } from "./legacy-request";
|
||||
|
||||
/** Viewer id used for the demo team's organizer when SCHEDULER_DEMO_MODE=1. */
|
||||
const DEMO_VIEWER_ID = 1;
|
||||
|
||||
/**
|
||||
* Demo mode bypasses Authentik and impersonates the organizer, so it must never
|
||||
* be honored in production even if the env var is accidentally left set.
|
||||
*/
|
||||
function isDemoMode(): boolean {
|
||||
return process.env.SCHEDULER_DEMO_MODE === "1" && process.env.NODE_ENV !== "production";
|
||||
}
|
||||
|
||||
export async function getSchedulerSession(request: NextRequest) {
|
||||
return getServerSession({ req: buildLegacyRequest(request) });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Demo mode swaps real Authentik auth and DB reads for in-memory fixtures.
|
||||
* It impersonates the organizer, so it must NEVER be honored in production even
|
||||
* if SCHEDULER_DEMO_MODE is accidentally left set. This is the single source of
|
||||
* truth for that guard — both the auth layer and the service layer import it so
|
||||
* the production check can never drift between them.
|
||||
*/
|
||||
export function isDemoMode(): boolean {
|
||||
return process.env.SCHEDULER_DEMO_MODE === "1" && process.env.NODE_ENV !== "production";
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
demoUsers,
|
||||
type DemoConnection,
|
||||
} from "./demo-data";
|
||||
import { isDemoMode } from "./demo-mode";
|
||||
import { filterBusyBlocksForViewer } from "./privacy";
|
||||
import type {
|
||||
BusyBlock,
|
||||
@@ -22,10 +23,6 @@ import { z } from "zod";
|
||||
|
||||
const DEFAULT_TIME_ZONE = "America/Denver";
|
||||
|
||||
function isDemoMode(): boolean {
|
||||
return process.env.SCHEDULER_DEMO_MODE === "1";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mappers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -396,9 +393,13 @@ export async function getAvailability(
|
||||
return { busy: filterBusyBlocksForViewer(relevant, viewerId), mutualSlots };
|
||||
}
|
||||
|
||||
// Only expose free/busy for actual teammates (single-team = unlocked users),
|
||||
// so an authenticated viewer cannot probe arbitrary user ids.
|
||||
const allowedIds = await teamMemberIds(attendeeIds);
|
||||
|
||||
const [busyBlocks, schedules] = await Promise.all([
|
||||
bookingsToBusyBlocks(attendeeIds, rangeStart, rangeEnd),
|
||||
loadSchedules(attendeeIds),
|
||||
bookingsToBusyBlocks(allowedIds, rangeStart, rangeEnd),
|
||||
loadSchedules(allowedIds),
|
||||
]);
|
||||
|
||||
const mutualSlots = computeMutualSlots({
|
||||
@@ -412,6 +413,19 @@ export async function getAvailability(
|
||||
return { busy: filterBusyBlocksForViewer(busyBlocks, viewerId), mutualSlots };
|
||||
}
|
||||
|
||||
/** Intersects requested ids with the single team (unlocked users), preserving order. */
|
||||
async function teamMemberIds(requestedIds: number[]): Promise<number[]> {
|
||||
if (requestedIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const members = await prisma.user.findMany({
|
||||
where: { id: { in: requestedIds }, locked: false },
|
||||
select: { id: true },
|
||||
});
|
||||
const allowed = new Set(members.map((member) => member.id));
|
||||
return requestedIds.filter((id) => allowed.has(id));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Meetings
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user