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()
.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) +
@@ -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,
@@ -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;
@@ -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 = {
@@ -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}
/>
<div className="thin-scrollbar mt-4">
<TableContainer
@@ -38,13 +38,15 @@ type Props = {
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
};
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 = ({
<CollapsibleSecretImports
importedBy={importedBy}
secretsToDelete={secretsToDeleteKeys}
usedBySecretSyncs={usedBySecretSyncsFiltered}
/>
)
}
@@ -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}
/>
<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">
@@ -530,6 +532,7 @@ const Page = () => {
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
/>
)}
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
@@ -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 = ({
<CollapsibleSecretImports
importedBy={importedBy}
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 { 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<CollapsibleSecretImportsProps> = ({
importedBy = [],
usedBySecretSyncs = [],
secretsToDelete
}) => {
const { currentWorkspace } = useWorkspace();
@@ -51,6 +61,14 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
};
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<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) => {
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<CollapsibleSecretImportsProps> =
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<CollapsibleSecretImportsProps> =
return false;
});
if (!hasImportedItems) {
const hasSecretSyncItems = usedBySecretSyncs && usedBySecretSyncs.length > 0;
if (!hasImportedItems && !hasSecretSyncItems) {
return null;
}
@@ -168,12 +205,28 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
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}`
}
>
<Td>
<FontAwesomeIcon
icon={item.type === ItemType.Secret ? faKey : faFileImport}
className={`h-4 w-4 ${item.type === ItemType.Secret ? "text-gray-400" : "text-green-700"}`}
icon={
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"
/>
</Td>
@@ -29,6 +29,7 @@ type Props = {
tags?: WsTag[];
isVisible?: boolean;
isProtectedBranch?: boolean;
usedBySecretSyncs?: { environment: string; name: string; destination: string }[];
importedBy?: {
environment: { name: string; slug: string };
folders: {
@@ -47,6 +48,7 @@ export const SecretListView = ({
tags: wsTags = [],
isVisible,
isProtectedBranch = false,
usedBySecretSyncs,
importedBy
}: Props) => {
const queryClient = useQueryClient();
@@ -372,6 +374,7 @@ export const SecretListView = ({
importedBy.length > 0 && (
<CollapsibleSecretImports
importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
secretsToDelete={[(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key || ""]}
/>
)