Skip to content

Commit d7af827

Browse files
authored
Merge pull request #4279 from Dokploy/fix/GHSA-7wmr-57mg-h5q6-schedule-authz
fix(schedule): add authz checks for server and host-level schedules
2 parents 98a5864 + c3fa638 commit d7af827

1 file changed

Lines changed: 260 additions & 1 deletion

File tree

apps/dokploy/server/api/routers/schedule.ts

Lines changed: 260 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,25 @@ import {
77
updateScheduleSchema,
88
} from "@dokploy/server/db/schema/schedule";
99
import { runCommand } from "@dokploy/server/index";
10-
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
10+
import {
11+
checkPermission,
12+
checkServicePermissionAndAccess,
13+
findMemberByUserId,
14+
} from "@dokploy/server/services/permission";
1115
import {
1216
createSchedule,
1317
deleteSchedule,
1418
findScheduleById,
1519
updateSchedule,
1620
} from "@dokploy/server/services/schedule";
21+
import { findServerById } from "@dokploy/server/services/server";
1722
import { TRPCError } from "@trpc/server";
1823
import { asc, desc, eq } from "drizzle-orm";
1924
import { z } from "zod";
2025
import { audit } from "@/server/api/utils/audit";
2126
import { removeJob, schedule } from "@/server/utils/backup";
2227
import { createTRPCRouter, protectedProcedure } from "../trpc";
28+
2329
export const scheduleRouter = createTRPCRouter({
2430
create: protectedProcedure
2531
.input(createScheduleSchema)
@@ -29,6 +35,45 @@ export const scheduleRouter = createTRPCRouter({
2935
await checkServicePermissionAndAccess(ctx, serviceId, {
3036
schedule: ["create"],
3137
});
38+
} else {
39+
if (input.scheduleType === "dokploy-server" && IS_CLOUD) {
40+
throw new TRPCError({
41+
code: "FORBIDDEN",
42+
message:
43+
"Host-level schedules are not available in the cloud version.",
44+
});
45+
}
46+
47+
await checkPermission(ctx, { schedule: ["create"] });
48+
49+
if (
50+
input.scheduleType === "server" ||
51+
input.scheduleType === "dokploy-server"
52+
) {
53+
const member = await findMemberByUserId(
54+
ctx.user.id,
55+
ctx.session.activeOrganizationId,
56+
);
57+
if (member.role !== "owner" && member.role !== "admin") {
58+
throw new TRPCError({
59+
code: "FORBIDDEN",
60+
message:
61+
"Only owners and admins can manage server-level schedules.",
62+
});
63+
}
64+
}
65+
66+
if (input.scheduleType === "server" && input.serverId) {
67+
const targetServer = await findServerById(input.serverId);
68+
if (
69+
targetServer.organizationId !== ctx.session.activeOrganizationId
70+
) {
71+
throw new TRPCError({
72+
code: "UNAUTHORIZED",
73+
message: "You don't have access to this server.",
74+
});
75+
}
76+
}
3277
}
3378
const newSchedule = await createSchedule(input);
3479

@@ -57,12 +102,77 @@ export const scheduleRouter = createTRPCRouter({
57102
.input(updateScheduleSchema)
58103
.mutation(async ({ input, ctx }) => {
59104
const existingSchedule = await findScheduleById(input.scheduleId);
105+
106+
if (
107+
IS_CLOUD &&
108+
input.scheduleType &&
109+
input.scheduleType !== existingSchedule.scheduleType
110+
) {
111+
throw new TRPCError({
112+
code: "FORBIDDEN",
113+
message: "Changing scheduleType is not allowed in the cloud version.",
114+
});
115+
}
116+
60117
const serviceId =
61118
existingSchedule.applicationId || existingSchedule.composeId;
62119
if (serviceId) {
63120
await checkServicePermissionAndAccess(ctx, serviceId, {
64121
schedule: ["update"],
65122
});
123+
} else {
124+
if (existingSchedule.scheduleType === "dokploy-server" && IS_CLOUD) {
125+
throw new TRPCError({
126+
code: "FORBIDDEN",
127+
message:
128+
"Host-level schedules are not available in the cloud version.",
129+
});
130+
}
131+
132+
await checkPermission(ctx, { schedule: ["update"] });
133+
134+
if (
135+
existingSchedule.scheduleType === "server" ||
136+
existingSchedule.scheduleType === "dokploy-server"
137+
) {
138+
const member = await findMemberByUserId(
139+
ctx.user.id,
140+
ctx.session.activeOrganizationId,
141+
);
142+
if (member.role !== "owner" && member.role !== "admin") {
143+
throw new TRPCError({
144+
code: "FORBIDDEN",
145+
message:
146+
"Only owners and admins can manage server-level schedules.",
147+
});
148+
}
149+
}
150+
151+
if (
152+
existingSchedule.scheduleType === "server" &&
153+
existingSchedule.serverId
154+
) {
155+
const targetServer = await findServerById(existingSchedule.serverId);
156+
if (
157+
targetServer.organizationId !== ctx.session.activeOrganizationId
158+
) {
159+
throw new TRPCError({
160+
code: "UNAUTHORIZED",
161+
message: "You don't have access to this server.",
162+
});
163+
}
164+
}
165+
166+
if (
167+
existingSchedule.scheduleType === "dokploy-server" &&
168+
existingSchedule.userId &&
169+
existingSchedule.userId !== ctx.user.id
170+
) {
171+
throw new TRPCError({
172+
code: "UNAUTHORIZED",
173+
message: "You can only manage your own host-level schedules.",
174+
});
175+
}
66176
}
67177
const updatedSchedule = await updateSchedule(input);
68178

@@ -107,6 +217,56 @@ export const scheduleRouter = createTRPCRouter({
107217
await checkServicePermissionAndAccess(ctx, serviceId, {
108218
schedule: ["delete"],
109219
});
220+
} else {
221+
if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) {
222+
throw new TRPCError({
223+
code: "FORBIDDEN",
224+
message:
225+
"Host-level schedules are not available in the cloud version.",
226+
});
227+
}
228+
229+
await checkPermission(ctx, { schedule: ["delete"] });
230+
231+
if (
232+
scheduleItem.scheduleType === "server" ||
233+
scheduleItem.scheduleType === "dokploy-server"
234+
) {
235+
const member = await findMemberByUserId(
236+
ctx.user.id,
237+
ctx.session.activeOrganizationId,
238+
);
239+
if (member.role !== "owner" && member.role !== "admin") {
240+
throw new TRPCError({
241+
code: "FORBIDDEN",
242+
message:
243+
"Only owners and admins can manage server-level schedules.",
244+
});
245+
}
246+
}
247+
248+
if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) {
249+
const targetServer = await findServerById(scheduleItem.serverId);
250+
if (
251+
targetServer.organizationId !== ctx.session.activeOrganizationId
252+
) {
253+
throw new TRPCError({
254+
code: "UNAUTHORIZED",
255+
message: "You don't have access to this server.",
256+
});
257+
}
258+
}
259+
260+
if (
261+
scheduleItem.scheduleType === "dokploy-server" &&
262+
scheduleItem.userId &&
263+
scheduleItem.userId !== ctx.user.id
264+
) {
265+
throw new TRPCError({
266+
code: "UNAUTHORIZED",
267+
message: "You can only manage your own host-level schedules.",
268+
});
269+
}
110270
}
111271
await deleteSchedule(input.scheduleId);
112272

@@ -148,6 +308,30 @@ export const scheduleRouter = createTRPCRouter({
148308
await checkServicePermissionAndAccess(ctx, input.id, {
149309
schedule: ["read"],
150310
});
311+
} else {
312+
await checkPermission(ctx, { schedule: ["read"] });
313+
314+
if (input.scheduleType === "server") {
315+
const targetServer = await findServerById(input.id);
316+
if (
317+
targetServer.organizationId !== ctx.session.activeOrganizationId
318+
) {
319+
throw new TRPCError({
320+
code: "UNAUTHORIZED",
321+
message: "You don't have access to this server.",
322+
});
323+
}
324+
}
325+
326+
if (
327+
input.scheduleType === "dokploy-server" &&
328+
input.id !== ctx.user.id
329+
) {
330+
throw new TRPCError({
331+
code: "UNAUTHORIZED",
332+
message: "You can only list your own host-level schedules.",
333+
});
334+
}
151335
}
152336
const where = {
153337
application: eq(schedules.applicationId, input.id),
@@ -178,6 +362,31 @@ export const scheduleRouter = createTRPCRouter({
178362
await checkServicePermissionAndAccess(ctx, serviceId, {
179363
schedule: ["read"],
180364
});
365+
} else {
366+
await checkPermission(ctx, { schedule: ["read"] });
367+
368+
if (schedule.scheduleType === "server" && schedule.serverId) {
369+
const targetServer = await findServerById(schedule.serverId);
370+
if (
371+
targetServer.organizationId !== ctx.session.activeOrganizationId
372+
) {
373+
throw new TRPCError({
374+
code: "UNAUTHORIZED",
375+
message: "You don't have access to this schedule.",
376+
});
377+
}
378+
}
379+
380+
if (
381+
schedule.scheduleType === "dokploy-server" &&
382+
schedule.userId &&
383+
schedule.userId !== ctx.user.id
384+
) {
385+
throw new TRPCError({
386+
code: "UNAUTHORIZED",
387+
message: "You don't have access to this schedule.",
388+
});
389+
}
181390
}
182391
return schedule;
183392
}),
@@ -191,6 +400,56 @@ export const scheduleRouter = createTRPCRouter({
191400
await checkServicePermissionAndAccess(ctx, serviceId, {
192401
schedule: ["create"],
193402
});
403+
} else {
404+
if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) {
405+
throw new TRPCError({
406+
code: "FORBIDDEN",
407+
message:
408+
"Host-level schedules are not available in the cloud version.",
409+
});
410+
}
411+
412+
await checkPermission(ctx, { schedule: ["create"] });
413+
414+
if (
415+
scheduleItem.scheduleType === "server" ||
416+
scheduleItem.scheduleType === "dokploy-server"
417+
) {
418+
const member = await findMemberByUserId(
419+
ctx.user.id,
420+
ctx.session.activeOrganizationId,
421+
);
422+
if (member.role !== "owner" && member.role !== "admin") {
423+
throw new TRPCError({
424+
code: "FORBIDDEN",
425+
message:
426+
"Only owners and admins can manage server-level schedules.",
427+
});
428+
}
429+
}
430+
431+
if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) {
432+
const targetServer = await findServerById(scheduleItem.serverId);
433+
if (
434+
targetServer.organizationId !== ctx.session.activeOrganizationId
435+
) {
436+
throw new TRPCError({
437+
code: "UNAUTHORIZED",
438+
message: "You don't have access to this server.",
439+
});
440+
}
441+
}
442+
443+
if (
444+
scheduleItem.scheduleType === "dokploy-server" &&
445+
scheduleItem.userId &&
446+
scheduleItem.userId !== ctx.user.id
447+
) {
448+
throw new TRPCError({
449+
code: "UNAUTHORIZED",
450+
message: "You can only manage your own host-level schedules.",
451+
});
452+
}
194453
}
195454
try {
196455
await runCommand(input.scheduleId);

0 commit comments

Comments
 (0)