mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-19 07:01:35 +00:00
fix(application): validate registry org ownership on application.update
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
) => {
|
||||
|
||||
Reference in New Issue
Block a user