From 35b7b5bd6810a790079ca45d3fa956d22516cc57 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 5 Sep 2025 00:23:01 -0600 Subject: [PATCH] feat: implement environment access control and service filtering based on user permissions, enhancing security and usability in environment management --- .../project/advanced-environment-selector.tsx | 109 +++++++++------- .../dokploy/server/api/routers/environment.ts | 117 +++++++++++++----- packages/server/src/services/user.ts | 58 +++++++++ 3 files changed, 205 insertions(+), 79 deletions(-) diff --git a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx index 88bca9c97..d6497fd0f 100644 --- a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx +++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx @@ -208,66 +208,81 @@ export const AdvancedEnvironmentSelector = ({ Environments - {environments?.map((environment) => ( -
- { - router.push( - `/dashboard/project/${projectId}/environment/${environment.environmentId}`, - ); - }} + {environments?.map((environment) => { + const servicesCount = + environment.mariadb.length + + environment.mongo.length + + environment.mysql.length + + environment.postgres.length + + environment.redis.length + + environment.applications.length + + environment.compose.length; + return ( +
-
- {environment.name} - {environment.environmentId === currentEnvironmentId && ( -
- )} -
- - - {/* Action buttons for non-production environments */} - - - - {environment.name !== "production" && ( -
+
+ + {environment.name} ({servicesCount}) + + {environment.environmentId === currentEnvironmentId && ( +
+ )} +
+ + + {/* Action buttons for non-production environments */} + + + {environment.name !== "production" && ( +
+ - -
- )} -
- ))} + +
+ )} +
+ ); + })} ({ + ...environment, + applications: environment.applications.filter((app: any) => + accessedServices.includes(app.applicationId), + ), + mariadb: environment.mariadb.filter((db: any) => + accessedServices.includes(db.mariadbId), + ), + mongo: environment.mongo.filter((db: any) => + accessedServices.includes(db.mongoId), + ), + mysql: environment.mysql.filter((db: any) => + accessedServices.includes(db.mysqlId), + ), + postgres: environment.postgres.filter((db: any) => + accessedServices.includes(db.postgresId), + ), + redis: environment.redis.filter((db: any) => + accessedServices.includes(db.redisId), + ), + compose: environment.compose.filter((comp: any) => + accessedServices.includes(comp.composeId), + ), +}); + export const environmentRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateEnvironment) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { try { // Check if user has access to the project // This would typically involve checking project ownership/membership // For now, we'll use a basic organization check const environment = await createEnvironment(input); + + if (ctx.user.role === "member") { + await addNewEnvironment( + ctx.user.id, + environment.environmentId, + ctx.session.activeOrganizationId, + ); + } return environment; } catch (error) { throw new TRPCError({ @@ -42,6 +81,14 @@ export const environmentRouter = createTRPCRouter({ .input(apiFindOneEnvironment) .query(async ({ input, ctx }) => { try { + if (ctx.user.role === "member") { + await checkEnvironmentAccess( + ctx.user.id, + input.environmentId, + ctx.session.activeOrganizationId, + "access", + ); + } const environment = await findEnvironmentById(input.environmentId); if ( environment.project.organizationId !== @@ -66,30 +113,10 @@ export const environmentRouter = createTRPCRouter({ } // Filter services based on member permissions - const filteredEnvironment = { - ...environment, - applications: environment.applications.filter((app) => - accessedServices.includes(app.applicationId), - ), - mariadb: environment.mariadb.filter((db) => - accessedServices.includes(db.mariadbId), - ), - mongo: environment.mongo.filter((db) => - accessedServices.includes(db.mongoId), - ), - mysql: environment.mysql.filter((db) => - accessedServices.includes(db.mysqlId), - ), - postgres: environment.postgres.filter((db) => - accessedServices.includes(db.postgresId), - ), - redis: environment.redis.filter((db) => - accessedServices.includes(db.redisId), - ), - compose: environment.compose.filter((comp) => - accessedServices.includes(comp.composeId), - ), - }; + const filteredEnvironment = filterEnvironmentServices( + environment, + accessedServices, + ); return filteredEnvironment; } @@ -125,15 +152,17 @@ export const environmentRouter = createTRPCRouter({ // Filter environments for members based on their permissions if (ctx.user.role === "member") { - const { accessedEnvironments } = await findMemberById( - ctx.user.id, - ctx.session.activeOrganizationId, - ); + const { accessedEnvironments, accessedServices } = + await findMemberById(ctx.user.id, ctx.session.activeOrganizationId); // Filter environments to only show those the member has access to - const filteredEnvironments = environments.filter((environment) => - accessedEnvironments.includes(environment.environmentId), - ); + const filteredEnvironments = environments + .filter((environment) => + accessedEnvironments.includes(environment.environmentId), + ) + .map((environment) => + filterEnvironmentServices(environment, accessedServices), + ); return filteredEnvironments; } @@ -151,6 +180,14 @@ export const environmentRouter = createTRPCRouter({ .input(apiRemoveEnvironment) .mutation(async ({ input, ctx }) => { try { + if (ctx.user.role === "member") { + await checkEnvironmentAccess( + ctx.user.id, + input.environmentId, + ctx.session.activeOrganizationId, + "access", + ); + } const environment = await findEnvironmentById(input.environmentId); if ( environment.project.organizationId !== @@ -192,6 +229,14 @@ export const environmentRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { try { const { environmentId, ...updateData } = input; + if (ctx.user.role === "member") { + await checkEnvironmentAccess( + ctx.user.id, + environmentId, + ctx.session.activeOrganizationId, + "access", + ); + } const currentEnvironment = await findEnvironmentById(environmentId); if ( currentEnvironment.project.organizationId !== @@ -237,6 +282,14 @@ export const environmentRouter = createTRPCRouter({ .input(apiDuplicateEnvironment) .mutation(async ({ input, ctx }) => { try { + if (ctx.user.role === "member") { + await checkEnvironmentAccess( + ctx.user.id, + input.environmentId, + ctx.session.activeOrganizationId, + "access", + ); + } const environment = await findEnvironmentById(input.environmentId); if ( environment.project.organizationId !== diff --git a/packages/server/src/services/user.ts b/packages/server/src/services/user.ts index 39ac95cef..728d5b8ee 100644 --- a/packages/server/src/services/user.ts +++ b/packages/server/src/services/user.ts @@ -23,6 +23,23 @@ export const addNewProject = async ( ); }; +export const addNewEnvironment = async ( + userId: string, + environmentId: string, + organizationId: string, +) => { + const userR = await findMemberById(userId, organizationId); + + await db + .update(member) + .set({ + accessedEnvironments: [...userR.accessedEnvironments, environmentId], + }) + .where( + and(eq(member.id, userR.id), eq(member.organizationId, organizationId)), + ); +}; + export const addNewService = async ( userId: string, serviceId: string, @@ -131,6 +148,21 @@ export const canPerformAccessProject = async ( return false; }; +export const canPerformAccessEnvironment = async ( + userId: string, + environmentId: string, + organizationId: string, +) => { + const { accessedEnvironments } = await findMemberById(userId, organizationId); + const haveAccessToEnvironment = accessedEnvironments.includes(environmentId); + + if (haveAccessToEnvironment) { + return true; + } + + return false; +}; + export const canAccessToTraefikFiles = async ( userId: string, organizationId: string, @@ -182,6 +214,32 @@ export const checkServiceAccess = async ( } }; +export const checkEnvironmentAccess = async ( + userId: string, + environmentId: string, + organizationId: string, + action = "access" as const, +) => { + let hasPermission = false; + switch (action) { + case "access": + hasPermission = await canPerformAccessEnvironment( + userId, + environmentId, + organizationId, + ); + break; + default: + hasPermission = false; + } + if (!hasPermission) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Permission denied", + }); + } +}; + export const checkProjectAccess = async ( authId: string, action: "create" | "delete" | "access",