Skip to content

Commit de3db08

Browse files
authored
Merge pull request #4020 from Dokploy/canary
🚀 Release v0.28.7
2 parents a2d6550 + 9067452 commit de3db08

File tree

137 files changed

+18034
-5621
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

137 files changed

+18034
-5621
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const mockMemberData = (
4+
role: string,
5+
overrides: Record<string, boolean> = {},
6+
) => ({
7+
id: "member-1",
8+
role,
9+
userId: "user-1",
10+
organizationId: "org-1",
11+
accessedProjects: [] as string[],
12+
accessedServices: [] as string[],
13+
accessedEnvironments: [] as string[],
14+
canCreateProjects: overrides.canCreateProjects ?? false,
15+
canDeleteProjects: overrides.canDeleteProjects ?? false,
16+
canCreateServices: overrides.canCreateServices ?? false,
17+
canDeleteServices: overrides.canDeleteServices ?? false,
18+
canCreateEnvironments: overrides.canCreateEnvironments ?? false,
19+
canDeleteEnvironments: overrides.canDeleteEnvironments ?? false,
20+
canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false,
21+
canAccessToDocker: overrides.canAccessToDocker ?? false,
22+
canAccessToAPI: overrides.canAccessToAPI ?? false,
23+
canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false,
24+
canAccessToGitProviders: overrides.canAccessToGitProviders ?? false,
25+
user: { id: "user-1", email: "test@test.com" },
26+
});
27+
28+
let memberToReturn: ReturnType<typeof mockMemberData> =
29+
mockMemberData("member");
30+
31+
vi.mock("@dokploy/server/db", () => ({
32+
db: {
33+
query: {
34+
member: {
35+
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
36+
findMany: vi.fn(() => Promise.resolve([])),
37+
},
38+
organizationRole: {
39+
findFirst: vi.fn(),
40+
findMany: vi.fn(() => Promise.resolve([])),
41+
},
42+
},
43+
},
44+
}));
45+
46+
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
47+
hasValidLicense: vi.fn(() => Promise.resolve(false)),
48+
}));
49+
50+
const { checkPermission } = await import("@dokploy/server/services/permission");
51+
52+
const ctx = {
53+
user: { id: "user-1" },
54+
session: { activeOrganizationId: "org-1" },
55+
};
56+
57+
beforeEach(() => {
58+
vi.clearAllMocks();
59+
});
60+
61+
describe("static roles bypass enterprise resources", () => {
62+
it("owner bypasses deployment.read", async () => {
63+
memberToReturn = mockMemberData("owner");
64+
await expect(
65+
checkPermission(ctx, { deployment: ["read"] }),
66+
).resolves.toBeUndefined();
67+
});
68+
69+
it("admin bypasses backup.create", async () => {
70+
memberToReturn = mockMemberData("admin");
71+
await expect(
72+
checkPermission(ctx, { backup: ["create"] }),
73+
).resolves.toBeUndefined();
74+
});
75+
76+
it("member bypasses schedule.delete", async () => {
77+
memberToReturn = mockMemberData("member");
78+
await expect(
79+
checkPermission(ctx, { schedule: ["delete"] }),
80+
).resolves.toBeUndefined();
81+
});
82+
83+
it("member bypasses multiple enterprise permissions at once", async () => {
84+
memberToReturn = mockMemberData("member");
85+
await expect(
86+
checkPermission(ctx, {
87+
deployment: ["read"],
88+
backup: ["create"],
89+
domain: ["delete"],
90+
}),
91+
).resolves.toBeUndefined();
92+
});
93+
});
94+
95+
describe("static roles validate free-tier resources", () => {
96+
it("owner passes project.create", async () => {
97+
memberToReturn = mockMemberData("owner");
98+
await expect(
99+
checkPermission(ctx, { project: ["create"] }),
100+
).resolves.toBeUndefined();
101+
});
102+
103+
it("member fails project.create (no legacy override)", async () => {
104+
memberToReturn = mockMemberData("member");
105+
await expect(
106+
checkPermission(ctx, { project: ["create"] }),
107+
).rejects.toThrow();
108+
});
109+
110+
it("member passes service.read", async () => {
111+
memberToReturn = mockMemberData("member");
112+
await expect(
113+
checkPermission(ctx, { service: ["read"] }),
114+
).resolves.toBeUndefined();
115+
});
116+
117+
it("member fails service.create", async () => {
118+
memberToReturn = mockMemberData("member");
119+
await expect(
120+
checkPermission(ctx, { service: ["create"] }),
121+
).rejects.toThrow();
122+
});
123+
});
124+
125+
describe("legacy boolean overrides for member", () => {
126+
it("member passes project.create with canCreateProjects=true", async () => {
127+
memberToReturn = mockMemberData("member", { canCreateProjects: true });
128+
await expect(
129+
checkPermission(ctx, { project: ["create"] }),
130+
).resolves.toBeUndefined();
131+
});
132+
133+
it("member passes docker.read with canAccessToDocker=true", async () => {
134+
memberToReturn = mockMemberData("member", { canAccessToDocker: true });
135+
await expect(
136+
checkPermission(ctx, { docker: ["read"] }),
137+
).resolves.toBeUndefined();
138+
});
139+
140+
it("member fails docker.read with canAccessToDocker=false", async () => {
141+
memberToReturn = mockMemberData("member");
142+
await expect(checkPermission(ctx, { docker: ["read"] })).rejects.toThrow();
143+
});
144+
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, it, expect } from "vitest";
2+
import {
3+
enterpriseOnlyResources,
4+
statements,
5+
} from "@dokploy/server/lib/access-control";
6+
7+
const FREE_TIER_RESOURCES = [
8+
"organization",
9+
"member",
10+
"invitation",
11+
"team",
12+
"ac",
13+
"project",
14+
"service",
15+
"environment",
16+
"docker",
17+
"sshKeys",
18+
"gitProviders",
19+
"traefikFiles",
20+
"api",
21+
];
22+
23+
const ENTERPRISE_RESOURCES = [
24+
"volume",
25+
"deployment",
26+
"envVars",
27+
"projectEnvVars",
28+
"environmentEnvVars",
29+
"server",
30+
"registry",
31+
"certificate",
32+
"backup",
33+
"volumeBackup",
34+
"schedule",
35+
"domain",
36+
"destination",
37+
"notification",
38+
"logs",
39+
"monitoring",
40+
"auditLog",
41+
];
42+
43+
describe("enterpriseOnlyResources set", () => {
44+
it("contains all enterprise resources", () => {
45+
for (const resource of ENTERPRISE_RESOURCES) {
46+
expect(enterpriseOnlyResources.has(resource)).toBe(true);
47+
}
48+
});
49+
50+
it("does NOT contain free-tier resources", () => {
51+
for (const resource of FREE_TIER_RESOURCES) {
52+
expect(enterpriseOnlyResources.has(resource)).toBe(false);
53+
}
54+
});
55+
56+
it("every resource in statements is either free or enterprise", () => {
57+
const allResources = Object.keys(statements);
58+
for (const resource of allResources) {
59+
const isFree = FREE_TIER_RESOURCES.includes(resource);
60+
const isEnterprise = enterpriseOnlyResources.has(resource);
61+
expect(isFree || isEnterprise).toBe(true);
62+
}
63+
});
64+
65+
it("free and enterprise sets don't overlap", () => {
66+
for (const resource of FREE_TIER_RESOURCES) {
67+
expect(enterpriseOnlyResources.has(resource)).toBe(false);
68+
}
69+
});
70+
71+
it("all statement resources are accounted for", () => {
72+
const allResources = Object.keys(statements);
73+
const categorized = [...FREE_TIER_RESOURCES, ...ENTERPRISE_RESOURCES];
74+
for (const resource of allResources) {
75+
expect(categorized).toContain(resource);
76+
}
77+
});
78+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const mockMemberData = (
4+
role: string,
5+
overrides: Record<string, boolean> = {},
6+
) => ({
7+
id: "member-1",
8+
role,
9+
userId: "user-1",
10+
organizationId: "org-1",
11+
accessedProjects: [] as string[],
12+
accessedServices: [] as string[],
13+
accessedEnvironments: [] as string[],
14+
canCreateProjects: overrides.canCreateProjects ?? false,
15+
canDeleteProjects: overrides.canDeleteProjects ?? false,
16+
canCreateServices: overrides.canCreateServices ?? false,
17+
canDeleteServices: overrides.canDeleteServices ?? false,
18+
canCreateEnvironments: overrides.canCreateEnvironments ?? false,
19+
canDeleteEnvironments: overrides.canDeleteEnvironments ?? false,
20+
canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false,
21+
canAccessToDocker: overrides.canAccessToDocker ?? false,
22+
canAccessToAPI: overrides.canAccessToAPI ?? false,
23+
canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false,
24+
canAccessToGitProviders: overrides.canAccessToGitProviders ?? false,
25+
user: { id: "user-1", email: "test@test.com" },
26+
});
27+
28+
let memberToReturn: ReturnType<typeof mockMemberData> =
29+
mockMemberData("member");
30+
31+
vi.mock("@dokploy/server/db", () => ({
32+
db: {
33+
query: {
34+
member: {
35+
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
36+
findMany: vi.fn(() => Promise.resolve([])),
37+
},
38+
organizationRole: {
39+
findFirst: vi.fn(),
40+
findMany: vi.fn(() => Promise.resolve([])),
41+
},
42+
},
43+
},
44+
}));
45+
46+
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
47+
hasValidLicense: vi.fn(() => Promise.resolve(false)),
48+
}));
49+
50+
const { resolvePermissions } = await import(
51+
"@dokploy/server/services/permission"
52+
);
53+
const { enterpriseOnlyResources, statements } = await import(
54+
"@dokploy/server/lib/access-control"
55+
);
56+
57+
const ctx = {
58+
user: { id: "user-1" },
59+
session: { activeOrganizationId: "org-1" },
60+
};
61+
62+
beforeEach(() => {
63+
vi.clearAllMocks();
64+
});
65+
66+
describe("enterprise resources for static roles", () => {
67+
it("owner gets true for all enterprise resources", async () => {
68+
memberToReturn = mockMemberData("owner");
69+
const perms = await resolvePermissions(ctx);
70+
71+
for (const resource of enterpriseOnlyResources) {
72+
const actions = statements[resource as keyof typeof statements];
73+
for (const action of actions) {
74+
expect((perms as any)[resource][action]).toBe(true);
75+
}
76+
}
77+
});
78+
79+
it("admin gets true for all enterprise resources", async () => {
80+
memberToReturn = mockMemberData("admin");
81+
const perms = await resolvePermissions(ctx);
82+
83+
for (const resource of enterpriseOnlyResources) {
84+
const actions = statements[resource as keyof typeof statements];
85+
for (const action of actions) {
86+
expect((perms as any)[resource][action]).toBe(true);
87+
}
88+
}
89+
});
90+
91+
it("member gets true for service-level enterprise resources", async () => {
92+
memberToReturn = mockMemberData("member");
93+
const perms = await resolvePermissions(ctx);
94+
95+
expect(perms.deployment.read).toBe(true);
96+
expect(perms.deployment.create).toBe(true);
97+
expect(perms.domain.read).toBe(true);
98+
expect(perms.backup.read).toBe(true);
99+
expect(perms.logs.read).toBe(true);
100+
expect(perms.monitoring.read).toBe(true);
101+
});
102+
103+
it("member gets false for org-level enterprise resources", async () => {
104+
memberToReturn = mockMemberData("member");
105+
const perms = await resolvePermissions(ctx);
106+
107+
expect(perms.server.read).toBe(false);
108+
expect(perms.registry.read).toBe(false);
109+
expect(perms.certificate.read).toBe(false);
110+
expect(perms.destination.read).toBe(false);
111+
expect(perms.notification.read).toBe(false);
112+
expect(perms.auditLog.read).toBe(false);
113+
});
114+
});
115+
116+
describe("free-tier resources for member", () => {
117+
it("member gets service.read=true", async () => {
118+
memberToReturn = mockMemberData("member");
119+
const perms = await resolvePermissions(ctx);
120+
expect(perms.service.read).toBe(true);
121+
});
122+
123+
it("member gets project.create=false without legacy override", async () => {
124+
memberToReturn = mockMemberData("member");
125+
const perms = await resolvePermissions(ctx);
126+
expect(perms.project.create).toBe(false);
127+
});
128+
129+
it("member gets project.create=true with canCreateProjects", async () => {
130+
memberToReturn = mockMemberData("member", { canCreateProjects: true });
131+
const perms = await resolvePermissions(ctx);
132+
expect(perms.project.create).toBe(true);
133+
});
134+
135+
it("member gets docker.read=false without legacy override", async () => {
136+
memberToReturn = mockMemberData("member");
137+
const perms = await resolvePermissions(ctx);
138+
expect(perms.docker.read).toBe(false);
139+
});
140+
141+
it("member gets docker.read=true with canAccessToDocker", async () => {
142+
memberToReturn = mockMemberData("member", { canAccessToDocker: true });
143+
const perms = await resolvePermissions(ctx);
144+
expect(perms.docker.read).toBe(true);
145+
});
146+
});
147+
148+
describe("free-tier resources for owner", () => {
149+
it("owner gets all free-tier permissions as true", async () => {
150+
memberToReturn = mockMemberData("owner");
151+
const perms = await resolvePermissions(ctx);
152+
expect(perms.project.create).toBe(true);
153+
expect(perms.project.delete).toBe(true);
154+
expect(perms.service.create).toBe(true);
155+
expect(perms.service.read).toBe(true);
156+
expect(perms.service.delete).toBe(true);
157+
expect(perms.docker.read).toBe(true);
158+
expect(perms.traefikFiles.read).toBe(true);
159+
expect(perms.traefikFiles.write).toBe(true);
160+
});
161+
});

0 commit comments

Comments
 (0)