🚀 Release v0.29.2#4299
Conversation
The search filter on the Requests tab was incorrectly filtering by RequestPath instead of RequestHost, causing "filter by name" to match URL paths rather than hostnames. Updated the placeholder text to reflect the correct field being searched. Fixes #4249
…strictions - Added comprehensive permission checks for creating, updating, and deleting schedules based on user roles (owner/admin) and schedule types (server/dokploy-server). - Implemented restrictions for cloud users to prevent managing host-level schedules and changing schedule types. - Improved access control for server-level schedules to ensure users can only manage schedules associated with their organization.
…e-authz fix(schedule): add authz checks for server and host-level schedules
…nd WebSocket endpoints Prevents owner/admin users of one organization from accessing servers, destinations, and Docker Swarm join tokens belonging to other organizations by validating organizationId on all endpoints that accept serverId or destinationId as direct input. - cluster: validate serverId org on getNodes, addWorker, addManager, removeWorker - deployment: validate serverId org on allByServer - backup: validate destinationId + serverId org on listBackupFiles - volume-backups: validate destinationId + serverId org on restoreVolumeBackupWithLogs - wss: validate server org on docker-container-logs, docker-container-terminal, listen-deployment, and terminal WebSocket handlers - auth: fix TypeScript type for API key metadata parsing
- Implemented checks in the WebSocket server setups for Docker container logs, terminal, and deployment logs to ensure users can only access resources associated with their active organization. - Enhanced security by closing WebSocket connections if the organization ID does not match the session's active organization ID.
…rg-idor Fix/ghsa f8wj 5c4w frhg cross org idor
| if (server.organizationId !== session.activeOrganizationId) { | ||
| ws.close(); | ||
| return; | ||
| } |
There was a problem hiding this comment.
pingInterval not cleared on early org-check return
The pingInterval created on line 79 is only cleaned up inside the ws.on("close", ...) handlers registered later (lines 141 and 172), but neither handler is ever registered when the organization check fails and the function returns early. The interval will keep firing every 45 seconds for the lifetime of the process. Because ws.readyState won't equal OPEN after ws.close(), no pings are actually sent, but the interval handle accumulates across all rejected connections.
| if (server.organizationId !== session.activeOrganizationId) { | |
| ws.close(); | |
| return; | |
| } | |
| if (server.organizationId !== session.activeOrganizationId) { | |
| clearInterval(pingInterval); | |
| ws.close(); | |
| return; | |
| } |
fix: drop .zip deployment - isDate is not a function
- Added `sendInvitationEmail` function to send invitation emails when a new organization is created in the cloud environment. - Updated email template to enhance the invitation message and included a direct link for users to accept the invitation. - Refactored email sending logic in the user router to utilize the new invitation email rendering function. - Improved organization invitation email design for better user experience.
feat: implement invitation email functionality for organization creation
| const serviceId = | ||
| existingSchedule.applicationId || existingSchedule.composeId; | ||
| if (serviceId) { | ||
| await checkServicePermissionAndAccess(ctx, serviceId, { | ||
| schedule: ["update"], | ||
| }); |
There was a problem hiding this comment.
scheduleType change bypasses owner/admin role check in non-cloud mode
When an existing schedule has applicationId set, the serviceId branch is taken and only checkServicePermissionAndAccess runs. In non-cloud mode there is no guard against changing scheduleType, so a non-admin user who holds schedule:update permission on an application can send scheduleType: "server" and an arbitrary serverId in the update payload. updateSchedule(input) persists that change; the schedule then executes against a server (potentially from another org) without the owner/admin role check that the else branch enforces.
The IS_CLOUD type-change guard should also apply in self-hosted mode, or the server org/role checks should be repeated whenever input.scheduleType shifts to "server" or "dokploy-server".
…ror-responses fix: stop leaking Drizzle SQL queries in webhook error responses (#4276)
fix: filter requests by hostname instead of path
| await sendInvitationEmail({ | ||
| email, | ||
| inviteLink, | ||
| organizationName: org?.name || "organization", | ||
| }); |
There was a problem hiding this comment.
Email failure orphans the invitation record
sendInvitationEmail is called after the invitation has already been committed to the database. If the SMTP call fails (server unreachable, wrong credentials, etc.), the error propagates uncaught and TRPC returns a 500 to the client — but the invitation row is already persisted and the audit() on line 346 is skipped. On retry, the caller hits the "invitation already sent" conflict guard and gets a confusing error, since the invitation is valid in the DB but they never got a confirmation.
Contrast this with the equivalent path in user.ts, where the email send is wrapped in try/catch so a transient mailer failure doesn't fail the whole operation. The email send here should be wrapped similarly:
try {
await sendInvitationEmail({ email, inviteLink, organizationName: org?.name || "organization" });
} catch (error) {
console.error("Failed to send invitation email:", error);
}…wnstream fix: fallback to DownstreamStatus when OriginStatus is 0 in requests table
| if ( | ||
| existingSchedule.scheduleType === "server" && | ||
| existingSchedule.serverId | ||
| ) { | ||
| const targetServer = await findServerById(existingSchedule.serverId); | ||
| if ( | ||
| targetServer.organizationId !== ctx.session.activeOrganizationId | ||
| ) { | ||
| throw new TRPCError({ | ||
| code: "UNAUTHORIZED", | ||
| message: "You don't have access to this server.", | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
input.serverId not validated in update — cross-org server hijack
The org-membership check on lines 151–164 validates existingSchedule.serverId (the server currently stored), but updateSchedule(input) on line 177 persists the entire input payload — including any new serverId the caller provides. updateScheduleSchema extends createScheduleSchema which includes serverId, and updateSchedule does a plain .set(rest) on all input fields, so a user with update permission on a server-type schedule can silently redirect it to any server in any org simply by supplying a foreign serverId in the request body. Add the same org check against input.serverId before calling updateSchedule:
if (input.serverId && input.serverId !== existingSchedule.serverId) {
const newServer = await findServerById(input.serverId);
if (newServer.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this server.",
});
}
}| on: | ||
| release: | ||
| types: [published] | ||
| push: | ||
| tags: | ||
| - 'v*' |
There was a problem hiding this comment.
Double-trigger when release is published with a new tag
When a GitHub Release is created and a new tag is created in the same action (e.g., via the GitHub API or the Release UI with "Create new tag"), GitHub fires both release: published and push: tags: v* in parallel. Both workflow runs will clone the external repos, commit, and attempt to push. The first run's push succeeds; the second run fails with a non-fast-forward error because the remote has diverged. This causes a visible workflow failure on every automated release.
A common fix is to use a concurrency group with cancel-in-progress: true, or to guard with an if condition that distinguishes the two event types:
jobs:
sync-version:
concurrency:
group: sync-version-${{ github.ref }}
cancel-in-progress: true
This PR promotes changes from
canarytomainfor version v0.29.2.🔍 Changes Include:
✅ Pre-merge Checklist:
Greptile Summary
v0.29.2 release promoting canary to main. Key changes include cross-org security hardening across cluster, deployment, backup, schedule, and WebSocket handlers, a
logWebhookErrorhelper to stop leaking Drizzle SQL in webhook error responses, a redesigned invitation email template with organization name personalization, and assh2upgrade to 1.16.0.sync-version.yml: Addingpush: tags: v*alongsiderelease: publishedcauses both events to fire when a release is created with a new tag. The parallel runs both attemptgit pushto the MCP and CLI repos; the second always fails with a non-fast-forward error, leaving every automated release with a failed workflow run.Confidence Score: 4/5
Safe to merge for product code; the sync-version workflow will report a failure on every automated release until the double-trigger is fixed.
One P1 finding (workflow double-trigger causing push failures) caps the score at 4. All product-code changes are security improvements or bug fixes with no new regressions identified.
.github/workflows/sync-version.yml — double-trigger risk from simultaneous release and push:tags events.
Reviews (6): Last reviewed commit: "chore: add push trigger for version sync..." | Re-trigger Greptile