diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index fc33bcafe..f12a5d549 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -470,7 +470,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalFolderCount: z.number().optional(), totalDynamicSecretCount: 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 { imports, folders, @@ -709,7 +726,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalDynamicSecretCount, totalSecretCount, totalCount: - (totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + (totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0), + importedBy }; } }); diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index da25f4d30..4768c4e38 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -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 { ...secretImportOrm, find, @@ -176,6 +197,7 @@ export const secretImportDALFactory = (db: TDbClient) => { findByFolderIds, findLastImportPosition, updateAllPosition, - getProjectImportCount + getProjectImportCount, + getFolderIsImportedBy }; }; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index fdd326b5f..fef34e42f 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -793,6 +793,39 @@ export const secretImportServiceFactory = ({ 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 { createImport, updateImport, @@ -805,6 +838,7 @@ export const secretImportServiceFactory = ({ getProjectImportCount, fnSecretsFromImports, getProjectImportMultiEnvCount, - getImportsMultiEnv + getImportsMultiEnv, + getFolderIsImportedBy }; }; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 556f76eaf..62be209dc 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -31,6 +31,7 @@ export type DashboardProjectSecretsDetailsResponse = { totalDynamicSecretCount?: number; totalSecretCount?: number; totalCount: number; + importedBy?: { envName: string; folderName: string }[]; }; export type DashboardProjectSecretsByKeys = { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 4b90e1fbb..e9f212eae 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -206,7 +206,8 @@ const Page = () => { totalFolderCount = 0, totalDynamicSecretCount = 0, totalSecretCount = 0, - totalCount = 0 + totalCount = 0, + importedBy } = data ?? {}; useResetPageHelper({ @@ -507,6 +508,7 @@ const Page = () => { workspaceId={workspaceId} secretPath={secretPath} isProtectedBranch={isProtectedBranch} + importedBy={importedBy} /> )} {canReadSecret && } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx index 65af9f895..3e29a400e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx @@ -1,4 +1,5 @@ import { useCallback } from "react"; +import { faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQueryClient } from "@tanstack/react-query"; @@ -28,8 +29,11 @@ type Props = { tags?: WsTag[]; isVisible?: boolean; isProtectedBranch?: boolean; + importedBy?: { envName: string; folderName: string }[]; }; +type GroupedFolders = Record; + export const SecretListView = ({ secrets = [], environment, @@ -37,7 +41,8 @@ export const SecretListView = ({ secretPath = "/", tags: wsTags = [], isVisible, - isProtectedBranch = false + isProtectedBranch = false, + importedBy }: Props) => { const queryClient = useQueryClient(); const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ @@ -352,7 +357,50 @@ export const SecretListView = ({ onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)} onDeleteApproved={handleSecretDelete} buttonText="Delete Secret" - /> + > + {importedBy && importedBy.length > 0 && ( +
+
+
+ +
+
+

+ Warning: This secret is currently being imported by another folder, so deletion + will affect both locations. +

+ +
+ {Object.entries( + importedBy.reduce((acc, { envName, folderName }) => { + if (!acc[envName]) acc[envName] = []; + acc[envName].push(folderName); + return acc; + }, {}) + ).map(([envName, folders], index, array) => ( +
+
{envName}
+
+ {folders.map((folderName, idx) => ( +
+ {folderName} +
+ ))} +
+
+ ))} +
+
+
+
+ )} +