mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 10:28:50 +00:00
feat(secrets-ui): add secret syncs to referenced secret warning
This commit is contained in:
@@ -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
|
||||
|
||||
+10
-1
@@ -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} />}
|
||||
|
||||
+4
-1
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+60
-7
@@ -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>
|
||||
|
||||
+3
@@ -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 || ""]}
|
||||
/>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user