mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 13:28:27 +00:00
Merge pull request #3903 from Infisical/fix/blockFolderDeletionOnPolicyInPlace
feat(change-approvals): block folder deletion if there is at least one secret protected by a policy
This commit is contained in:
@@ -1189,7 +1189,9 @@ export const registerRoutes = async (
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderCommitService
|
folderCommitService,
|
||||||
|
secretApprovalPolicyService,
|
||||||
|
secretV2BridgeDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretImportService = secretImportServiceFactory({
|
const secretImportService = secretImportServiceFactory({
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { ForbiddenError, subject } from "@casl/ability";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
|
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
|
||||||
|
|
||||||
import { ActionProjectType, TSecretFoldersInsert } from "@app/db/schemas";
|
import { ActionProjectType, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
import { PgSqlLock } from "@app/keystore/keystore";
|
import { PgSqlLock } from "@app/keystore/keystore";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
@@ -14,6 +15,7 @@ import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
|||||||
import { ChangeType, CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
import { ChangeType, CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||||
import {
|
import {
|
||||||
TCreateFolderDTO,
|
TCreateFolderDTO,
|
||||||
@@ -34,6 +36,8 @@ type TSecretFolderServiceFactoryDep = {
|
|||||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany" | "find">;
|
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany" | "find">;
|
||||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
|
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
|
||||||
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderIds">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
|
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
|
||||||
@@ -45,7 +49,9 @@ export const secretFolderServiceFactory = ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
folderVersionDAL,
|
folderVersionDAL,
|
||||||
folderCommitService,
|
folderCommitService,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
secretApprovalPolicyService,
|
||||||
|
secretV2BridgeDAL
|
||||||
}: TSecretFolderServiceFactoryDep) => {
|
}: TSecretFolderServiceFactoryDep) => {
|
||||||
const createFolder = async ({
|
const createFolder = async ({
|
||||||
projectId,
|
projectId,
|
||||||
@@ -464,6 +470,66 @@ export const secretFolderServiceFactory = ({
|
|||||||
return { folder: newFolder, old: folder };
|
return { folder: newFolder, old: folder };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $checkFolderPolicy = async ({
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
parentId
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
parentId: string;
|
||||||
|
}) => {
|
||||||
|
// get environment root folder (as it's needed to get all folders under it)
|
||||||
|
const rootFolder = await folderDAL.findBySecretPath(projectId, environment, "/");
|
||||||
|
if (!rootFolder) throw new NotFoundError({ message: `Root folder not found` });
|
||||||
|
// get all folders under environment root folder
|
||||||
|
const folderPaths = await folderDAL.findByEnvsDeep({ parentIds: [rootFolder.id] });
|
||||||
|
|
||||||
|
// create a map of folders by parent id
|
||||||
|
const normalizeKey = (key: string | null | undefined): string => key ?? "root";
|
||||||
|
const folderMap = new Map<string, (TSecretFolders & { path: string; depth: number; environment: string })[]>();
|
||||||
|
for (const folder of folderPaths) {
|
||||||
|
if (!folderMap.has(normalizeKey(folder.parentId))) {
|
||||||
|
folderMap.set(normalizeKey(folder.parentId), []);
|
||||||
|
}
|
||||||
|
folderMap.get(normalizeKey(folder.parentId))?.push(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively collect all folders under the given parentId
|
||||||
|
const collectDescendants = (
|
||||||
|
id: string
|
||||||
|
): (TSecretFolders & { path: string; depth: number; environment: string })[] => {
|
||||||
|
const children = folderMap.get(normalizeKey(id)) || [];
|
||||||
|
return [...children, ...children.flatMap((child) => collectDescendants(child.id))];
|
||||||
|
};
|
||||||
|
|
||||||
|
const foldersUnderParent = collectDescendants(parentId);
|
||||||
|
|
||||||
|
const folderPolicyPaths = foldersUnderParent.map((folder) => ({
|
||||||
|
path: folder.path,
|
||||||
|
id: folder.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
// get secrets under the given folders
|
||||||
|
const secrets = await secretV2BridgeDAL.findByFolderIds({ folderIds: folderPolicyPaths.map((p) => p.id) });
|
||||||
|
for await (const folderPolicyPath of folderPolicyPaths) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
if (!secrets.some((s) => s.folderId === folderPolicyPath.id)) continue;
|
||||||
|
const policy = await secretApprovalPolicyService.getSecretApprovalPolicy(
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
folderPolicyPath.path
|
||||||
|
);
|
||||||
|
// if there is a policy and there are secrets under the given folder, throw error
|
||||||
|
if (policy) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `You cannot delete the selected folder because it contains one or more secrets that are protected by the change policy "${policy.name}" at folder path "${folderPolicyPath.path}". Please remove the secrets at folder path "${folderPolicyPath.path}" and try again.`,
|
||||||
|
name: "DeleteFolderProtectedByPolicy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteFolder = async ({
|
const deleteFolder = async ({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
@@ -498,6 +564,8 @@ export const secretFolderServiceFactory = ({
|
|||||||
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
|
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await $checkFolderPolicy({ projectId, environment, parentId: parentFolder.id });
|
||||||
|
|
||||||
const [doc] = await folderDAL.delete(
|
const [doc] = await folderDAL.delete(
|
||||||
{
|
{
|
||||||
envId: env.id,
|
envId: env.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user