mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 06:28:11 +00:00
Add project delete protection
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection");
|
||||||
|
if (!hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.boolean("hasDeleteProtection").defaultTo(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.Project, "hasDeleteProtection");
|
||||||
|
if (hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||||
|
t.dropColumn("hasDeleteProtection");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,8 @@ export const ProjectsSchema = z.object({
|
|||||||
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
|
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
enforceCapitalization: z.boolean().default(false)
|
enforceCapitalization: z.boolean().default(false),
|
||||||
|
hasDeleteProtection: z.boolean().default(true).nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -255,7 +255,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
|
|||||||
upgradeStatus: true,
|
upgradeStatus: true,
|
||||||
pitVersionLimit: true,
|
pitVersionLimit: true,
|
||||||
kmsCertificateKeyId: true,
|
kmsCertificateKeyId: true,
|
||||||
auditLogsRetentionDays: true
|
auditLogsRetentionDays: true,
|
||||||
|
hasDeleteProtection: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
||||||
|
|||||||
@@ -390,6 +390,43 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:workspaceId/delete-protection",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
hasDeleteProtection: z.boolean()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
workspace: SanitizedProjectSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.toggleDeleteProtection({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
hasDeleteProtection: req.body.hasDeleteProtection
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message: "Successfully changed workspace settings",
|
||||||
|
workspace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: "/:workspaceSlug/version-limit",
|
url: "/:workspaceSlug/version-limit",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import {
|
|||||||
TProjectAccessRequestDTO,
|
TProjectAccessRequestDTO,
|
||||||
TSearchProjectsDTO,
|
TSearchProjectsDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
|
TToggleProjectDeleteProtectionDTO,
|
||||||
TUpdateAuditLogsRetentionDTO,
|
TUpdateAuditLogsRetentionDTO,
|
||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
TUpdateProjectKmsDTO,
|
TUpdateProjectKmsDTO,
|
||||||
@@ -482,6 +483,12 @@ export const projectServiceFactory = ({
|
|||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
|
if (project.hasDeleteProtection) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Project delete protection is enabled"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const deletedProject = await projectDAL.transaction(async (tx) => {
|
const deletedProject = await projectDAL.transaction(async (tx) => {
|
||||||
// delete these so that project custom roles can be deleted in cascade effect
|
// delete these so that project custom roles can be deleted in cascade effect
|
||||||
// direct deletion of project without these will cause fk error
|
// direct deletion of project without these will cause fk error
|
||||||
@@ -648,6 +655,29 @@ export const projectServiceFactory = ({
|
|||||||
return updatedProject;
|
return updatedProject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleDeleteProtection = async ({
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
hasDeleteProtection
|
||||||
|
}: TToggleProjectDeleteProtectionDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.Any
|
||||||
|
});
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
const updatedProject = await projectDAL.updateById(projectId, { hasDeleteProtection });
|
||||||
|
|
||||||
|
return updatedProject;
|
||||||
|
};
|
||||||
|
|
||||||
const updateVersionLimit = async ({
|
const updateVersionLimit = async ({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -1499,6 +1529,7 @@ export const projectServiceFactory = ({
|
|||||||
getProjectUpgradeStatus,
|
getProjectUpgradeStatus,
|
||||||
getAProject,
|
getAProject,
|
||||||
toggleAutoCapitalization,
|
toggleAutoCapitalization,
|
||||||
|
toggleDeleteProtection,
|
||||||
updateName,
|
updateName,
|
||||||
upgradeProject,
|
upgradeProject,
|
||||||
listProjectCas,
|
listProjectCas,
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ export type TToggleProjectAutoCapitalizationDTO = {
|
|||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TToggleProjectDeleteProtectionDTO = {
|
||||||
|
hasDeleteProtection: boolean;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateProjectVersionLimitDTO = {
|
export type TUpdateProjectVersionLimitDTO = {
|
||||||
pitVersionLimit: number;
|
pitVersionLimit: number;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
TGetUpgradeProjectStatusDTO,
|
TGetUpgradeProjectStatusDTO,
|
||||||
TListProjectIdentitiesDTO,
|
TListProjectIdentitiesDTO,
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
|
ToggleDeleteProjectProtectionDTO,
|
||||||
TSearchProjectsDTO,
|
TSearchProjectsDTO,
|
||||||
TUpdateWorkspaceIdentityRoleDTO,
|
TUpdateWorkspaceIdentityRoleDTO,
|
||||||
TUpdateWorkspaceUserRoleDTO,
|
TUpdateWorkspaceUserRoleDTO,
|
||||||
@@ -306,6 +307,25 @@ export const useToggleAutoCapitalization = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useToggleDeleteProjectProtection = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<Workspace, object, ToggleDeleteProjectProtectionDTO>({
|
||||||
|
mutationFn: async ({ workspaceID, state }) => {
|
||||||
|
const { data } = await apiRequest.post<{ workspace: Workspace }>(
|
||||||
|
`/api/v1/workspace/${workspaceID}/delete-protection`,
|
||||||
|
{
|
||||||
|
hasDeleteProtection: state
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data.workspace;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: workspaceKeys.getAllUserWorkspace() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useUpdateWorkspaceVersionLimit = () => {
|
export const useUpdateWorkspaceVersionLimit = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export type Workspace = {
|
|||||||
slug: string;
|
slug: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
roles?: TProjectRole[];
|
roles?: TProjectRole[];
|
||||||
|
hasDeleteProtection: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceEnv = {
|
export type WorkspaceEnv = {
|
||||||
@@ -80,6 +81,7 @@ export type UpdateProjectDTO = {
|
|||||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||||
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
||||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||||
|
export type ToggleDeleteProjectProtectionDTO = { workspaceID: string; state: boolean };
|
||||||
|
|
||||||
export type DeleteWorkspaceDTO = { workspaceID: string };
|
export type DeleteWorkspaceDTO = { workspaceID: string };
|
||||||
|
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Checkbox } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { useToggleDeleteProjectProtection } from "@app/hooks/api/workspace/queries";
|
||||||
|
|
||||||
|
export const DeleteProjectProtection = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { mutateAsync } = useToggleDeleteProjectProtection();
|
||||||
|
|
||||||
|
const handleToggleDeleteProjectProtection = async (state: boolean) => {
|
||||||
|
try {
|
||||||
|
if (!currentWorkspace?.id) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
workspaceID: currentWorkspace.id,
|
||||||
|
state
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = `Successfully ${state ? "enabled" : "disabled"} delete protection`;
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update delete protection",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<p className="mb-3 text-xl font-semibold">Delete Protection</p>
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<div className="w-max">
|
||||||
|
<Checkbox
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
id="hasDeleteProtection"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
isChecked={currentWorkspace?.hasDeleteProtection ?? false}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
handleToggleDeleteProjectProtection(state as boolean);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Protects the project from being deleted accidentally. While this option is enabled,
|
||||||
|
you can't delete the project.
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { DeleteProjectProtection } from "./DeleteProjectProtection";
|
||||||
+1
-1
@@ -144,7 +144,7 @@ export const DeleteProjectSection = () => {
|
|||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
isLoading={isDeleting}
|
isLoading={isDeleting}
|
||||||
isDisabled={!isAllowed || isDeleting}
|
isDisabled={!isAllowed || isDeleting || currentWorkspace?.hasDeleteProtection}
|
||||||
colorSchema="danger"
|
colorSchema="danger"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
|||||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||||
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
|
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
|
||||||
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
|
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
|
||||||
|
import { DeleteProjectProtection } from "../DeleteProjectProtection";
|
||||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||||
import { EnvironmentSection } from "../EnvironmentSection";
|
import { EnvironmentSection } from "../EnvironmentSection";
|
||||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||||
@@ -27,6 +28,7 @@ export const ProjectGeneralTab = () => {
|
|||||||
{currentWorkspace?.version !== ProjectVersion.V3 && isSecretManager && (
|
{currentWorkspace?.version !== ProjectVersion.V3 && isSecretManager && (
|
||||||
<RebuildSecretIndicesSection />
|
<RebuildSecretIndicesSection />
|
||||||
)}
|
)}
|
||||||
|
<DeleteProjectProtection />
|
||||||
<DeleteProjectSection />
|
<DeleteProjectSection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user