mirror of
https://github.com/LukeHagar/dokploy.git
synced 2025-12-06 04:19:37 +00:00
Merge pull request #2599 from Harikrishnan1367709/separate-permission-for-deleting-environments-#2594
feat: Add Environment Deletion Permission Control-#2594
This commit is contained in:
@@ -63,14 +63,6 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
// API mutations
|
|
||||||
const { data: environment } = api.environment.one.useQuery(
|
|
||||||
{ environmentId: currentEnvironmentId || "" },
|
|
||||||
{
|
|
||||||
enabled: !!currentEnvironmentId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get current user's permissions
|
// Get current user's permissions
|
||||||
const { data: currentUser } = api.user.get.useQuery();
|
const { data: currentUser } = api.user.get.useQuery();
|
||||||
|
|
||||||
@@ -80,6 +72,12 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
currentUser?.role === "admin" ||
|
currentUser?.role === "admin" ||
|
||||||
currentUser?.canCreateEnvironments === true;
|
currentUser?.canCreateEnvironments === true;
|
||||||
|
|
||||||
|
// Check if user can delete environments
|
||||||
|
const canDeleteEnvironments =
|
||||||
|
currentUser?.role === "owner" ||
|
||||||
|
currentUser?.role === "admin" ||
|
||||||
|
currentUser?.canDeleteEnvironments === true;
|
||||||
|
|
||||||
const haveServices =
|
const haveServices =
|
||||||
selectedEnvironment &&
|
selectedEnvironment &&
|
||||||
((selectedEnvironment?.mariadb?.length || 0) > 0 ||
|
((selectedEnvironment?.mariadb?.length || 0) > 0 ||
|
||||||
@@ -276,6 +274,7 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
<PencilIcon className="h-3 w-3" />
|
<PencilIcon className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{canDeleteEnvironments && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -287,6 +286,7 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
>
|
>
|
||||||
<TrashIcon className="h-3 w-3" />
|
<TrashIcon className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ const addPermissions = z.object({
|
|||||||
canCreateServices: z.boolean().optional().default(false),
|
canCreateServices: z.boolean().optional().default(false),
|
||||||
canDeleteProjects: z.boolean().optional().default(false),
|
canDeleteProjects: z.boolean().optional().default(false),
|
||||||
canDeleteServices: z.boolean().optional().default(false),
|
canDeleteServices: z.boolean().optional().default(false),
|
||||||
|
canDeleteEnvironments: z.boolean().optional().default(false),
|
||||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||||
canAccessToDocker: z.boolean().optional().default(false),
|
canAccessToDocker: z.boolean().optional().default(false),
|
||||||
canAccessToAPI: z.boolean().optional().default(false),
|
canAccessToAPI: z.boolean().optional().default(false),
|
||||||
@@ -196,6 +197,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
accessedProjects: [],
|
accessedProjects: [],
|
||||||
accessedEnvironments: [],
|
accessedEnvironments: [],
|
||||||
accessedServices: [],
|
accessedServices: [],
|
||||||
|
canDeleteEnvironments: false,
|
||||||
canCreateProjects: false,
|
canCreateProjects: false,
|
||||||
canCreateServices: false,
|
canCreateServices: false,
|
||||||
canDeleteProjects: false,
|
canDeleteProjects: false,
|
||||||
@@ -216,16 +218,17 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
accessedEnvironments: data.accessedEnvironments || [],
|
accessedEnvironments: data.accessedEnvironments || [],
|
||||||
accessedServices: data.accessedServices || [],
|
accessedServices: data.accessedServices || [],
|
||||||
canCreateProjects: data.canCreateProjects || false,
|
canCreateProjects: data.canCreateProjects,
|
||||||
canCreateServices: data.canCreateServices || false,
|
canCreateServices: data.canCreateServices,
|
||||||
canDeleteProjects: data.canDeleteProjects || false,
|
canDeleteProjects: data.canDeleteProjects,
|
||||||
canDeleteServices: data.canDeleteServices || false,
|
canDeleteServices: data.canDeleteServices,
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles || false,
|
canDeleteEnvironments: data.canDeleteEnvironments || false,
|
||||||
canAccessToDocker: data.canAccessToDocker || false,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
canAccessToAPI: data.canAccessToAPI || false,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
canAccessToSSHKeys: data.canAccessToSSHKeys || false,
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
canAccessToGitProviders: data.canAccessToGitProviders || false,
|
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||||
canCreateEnvironments: data.canCreateEnvironments || false,
|
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||||
|
canCreateEnvironments: data.canCreateEnvironments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data, isOpen]);
|
}, [form, form.reset, data, isOpen]);
|
||||||
@@ -237,6 +240,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canCreateProjects: data.canCreateProjects,
|
canCreateProjects: data.canCreateProjects,
|
||||||
canDeleteServices: data.canDeleteServices,
|
canDeleteServices: data.canDeleteServices,
|
||||||
canDeleteProjects: data.canDeleteProjects,
|
canDeleteProjects: data.canDeleteProjects,
|
||||||
|
canDeleteEnvironments: data.canDeleteEnvironments,
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
accessedEnvironments: data.accessedEnvironments || [],
|
accessedEnvironments: data.accessedEnvironments || [],
|
||||||
@@ -379,6 +383,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="canDeleteEnvironments"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Delete Environments</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Allow the user to delete environments
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="canAccessToTraefikFiles"
|
name="canAccessToTraefikFiles"
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0116_amusing_firedrake.sql
Normal file
1
apps/dokploy/drizzle/0116_amusing_firedrake.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "canDeleteEnvironments" boolean DEFAULT false NOT NULL;
|
||||||
6622
apps/dokploy/drizzle/meta/0116_snapshot.json
Normal file
6622
apps/dokploy/drizzle/meta/0116_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -813,6 +813,13 @@
|
|||||||
"when": 1759644540829,
|
"when": 1759644540829,
|
||||||
"tag": "0115_serious_black_bird",
|
"tag": "0115_serious_black_bird",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 116,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1759645163834,
|
||||||
|
"tag": "0116_amusing_firedrake",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
addNewEnvironment,
|
addNewEnvironment,
|
||||||
checkEnvironmentAccess,
|
checkEnvironmentAccess,
|
||||||
checkEnvironmentCreationPermission,
|
checkEnvironmentCreationPermission,
|
||||||
|
checkEnvironmentDeletionPermission,
|
||||||
createEnvironment,
|
createEnvironment,
|
||||||
deleteEnvironment,
|
deleteEnvironment,
|
||||||
duplicateEnvironment,
|
duplicateEnvironment,
|
||||||
@@ -194,14 +195,6 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
.input(apiRemoveEnvironment)
|
.input(apiRemoveEnvironment)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
|
||||||
await checkEnvironmentAccess(
|
|
||||||
ctx.user.id,
|
|
||||||
input.environmentId,
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
"access",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
if (
|
if (
|
||||||
environment.project.organizationId !==
|
environment.project.organizationId !==
|
||||||
@@ -213,27 +206,33 @@ export const environmentRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check environment access for members
|
// Check environment deletion permission
|
||||||
if (ctx.user.role === "member") {
|
await checkEnvironmentDeletionPermission(
|
||||||
const { accessedEnvironments } = await findMemberById(
|
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
|
environment.projectId,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
// Additional check for environment access for members
|
||||||
throw new TRPCError({
|
if (ctx.user.role === "member") {
|
||||||
code: "FORBIDDEN",
|
await checkEnvironmentAccess(
|
||||||
message: "You are not allowed to delete this environment",
|
ctx.user.id,
|
||||||
});
|
input.environmentId,
|
||||||
}
|
ctx.session.activeOrganizationId,
|
||||||
|
"access",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
||||||
return deletedEnvironment;
|
return deletedEnvironment;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof TRPCError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: `Error deleting the environment: ${error instanceof Error ? error.message : error}`,
|
message: `Error deleting the environment: ${error instanceof Error ? error.message : error}`,
|
||||||
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ export const member = pgTable("member", {
|
|||||||
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
canDeleteEnvironments: boolean("canDeleteEnvironments")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
canCreateEnvironments: boolean("canCreateEnvironments")
|
canCreateEnvironments: boolean("canCreateEnvironments")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ export const apiAssignPermissions = createSchema
|
|||||||
canAccessToAPI: z.boolean().optional(),
|
canAccessToAPI: z.boolean().optional(),
|
||||||
canAccessToSSHKeys: z.boolean().optional(),
|
canAccessToSSHKeys: z.boolean().optional(),
|
||||||
canAccessToGitProviders: z.boolean().optional(),
|
canAccessToGitProviders: z.boolean().optional(),
|
||||||
|
canDeleteEnvironments: z.boolean().optional(),
|
||||||
canCreateEnvironments: z.boolean().optional(),
|
canCreateEnvironments: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|||||||
@@ -163,6 +163,24 @@ export const canPerformAccessEnvironment = async (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const canPerformDeleteEnvironment = async (
|
||||||
|
userId: string,
|
||||||
|
projectId: string,
|
||||||
|
organizationId: string,
|
||||||
|
) => {
|
||||||
|
const { accessedProjects, canDeleteEnvironments } = await findMemberById(
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
);
|
||||||
|
const haveAccessToProject = accessedProjects.includes(projectId);
|
||||||
|
|
||||||
|
if (canDeleteEnvironments && haveAccessToProject) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
export const canAccessToTraefikFiles = async (
|
export const canAccessToTraefikFiles = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
@@ -240,6 +258,42 @@ export const checkEnvironmentAccess = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkEnvironmentDeletionPermission = async (
|
||||||
|
userId: string,
|
||||||
|
projectId: string,
|
||||||
|
organizationId: string,
|
||||||
|
) => {
|
||||||
|
const member = await findMemberById(userId, organizationId);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "User not found in organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.role === "owner" || member.role === "admin") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!member.canDeleteEnvironments) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have permission to delete environments",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasProjectAccess = member.accessedProjects.includes(projectId);
|
||||||
|
if (!hasProjectAccess) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this project",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const checkProjectAccess = async (
|
export const checkProjectAccess = async (
|
||||||
authId: string,
|
authId: string,
|
||||||
action: "create" | "delete" | "access",
|
action: "create" | "delete" | "access",
|
||||||
|
|||||||
Reference in New Issue
Block a user