feat: add comprehensive permission tests and enhance permission checks in components

- Introduced new test files for permission checks, including `check-permission.test.ts`, `enterprise-only-resources.test.ts`, `resolve-permissions.test.ts`, and `service-access.test.ts`.
- Implemented permission checks in various components to ensure actions are gated by user permissions, including `ShowTraefikConfig`, `UpdateTraefikConfig`, `ShowVolumes`, `ShowDomains`, and others.
- Enhanced the logic for displaying UI elements based on user permissions, ensuring that only authorized users can access or modify resources.
This commit is contained in:
Mauricio Siu
2026-03-15 16:42:48 -06:00
parent 8eace173b9
commit 8127dc4536
118 changed files with 25020 additions and 5430 deletions
+181 -150
View File
@@ -1,7 +1,4 @@
import {
addNewEnvironment,
addNewProject,
checkProjectAccess,
createApplication,
createBackup,
createCompose,
@@ -22,7 +19,6 @@ import {
findComposeById,
findEnvironmentById,
findMariadbById,
findMemberById,
findMongoById,
findMySqlById,
findPostgresById,
@@ -32,15 +28,23 @@ import {
IS_CLOUD,
updateProjectById,
} from "@dokploy/server";
import {
addNewEnvironment,
addNewProject,
checkPermission,
checkProjectAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import {
apiCreateProject,
@@ -63,13 +67,7 @@ export const projectRouter = createTRPCRouter({
.input(apiCreateProject)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"create",
ctx.session.activeOrganizationId,
);
}
await checkProjectAccess(ctx, "create");
const admin = await findUserById(ctx.user.ownerId);
@@ -84,20 +82,16 @@ export const projectRouter = createTRPCRouter({
input,
ctx.session.activeOrganizationId,
);
if (ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
project.project.projectId,
ctx.session.activeOrganizationId,
);
await addNewProject(ctx, project.project.projectId);
await addNewEnvironment(
ctx.user.id,
project?.environment?.environmentId || "",
ctx.session.activeOrganizationId,
);
}
await addNewEnvironment(ctx, project?.environment?.environmentId || "");
await audit(ctx, {
action: "create",
resourceType: "project",
resourceId: project.project.projectId,
resourceName: project.project.name,
});
return project;
} catch (error) {
throw new TRPCError({
@@ -111,18 +105,18 @@ export const projectRouter = createTRPCRouter({
one: protectedProcedure
.input(apiFindOneProject)
.query(async ({ input, ctx }) => {
if (ctx.user.role === "member") {
const { accessedServices } = await findMemberById(
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedServices, accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
await checkProjectAccess(
ctx.user.id,
"access",
ctx.session.activeOrganizationId,
input.projectId,
);
if (!accessedProjects.includes(input.projectId)) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
const project = await db.query.projects.findFirst({
where: and(
@@ -189,15 +183,14 @@ export const projectRouter = createTRPCRouter({
return project;
}),
all: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.role === "member") {
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects, accessedEnvironments, accessedServices } =
await findMemberById(ctx.user.id, ctx.session.activeOrganizationId);
await findMemberByUserId(ctx.user.id, ctx.session.activeOrganizationId);
if (accessedProjects.length === 0) {
return [];
}
// Build environment filter
const environmentFilter =
accessedEnvironments.length === 0
? sql`false`
@@ -348,105 +341,106 @@ export const projectRouter = createTRPCRouter({
});
}),
/** All projects with full environments and services for the admin permissions UI. Admin only. */
allForPermissions: adminProcedure.query(async ({ ctx }) => {
return await db.query.projects.findMany({
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
},
with: {
environments: {
columns: {
environmentId: true,
name: true,
isDefault: true,
},
with: {
applications: {
columns: {
applicationId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
allForPermissions: withPermission("member", "update").query(
async ({ ctx }) => {
return await db.query.projects.findMany({
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
orderBy: desc(projects.createdAt),
columns: {
projectId: true,
name: true,
},
with: {
environments: {
columns: {
environmentId: true,
name: true,
isDefault: true,
},
mariadb: {
columns: {
mariadbId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
with: {
applications: {
columns: {
applicationId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
postgres: {
columns: {
postgresId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
mariadb: {
columns: {
mariadbId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
mysql: {
columns: {
mysqlId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
postgres: {
columns: {
postgresId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
mongo: {
columns: {
mongoId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
mysql: {
columns: {
mysqlId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
redis: {
columns: {
redisId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
mongo: {
columns: {
mongoId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
},
compose: {
columns: {
composeId: true,
appName: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
redis: {
columns: {
redisId: true,
appName: true,
name: true,
createdAt: true,
applicationStatus: true,
description: true,
serverId: true,
},
},
compose: {
columns: {
composeId: true,
appName: true,
name: true,
createdAt: true,
composeStatus: true,
description: true,
serverId: true,
},
},
},
},
},
},
});
}),
});
},
),
search: protectedProcedure
.input(
@@ -482,8 +476,8 @@ export const projectRouter = createTRPCRouter({
);
}
if (ctx.user.role === "member") {
const { accessedProjects } = await findMemberById(
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
@@ -529,13 +523,6 @@ export const projectRouter = createTRPCRouter({
.input(apiRemoveProject)
.mutation(async ({ input, ctx }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"delete",
ctx.session.activeOrganizationId,
);
}
const currentProject = await findProjectById(input.projectId);
if (
currentProject.organizationId !== ctx.session.activeOrganizationId
@@ -545,8 +532,15 @@ export const projectRouter = createTRPCRouter({
message: "You are not authorized to delete this project",
});
}
await checkProjectAccess(ctx, "delete", input.projectId);
const deletedProject = await deleteProject(input.projectId);
await audit(ctx, {
action: "delete",
resourceType: "project",
resourceId: currentProject.projectId,
resourceName: currentProject.name,
});
return deletedProject;
} catch (error) {
throw error;
@@ -565,10 +559,36 @@ export const projectRouter = createTRPCRouter({
message: "You are not authorized to update this project",
});
}
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
const { accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!accessedProjects.includes(input.projectId)) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
}
if (input.env !== undefined) {
await checkPermission(ctx, { projectEnvVars: ["write"] });
}
const project = await updateProjectById(input.projectId, {
...input,
});
if (project) {
await audit(ctx, {
action: "update",
resourceType: "project",
resourceId: input.projectId,
resourceName: project.name,
});
}
return project;
} catch (error) {
throw error;
@@ -602,15 +622,8 @@ export const projectRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
try {
if (ctx.user.role === "member") {
await checkProjectAccess(
ctx.user.id,
"create",
ctx.session.activeOrganizationId,
);
}
await checkProjectAccess(ctx, "create");
// Get source project
const sourceEnvironment = input.duplicateInSameProject
? await findEnvironmentById(input.sourceEnvironmentId)
: null;
@@ -626,7 +639,24 @@ export const projectRouter = createTRPCRouter({
});
}
// Create new project or use existing one
if (
input.duplicateInSameProject &&
sourceEnvironment &&
ctx.user.role !== "owner" &&
ctx.user.role !== "admin"
) {
const { accessedProjects } = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (!accessedProjects.includes(sourceEnvironment.project.projectId)) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
}
const targetProject = input.duplicateInSameProject
? sourceEnvironment
: await createProject(
@@ -643,7 +673,6 @@ export const projectRouter = createTRPCRouter({
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
// Helper function to duplicate a service
const duplicateService = async (id: string, type: string) => {
switch (type) {
case "application": {
@@ -947,20 +976,22 @@ export const projectRouter = createTRPCRouter({
}
};
// Duplicate selected services
for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type);
}
}
if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
targetProject?.projectId || "",
ctx.session.activeOrganizationId,
);
if (!input.duplicateInSameProject) {
await addNewProject(ctx, targetProject?.projectId || "");
}
await audit(ctx, {
action: "create",
resourceType: "project",
resourceId: targetProject?.projectId || "",
resourceName: input.name,
metadata: { duplicatedFrom: input.sourceEnvironmentId },
});
return targetProject;
} catch (error) {
throw new TRPCError({