diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 5a52d4748..846e25eb8 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -166,6 +166,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { }) .array() .optional(), + usedBySecretSyncs: z + .object({ + name: z.string(), + destination: z.string(), + environment: z.string() + }) + .array() + .optional(), totalFolderCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(), totalSecretCount: z.number().optional(), @@ -500,6 +508,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { } } + const usedBySecretSyncs: { name: string; destination: string; environment: string }[] = []; + for await (const environment of environments) { + const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath( + { projectId, secretPath, environment }, + req.permission + ); + secretSyncs.forEach((sync) => { + usedBySecretSyncs.push({ + name: sync.name, + destination: sync.destination, + environment + }); + }); + } + return { folders, dynamicSecrets, @@ -512,6 +535,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalSecretCount, totalSecretRotationCount, importedByEnvs, + usedBySecretSyncs, totalCount: (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + @@ -605,6 +629,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalFolderCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(), totalSecretCount: z.number().optional(), + usedBySecretSyncs: z + .object({ + name: z.string(), + destination: z.string(), + environment: z.string() + }) + .array() + .optional(), importedBy: z .object({ environment: z.object({ @@ -898,6 +930,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secrets }); + const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath( + { projectId, secretPath, environment }, + req.permission + ); + const usedBySecretSyncs = secretSyncs.map((sync) => ({ + name: sync.name, + destination: sync.destination, + environment: environment + })); + if (secrets?.length || secretRotations?.length) { const secretCount = (secrets?.length ?? 0) + @@ -944,6 +986,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalSecretCount, totalSecretRotationCount, importedBy, + usedBySecretSyncs, totalCount: (totalImportCount ?? 0) + (totalFolderCount ?? 0) + diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index 14a1a1cf0..a19ebe6f7 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -23,6 +23,7 @@ import { TDeleteSecretSyncDTO, TFindSecretSyncByIdDTO, TFindSecretSyncByNameDTO, + TListSecretSyncsByFolderId, TListSecretSyncsByProjectId, TSecretSync, TTriggerSecretSyncImportSecretsByIdDTO, @@ -85,6 +86,34 @@ export const secretSyncServiceFactory = ({ return secretSyncs as TSecretSync[]; }; + const listSecretSyncsBySecretPath = async ( + { projectId, secretPath, environment, destination }: TListSecretSyncsByFolderId, + actor: OrgServiceActor + ) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorId: actor.id, + actorAuthMethod: actor.authMethod, + actorOrgId: actor.orgId, + actionProjectType: ActionProjectType.SecretManager, + projectId + }); + + if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) { + return []; + } + + const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); + if (!folder) return []; + + const secretSyncs = await secretSyncDAL.find({ + ...(destination && { destination }), + folderId: folder.id + }); + + return secretSyncs as TSecretSync[]; + }; + const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => { const secretSync = await secretSyncDAL.findById(syncId); @@ -518,6 +547,7 @@ export const secretSyncServiceFactory = ({ return { listSecretSyncOptions, listSecretSyncsByProjectId, + listSecretSyncsBySecretPath, findSecretSyncById, findSecretSyncByName, createSecretSync, diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 03e92a57a..7787fe166 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -134,6 +134,13 @@ export type TListSecretSyncsByProjectId = { destination?: SecretSync; }; +export type TListSecretSyncsByFolderId = { + projectId: string; + secretPath: string; + environment: string; + destination?: SecretSync; +}; + export type TFindSecretSyncByIdDTO = { syncId: string; destination: SecretSync; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index e80874f8e..4315faecc 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -25,6 +25,7 @@ export type DashboardProjectSecretsOverviewResponse = { totalUniqueFoldersInPage: number; totalUniqueSecretImportsInPage: number; importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; + usedBySecretSyncs?: { name: string; destination: string; environment: string }[]; totalUniqueSecretRotationsInPage: number; }; @@ -43,6 +44,7 @@ export type DashboardProjectSecretsDetailsResponse = { totalSecretRotationCount?: number; totalCount: number; importedBy?: ProjectSecretsImportedBy[]; + usedBySecretSyncs?: { name: string; destination: string; environment: string }[]; }; export type ProjectSecretsImportedBy = { diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index f974a7f1a..6d30af10b 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -274,7 +274,8 @@ export const OverviewPage = () => { totalUniqueSecretImportsInPage, totalUniqueDynamicSecretsInPage, totalUniqueSecretRotationsInPage, - importedByEnvs + importedByEnvs, + usedBySecretSyncs } = overview ?? {}; const secretImportsShaped = secretImports @@ -1045,6 +1046,7 @@ export const OverviewPage = () => { selectedEntries={selectedEntries} resetSelectedEntries={resetSelectedEntries} importedByEnvs={importedByEnvs} + usedBySecretSyncs={usedBySecretSyncs} />
>; }; importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; + usedBySecretSyncs?: { name: string; destination: string; environment: string }[]; }; export const SelectionPanel = ({ secretPath, resetSelectedEntries, selectedEntries, - importedByEnvs + importedByEnvs, + usedBySecretSyncs }: Props) => { const { permission } = useProjectPermission(); @@ -156,6 +158,12 @@ export const SelectionPanel = ({ ); }, [importedByEnvs, selectedEntries, selectedKeysCount]); + const usedBySecretSyncsFiltered = useMemo(() => { + if (selectedKeysCount === 0 || !usedBySecretSyncs) return null; + const envs = Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)); + return usedBySecretSyncs.filter((syncItem) => envs.includes(syncItem.environment)); + }, [selectedEntries, usedBySecretSyncs, selectedKeysCount]); + const getDeleteModalTitle = () => { if (selectedFolderCount > 0 && selectedKeysCount > 0) { return "Do you want to delete the selected secrets and folders across environments?"; @@ -331,6 +339,7 @@ export const SelectionPanel = ({ ) } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 63398d152..d28f28392 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -220,6 +220,7 @@ const Page = () => { totalSecretCount = 0, totalCount = 0, importedBy, + usedBySecretSyncs, totalSecretRotationCount = 0 } = data ?? {}; @@ -441,6 +442,7 @@ const Page = () => { onClickRollbackMode={() => handlePopUpToggle("snapshots", true)} protectedBranchPolicyName={boardPolicy?.name} importedBy={importedBy} + usedBySecretSyncs={usedBySecretSyncs} />
@@ -530,6 +532,7 @@ const Page = () => { secretPath={secretPath} isProtectedBranch={isProtectedBranch} importedBy={importedBy} + usedBySecretSyncs={usedBySecretSyncs} /> )} {noAccessSecretCount > 0 && } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 4ac4ac804..92dca2023 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -113,6 +113,7 @@ type Props = { onVisibilityToggle: () => void; onToggleRowType: (rowType: RowType) => void; onClickRollbackMode: () => void; + usedBySecretSyncs?: { environment: string; name: string; destination: string }[]; importedBy?: { environment: { name: string; slug: string }; folders: { @@ -139,7 +140,8 @@ export const ActionBar = ({ onClickRollbackMode, onToggleRowType, protectedBranchPolicyName, - importedBy + importedBy, + usedBySecretSyncs }: Props) => { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "addFolder", @@ -1076,6 +1078,7 @@ export const ActionBar = ({ s.key)} + usedBySecretSyncs={usedBySecretSyncs} /> ) } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx index 9a66b23f5..7cb035e79 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx @@ -1,5 +1,6 @@ +/* eslint-disable no-nested-ternary */ import React, { useMemo } from "react"; -import { faFileImport, faKey, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { faFileImport, faKey, faSync, faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2"; @@ -7,7 +8,8 @@ import { useWorkspace } from "@app/context"; enum ItemType { Folder = "Folder", - Secret = "Secret" + Secret = "Secret", + SecretSync = "SecretSync" } interface FlatItem { @@ -28,11 +30,19 @@ interface CollapsibleSecretImportsProps { isImported: boolean; }[]; }[]; + usedBySecretSyncs?: + | { + name: string; + destination: string; + environment: string; + }[] + | null; secretsToDelete: string[]; } export const CollapsibleSecretImports: React.FC = ({ importedBy = [], + usedBySecretSyncs = [], secretsToDelete }) => { const { currentWorkspace } = useWorkspace(); @@ -51,6 +61,14 @@ export const CollapsibleSecretImports: React.FC = }; const handlePathClick = (item: FlatItem) => { + if (item.type === ItemType.SecretSync) { + window.open( + `/secret-manager/${currentWorkspace.id}/integrations?selectedTab=secret-syncs`, + "_blank" + ); + return; + } + let pathToNavigate; if (item.type === ItemType.Folder) { pathToNavigate = item.path; @@ -103,7 +121,24 @@ export const CollapsibleSecretImports: React.FC = }); }); + // Add secret sync items + usedBySecretSyncs?.forEach((syncItem) => { + items.push({ + type: ItemType.SecretSync, + path: syncItem.destination, + id: `secret-sync-${syncItem.name}-${syncItem.destination}`, + reference: "Secret Sync", + environment: { name: syncItem.environment, slug: "" } + }); + }); + return items.sort((a, b) => { + if (a.type === ItemType.SecretSync && b.type !== ItemType.SecretSync) return 1; + if (a.type !== ItemType.SecretSync && b.type === ItemType.SecretSync) return -1; + + if (a.type === ItemType.SecretSync && b.type === ItemType.SecretSync) { + return a.path.localeCompare(b.path); + } const envCompare = a.environment.name.localeCompare(b.environment.name); if (envCompare !== 0) return envCompare; @@ -119,7 +154,7 @@ export const CollapsibleSecretImports: React.FC = return aPath.localeCompare(bPath); }); - }, [importedBy]); + }, [importedBy, usedBySecretSyncs, secretsToDelete]); const hasImportedItems = importedBy.some((element) => { if (element.folders && element.folders.length > 0) { @@ -135,7 +170,9 @@ export const CollapsibleSecretImports: React.FC = return false; }); - if (!hasImportedItems) { + const hasSecretSyncItems = usedBySecretSyncs && usedBySecretSyncs.length > 0; + + if (!hasImportedItems && !hasSecretSyncItems) { return null; } @@ -168,12 +205,28 @@ export const CollapsibleSecretImports: React.FC = key={item.id} onClick={() => handlePathClick(item)} className="cursor-pointer hover:bg-mineshaft-700" - title={`Navigate to ${item.path}`} + title={ + item.type === ItemType.SecretSync + ? "Navigate to Secret Syncs" + : `Navigate to ${item.path}` + } >