mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 06:26:27 +00:00
Add warning on secret deletions where it's being imported by another folder
This commit is contained in:
@@ -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} />}
|
||||||
|
|||||||
+50
-2
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user