From 4420985669848a2376b97d8fbdf0fb2579a34ccd Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 2 Apr 2025 18:58:34 -0300 Subject: [PATCH 01/13] Add warning on secret deletions where it's being imported by another folder --- .../src/server/routes/v1/dashboard-router.ts | 22 +++++++- .../secret-import/secret-import-dal.ts | 24 ++++++++- .../secret-import/secret-import-service.ts | 36 ++++++++++++- frontend/src/hooks/api/dashboard/types.ts | 1 + .../SecretDashboardPage.tsx | 4 +- .../SecretListView/SecretListView.tsx | 52 ++++++++++++++++++- 6 files changed, 132 insertions(+), 7 deletions(-) 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} +
+ ))} +
+
+ ))} +
+
+
+
+ )} + Date: Wed, 2 Apr 2025 19:02:24 -0300 Subject: [PATCH 02/13] Truncate folder name on warning message --- .../components/SecretListView/SecretListView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3e29a400e..112e2f437 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx @@ -387,7 +387,7 @@ export const SecretListView = ({ {folders.map((folderName, idx) => (
{folderName}
From 4d8598a019c85bcb6ad64692f6dedc6b5cbdebf0 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Wed, 2 Apr 2025 19:06:27 -0300 Subject: [PATCH 03/13] Fix lint issue --- backend/src/lib/api-docs/constants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index bd6056359..ca59c1fc8 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -633,7 +633,8 @@ export const FOLDERS = { path: "The path to list folders from.", directory: "The directory to list folders from. (Deprecated in favor of path)", recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories.", - lastSecretModified: "The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)" + lastSecretModified: + "The timestamp used to filter folders with secrets modified after the specified date. The format for this timestamp is ISO 8601 (e.g. 2025-04-01T09:41:45-04:00)" }, GET_BY_ID: { folderId: "The ID of the folder to get details." From a0bf03b2ae0e77123a99d3782b8a400ec96a7d10 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Thu, 3 Apr 2025 16:00:41 -0300 Subject: [PATCH 04/13] UI improvements on secret deletion warning --- .../src/server/routes/v1/dashboard-router.ts | 7 +- .../secret-import/secret-import-dal.ts | 100 +++++++++++++-- .../secret-import/secret-import-service.ts | 5 +- .../secret-import/secret-import-types.ts | 21 ++++ frontend/src/hooks/api/dashboard/types.ts | 2 +- .../SecretListView/SecretDeletionImpact.tsx | 118 ++++++++++++++++++ .../SecretListView/SecretListView.tsx | 49 +------- 7 files changed, 243 insertions(+), 59 deletions(-) create mode 100644 frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDeletionImpact.tsx diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index f12a5d549..d27e53bb5 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -474,7 +474,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { importedBy: z .object({ envName: z.string(), - folderName: z.string() + folders: z + .object({ + folderName: z.string(), + secrets: z.array(z.string()).optional() + }) + .array() }) .array() .optional() diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index 4768c4e38..933a78c21 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -5,6 +5,8 @@ import { TableName, TSecretImports } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { ormify } from "@app/lib/knex"; +import { EnvInfo, FolderInfo, FolderResult, SecretResult } from "./secret-import-types"; + export type TSecretImportDALFactory = ReturnType; export const secretImportDALFactory = (db: TDbClient) => { @@ -169,24 +171,104 @@ export const secretImportDALFactory = (db: TDbClient) => { } }; - const getFolderIsImportedBy = async (secretPath: string, environment: string, tx?: Knex) => { + const getFolderIsImportedBy = async ( + secretPath: string, + environmentId: string, + environment: string, + projectId: string, + tx?: Knex + ) => { try { - const docs = await (tx || db.replicaNode())(TableName.SecretImport) - .where({ importPath: secretPath, importEnv: environment }) + const knexInstance = tx || db.replicaNode(); + + const folderImports = await knexInstance(TableName.SecretImport) + .where({ importPath: secretPath, importEnv: environmentId }) .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") + knexInstance.ref("name").withSchema(TableName.Environment).as("envName"), + knexInstance.ref("name").withSchema(TableName.SecretFolder).as("folderName"), + knexInstance.ref("parentId").withSchema(TableName.SecretFolder).as("parentFolderId") ); - return docs.map(({ envName, folderName, parentFolderId }) => ({ + const secretReferences = await knexInstance(TableName.SecretReferenceV2) + .where({ secretPath, environment }) + .join(TableName.SecretV2, `${TableName.SecretReferenceV2}.secretId`, `${TableName.SecretV2}.id`) + .join(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`) + .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) + .where(`${TableName.Environment}.projectId`, projectId) + .select( + knexInstance.ref("key").withSchema(TableName.SecretV2).as("secretId"), + knexInstance.ref("name").withSchema(TableName.SecretFolder).as("folderName"), + knexInstance.ref("name").withSchema(TableName.Environment).as("envName"), + knexInstance.ref("parentId").withSchema(TableName.SecretFolder).as("parentFolderId") + ); + + const folderResults = folderImports.map(({ envName, folderName, parentFolderId }) => ({ envName, - folderName: parentFolderId ? folderName : "Project Root Folder" + folderName: parentFolderId ? folderName : "/" })); + + const secretResults = secretReferences.map(({ envName, secretId, folderName, parentFolderId }) => ({ + envName, + secretId, + folderName: parentFolderId ? folderName : "/" + })); + + type ResultItem = FolderResult | SecretResult; + const allResults: ResultItem[] = [...folderResults, ...secretResults]; + + type EnvFolderMap = { + [envName: string]: { + [folderName: string]: { + secrets: string[]; + }; + }; + }; + + const groupedByEnv = allResults.reduce((acc, item) => { + const env = item.envName; + const folder = item.folderName; + + const updatedAcc = { ...acc }; + + if (!updatedAcc[env]) { + updatedAcc[env] = {}; + } + + if (!updatedAcc[env][folder]) { + updatedAcc[env][folder] = { secrets: [] }; + } + + if ("secretId" in item && item.secretId) { + updatedAcc[env][folder].secrets = [...updatedAcc[env][folder].secrets, item.secretId]; + } + + return updatedAcc; + }, {}); + + const formattedResult: EnvInfo[] = Object.keys(groupedByEnv).map((envName) => { + const envData = groupedByEnv[envName]; + + const folders: FolderInfo[] = Object.keys(envData).map((folderName) => { + const folderData = envData[folderName]; + const hasSecrets = folderData.secrets.length > 0; + + return { + folderName, + ...(hasSecrets && { secrets: folderData.secrets }) + }; + }); + + return { + envName, + folders + }; + }); + + return formattedResult; } catch (error) { - throw new DatabaseError({ error, name: "get secret imports count" }); + throw new DatabaseError({ error, name: "get secret imports and references" }); } }; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index fef34e42f..31450513e 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -38,6 +38,7 @@ import { TResyncSecretImportReplicationDTO, TUpdateSecretImportDTO } from "./secret-import-types"; +import { logger } from "@app/lib/logger"; type TSecretImportServiceFactoryDep = { secretImportDAL: TSecretImportDALFactory; @@ -821,8 +822,8 @@ export const secretImportServiceFactory = ({ message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found` }); - const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId); - + const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId); + logger.info("importedBy", JSON.stringify(importedBy)); return importedBy; }; diff --git a/backend/src/services/secret-import/secret-import-types.ts b/backend/src/services/secret-import/secret-import-types.ts index 638e36cb1..62f8eebd3 100644 --- a/backend/src/services/secret-import/secret-import-types.ts +++ b/backend/src/services/secret-import/secret-import-types.ts @@ -45,3 +45,24 @@ export type TGetSecretsFromImportDTO = { environment: string; path: string; } & TProjectPermission; + +export type FolderResult = { + envName: string; + folderName: string; +}; + +export type SecretResult = { + envName: string; + secretId: string; + folderName: string; +}; + +export type FolderInfo = { + folderName: string; + secrets?: string[]; +}; + +export type EnvInfo = { + envName: string; + folders: FolderInfo[]; +}; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 62be209dc..4dc2ac8f7 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -31,7 +31,7 @@ export type DashboardProjectSecretsDetailsResponse = { totalDynamicSecretCount?: number; totalSecretCount?: number; totalCount: number; - importedBy?: { envName: string; folderName: string }[]; + importedBy?: { envName: string; folders: { folderName: string; secrets?: string[] }[] }[]; }; export type DashboardProjectSecretsByKeys = { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDeletionImpact.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDeletionImpact.tsx new file mode 100644 index 000000000..2b1e0151f --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDeletionImpact.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import { faFolder, faKey, faServer, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +interface Folder { + folderName: string; + secrets?: string[]; +} + +interface ImportedByEnvironment { + envName: string; + folders: Folder[]; +} + +interface SecretDeletionImpactProps { + importedBy?: ImportedByEnvironment[]; +} + +export const SecretDeletionImpact: React.FC = ({ importedBy = [] }) => { + const truncateId = (id: string): string => { + if (id.length <= 16) return id; + return `${id.substring(0, 8)}...${id.substring(id.length - 8)}`; + }; + + return ( +
+
+
+ +
+

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

+
+ +
+ {importedBy.map((envData, envIndex) => ( +
+
+ + {envData.envName} Environment +
+ + {envData.folders.length > 0 && ( +
+
+ Affected Folders +
+
+ {envData.folders + .filter((folder) => !folder.secrets || folder.secrets.length === 0) + .map((folder, folderIndex) => ( +
+ + {folder.folderName} +
+ ))} +
+
+ )} + + {envData.folders.some((folder) => folder.secrets && folder.secrets.length > 0) && ( +
+
+ Affected Secrets +
+
+ {envData.folders + .filter((folder) => folder.secrets && folder.secrets.length > 0) + .map((folder) => ( +
+
+ + {folder.folderName} +
+ +
+ {(folder.secrets || []).map((secret) => ( +
+ +
+ + {truncateId(secret)} + +
+
+ ))} +
+
+ ))} +
+
+ )} +
+ ))} +
+
+ ); +}; 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 112e2f437..493663661 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx @@ -1,5 +1,4 @@ import { useCallback } from "react"; -import { faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQueryClient } from "@tanstack/react-query"; @@ -20,6 +19,7 @@ import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPa import { SecretDetailSidebar } from "./SecretDetailSidebar"; import { SecretItem } from "./SecretItem"; import { FontAwesomeSpriteSymbols } from "./SecretListView.utils"; +import { SecretDeletionImpact } from "./SecretDeletionImpact"; type Props = { secrets?: SecretV3RawSanitized[]; @@ -29,11 +29,9 @@ type Props = { tags?: WsTag[]; isVisible?: boolean; isProtectedBranch?: boolean; - importedBy?: { envName: string; folderName: string }[]; + importedBy?: { envName: string; folders: { folderName: string; secrets?: string[] }[] }[]; }; -type GroupedFolders = Record; - export const SecretListView = ({ secrets = [], environment, @@ -358,48 +356,7 @@ export const SecretListView = ({ 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} -
- ))} -
-
- ))} -
-
-
-
- )} + {importedBy && importedBy.length > 0 && } Date: Thu, 3 Apr 2025 17:40:08 -0300 Subject: [PATCH 05/13] UI improvements on secret deletion warning --- .../secret-import/secret-import-dal.ts | 44 ++-- .../secret-import/secret-import-service.ts | 28 ++- .../secret-import/secret-import-types.ts | 7 + .../SecretTreeView.tsx | 54 +++-- .../SecretListView/SecretDeletionImpact.tsx | 192 ++++++++++-------- 5 files changed, 204 insertions(+), 121 deletions(-) diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index 933a78c21..2732c8eca 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -187,8 +187,9 @@ export const secretImportDALFactory = (db: TDbClient) => { .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .select( knexInstance.ref("name").withSchema(TableName.Environment).as("envName"), + knexInstance.ref("slug").withSchema(TableName.Environment).as("envSlug"), knexInstance.ref("name").withSchema(TableName.SecretFolder).as("folderName"), - knexInstance.ref("parentId").withSchema(TableName.SecretFolder).as("parentFolderId") + knexInstance.ref("id").withSchema(TableName.SecretFolder).as("folderId") ); const secretReferences = await knexInstance(TableName.SecretReferenceV2) @@ -201,18 +202,23 @@ export const secretImportDALFactory = (db: TDbClient) => { knexInstance.ref("key").withSchema(TableName.SecretV2).as("secretId"), knexInstance.ref("name").withSchema(TableName.SecretFolder).as("folderName"), knexInstance.ref("name").withSchema(TableName.Environment).as("envName"), - knexInstance.ref("parentId").withSchema(TableName.SecretFolder).as("parentFolderId") + knexInstance.ref("slug").withSchema(TableName.Environment).as("envSlug"), + knexInstance.ref("id").withSchema(TableName.SecretFolder).as("folderId") ); - const folderResults = folderImports.map(({ envName, folderName, parentFolderId }) => ({ + const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({ envName, - folderName: parentFolderId ? folderName : "/" + envSlug, + folderName, + folderId })); - const secretResults = secretReferences.map(({ envName, secretId, folderName, parentFolderId }) => ({ + const secretResults = secretReferences.map(({ envName, envSlug, secretId, folderName, folderId }) => ({ envName, + envSlug, secretId, - folderName: parentFolderId ? folderName : "/" + folderName, + folderId })); type ResultItem = FolderResult | SecretResult; @@ -220,8 +226,12 @@ export const secretImportDALFactory = (db: TDbClient) => { type EnvFolderMap = { [envName: string]: { - [folderName: string]: { - secrets: string[]; + envSlug: string; + folders: { + [folderName: string]: { + secrets: string[]; + folderId: string; + }; }; }; }; @@ -229,19 +239,23 @@ export const secretImportDALFactory = (db: TDbClient) => { const groupedByEnv = allResults.reduce((acc, item) => { const env = item.envName; const folder = item.folderName; + const { envSlug } = item; const updatedAcc = { ...acc }; if (!updatedAcc[env]) { - updatedAcc[env] = {}; + updatedAcc[env] = { + envSlug, + folders: {} + }; } - if (!updatedAcc[env][folder]) { - updatedAcc[env][folder] = { secrets: [] }; + if (!updatedAcc[env].folders[folder]) { + updatedAcc[env].folders[folder] = { secrets: [], folderId: item.folderId }; } if ("secretId" in item && item.secretId) { - updatedAcc[env][folder].secrets = [...updatedAcc[env][folder].secrets, item.secretId]; + updatedAcc[env].folders[folder].secrets = [...updatedAcc[env].folders[folder].secrets, item.secretId]; } return updatedAcc; @@ -250,18 +264,20 @@ export const secretImportDALFactory = (db: TDbClient) => { const formattedResult: EnvInfo[] = Object.keys(groupedByEnv).map((envName) => { const envData = groupedByEnv[envName]; - const folders: FolderInfo[] = Object.keys(envData).map((folderName) => { - const folderData = envData[folderName]; + const folders: FolderInfo[] = Object.keys(envData.folders).map((folderName) => { + const folderData = envData.folders[folderName]; const hasSecrets = folderData.secrets.length > 0; return { folderName, + folderId: folderData.folderId, ...(hasSecrets && { secrets: folderData.secrets }) }; }); return { envName, + envSlug: envData.envSlug, folders }; }); diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index 31450513e..2d2a71de2 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -27,6 +27,7 @@ import { decryptSecretRaw } from "../secret/secret-fns"; import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; +import { recursivelyGetSecretPaths } from "../secret-v2-bridge/secret-v2-bridge-fns"; import { TSecretImportDALFactory } from "./secret-import-dal"; import { fnSecretsFromImports, fnSecretsV2FromImports } from "./secret-import-fns"; import { @@ -38,7 +39,6 @@ import { TResyncSecretImportReplicationDTO, TUpdateSecretImportDTO } from "./secret-import-types"; -import { logger } from "@app/lib/logger"; type TSecretImportServiceFactoryDep = { secretImportDAL: TSecretImportDALFactory; @@ -823,8 +823,30 @@ export const secretImportServiceFactory = ({ }); const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId); - logger.info("importedBy", JSON.stringify(importedBy)); - return importedBy; + + const deepPaths: { path: string; folderId: string }[] = []; + + await Promise.all( + importedBy.map(async (el) => { + const envDeepPaths = await recursivelyGetSecretPaths({ + folderDAL, + projectEnvDAL, + projectId, + environment: el.envSlug, + currentPath: "/" + }); + deepPaths.push(...envDeepPaths); + }) + ); + + return importedBy.map((el) => ({ + ...el, + folders: el.folders.map((folderItem) => ({ + folderId: folderItem.folderId, + secrets: folderItem.secrets, + folderName: deepPaths.find((p) => p.folderId === folderItem.folderId)?.path || `...${folderItem.folderName}` + })) + })); }; return { diff --git a/backend/src/services/secret-import/secret-import-types.ts b/backend/src/services/secret-import/secret-import-types.ts index 62f8eebd3..b598b2530 100644 --- a/backend/src/services/secret-import/secret-import-types.ts +++ b/backend/src/services/secret-import/secret-import-types.ts @@ -49,20 +49,27 @@ export type TGetSecretsFromImportDTO = { export type FolderResult = { envName: string; folderName: string; + folderId: string; + envSlug: string; }; export type SecretResult = { envName: string; secretId: string; folderName: string; + folderId: string; + envSlug: string; }; export type FolderInfo = { folderName: string; secrets?: string[]; + folderId: string; + envSlug?: string; }; export type EnvInfo = { envName: string; + envSlug: string; folders: FolderInfo[]; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx index 4119f54e3..eba5e75d7 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ReplicateFolderFromBoard/SecretTreeView.tsx @@ -41,6 +41,7 @@ interface FolderProps { isExpanded?: boolean; level: number; basePath?: string; + isDisabled?: boolean; } interface TreeViewProps { @@ -48,6 +49,7 @@ interface TreeViewProps { basePath?: string; className?: string; onChange: (items: SecretItem[]) => void; + isDisabled?: boolean; } const getAllItemsInFolder = (folder: FolderStructure): SecretItem[] => { @@ -81,7 +83,8 @@ const Folder: React.FC = ({ onFolderSelect, isExpanded = false, level, - basePath + basePath, + isDisabled = false }) => { const [open, setOpen] = useState(isExpanded); const displayName = useMemo(() => getDisplayName(name), [name]); @@ -127,13 +130,16 @@ const Folder: React.FC = ({ className={`h-4 w-4 text-${level === 0 ? "mineshaft-300" : "yellow"}`} /> - + {!isDisabled && ( + + )}