fix(application): validate registry org ownership on application.update

This commit is contained in:
detail-app[bot]
2026-09-04 02:52:13 +00:00
committed by GitHub
parent 1572008cdf
commit 1276d40ff5
4 changed files with 135 additions and 0 deletions
@@ -0,0 +1,57 @@
import { apiUpdateApplication } from "@dokploy/server/db/schema";
import { describe, expect, it } from "vitest";
const APP_ID = "app-1";
describe("apiUpdateApplication — registry FK columns", () => {
it("keeps registryId, buildRegistryId and rollbackRegistryId writable as strings", () => {
const r = apiUpdateApplication.safeParse({
applicationId: APP_ID,
registryId: "r1",
buildRegistryId: "r2",
rollbackRegistryId: "r3",
});
expect(r.success).toBe(true);
if (r.success) {
expect(r.data.registryId).toBe("r1");
expect(r.data.buildRegistryId).toBe("r2");
expect(r.data.rollbackRegistryId).toBe("r3");
}
});
it("accepts null to clear the registry FKs (registry/rollback picker 'None')", () => {
const r = apiUpdateApplication.safeParse({
applicationId: APP_ID,
registryId: null,
buildRegistryId: null,
rollbackRegistryId: null,
});
expect(r.success).toBe(true);
if (r.success) {
expect(r.data.registryId).toBeNull();
expect(r.data.buildRegistryId).toBeNull();
expect(r.data.rollbackRegistryId).toBeNull();
}
});
it("still omits serverId from the writable surface", () => {
const r = apiUpdateApplication.safeParse({
applicationId: APP_ID,
serverId: "srv-1",
} as any);
expect(r.success).toBe(true);
if (r.success) {
expect((r.data as Record<string, unknown>).serverId).toBeUndefined();
expect("serverId" in r.data).toBe(false);
}
});
it("requires applicationId (rejects missing and empty)", () => {
expect(apiUpdateApplication.safeParse({ registryId: "r1" }).success).toBe(
false,
);
expect(apiUpdateApplication.safeParse({ applicationId: "" }).success).toBe(
false,
);
});
});
@@ -0,0 +1,50 @@
import { db } from "@dokploy/server/db";
import { assertRegistryBelongsToOrganization } from "@dokploy/server/services/registry";
import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
// db is globally mocked in __test__/setup.ts; each test overrides
// db.query.registry.findFirst to model the row findRegistryById resolves.
const ORG = "org-1";
const OTHER_ORG = "org-2";
const REGISTRY_ID = "reg-1";
function mockRegistry(organizationId: string) {
vi.mocked(db.query.registry.findFirst).mockResolvedValue({
registryId: REGISTRY_ID,
organizationId,
} as any);
}
beforeEach(() => {
vi.clearAllMocks();
});
describe("assertRegistryBelongsToOrganization", () => {
it("resolves when the registry belongs to the caller's organization", async () => {
mockRegistry(ORG);
await expect(
assertRegistryBelongsToOrganization(REGISTRY_ID, ORG),
).resolves.toBeUndefined();
});
it("rejects a registry from another organization with UNAUTHORIZED", async () => {
mockRegistry(OTHER_ORG);
const err = await assertRegistryBelongsToOrganization(
REGISTRY_ID,
ORG,
).catch((e: unknown) => e);
expect(err).toBeInstanceOf(TRPCError);
expect((err as TRPCError).code).toBe("UNAUTHORIZED");
expect((err as TRPCError).message).toBe(
"You are not authorized to use this registry",
);
});
it("propagates NOT_FOUND when the referenced registry does not exist", async () => {
vi.mocked(db.query.registry.findFirst).mockResolvedValue(undefined as any);
await expect(
assertRegistryBelongsToOrganization("missing", ORG),
).rejects.toMatchObject({ code: "NOT_FOUND" });
});
});
@@ -1,4 +1,5 @@
import {
assertRegistryBelongsToOrganization,
clearOldDeployments,
createApplication,
createDomain,
@@ -774,6 +775,20 @@ export const applicationRouter = createTRPCRouter({
}
}
const organizationId = ctx.session.activeOrganizationId;
for (const candidateRegistryId of [
input.registryId,
input.rollbackRegistryId,
input.buildRegistryId,
]) {
if (candidateRegistryId) {
await assertRegistryBelongsToOrganization(
candidateRegistryId,
organizationId,
);
}
}
const { applicationId, ...rest } = input;
const updateApp = await updateApplication(applicationId, {
...rest,
+13
View File
@@ -198,6 +198,19 @@ export const findRegistryByIdWithCredentials = async (registryId: string) => {
return registryResponse;
};
export const assertRegistryBelongsToOrganization = async (
registryId: string,
organizationId: string,
) => {
const registryResult = await findRegistryById(registryId);
if (registryResult.organizationId !== organizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to use this registry",
});
}
};
export const findAllRegistryByOrganizationId = async (
organizationId: string,
) => {