From 20dd8a2c37d6b6232d0915649eca8ff8362cbf76 Mon Sep 17 00:00:00 2001 From: "detail-app[bot]" <180357370+detail-app[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:53:22 +0000 Subject: [PATCH] fix(security): enforce org-scope on move and update procedures --- ...update-schemas-omit-environment-id.test.ts | 77 +++++++ .../application-move-org-scope.test.ts | 207 ++++++++++++++++++ .../application-update-environment-id.test.ts | 125 +++++++++++ .../database-move-org-scope.test.ts | 132 +++++++++++ .../dokploy/server/api/routers/application.ts | 24 ++ apps/dokploy/server/api/routers/compose.ts | 24 ++ apps/dokploy/server/api/routers/libsql.ts | 24 ++ apps/dokploy/server/api/routers/mariadb.ts | 24 ++ apps/dokploy/server/api/routers/mongo.ts | 24 ++ apps/dokploy/server/api/routers/mysql.ts | 24 ++ apps/dokploy/server/api/routers/postgres.ts | 24 ++ apps/dokploy/server/api/routers/redis.ts | 24 ++ packages/server/src/db/schema/application.ts | 2 +- packages/server/src/db/schema/compose.ts | 2 +- packages/server/src/db/schema/libsql.ts | 2 +- packages/server/src/db/schema/mariadb.ts | 2 +- packages/server/src/db/schema/mongo.ts | 2 +- packages/server/src/db/schema/mysql.ts | 2 +- packages/server/src/db/schema/postgres.ts | 2 +- packages/server/src/db/schema/redis.ts | 2 +- 20 files changed, 741 insertions(+), 8 deletions(-) create mode 100644 apps/dokploy/__test__/permissions/api-update-schemas-omit-environment-id.test.ts create mode 100644 apps/dokploy/__test__/permissions/application-move-org-scope.test.ts create mode 100644 apps/dokploy/__test__/permissions/application-update-environment-id.test.ts create mode 100644 apps/dokploy/__test__/permissions/database-move-org-scope.test.ts diff --git a/apps/dokploy/__test__/permissions/api-update-schemas-omit-environment-id.test.ts b/apps/dokploy/__test__/permissions/api-update-schemas-omit-environment-id.test.ts new file mode 100644 index 000000000..e2f0fe968 --- /dev/null +++ b/apps/dokploy/__test__/permissions/api-update-schemas-omit-environment-id.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; + +// The cross-org re-parenting vector through `application.update` exists for every +// service type: each apiUpdate* schema derives from createSchema.partial() and +// previously only omitted serverId, leaving environmentId settable through the +// `update` mutation with no target-env authorization. The fix omits environmentId +// from every apiUpdate* schema. These tests pin that contract so a future +// createSchema/.partial() refactor cannot silently re-introduce it. + +import { + apiUpdateCompose, + apiUpdateLibsql, + apiUpdateMariaDB, + apiUpdateMongo, + apiUpdateMySql, + apiUpdatePostgres, + apiUpdateRedis, +} from "@/server/db/schema"; + +const schemas = { + apiUpdatePostgres: { + schema: apiUpdatePostgres, + idField: "postgresId", + extra: { dockerImage: "postgres:16" }, + }, + apiUpdateRedis: { + schema: apiUpdateRedis, + idField: "redisId", + extra: { dockerImage: "redis:7" }, + }, + apiUpdateMariaDB: { + schema: apiUpdateMariaDB, + idField: "mariadbId", + extra: { dockerImage: "mariadb:11" }, + }, + apiUpdateMySql: { + schema: apiUpdateMySql, + idField: "mysqlId", + extra: { dockerImage: "mysql:8" }, + }, + apiUpdateMongo: { + schema: apiUpdateMongo, + idField: "mongoId", + extra: { dockerImage: "mongo:7" }, + }, + apiUpdateLibsql: { schema: apiUpdateLibsql, idField: "libsqlId", extra: {} }, + apiUpdateCompose: { + schema: apiUpdateCompose, + idField: "composeId", + extra: { composeFile: "services:\n web:\n image: nginx" }, + }, +}; + +describe("apiUpdate* schemas omit environmentId (no re-parenting via update)", () => { + for (const [name, { schema, idField, extra }] of Object.entries(schemas)) { + describe(name, () => { + it("drops environmentId from the parsed payload", () => { + const parsed = schema.parse({ + [idField]: "svc-1", + environmentId: "foreign-env", + } as Record); + expect(parsed).not.toHaveProperty("environmentId"); + }); + + it("still parses legitimate update fields", () => { + const parsed = schema.parse({ + [idField]: "svc-1", + ...extra, + } as Record); + expect(parsed).toHaveProperty(idField, "svc-1"); + for (const [k, v] of Object.entries(extra)) { + expect(parsed).toHaveProperty(k, v); + } + }); + }); + } +}); diff --git a/apps/dokploy/__test__/permissions/application-move-org-scope.test.ts b/apps/dokploy/__test__/permissions/application-move-org-scope.test.ts new file mode 100644 index 000000000..615f5c79f --- /dev/null +++ b/apps/dokploy/__test__/permissions/application-move-org-scope.test.ts @@ -0,0 +1,207 @@ +import { TRPCError } from "@trpc/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Bridge to the application router is exercised through tRPC's createCaller so +// the org-scope guards that live inline in the procedure (restored by the fix) +// are verified exactly as a real client request would hit them. +// +// The DB is mocked one layer down (same pattern as git-provider-idor.test.ts) +// so the real findApplicationById / findEnvironmentById / checkServicePermissionAndAccess +// run against controlled org/project tree data. + +const mockDb = vi.hoisted(() => { + const returning = vi.fn(() => + Promise.resolve([{ applicationId: "app-1", appName: "app-1" }]), + ); + const set = vi.fn(() => ({ where: () => ({ returning }) })); + const update = vi.fn(() => ({ set })); + return { + update, + _ret: returning, + _set: set, + query: { + member: { findFirst: vi.fn() }, + organizationRole: { findMany: vi.fn(() => Promise.resolve([])) }, + applications: { findFirst: vi.fn() }, + environments: { findFirst: vi.fn() }, + }, + }; +}); +vi.mock("@dokploy/server/db", () => ({ db: mockDb })); + +vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ + hasValidLicense: vi.fn(() => Promise.resolve(false)), +})); + +vi.mock("@dokploy/server/services/proprietary/audit-log", () => ({ + createAuditLog: vi.fn(() => Promise.resolve()), +})); + +// getAccessibleServerIds is only used by the application.update procedure when +// buildServerId is supplied; keep it deterministic in case it is reached. +vi.mock("@dokploy/server/services/server", () => ({ + getAccessibleServerIds: vi.fn(() => Promise.resolve(new Set())), +})); + +import { applicationRouter } from "@/server/api/routers/application"; + +const ORG_A = "org-a"; +const ORG_B = "org-b"; + +// tRPC's createCaller is typed against the full CreateContextOptions (db/req/res +// + the full better-auth Session). The procedures only read `ctx.user` and +// `ctx.session.activeOrganizationId`; typing the helper as `any` lets createCaller +// accept the minimal ctx while the returned caller stays fully typed by the router. +const ctx = (role: "owner" | "admin" | "member", org = ORG_A): any => ({ + user: { + id: "user-1", + email: "owner@example.com", + role, + ownerId: "owner-1", + }, + session: { activeOrganizationId: org }, +}); + +const appRow = (orgId: string) => ({ + applicationId: "app-1", + appName: "app-1", + environment: { project: { organizationId: orgId } }, +}); + +const envRow = (orgId: string) => ({ + environmentId: "env-target", + project: { organizationId: orgId }, +}); + +const memberRow = ( + role: "owner" | "admin" | "member", + accessedServices: string[] = ["app-1"], +) => ({ + id: "member-1", + role, + userId: "user-1", + organizationId: ORG_A, + accessedServices, + accessedProjects: [], + accessedEnvironments: [], + canCreateServices: true, + canCreateProjects: true, + canCreateEnvironments: true, + canDeleteServices: true, + canDeleteProjects: true, + canDeleteEnvironments: true, + canAccessToTraefikFiles: true, + canAccessToDocker: true, + canAccessToAPI: true, + canAccessToSSHKeys: true, + canAccessToGitProviders: true, + user: { id: "user-1", email: "owner@example.com" }, +}); + +beforeEach(() => { + vi.clearAllMocks(); + mockDb.query.member.findFirst.mockResolvedValue(memberRow("owner")); + mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_A)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A)); + mockDb.query.organizationRole.findMany.mockResolvedValue([]); + mockDb._ret.mockResolvedValue([{ applicationId: "app-1", appName: "app-1" }]); +}); + +describe("application.move org-scope guards", () => { + it("allows moving an application between environments of the same organization", async () => { + mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_A)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A)); + + const caller = applicationRouter.createCaller(ctx("owner")); + await expect( + caller.move({ + applicationId: "app-1", + targetEnvironmentId: "env-target", + }), + ).resolves.toMatchObject({ applicationId: "app-1" }); + expect(mockDb.update).toHaveBeenCalledOnce(); + }); + + it("rejects moving an application that belongs to a foreign organization (source check)", async () => { + // Headline exfiltration vector: owner of Org A pulls an Org B app into an Org A env. + mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_B)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A)); + + const caller = applicationRouter.createCaller(ctx("owner")); + await expect( + caller.move({ + applicationId: "app-1", + targetEnvironmentId: "env-target", + }), + ).rejects.toMatchObject({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this application", + }); + // The DB write must never run for a rejected move. + expect(mockDb.update).not.toHaveBeenCalled(); + }); + + it("rejects moving to a target environment that belongs to a foreign organization (target check)", async () => { + mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_A)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_B)); + + const caller = applicationRouter.createCaller(ctx("owner")); + await expect( + caller.move({ + applicationId: "app-1", + targetEnvironmentId: "env-target", + }), + ).rejects.toMatchObject({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + expect(mockDb.update).not.toHaveBeenCalled(); + }); + + it("still rejects the foreign-source move for an owner even when accessedServices would otherwise be skipped", async () => { + // Owner role bypasses the accessedServices list, so the only thing standing + // between a foreign applicationId and re-parenting is the restored source check. + mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_B)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A)); + + const caller = applicationRouter.createCaller(ctx("owner")); + await expect( + caller.move({ + applicationId: "app-foreign", + targetEnvironmentId: "env-target", + }), + ).rejects.toMatchObject({ code: "UNAUTHORIZED" }); + expect(mockDb.update).not.toHaveBeenCalled(); + }); + + it("rejects a non-owner (with access to the source app) moving to a foreign organization (orphaning path)", async () => { + mockDb.query.member.findFirst.mockResolvedValue( + memberRow("member", ["app-1"]), + ); + mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_A)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_B)); + + const caller = applicationRouter.createCaller(ctx("member")); + await expect( + caller.move({ + applicationId: "app-1", + targetEnvironmentId: "env-target", + }), + ).rejects.toMatchObject({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + expect(mockDb.update).not.toHaveBeenCalled(); + }); + + it("throws a TRPCError (so tRPC maps the HTTP status) on the source check", async () => { + mockDb.query.applications.findFirst.mockResolvedValue(appRow(ORG_B)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A)); + + const caller = applicationRouter.createCaller(ctx("owner")); + const err = await caller + .move({ applicationId: "app-1", targetEnvironmentId: "env-target" }) + .catch((e) => e); + expect(err).toBeInstanceOf(TRPCError); + }); +}); diff --git a/apps/dokploy/__test__/permissions/application-update-environment-id.test.ts b/apps/dokploy/__test__/permissions/application-update-environment-id.test.ts new file mode 100644 index 000000000..7c7d706b2 --- /dev/null +++ b/apps/dokploy/__test__/permissions/application-update-environment-id.test.ts @@ -0,0 +1,125 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// The `application.update` mutation previously accepted `environmentId` via the +// `apiUpdateApplication` schema (derived from createSchema.partial()) and passed +// it straight through to updateApplication -> db.update(applications).set(...). +// That was a second cross-org re-parenting path with no target-env authorization. +// The fix omits `environmentId` from apiUpdateApplication so the field can no longer +// be set through update — environment changes must go through the guarded `move`. + +const mockDb = vi.hoisted(() => { + const returning = vi.fn(() => + Promise.resolve([{ applicationId: "app-1", appName: "app-1" }]), + ); + const set = vi.fn((_data: Record) => ({ + where: () => ({ returning }), + })); + const update = vi.fn(() => ({ set })); + return { + update, + _set: set, + _ret: returning, + query: { + member: { findFirst: vi.fn() }, + organizationRole: { findMany: vi.fn(() => Promise.resolve([])) }, + }, + }; +}); +vi.mock("@dokploy/server/db", () => ({ db: mockDb })); + +vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ + hasValidLicense: vi.fn(() => Promise.resolve(false)), +})); + +vi.mock("@dokploy/server/services/proprietary/audit-log", () => ({ + createAuditLog: vi.fn(() => Promise.resolve()), +})); + +vi.mock("@dokploy/server/services/server", () => ({ + getAccessibleServerIds: vi.fn(() => Promise.resolve(new Set())), +})); + +import { applicationRouter } from "@/server/api/routers/application"; +import { apiUpdateApplication } from "@/server/db/schema"; + +const ORG_A = "org-a"; + +const ctx = (): any => ({ + user: { + id: "user-1", + email: "owner@example.com", + role: "owner" as const, + ownerId: "owner-1", + }, + session: { activeOrganizationId: ORG_A }, +}); + +const memberRow = () => ({ + id: "member-1", + role: "owner", + userId: "user-1", + organizationId: ORG_A, + accessedServices: ["app-1"], + accessedProjects: [], + accessedEnvironments: [], + canCreateServices: true, + canCreateProjects: true, + canCreateEnvironments: true, + canDeleteServices: true, + canDeleteProjects: true, + canDeleteEnvironments: true, + canAccessToTraefikFiles: true, + canAccessToDocker: true, + canAccessToAPI: true, + canAccessToSSHKeys: true, + canAccessToGitProviders: true, + user: { id: "user-1", email: "owner@example.com" }, +}); + +beforeEach(() => { + vi.clearAllMocks(); + mockDb.query.member.findFirst.mockResolvedValue(memberRow()); + mockDb.query.organizationRole.findMany.mockResolvedValue([]); + mockDb._ret.mockResolvedValue([{ applicationId: "app-1", appName: "app-1" }]); +}); + +describe("apiUpdateApplication schema (environmentId omitted)", () => { + it("does not expose environmentId as a parsed key", () => { + const parsed = apiUpdateApplication.parse({ + applicationId: "app-1", + environmentId: "foreign-env", + autoDeploy: true, + }); + expect(parsed).not.toHaveProperty("environmentId"); + expect(parsed).toHaveProperty("applicationId", "app-1"); + expect(parsed).toHaveProperty("autoDeploy", true); + }); + + it("silently drops environmentId supplied as an unknown key", () => { + const parsed = apiUpdateApplication.parse({ + applicationId: "app-1", + environmentId: "foreign-env", + }); + expect(Object.keys(parsed)).not.toContain("environmentId"); + }); +}); + +describe("application.update does not re-parent via environmentId", () => { + it("ignores environmentId in the mutation payload and never writes it to the db", async () => { + const caller = applicationRouter.createCaller(ctx()); + + // A raw client could still send `environmentId` over the wire; zod now strips + // it (it's omitted from apiUpdateApplication). Cast to simulate that payload. + await caller.update({ + applicationId: "app-1", + environmentId: "foreign-env", + autoDeploy: true, + } as any); + + expect(mockDb._set).toHaveBeenCalledOnce(); + const setData = mockDb._set.mock.calls[0]![0]; + expect(setData).not.toHaveProperty("environmentId"); + // Sanity: the legitimate field still flows through. + expect(setData).toHaveProperty("autoDeploy", true); + }); +}); diff --git a/apps/dokploy/__test__/permissions/database-move-org-scope.test.ts b/apps/dokploy/__test__/permissions/database-move-org-scope.test.ts new file mode 100644 index 000000000..8a2d4f7d3 --- /dev/null +++ b/apps/dokploy/__test__/permissions/database-move-org-scope.test.ts @@ -0,0 +1,132 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Representative sibling guard: the same org-scope checks restored on +// application.move were also restored on the database-service `move` procedures +// (postgres, redis, mariadb, mysql, mongo, libsql, compose). Postgres is exercised +// here through tRPC's createCaller; the remaining siblings share the identical +// procedure shape and are covered by the schema + manual verification plan. + +const mockDb = vi.hoisted(() => { + const returning = vi.fn(() => + Promise.resolve([{ postgresId: "pg-1", appName: "pg-1" }]), + ); + const set = vi.fn(() => ({ where: () => ({ returning }) })); + const update = vi.fn(() => ({ set })); + return { + update, + _set: set, + _ret: returning, + query: { + member: { findFirst: vi.fn() }, + organizationRole: { findMany: vi.fn(() => Promise.resolve([])) }, + postgres: { findFirst: vi.fn() }, + environments: { findFirst: vi.fn() }, + }, + }; +}); +vi.mock("@dokploy/server/db", () => ({ db: mockDb })); + +vi.mock("@dokploy/server/services/proprietary/license-key", () => ({ + hasValidLicense: vi.fn(() => Promise.resolve(false)), +})); + +vi.mock("@dokploy/server/services/proprietary/audit-log", () => ({ + createAuditLog: vi.fn(() => Promise.resolve()), +})); + +import { postgresRouter } from "@/server/api/routers/postgres"; + +const ORG_A = "org-a"; +const ORG_B = "org-b"; + +const ctx = (): any => ({ + user: { + id: "user-1", + email: "owner@example.com", + role: "owner" as const, + ownerId: "owner-1", + }, + session: { activeOrganizationId: ORG_A }, +}); + +const pgRow = (orgId: string) => ({ + postgresId: "pg-1", + appName: "pg-1", + environment: { project: { organizationId: orgId } }, +}); + +const envRow = (orgId: string) => ({ + environmentId: "env-target", + project: { organizationId: orgId }, +}); + +const memberRow = () => ({ + id: "member-1", + role: "owner", + userId: "user-1", + organizationId: ORG_A, + accessedServices: ["pg-1"], + accessedProjects: [], + accessedEnvironments: [], + canCreateServices: true, + canCreateProjects: true, + canCreateEnvironments: true, + canDeleteServices: true, + canDeleteProjects: true, + canDeleteEnvironments: true, + canAccessToTraefikFiles: true, + canAccessToDocker: true, + canAccessToAPI: true, + canAccessToSSHKeys: true, + canAccessToGitProviders: true, + user: { id: "user-1", email: "owner@example.com" }, +}); + +beforeEach(() => { + vi.clearAllMocks(); + mockDb.query.member.findFirst.mockResolvedValue(memberRow()); + mockDb.query.postgres.findFirst.mockResolvedValue(pgRow(ORG_A)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A)); + mockDb.query.organizationRole.findMany.mockResolvedValue([]); +}); + +describe("postgres.move org-scope guards", () => { + it("allows moving a postgres within the same organization", async () => { + mockDb.query.postgres.findFirst.mockResolvedValue(pgRow(ORG_A)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A)); + + const caller = postgresRouter.createCaller(ctx()); + await expect( + caller.move({ postgresId: "pg-1", targetEnvironmentId: "env-target" }), + ).resolves.toMatchObject({ postgresId: "pg-1" }); + expect(mockDb.update).toHaveBeenCalledOnce(); + }); + + it("rejects moving a postgres that belongs to a foreign organization (source check)", async () => { + mockDb.query.postgres.findFirst.mockResolvedValue(pgRow(ORG_B)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_A)); + + const caller = postgresRouter.createCaller(ctx()); + await expect( + caller.move({ postgresId: "pg-1", targetEnvironmentId: "env-target" }), + ).rejects.toMatchObject({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this postgres", + }); + expect(mockDb.update).not.toHaveBeenCalled(); + }); + + it("rejects moving to a target environment in a foreign organization (target check)", async () => { + mockDb.query.postgres.findFirst.mockResolvedValue(pgRow(ORG_A)); + mockDb.query.environments.findFirst.mockResolvedValue(envRow(ORG_B)); + + const caller = postgresRouter.createCaller(ctx()); + await expect( + caller.move({ postgresId: "pg-1", targetEnvironmentId: "env-target" }), + ).rejects.toMatchObject({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + expect(mockDb.update).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 01a9534d9..26a4588d0 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -1018,6 +1018,30 @@ export const applicationRouter = createTRPCRouter({ service: ["create"], }); + const application = await findApplicationById(input.applicationId); + if ( + application.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this application", + }); + } + + const targetEnvironment = await findEnvironmentById( + input.targetEnvironmentId, + ); + if ( + targetEnvironment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + } + const updatedApplication = await db .update(applications) .set({ diff --git a/apps/dokploy/server/api/routers/compose.ts b/apps/dokploy/server/api/routers/compose.ts index 1de66824d..0dc2d00b4 100644 --- a/apps/dokploy/server/api/routers/compose.ts +++ b/apps/dokploy/server/api/routers/compose.ts @@ -786,6 +786,30 @@ export const composeRouter = createTRPCRouter({ service: ["create"], }); + const compose = await findComposeById(input.composeId); + if ( + compose.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this compose", + }); + } + + const targetEnvironment = await findEnvironmentById( + input.targetEnvironmentId, + ); + if ( + targetEnvironment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + } + const updatedCompose = await db .update(composeTable) .set({ diff --git a/apps/dokploy/server/api/routers/libsql.ts b/apps/dokploy/server/api/routers/libsql.ts index 3fdfcf55b..19a0c9687 100644 --- a/apps/dokploy/server/api/routers/libsql.ts +++ b/apps/dokploy/server/api/routers/libsql.ts @@ -433,6 +433,30 @@ export const libsqlRouter = createTRPCRouter({ service: ["create"], }); + const libsql = await findLibsqlById(input.libsqlId); + if ( + libsql.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this libsql", + }); + } + + const targetEnvironment = await findEnvironmentById( + input.targetEnvironmentId, + ); + if ( + targetEnvironment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + } + const updatedLibsql = await db .update(libsqlTable) .set({ diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index 98932f8af..5de26c830 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -460,6 +460,30 @@ export const mariadbRouter = createTRPCRouter({ service: ["create"], }); + const mariadb = await findMariadbById(input.mariadbId); + if ( + mariadb.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this mariadb", + }); + } + + const targetEnvironment = await findEnvironmentById( + input.targetEnvironmentId, + ); + if ( + targetEnvironment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + } + const updatedMariadb = await db .update(mariadbTable) .set({ diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index 42b410785..03a5228e8 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -475,6 +475,30 @@ export const mongoRouter = createTRPCRouter({ service: ["create"], }); + const mongo = await findMongoById(input.mongoId); + if ( + mongo.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this mongo", + }); + } + + const targetEnvironment = await findEnvironmentById( + input.targetEnvironmentId, + ); + if ( + targetEnvironment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + } + const updatedMongo = await db .update(mongoTable) .set({ diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index c315ec5d4..e54b513e0 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -478,6 +478,30 @@ export const mysqlRouter = createTRPCRouter({ service: ["create"], }); + const mysql = await findMySqlById(input.mysqlId); + if ( + mysql.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this mysql", + }); + } + + const targetEnvironment = await findEnvironmentById( + input.targetEnvironmentId, + ); + if ( + targetEnvironment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + } + const updatedMysql = await db .update(mysqlTable) .set({ diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index 97e398123..9b2351589 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -481,6 +481,30 @@ export const postgresRouter = createTRPCRouter({ service: ["create"], }); + const postgres = await findPostgresById(input.postgresId); + if ( + postgres.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this postgres", + }); + } + + const targetEnvironment = await findEnvironmentById( + input.targetEnvironmentId, + ); + if ( + targetEnvironment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + } + const updatedPostgres = await db .update(postgresTable) .set({ diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index fb0d8301c..a161147ed 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -462,6 +462,30 @@ export const redisRouter = createTRPCRouter({ service: ["create"], }); + const redis = await findRedisById(input.redisId); + if ( + redis.environment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move this redis", + }); + } + + const targetEnvironment = await findEnvironmentById( + input.targetEnvironmentId, + ); + if ( + targetEnvironment.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to move to this environment", + }); + } + const updatedRedis = await db .update(redisTable) .set({ diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index def6c3445..df75a9958 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -551,4 +551,4 @@ export const apiUpdateApplication = createSchema .extend({ applicationId: z.string().min(1), }) - .omit({ serverId: true }); + .omit({ serverId: true, environmentId: true }); diff --git a/packages/server/src/db/schema/compose.ts b/packages/server/src/db/schema/compose.ts index ff8c25479..e6d226022 100644 --- a/packages/server/src/db/schema/compose.ts +++ b/packages/server/src/db/schema/compose.ts @@ -264,7 +264,7 @@ export const apiUpdateCompose = createSchema composeFile: z.string().optional(), command: z.string().optional(), }) - .omit({ serverId: true }); + .omit({ serverId: true, environmentId: true }); export const apiSaveEnvironmentVariablesCompose = createSchema .pick({ diff --git a/packages/server/src/db/schema/libsql.ts b/packages/server/src/db/schema/libsql.ts index 0307991ae..1cc1d2be0 100644 --- a/packages/server/src/db/schema/libsql.ts +++ b/packages/server/src/db/schema/libsql.ts @@ -246,7 +246,7 @@ export const apiUpdateLibsql = createSchema .extend({ libsqlId: z.string().min(1), }) - .omit({ serverId: true }); + .omit({ serverId: true, environmentId: true }); export const apiRebuildLibsql = createSchema .pick({ diff --git a/packages/server/src/db/schema/mariadb.ts b/packages/server/src/db/schema/mariadb.ts index 48b48f5b6..63abe84a8 100644 --- a/packages/server/src/db/schema/mariadb.ts +++ b/packages/server/src/db/schema/mariadb.ts @@ -220,7 +220,7 @@ export const apiUpdateMariaDB = createSchema mariadbId: z.string().min(1), dockerImage: z.string().optional(), }) - .omit({ serverId: true }); + .omit({ serverId: true, environmentId: true }); export const apiRebuildMariadb = createSchema .pick({ diff --git a/packages/server/src/db/schema/mongo.ts b/packages/server/src/db/schema/mongo.ts index 5e91a72d1..35c1c7cf1 100644 --- a/packages/server/src/db/schema/mongo.ts +++ b/packages/server/src/db/schema/mongo.ts @@ -204,7 +204,7 @@ export const apiUpdateMongo = createSchema dockerImage: z.string().optional(), replicaSets: z.boolean().optional(), }) - .omit({ serverId: true }); + .omit({ serverId: true, environmentId: true }); export const apiResetMongo = createSchema .pick({ diff --git a/packages/server/src/db/schema/mysql.ts b/packages/server/src/db/schema/mysql.ts index 3a58f5aad..ef70bbff7 100644 --- a/packages/server/src/db/schema/mysql.ts +++ b/packages/server/src/db/schema/mysql.ts @@ -217,7 +217,7 @@ export const apiUpdateMySql = createSchema mysqlId: z.string().min(1), dockerImage: z.string().optional(), }) - .omit({ serverId: true }); + .omit({ serverId: true, environmentId: true }); export const apiRebuildMysql = createSchema .pick({ diff --git a/packages/server/src/db/schema/postgres.ts b/packages/server/src/db/schema/postgres.ts index 067b83e43..b4d72c55e 100644 --- a/packages/server/src/db/schema/postgres.ts +++ b/packages/server/src/db/schema/postgres.ts @@ -211,7 +211,7 @@ export const apiUpdatePostgres = createSchema postgresId: z.string().min(1), dockerImage: z.string().optional(), }) - .omit({ serverId: true }); + .omit({ serverId: true, environmentId: true }); export const apiRebuildPostgres = createSchema .pick({ diff --git a/packages/server/src/db/schema/redis.ts b/packages/server/src/db/schema/redis.ts index 95ffd8503..d8d827438 100644 --- a/packages/server/src/db/schema/redis.ts +++ b/packages/server/src/db/schema/redis.ts @@ -198,7 +198,7 @@ export const apiUpdateRedis = createSchema redisId: z.string().min(1), dockerImage: z.string().optional(), }) - .omit({ serverId: true }); + .omit({ serverId: true, environmentId: true }); export const apiRebuildRedis = createSchema .pick({