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",