mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-13 19:09:49 +00:00
fix: add cross-org ownership checks to cluster, deployment, backup, and 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
This commit is contained in:
@@ -458,9 +458,26 @@ export const backupRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
try {
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
if (destination.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this destination.",
|
||||
});
|
||||
}
|
||||
if (input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (
|
||||
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
const rcloneFlags = getS3Credentials(destination);
|
||||
const bucketPath = `:s3:${destination.bucket}`;
|
||||
|
||||
|
||||
@@ -11,6 +11,20 @@ import { audit } from "@/server/api/utils/audit";
|
||||
import { getLocalServerIp } from "@/server/wss/terminal";
|
||||
import { createTRPCRouter, withPermission } from "../trpc";
|
||||
|
||||
const assertServerBelongsToOrg = async (
|
||||
serverId: string | undefined,
|
||||
activeOrganizationId: string,
|
||||
) => {
|
||||
if (!serverId) return;
|
||||
const targetServer = await findServerById(serverId);
|
||||
if (targetServer.organizationId !== activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const clusterRouter = createTRPCRouter({
|
||||
getNodes: withPermission("server", "read")
|
||||
.input(
|
||||
@@ -18,7 +32,11 @@ export const clusterRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
await assertServerBelongsToOrg(
|
||||
input.serverId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
const docker = await getRemoteDocker(input.serverId);
|
||||
const workers: DockerNode[] = await docker.listNodes();
|
||||
return workers;
|
||||
@@ -32,6 +50,10 @@ export const clusterRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await assertServerBelongsToOrg(
|
||||
input.serverId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
try {
|
||||
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
|
||||
const removeCommand = `docker node rm ${input.nodeId} --force`;
|
||||
@@ -65,7 +87,11 @@ export const clusterRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
await assertServerBelongsToOrg(
|
||||
input.serverId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
const docker = await getRemoteDocker(input.serverId);
|
||||
const result = await docker.swarmInspect();
|
||||
const docker_version = await docker.version();
|
||||
@@ -88,7 +114,11 @@ export const clusterRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
await assertServerBelongsToOrg(
|
||||
input.serverId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
const docker = await getRemoteDocker(input.serverId);
|
||||
const result = await docker.swarmInspect();
|
||||
const docker_version = await docker.version();
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
checkServicePermissionAndAccess,
|
||||
findMemberByUserId,
|
||||
} from "@dokploy/server/services/permission";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
@@ -52,7 +53,14 @@ export const deploymentRouter = createTRPCRouter({
|
||||
}),
|
||||
allByServer: withPermission("deployment", "read")
|
||||
.input(apiFindAllByServer)
|
||||
.query(async ({ input }) => {
|
||||
.query(async ({ input, ctx }) => {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
return await findAllDeploymentsByServerId(input.serverId);
|
||||
}),
|
||||
allCentralized: withPermission("deployment", "read").query(
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
updateVolumeBackupSchema,
|
||||
volumeBackups,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||
import { findServerById } from "@dokploy/server/services/server";
|
||||
import {
|
||||
execAsyncRemote,
|
||||
execAsyncStream,
|
||||
@@ -265,7 +267,23 @@ export const volumeBackupsRouter = createTRPCRouter({
|
||||
serverId: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.subscription(async ({ input }) => {
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const destination = await findDestinationById(input.destinationId);
|
||||
if (destination.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this destination.",
|
||||
});
|
||||
}
|
||||
if (input.serverId) {
|
||||
const targetServer = await findServerById(input.serverId);
|
||||
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You don't have access to this server.",
|
||||
});
|
||||
}
|
||||
}
|
||||
return observable<string>((emit) => {
|
||||
const runRestore = async () => {
|
||||
try {
|
||||
|
||||
@@ -85,6 +85,11 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (server.organizationId !== session.activeOrganizationId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server.sshKeyId) return;
|
||||
const client = new Client();
|
||||
client
|
||||
|
||||
@@ -61,6 +61,12 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (server.organizationId !== session.activeOrganizationId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server.sshKeyId)
|
||||
throw new Error("No SSH key available for this server");
|
||||
|
||||
|
||||
@@ -57,6 +57,11 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (server.organizationId !== session.activeOrganizationId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server.sshKeyId) {
|
||||
ws.close();
|
||||
return;
|
||||
|
||||
@@ -154,6 +154,11 @@ export const setupTerminalWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (server.organizationId !== session.activeOrganizationId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const { ipAddress: host, port, username, sshKey, sshKeyId } = server;
|
||||
|
||||
if (!sshKeyId) {
|
||||
|
||||
@@ -481,8 +481,10 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
};
|
||||
}
|
||||
|
||||
const organizationId = JSON.parse(
|
||||
apiKeyRecord.metadata || "{}",
|
||||
const organizationId = (
|
||||
JSON.parse(apiKeyRecord.metadata || "{}") as {
|
||||
organizationId?: string;
|
||||
}
|
||||
).organizationId;
|
||||
|
||||
if (!organizationId) {
|
||||
|
||||
Reference in New Issue
Block a user