mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 21:31:11 +00:00
fix(settings): check server organization in traefik file endpoints
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import path from "node:path";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
findServerById: vi.fn(),
|
||||
readDirectory: vi.fn(),
|
||||
readConfigInPath: vi.fn(),
|
||||
writeTraefikConfigInPath: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/permission", () => ({
|
||||
checkPermission: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server", () => ({
|
||||
IS_CLOUD: false,
|
||||
paths: () => ({ MAIN_TRAEFIK_PATH: "/etc/dokploy/traefik" }),
|
||||
...mocks,
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/lib/auth", () => ({
|
||||
validateRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/server/api/utils/audit", () => ({
|
||||
audit: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@/server/queues/queueSetup", () => ({
|
||||
cleanAllDeploymentQueue: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/server/utils/backup", () => ({
|
||||
removeJob: vi.fn(),
|
||||
schedule: vi.fn(),
|
||||
}));
|
||||
|
||||
const { settingsRouter } = await import("@/server/api/routers/settings");
|
||||
|
||||
const caller = settingsRouter.createCaller({
|
||||
session: { activeOrganizationId: "org-a" },
|
||||
user: { id: "user-1", email: "user@example.com", role: "member" },
|
||||
} as Parameters<typeof settingsRouter.createCaller>[0]);
|
||||
|
||||
// apiReadTraefikConfig validates against the real local paths(), which outside
|
||||
// production live under the working directory.
|
||||
const filePath = path.join(process.cwd(), ".docker/traefik/dynamic/app.yml");
|
||||
|
||||
describe("traefik file endpoints scope remote servers to the active organization", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.findServerById.mockImplementation(async (serverId: string) => ({
|
||||
serverId,
|
||||
organizationId: serverId === "server-a" ? "org-a" : "org-b",
|
||||
}));
|
||||
mocks.readDirectory.mockResolvedValue([]);
|
||||
mocks.readConfigInPath.mockResolvedValue("http: {}");
|
||||
});
|
||||
|
||||
it("rejects listing a server from another organization", async () => {
|
||||
await expect(
|
||||
caller.readDirectories({ serverId: "server-b" }),
|
||||
).rejects.toMatchObject({ code: "UNAUTHORIZED" });
|
||||
expect(mocks.readDirectory).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects writing to a server from another organization", async () => {
|
||||
await expect(
|
||||
caller.updateTraefikFile({
|
||||
path: filePath,
|
||||
traefikConfig: "http: {}",
|
||||
serverId: "server-b",
|
||||
}),
|
||||
).rejects.toMatchObject({ code: "UNAUTHORIZED" });
|
||||
expect(mocks.writeTraefikConfigInPath).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects reading from a server from another organization", async () => {
|
||||
await expect(
|
||||
caller.readTraefikFile({ path: filePath, serverId: "server-b" }),
|
||||
).rejects.toMatchObject({ code: "UNAUTHORIZED" });
|
||||
expect(mocks.readConfigInPath).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows servers of the active organization", async () => {
|
||||
await caller.readDirectories({ serverId: "server-a" });
|
||||
await caller.readTraefikFile({ path: filePath, serverId: "server-a" });
|
||||
await caller.updateTraefikFile({
|
||||
path: filePath,
|
||||
traefikConfig: "http: {}",
|
||||
serverId: "server-a",
|
||||
});
|
||||
|
||||
expect(mocks.readDirectory).toHaveBeenCalledWith(
|
||||
"/etc/dokploy/traefik",
|
||||
"server-a",
|
||||
);
|
||||
expect(mocks.readConfigInPath).toHaveBeenCalledWith(filePath, "server-a");
|
||||
expect(mocks.writeTraefikConfigInPath).toHaveBeenCalledWith(
|
||||
filePath,
|
||||
"http: {}",
|
||||
"server-a",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not look up a server when none is given", async () => {
|
||||
await caller.readDirectories({});
|
||||
await caller.updateTraefikFile({
|
||||
path: filePath,
|
||||
traefikConfig: "http: {}",
|
||||
});
|
||||
|
||||
expect(mocks.findServerById).not.toHaveBeenCalled();
|
||||
expect(mocks.writeTraefikConfigInPath).toHaveBeenCalledWith(
|
||||
filePath,
|
||||
"http: {}",
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -586,6 +586,12 @@ export const settingsRouter = createTRPCRouter({
|
||||
.query(async ({ ctx, input }) => {
|
||||
try {
|
||||
await checkPermission(ctx, { traefikFiles: ["read"] });
|
||||
if (input?.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
const { MAIN_TRAEFIK_PATH } = paths(!!input?.serverId);
|
||||
const result = await readDirectory(MAIN_TRAEFIK_PATH, input?.serverId);
|
||||
return result || [];
|
||||
@@ -598,6 +604,12 @@ export const settingsRouter = createTRPCRouter({
|
||||
.input(apiModifyTraefikConfig)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await checkPermission(ctx, { traefikFiles: ["write"] });
|
||||
if (input.serverId) {
|
||||
const server = await findServerById(input.serverId);
|
||||
if (server.organizationId !== ctx.session?.activeOrganizationId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
}
|
||||
await writeTraefikConfigInPath(
|
||||
input.path,
|
||||
input.traefikConfig,
|
||||
|
||||
Reference in New Issue
Block a user