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 [description, setDescription] = useState("");
|
||||
|
||||
// API mutations
|
||||
const { data: environment } = api.environment.one.useQuery(
|
||||
{ environmentId: currentEnvironmentId || "" },
|
||||
{
|
||||
enabled: !!currentEnvironmentId,
|
||||
},
|
||||
);
|
||||
|
||||
// Get current user's permissions
|
||||
const { data: currentUser } = api.user.get.useQuery();
|
||||
|
||||
@@ -80,6 +72,12 @@ export const AdvancedEnvironmentSelector = ({
|
||||
currentUser?.role === "admin" ||
|
||||
currentUser?.canCreateEnvironments === true;
|
||||
|
||||
// Check if user can delete environments
|
||||
const canDeleteEnvironments =
|
||||
currentUser?.role === "owner" ||
|
||||
currentUser?.role === "admin" ||
|
||||
currentUser?.canDeleteEnvironments === true;
|
||||
|
||||
const haveServices =
|
||||
selectedEnvironment &&
|
||||
((selectedEnvironment?.mariadb?.length || 0) > 0 ||
|
||||
@@ -276,6 +274,7 @@ export const AdvancedEnvironmentSelector = ({
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
{canDeleteEnvironments && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -287,6 +286,7 @@ export const AdvancedEnvironmentSelector = ({
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -161,6 +161,7 @@ const addPermissions = z.object({
|
||||
canCreateServices: z.boolean().optional().default(false),
|
||||
canDeleteProjects: z.boolean().optional().default(false),
|
||||
canDeleteServices: z.boolean().optional().default(false),
|
||||
canDeleteEnvironments: z.boolean().optional().default(false),
|
||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||
canAccessToDocker: z.boolean().optional().default(false),
|
||||
canAccessToAPI: z.boolean().optional().default(false),
|
||||
@@ -196,6 +197,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
accessedProjects: [],
|
||||
accessedEnvironments: [],
|
||||
accessedServices: [],
|
||||
canDeleteEnvironments: false,
|
||||
canCreateProjects: false,
|
||||
canCreateServices: false,
|
||||
canDeleteProjects: false,
|
||||
@@ -216,16 +218,17 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
accessedServices: data.accessedServices || [],
|
||||
canCreateProjects: data.canCreateProjects || false,
|
||||
canCreateServices: data.canCreateServices || false,
|
||||
canDeleteProjects: data.canDeleteProjects || false,
|
||||
canDeleteServices: data.canDeleteServices || false,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles || false,
|
||||
canAccessToDocker: data.canAccessToDocker || false,
|
||||
canAccessToAPI: data.canAccessToAPI || false,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys || false,
|
||||
canAccessToGitProviders: data.canAccessToGitProviders || false,
|
||||
canCreateEnvironments: data.canCreateEnvironments || false,
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canCreateServices: data.canCreateServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
canDeleteEnvironments: data.canDeleteEnvironments || false,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||
canCreateEnvironments: data.canCreateEnvironments,
|
||||
});
|
||||
}
|
||||
}, [form, form.reset, data, isOpen]);
|
||||
@@ -237,6 +240,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
canCreateProjects: data.canCreateProjects,
|
||||
canDeleteServices: data.canDeleteServices,
|
||||
canDeleteProjects: data.canDeleteProjects,
|
||||
canDeleteEnvironments: data.canDeleteEnvironments,
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
accessedProjects: data.accessedProjects || [],
|
||||
accessedEnvironments: data.accessedEnvironments || [],
|
||||
@@ -379,6 +383,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
</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
|
||||
control={form.control}
|
||||
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,
|
||||
"tag": "0115_serious_black_bird",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 116,
|
||||
"version": "7",
|
||||
"when": 1759645163834,
|
||||
"tag": "0116_amusing_firedrake",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
addNewEnvironment,
|
||||
checkEnvironmentAccess,
|
||||
checkEnvironmentCreationPermission,
|
||||
checkEnvironmentDeletionPermission,
|
||||
createEnvironment,
|
||||
deleteEnvironment,
|
||||
duplicateEnvironment,
|
||||
@@ -194,14 +195,6 @@ 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 !==
|
||||
@@ -213,27 +206,33 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
// Check environment deletion permission
|
||||
await checkEnvironmentDeletionPermission(
|
||||
ctx.user.id,
|
||||
environment.projectId,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
if (!accessedEnvironments.includes(environment.environmentId)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to delete this environment",
|
||||
});
|
||||
}
|
||||
// Additional check for environment access for members
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
input.environmentId,
|
||||
ctx.session.activeOrganizationId,
|
||||
"access",
|
||||
);
|
||||
}
|
||||
|
||||
const deletedEnvironment = await deleteEnvironment(input.environmentId);
|
||||
return deletedEnvironment;
|
||||
} catch (error) {
|
||||
if (error instanceof TRPCError) {
|
||||
throw error;
|
||||
}
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
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")
|
||||
.notNull()
|
||||
.default(false),
|
||||
canDeleteEnvironments: boolean("canDeleteEnvironments")
|
||||
.notNull()
|
||||
.default(false),
|
||||
canCreateEnvironments: boolean("canCreateEnvironments")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
@@ -186,6 +186,7 @@ export const apiAssignPermissions = createSchema
|
||||
canAccessToAPI: z.boolean().optional(),
|
||||
canAccessToSSHKeys: z.boolean().optional(),
|
||||
canAccessToGitProviders: z.boolean().optional(),
|
||||
canDeleteEnvironments: z.boolean().optional(),
|
||||
canCreateEnvironments: z.boolean().optional(),
|
||||
})
|
||||
.required();
|
||||
|
||||
@@ -163,6 +163,24 @@ export const canPerformAccessEnvironment = async (
|
||||
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 (
|
||||
userId: 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 (
|
||||
authId: string,
|
||||
action: "create" | "delete" | "access",
|
||||
|
||||
Reference in New Issue
Block a user