Add warning on secret deletions where it's being imported by another folder

This commit is contained in:
carlosmonastyrski
2025-04-02 18:58:34 -03:00
parent 13485cecbb
commit 4420985669
6 changed files with 132 additions and 7 deletions
@@ -470,7 +470,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalFolderCount: z.number().optional(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(), totalSecretCount: z.number().optional(),
totalCount: z.number() totalCount: z.number(),
importedBy: z
.object({
envName: z.string(),
folderName: z.string()
})
.array()
.optional()
}) })
} }
}, },
@@ -699,6 +706,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
path: secretPath,
environment,
projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { return {
imports, imports,
folders, folders,
@@ -709,7 +726,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalDynamicSecretCount, totalDynamicSecretCount,
totalSecretCount, totalSecretCount,
totalCount: totalCount:
(totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) (totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0),
importedBy
}; };
} }
}); });
@@ -169,6 +169,27 @@ export const secretImportDALFactory = (db: TDbClient) => {
} }
}; };
const getFolderIsImportedBy = async (secretPath: string, environment: string, tx?: Knex) => {
try {
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
.where({ importPath: secretPath, importEnv: environment })
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.select(
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("name").withSchema(TableName.SecretFolder).as("folderName"),
db.ref("parentId").withSchema(TableName.SecretFolder).as("parentFolderId")
);
return docs.map(({ envName, folderName, parentFolderId }) => ({
envName,
folderName: parentFolderId ? folderName : "Project Root Folder"
}));
} catch (error) {
throw new DatabaseError({ error, name: "get secret imports count" });
}
};
return { return {
...secretImportOrm, ...secretImportOrm,
find, find,
@@ -176,6 +197,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
findByFolderIds, findByFolderIds,
findLastImportPosition, findLastImportPosition,
updateAllPosition, updateAllPosition,
getProjectImportCount getProjectImportCount,
getFolderIsImportedBy
}; };
}; };
@@ -793,6 +793,39 @@ export const secretImportServiceFactory = ({
return secImportsArrays.flat(); return secImportsArrays.flat();
}; };
const getFolderIsImportedBy = async ({
path: secretPath,
environment,
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TGetSecretImportsDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new NotFoundError({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
});
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId);
return importedBy;
};
return { return {
createImport, createImport,
updateImport, updateImport,
@@ -805,6 +838,7 @@ export const secretImportServiceFactory = ({
getProjectImportCount, getProjectImportCount,
fnSecretsFromImports, fnSecretsFromImports,
getProjectImportMultiEnvCount, getProjectImportMultiEnvCount,
getImportsMultiEnv getImportsMultiEnv,
getFolderIsImportedBy
}; };
}; };
@@ -31,6 +31,7 @@ export type DashboardProjectSecretsDetailsResponse = {
totalDynamicSecretCount?: number; totalDynamicSecretCount?: number;
totalSecretCount?: number; totalSecretCount?: number;
totalCount: number; totalCount: number;
importedBy?: { envName: string; folderName: string }[];
}; };
export type DashboardProjectSecretsByKeys = { export type DashboardProjectSecretsByKeys = {
@@ -206,7 +206,8 @@ const Page = () => {
totalFolderCount = 0, totalFolderCount = 0,
totalDynamicSecretCount = 0, totalDynamicSecretCount = 0,
totalSecretCount = 0, totalSecretCount = 0,
totalCount = 0 totalCount = 0,
importedBy
} = data ?? {}; } = data ?? {};
useResetPageHelper({ useResetPageHelper({
@@ -507,6 +508,7 @@ const Page = () => {
workspaceId={workspaceId} workspaceId={workspaceId}
secretPath={secretPath} secretPath={secretPath}
isProtectedBranch={isProtectedBranch} isProtectedBranch={isProtectedBranch}
importedBy={importedBy}
/> />
)} )}
{canReadSecret && <SecretNoAccessListView count={noAccessSecretCount} />} {canReadSecret && <SecretNoAccessListView count={noAccessSecretCount} />}
@@ -1,4 +1,5 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
@@ -28,8 +29,11 @@ type Props = {
tags?: WsTag[]; tags?: WsTag[];
isVisible?: boolean; isVisible?: boolean;
isProtectedBranch?: boolean; isProtectedBranch?: boolean;
importedBy?: { envName: string; folderName: string }[];
}; };
type GroupedFolders = Record<string, string[]>;
export const SecretListView = ({ export const SecretListView = ({
secrets = [], secrets = [],
environment, environment,
@@ -37,7 +41,8 @@ export const SecretListView = ({
secretPath = "/", secretPath = "/",
tags: wsTags = [], tags: wsTags = [],
isVisible, isVisible,
isProtectedBranch = false isProtectedBranch = false,
importedBy
}: Props) => { }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -352,7 +357,50 @@ export const SecretListView = ({
onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)}
onDeleteApproved={handleSecretDelete} onDeleteApproved={handleSecretDelete}
buttonText="Delete Secret" buttonText="Delete Secret"
/> >
{importedBy && importedBy.length > 0 && (
<div className="mb-4 mt-4 rounded-md border border-red-500/50 bg-red-900/30 p-3">
<div className="flex items-start gap-2.5">
<div className="mt-0.5 flex-shrink-0 text-red-500">
<FontAwesomeIcon icon={faWarning} />
</div>
<div className="w-full">
<p className="mb-3 text-sm text-red-400">
Warning: This secret is currently being imported by another folder, so deletion
will affect both locations.
</p>
<div className="flex w-full">
{Object.entries(
importedBy.reduce<GroupedFolders>((acc, { envName, folderName }) => {
if (!acc[envName]) acc[envName] = [];
acc[envName].push(folderName);
return acc;
}, {})
).map(([envName, folders], index, array) => (
<div
key={envName}
className={`${index !== array.length - 1 ? "mr-16" : ""} flex-1`}
>
<div className="mb-1 text-sm font-medium text-red-300">{envName}</div>
<div className="border-l border-red-500/30 pl-2">
{folders.map((folderName, idx) => (
<div
key={`imported-folder-${idx + 1}`}
className="py-0.5 text-xs text-red-300"
>
{folderName}
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
)}
</DeleteActionModal>
<SecretDetailSidebar <SecretDetailSidebar
environment={environment} environment={environment}
secretPath={secretPath} secretPath={secretPath}