feat(secrets-ui): add secret syncs to referenced secret warning

This commit is contained in:
carlosmonastyrski
2025-04-25 15:26:34 -03:00
parent f5bb0d4a86
commit d5065af7e9
10 changed files with 165 additions and 10 deletions
@@ -166,6 +166,14 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}) })
.array() .array()
.optional(), .optional(),
usedBySecretSyncs: z
.object({
name: z.string(),
destination: z.string(),
environment: z.string()
})
.array()
.optional(),
totalFolderCount: z.number().optional(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(),
totalSecretCount: 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 { return {
folders, folders,
dynamicSecrets, dynamicSecrets,
@@ -512,6 +535,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount, totalSecretCount,
totalSecretRotationCount, totalSecretRotationCount,
importedByEnvs, importedByEnvs,
usedBySecretSyncs,
totalCount: totalCount:
(totalFolderCount ?? 0) + (totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) + (totalDynamicSecretCount ?? 0) +
@@ -605,6 +629,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(),
usedBySecretSyncs: z
.object({
name: z.string(),
destination: z.string(),
environment: z.string()
})
.array()
.optional(),
importedBy: z importedBy: z
.object({ .object({
environment: z.object({ environment: z.object({
@@ -898,6 +930,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets 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) { if (secrets?.length || secretRotations?.length) {
const secretCount = const secretCount =
(secrets?.length ?? 0) + (secrets?.length ?? 0) +
@@ -944,6 +986,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount, totalSecretCount,
totalSecretRotationCount, totalSecretRotationCount,
importedBy, importedBy,
usedBySecretSyncs,
totalCount: totalCount:
(totalImportCount ?? 0) + (totalImportCount ?? 0) +
(totalFolderCount ?? 0) + (totalFolderCount ?? 0) +
@@ -23,6 +23,7 @@ import {
TDeleteSecretSyncDTO, TDeleteSecretSyncDTO,
TFindSecretSyncByIdDTO, TFindSecretSyncByIdDTO,
TFindSecretSyncByNameDTO, TFindSecretSyncByNameDTO,
TListSecretSyncsByFolderId,
TListSecretSyncsByProjectId, TListSecretSyncsByProjectId,
TSecretSync, TSecretSync,
TTriggerSecretSyncImportSecretsByIdDTO, TTriggerSecretSyncImportSecretsByIdDTO,
@@ -85,6 +86,34 @@ export const secretSyncServiceFactory = ({
return secretSyncs as TSecretSync[]; 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 findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
const secretSync = await secretSyncDAL.findById(syncId); const secretSync = await secretSyncDAL.findById(syncId);
@@ -518,6 +547,7 @@ export const secretSyncServiceFactory = ({
return { return {
listSecretSyncOptions, listSecretSyncOptions,
listSecretSyncsByProjectId, listSecretSyncsByProjectId,
listSecretSyncsBySecretPath,
findSecretSyncById, findSecretSyncById,
findSecretSyncByName, findSecretSyncByName,
createSecretSync, createSecretSync,
@@ -134,6 +134,13 @@ export type TListSecretSyncsByProjectId = {
destination?: SecretSync; destination?: SecretSync;
}; };
export type TListSecretSyncsByFolderId = {
projectId: string;
secretPath: string;
environment: string;
destination?: SecretSync;
};
export type TFindSecretSyncByIdDTO = { export type TFindSecretSyncByIdDTO = {
syncId: string; syncId: string;
destination: SecretSync; destination: SecretSync;
@@ -25,6 +25,7 @@ export type DashboardProjectSecretsOverviewResponse = {
totalUniqueFoldersInPage: number; totalUniqueFoldersInPage: number;
totalUniqueSecretImportsInPage: number; totalUniqueSecretImportsInPage: number;
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
usedBySecretSyncs?: { name: string; destination: string; environment: string }[];
totalUniqueSecretRotationsInPage: number; totalUniqueSecretRotationsInPage: number;
}; };
@@ -43,6 +44,7 @@ export type DashboardProjectSecretsDetailsResponse = {
totalSecretRotationCount?: number; totalSecretRotationCount?: number;
totalCount: number; totalCount: number;
importedBy?: ProjectSecretsImportedBy[]; importedBy?: ProjectSecretsImportedBy[];
usedBySecretSyncs?: { name: string; destination: string; environment: string }[];
}; };
export type ProjectSecretsImportedBy = { export type ProjectSecretsImportedBy = {
@@ -274,7 +274,8 @@ export const OverviewPage = () => {
totalUniqueSecretImportsInPage, totalUniqueSecretImportsInPage,
totalUniqueDynamicSecretsInPage, totalUniqueDynamicSecretsInPage,
totalUniqueSecretRotationsInPage, totalUniqueSecretRotationsInPage,
importedByEnvs importedByEnvs,
usedBySecretSyncs
} = overview ?? {}; } = overview ?? {};
const secretImportsShaped = secretImports const secretImportsShaped = secretImports
@@ -1045,6 +1046,7 @@ export const OverviewPage = () => {
selectedEntries={selectedEntries} selectedEntries={selectedEntries}
resetSelectedEntries={resetSelectedEntries} resetSelectedEntries={resetSelectedEntries}
importedByEnvs={importedByEnvs} importedByEnvs={importedByEnvs}
usedBySecretSyncs={usedBySecretSyncs}
/> />
<div className="thin-scrollbar mt-4"> <div className="thin-scrollbar mt-4">
<TableContainer <TableContainer
@@ -38,13 +38,15 @@ type Props = {
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>; [EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
}; };
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
usedBySecretSyncs?: { name: string; destination: string; environment: string }[];
}; };
export const SelectionPanel = ({ export const SelectionPanel = ({
secretPath, secretPath,
resetSelectedEntries, resetSelectedEntries,
selectedEntries, selectedEntries,
importedByEnvs importedByEnvs,
usedBySecretSyncs
}: Props) => { }: Props) => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
@@ -156,6 +158,12 @@ export const SelectionPanel = ({
); );
}, [importedByEnvs, selectedEntries, selectedKeysCount]); }, [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 = () => { const getDeleteModalTitle = () => {
if (selectedFolderCount > 0 && selectedKeysCount > 0) { if (selectedFolderCount > 0 && selectedKeysCount > 0) {
return "Do you want to delete the selected secrets and folders across environments?"; return "Do you want to delete the selected secrets and folders across environments?";
@@ -331,6 +339,7 @@ export const SelectionPanel = ({
<CollapsibleSecretImports <CollapsibleSecretImports
importedBy={importedBy} importedBy={importedBy}
secretsToDelete={secretsToDeleteKeys} secretsToDelete={secretsToDeleteKeys}
usedBySecretSyncs={usedBySecretSyncsFiltered}
/> />
) )
} }
@@ -220,6 +220,7 @@ const Page = () => {
totalSecretCount = 0, totalSecretCount = 0,
totalCount = 0, totalCount = 0,
importedBy, importedBy,
usedBySecretSyncs,
totalSecretRotationCount = 0 totalSecretRotationCount = 0
} = data ?? {}; } = data ?? {};
@@ -441,6 +442,7 @@ const Page = () => {
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)} onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
protectedBranchPolicyName={boardPolicy?.name} protectedBranchPolicyName={boardPolicy?.name}
importedBy={importedBy} importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
/> />
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300"> <div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard"> <div className="flex flex-col" id="dashboard">
@@ -530,6 +532,7 @@ const Page = () => {
secretPath={secretPath} secretPath={secretPath}
isProtectedBranch={isProtectedBranch} isProtectedBranch={isProtectedBranch}
importedBy={importedBy} importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
/> />
)} )}
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />} {noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
@@ -113,6 +113,7 @@ type Props = {
onVisibilityToggle: () => void; onVisibilityToggle: () => void;
onToggleRowType: (rowType: RowType) => void; onToggleRowType: (rowType: RowType) => void;
onClickRollbackMode: () => void; onClickRollbackMode: () => void;
usedBySecretSyncs?: { environment: string; name: string; destination: string }[];
importedBy?: { importedBy?: {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
@@ -139,7 +140,8 @@ export const ActionBar = ({
onClickRollbackMode, onClickRollbackMode,
onToggleRowType, onToggleRowType,
protectedBranchPolicyName, protectedBranchPolicyName,
importedBy importedBy,
usedBySecretSyncs
}: Props) => { }: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addFolder", "addFolder",
@@ -1076,6 +1078,7 @@ export const ActionBar = ({
<CollapsibleSecretImports <CollapsibleSecretImports
importedBy={importedBy} importedBy={importedBy}
secretsToDelete={Object.values(selectedSecrets).map((s) => s.key)} secretsToDelete={Object.values(selectedSecrets).map((s) => s.key)}
usedBySecretSyncs={usedBySecretSyncs}
/> />
) )
} }
@@ -1,5 +1,6 @@
/* eslint-disable no-nested-ternary */
import React, { useMemo } from "react"; 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2"; import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2";
@@ -7,7 +8,8 @@ import { useWorkspace } from "@app/context";
enum ItemType { enum ItemType {
Folder = "Folder", Folder = "Folder",
Secret = "Secret" Secret = "Secret",
SecretSync = "SecretSync"
} }
interface FlatItem { interface FlatItem {
@@ -28,11 +30,19 @@ interface CollapsibleSecretImportsProps {
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
usedBySecretSyncs?:
| {
name: string;
destination: string;
environment: string;
}[]
| null;
secretsToDelete: string[]; secretsToDelete: string[];
} }
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
importedBy = [], importedBy = [],
usedBySecretSyncs = [],
secretsToDelete secretsToDelete
}) => { }) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@@ -51,6 +61,14 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
}; };
const handlePathClick = (item: FlatItem) => { const handlePathClick = (item: FlatItem) => {
if (item.type === ItemType.SecretSync) {
window.open(
`/secret-manager/${currentWorkspace.id}/integrations?selectedTab=secret-syncs`,
"_blank"
);
return;
}
let pathToNavigate; let pathToNavigate;
if (item.type === ItemType.Folder) { if (item.type === ItemType.Folder) {
pathToNavigate = item.path; pathToNavigate = item.path;
@@ -103,7 +121,24 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
}); });
}); });
// 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) => { 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); const envCompare = a.environment.name.localeCompare(b.environment.name);
if (envCompare !== 0) return envCompare; if (envCompare !== 0) return envCompare;
@@ -119,7 +154,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
return aPath.localeCompare(bPath); return aPath.localeCompare(bPath);
}); });
}, [importedBy]); }, [importedBy, usedBySecretSyncs, secretsToDelete]);
const hasImportedItems = importedBy.some((element) => { const hasImportedItems = importedBy.some((element) => {
if (element.folders && element.folders.length > 0) { if (element.folders && element.folders.length > 0) {
@@ -135,7 +170,9 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
return false; return false;
}); });
if (!hasImportedItems) { const hasSecretSyncItems = usedBySecretSyncs && usedBySecretSyncs.length > 0;
if (!hasImportedItems && !hasSecretSyncItems) {
return null; return null;
} }
@@ -168,12 +205,28 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
key={item.id} key={item.id}
onClick={() => handlePathClick(item)} onClick={() => handlePathClick(item)}
className="cursor-pointer hover:bg-mineshaft-700" 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}`
}
> >
<Td> <Td>
<FontAwesomeIcon <FontAwesomeIcon
icon={item.type === ItemType.Secret ? faKey : faFileImport} icon={
className={`h-4 w-4 ${item.type === ItemType.Secret ? "text-gray-400" : "text-green-700"}`} item.type === ItemType.Secret
? faKey
: item.type === ItemType.Folder
? faFileImport
: faSync
}
className={`h-4 w-4 ${
item.type === ItemType.Secret
? "text-gray-400"
: item.type === ItemType.Folder
? "text-green-700"
: "text-blue-500"
}`}
aria-hidden="true" aria-hidden="true"
/> />
</Td> </Td>
@@ -29,6 +29,7 @@ type Props = {
tags?: WsTag[]; tags?: WsTag[];
isVisible?: boolean; isVisible?: boolean;
isProtectedBranch?: boolean; isProtectedBranch?: boolean;
usedBySecretSyncs?: { environment: string; name: string; destination: string }[];
importedBy?: { importedBy?: {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
@@ -47,6 +48,7 @@ export const SecretListView = ({
tags: wsTags = [], tags: wsTags = [],
isVisible, isVisible,
isProtectedBranch = false, isProtectedBranch = false,
usedBySecretSyncs,
importedBy importedBy
}: Props) => { }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -372,6 +374,7 @@ export const SecretListView = ({
importedBy.length > 0 && ( importedBy.length > 0 && (
<CollapsibleSecretImports <CollapsibleSecretImports
importedBy={importedBy} importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
secretsToDelete={[(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key || ""]} secretsToDelete={[(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key || ""]}
/> />
) )