Skip to content

fix: share global schedules across org admins/owners#4334

Open
ne1i wants to merge 1 commit intoDokploy:canaryfrom
ne1i:fix/global-schedules-org-sharing
Open

fix: share global schedules across org admins/owners#4334
ne1i wants to merge 1 commit intoDokploy:canaryfrom
ne1i:fix/global-schedules-org-sharing

Conversation

@ne1i
Copy link
Copy Markdown

@ne1i ne1i commented Apr 30, 2026

Closes #4300

dokploy-server schedules were scoped to the creating user's ID in both the list query and the update/delete/one/runManually access checks. This meant an admin couldn't see or manage global schedules created by the owner (and vice versa), even though both are in the same org.

Server schedules and application schedules don't have this restriction — they're org-scoped. This change brings global schedules in line.

schedule.list: admins and owners now get all dokploy-server schedules for every member in the active org. Members still only see their own.

schedule.update / delete / one / runManually: instead of blocking when schedule.userId !== ctx.user.id, admins and owners can now operate on any dokploy-server schedule whose owner belongs to the org (verified via findMemberByUserId). Members still only get their own.

Greptile Summary

This PR replaces the per-user ownership gate on dokploy-server schedules with a role-based org-scoped check, correctly fixing the listing path for admins/owners. However, in update, delete, one, and runManually the newly introduced scheduleOwnerInOrg query result is never acted upon — the if (scheduleOwnerInOrg) { /* allowed */ } body is empty and there is no else { throw } branch, so org-boundary enforcement has been silently removed for those four operations.

Confidence Score: 3/5

Not safe to merge as-is — the authorization check introduced for cross-org scoping is dead code, leaving multi-org self-hosted deployments without an org boundary on dokploy-server schedule mutations.

One P1 finding: the scheduleOwnerInOrg guard is a no-op across four procedures, meaning the org boundary was removed rather than relaxed. The listing fix is correct. Score sits at 3 because the same dead-code pattern is repeated in four separate procedures, indicating a systemic issue rather than a one-off oversight.

apps/dokploy/server/api/routers/schedule.ts — specifically the authorization blocks in update (lines 167-187), delete (271-288), one (436-445), and runManually (500-517).

Security Review

  • Cross-org IDOR (update, delete, one, runManually): The scheduleOwnerInOrg membership check is computed but its result is unused. Any admin/owner in org A can modify or execute a dokploy-server schedule created by a user in org B by knowing its scheduleId. Mitigated in practice by the IS_CLOUD guard that blocks dokploy-server schedules in the hosted environment, but the flaw exists in all self-hosted multi-org deployments.

Reviews (2): Last reviewed commit: "fix: share global schedules across org a..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@ne1i ne1i requested a review from Siumauricio as a code owner April 30, 2026 14:56
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. pr-open labels Apr 30, 2026
Comment on lines +186 to +189
await findMemberByUserId(
existingSchedule.userId,
ctx.session.activeOrganizationId,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Orphaned schedules become permanently unmanageable

findMemberByUserId throws UNAUTHORIZED when the target user is not found in the org. If a user who created a dokploy-server schedule leaves the organisation, this call throws and blocks admins from updating, deleting, viewing (one), or running that schedule — the schedule becomes a ghost that keeps executing but can never be managed. The intent is to verify cross-org access, but the current implementation conflates "schedule owner is in-org" with "the operation is permitted". An admin should still be able to delete a schedule whose creator has since departed. Consider replacing the throw with a simple org-membership check and allowing the operation to proceed (or explicitly choosing to block it on departure), rather than silently making the schedule unmanageable.

Comment on lines 172 to 185
const currentMember = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (
existingSchedule.scheduleType === "dokploy-server" &&
existingSchedule.userId &&
existingSchedule.userId !== ctx.user.id
currentMember.role !== "owner" &&
currentMember.role !== "admin"
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only manage your own host-level schedules.",
message:
"You can only manage your own host-level schedules.",
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant member lookup for the current user

The pre-existing owner/admin guard at line 139 already calls findMemberByUserId(ctx.user.id, ...) and throws if the user is not an owner or admin. By the time execution reaches this new inner block (which only triggers when userId !== ctx.user.id), only owners/admins can be here — so the call on line 172 and the role check on lines 176–185 always pass and add an unnecessary extra DB round-trip. The same pattern repeats in delete (lines 280–293) and runManually (lines 505–518).

Comment on lines +371 to +374
listWhere = and(
eq(schedules.scheduleType, "dokploy-server"),
inArray(schedules.userId, userIds),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 inArray with a potentially empty array

If orgMembers is somehow empty, inArray(schedules.userId, []) generates IN () which is invalid SQL and will throw a database error. In practice orgMembers always contains at least the calling admin, but adding an early-return guard (if (userIds.length === 0) return []) would make this robust and self-documenting.

Global (dokploy-server) schedules were scoped to the creator's user ID,
making them invisible to other admins and owners in the same organization.
This was inconsistent with server and application schedules which are
org-scoped.

- schedule.list: admins/owners now see all dokploy-server schedules from
  every member in the org, not just their own
- schedule.update/delete/one/runManually: admins/owners can now manage
  dokploy-server schedules created by other org members
- Use direct DB query instead of findMemberByUserId for the schedule
  owner check so schedules remain manageable after the creator leaves
  the org
@ne1i ne1i force-pushed the fix/global-schedules-org-sharing branch from f43aa6e to 057200b Compare April 30, 2026 15:08
Comment on lines +167 to 187
if (
existingSchedule.scheduleType === "dokploy-server" &&
existingSchedule.userId &&
existingSchedule.userId !== ctx.user.id
) {
// Admin/owner role already verified above.
// Don't use findMemberByUserId here — it would throw if the
// schedule owner left the org, making the schedule unmanageable.
const scheduleOwnerInOrg = await db.query.member.findFirst({
where: and(
eq(member.userId, existingSchedule.userId),
eq(member.organizationId, ctx.session.activeOrganizationId),
),
columns: { id: true },
});
if (scheduleOwnerInOrg) {
// Owner is still in org — allowed.
}
// If owner left the org, still allow the admin/owner to manage it.
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Authorization check is dead code — org boundary is never enforced

The scheduleOwnerInOrg lookup is performed but its result is never used to restrict access: the if (scheduleOwnerInOrg) { /* allowed */ } body is empty and there is no else { throw } branch. Execution always falls through to updateSchedule(input) regardless of whether the schedule owner is in the org or not. This means the only guard remaining is the admin/owner role check — org-scoping for dokploy-server schedules has been fully removed.

In a multi-org self-hosted deployment an admin from org A can update, delete, view (one), and run any dokploy-server schedule created by a user in org B by knowing its scheduleId. The same dead-code pattern repeats in delete (lines 271-288), one (lines 436-445), and runManually (lines 500-517).

If cross-org access is intentionally accepted, remove the dead lookup entirely (it wastes a DB round-trip). If within-org + orphaned-schedule access is the intended behaviour, add an explicit throw when the owner is found to belong to a different org's member record. As written, the DB query is misleading: it implies a check is being made while actually allowing everything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-open size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Global Scheduled Tasks are not shared between Owner and Admin users

1 participant