Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions apps/dokploy/__test__/backup/restore-security.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { REDACTED_SECRET_VALUE } from "@dokploy/server/utils/security/redaction";
import { TRPCError } from "@trpc/server";
import { parse } from "shell-quote";
import { beforeEach, describe, expect, it, vi } from "vitest";
Expand Down Expand Up @@ -1225,3 +1226,59 @@ describe("backup restore command safety", () => {
);
});
});

describe("backup read redaction", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.checkServicePermissionAndAccess.mockResolvedValue(undefined);
});

it("redacts the bound compose service and metadata passwords in one", async () => {
mocks.findBackupById.mockResolvedValue({
backupId: "backup-1",
backupType: "compose",
databaseType: "mariadb",
destinationId: "destination-1",
composeId: "compose-1",
serviceName: "db",
metadata: {
mariadb: {
databaseUser: "app",
databasePassword: "metadata-password",
},
},
compose: {
composeId: "compose-1",
appName: "compose-one",
env: "COMPOSE_SECRET=compose-env-secret",
composeFile:
"services:\n db:\n environment:\n PASS: file-secret",
customGitUrl: "https://repo-user:repo-token@example.com/org/repo.git",
refreshToken: "compose-refresh-token",
},
});

const backup = await createCaller().one({ backupId: "backup-1" });

expect(backup.compose).toMatchObject({
appName: "compose-one",
env: REDACTED_SECRET_VALUE,
composeFile: REDACTED_SECRET_VALUE,
customGitUrl: `https://${REDACTED_SECRET_VALUE}@example.com/org/repo.git`,
refreshToken: REDACTED_SECRET_VALUE,
});
expect(backup.metadata).toEqual({
mariadb: { databaseUser: "app", databasePassword: REDACTED_SECRET_VALUE },
});
const serialized = JSON.stringify(backup);
for (const secret of [
"compose-env-secret",
"file-secret",
"repo-token",
"compose-refresh-token",
"metadata-password",
]) {
expect(serialized).not.toContain(secret);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,39 @@ describe("certificate router assigned-server boundary", () => {
]);
});

it("redacts the assigned server's command and metrics token from all", async () => {
mocks.certificateFindMany.mockResolvedValue([
{
certificateId: "certificate-1",
name: "accessible",
privateKey: "private-key",
organizationId: "org-1",
serverId: "server-1",
server: {
serverId: "server-1",
name: "remote",
command: "curl -H 'Authorization: Bearer server-secret' example.com",
metricsConfig: {
server: { token: "metrics-token", port: 4500 },
containers: { refreshRate: 60 },
},
},
},
]);

const [certificate] = await createCaller().all();

expect(certificate?.server).toMatchObject({
name: "remote",
command: REDACTED_SECRET_VALUE,
metricsConfig: {
server: { token: REDACTED_SECRET_VALUE, port: 4500 },
},
});
expect(JSON.stringify(certificate)).not.toContain("server-secret");
expect(JSON.stringify(certificate)).not.toContain("metrics-token");
});

it("preserves stored certificate private keys when update receives the redacted placeholder", async () => {
const result = await createCaller().update({
certificateId: "certificate-1",
Expand Down
219 changes: 219 additions & 0 deletions apps/dokploy/__test__/schedule/schedule-read-redaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { REDACTED_SECRET_VALUE } from "@dokploy/server/utils/security/redaction";
import { beforeEach, describe, expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({
checkPermission: vi.fn(),
checkServicePermissionAndAccess: vi.fn(),
findMemberByUserId: vi.fn(),
findScheduleById: vi.fn(),
schedulesFindMany: vi.fn(),
}));

vi.mock("@dokploy/server", () => ({
IS_CLOUD: false,
removeScheduleJob: vi.fn(),
scheduleJob: vi.fn(),
}));

vi.mock("@dokploy/server/db", () => ({
db: {
query: {
schedules: {
findMany: mocks.schedulesFindMany,
},
},
},
}));

vi.mock("@dokploy/server/index", () => ({
IS_CLOUD: false,
removeScheduleJob: vi.fn(),
runCommand: vi.fn(),
scheduleJob: vi.fn(),
}));

vi.mock("@dokploy/server/services/permission", () => ({
checkPermission: mocks.checkPermission,
checkServicePermissionAndAccess: mocks.checkServicePermissionAndAccess,
findMemberByUserId: mocks.findMemberByUserId,
}));

vi.mock("@dokploy/server/services/schedule", async (importOriginal) => {
const actual =
await importOriginal<typeof import("@dokploy/server/services/schedule")>();
return {
...actual,
findScheduleById: mocks.findScheduleById,
};
});

vi.mock("@/server/api/utils/audit", () => ({
audit: vi.fn(),
}));

vi.mock("@/server/api/utils/placement-access", () => ({
assertTargetServerAccess: vi.fn(),
}));

vi.mock("@/server/utils/backup", () => ({
removeJob: vi.fn(),
schedule: vi.fn(),
}));

const { scheduleRouter } = await import("../../server/api/routers/schedule");

const createCaller = () =>
scheduleRouter.createCaller({
user: { id: "user-1" },
session: { activeOrganizationId: "org-1" },
} as never);

const sharedEnvironment = {
environmentId: "environment-1",
env: "ENVIRONMENT_SHARED=environment-secret",
project: {
projectId: "project-1",
organizationId: "org-1",
env: "PROJECT_SHARED=project-secret",
},
};

const remoteServer = {
serverId: "server-1",
name: "remote",
organizationId: "org-1",
command: "echo server-command-secret",
metricsConfig: {
server: { token: "metrics-token", port: 4500 },
containers: { refreshRate: 60 },
},
};

const expectNoSecrets = (value: unknown) => {
const serialized = JSON.stringify(value);
for (const secret of [
"compose-env-secret",
"file-secret",
"repo-token",
"compose-refresh-token",
"environment-secret",
"project-secret",
"server-command-secret",
"metrics-token",
]) {
expect(serialized).not.toContain(secret);
}
};

describe("schedule read redaction", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.checkPermission.mockResolvedValue(undefined);
mocks.checkServicePermissionAndAccess.mockResolvedValue(undefined);
mocks.findMemberByUserId.mockResolvedValue({ role: "admin" });
});

it("redacts the bound compose service and shared env in one", async () => {
mocks.findScheduleById.mockResolvedValue({
scheduleId: "schedule-1",
name: "nightly",
scheduleType: "compose",
command: "echo ok",
applicationId: null,
composeId: "compose-1",
serverId: null,
organizationId: null,
application: null,
server: null,
compose: {
composeId: "compose-1",
appName: "compose-one",
env: "COMPOSE_SECRET=compose-env-secret",
composeFile:
"services:\n api:\n environment:\n TOKEN: file-secret",
customGitUrl: "https://repo-user:repo-token@example.com/org/repo.git",
refreshToken: "compose-refresh-token",
environment: sharedEnvironment,
},
});

const schedule = await createCaller().one({ scheduleId: "schedule-1" });

expect(schedule).toMatchObject({
command: "echo ok",
compose: {
appName: "compose-one",
env: REDACTED_SECRET_VALUE,
composeFile: REDACTED_SECRET_VALUE,
refreshToken: REDACTED_SECRET_VALUE,
environment: {
env: REDACTED_SECRET_VALUE,
project: { env: REDACTED_SECRET_VALUE, organizationId: "org-1" },
},
},
});
expectNoSecrets(schedule);
});

it("redacts the bound server and application shared env in one", async () => {
mocks.findScheduleById.mockResolvedValue({
scheduleId: "schedule-1",
name: "nightly",
scheduleType: "server",
command: "echo ok",
applicationId: null,
composeId: null,
serverId: "server-1",
organizationId: null,
application: {
applicationId: "app-1",
appName: "app-one",
environment: sharedEnvironment,
},
compose: null,
server: { ...remoteServer, organization: { id: "org-1" } },
});

const schedule = await createCaller().one({ scheduleId: "schedule-1" });

expect(schedule.server).toMatchObject({
name: "remote",
command: REDACTED_SECRET_VALUE,
metricsConfig: { server: { token: REDACTED_SECRET_VALUE } },
organization: { id: "org-1" },
});
expectNoSecrets(schedule);
});

it("redacts the server row in list", async () => {
mocks.schedulesFindMany.mockResolvedValue([
{
scheduleId: "schedule-1",
name: "nightly",
scheduleType: "server",
command: "echo ok",
serverId: "server-1",
application: null,
compose: null,
server: remoteServer,
deployments: [],
},
]);

const schedules = await createCaller().list({
id: "server-1",
scheduleType: "server",
});

expect(schedules).toHaveLength(1);
expect(schedules[0]).toMatchObject({
command: "echo ok",
server: {
name: "remote",
command: REDACTED_SECRET_VALUE,
metricsConfig: { server: { token: REDACTED_SECRET_VALUE, port: 4500 } },
},
});
expectNoSecrets(schedules);
});
});
Loading
Loading