Improve referenced secret deletion message logic

This commit is contained in:
carlosmonastyrski
2025-04-11 09:29:01 -03:00
parent f15b30ff85
commit aeaa5babab
7 changed files with 195 additions and 85 deletions
@@ -577,13 +577,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount: z.number().optional(), totalSecretCount: z.number().optional(),
importedBy: z importedBy: z
.object({ .object({
envName: z.string(), environment: z.object({
envSlug: z.string(), name: z.string(),
slug: z.string()
}),
folders: z folders: z
.object({ .object({
folderName: z.string(), name: z.string(),
folderImported: z.boolean(), isImported: z.boolean(),
secrets: z.array(z.string()).optional() secrets: z
.object({
secretId: z.string(),
referencedSecretKey: z.string()
})
.array()
.optional()
}) })
.array() .array()
}) })
@@ -856,7 +864,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId,
secrets
}); });
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
@@ -5,7 +5,7 @@ import { TableName, TSecretImports } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex"; import { ormify } from "@app/lib/knex";
import { EnvInfo, FolderInfo, FolderResult, SecretResult } from "./secret-import-types"; import { EnvironmentInfo, FolderInfo, FolderResult, SecretResult } from "./secret-import-types";
export type TSecretImportDALFactory = ReturnType<typeof secretImportDALFactory>; export type TSecretImportDALFactory = ReturnType<typeof secretImportDALFactory>;
@@ -179,31 +179,31 @@ export const secretImportDALFactory = (db: TDbClient) => {
tx?: Knex tx?: Knex
) => { ) => {
try { try {
const knexInstance = tx || db.replicaNode(); const folderImports = await (tx || db.replicaNode())(TableName.SecretImport)
const folderImports = await knexInstance(TableName.SecretImport)
.where({ importPath: secretPath, importEnv: environmentId }) .where({ importPath: secretPath, importEnv: environmentId })
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.select( .select(
knexInstance.ref("name").withSchema(TableName.Environment).as("envName"), db.ref("name").withSchema(TableName.Environment).as("envName"),
knexInstance.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
knexInstance.ref("name").withSchema(TableName.SecretFolder).as("folderName"), db.ref("name").withSchema(TableName.SecretFolder).as("folderName"),
knexInstance.ref("id").withSchema(TableName.SecretFolder).as("folderId") db.ref("id").withSchema(TableName.SecretFolder).as("folderId")
); );
const secretReferences = await knexInstance(TableName.SecretReferenceV2) const secretReferences = await (tx || db.replicaNode())(TableName.SecretReferenceV2)
.where({ secretPath, environment }) .where({ secretPath, environment })
.join(TableName.SecretV2, `${TableName.SecretReferenceV2}.secretId`, `${TableName.SecretV2}.id`) .join(TableName.SecretV2, `${TableName.SecretReferenceV2}.secretId`, `${TableName.SecretV2}.id`)
.join(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.where(`${TableName.Environment}.projectId`, projectId) .where(`${TableName.Environment}.projectId`, projectId)
.where(`${TableName.SecretFolder}.isReserved`, false)
.select( .select(
knexInstance.ref("key").withSchema(TableName.SecretV2).as("secretId"), db.ref("key").withSchema(TableName.SecretV2).as("secretId"),
knexInstance.ref("name").withSchema(TableName.SecretFolder).as("folderName"), db.ref("name").withSchema(TableName.SecretFolder).as("folderName"),
knexInstance.ref("name").withSchema(TableName.Environment).as("envName"), db.ref("name").withSchema(TableName.Environment).as("envName"),
knexInstance.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
knexInstance.ref("id").withSchema(TableName.SecretFolder).as("folderId") db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey")
); );
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({ const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
@@ -213,13 +213,16 @@ export const secretImportDALFactory = (db: TDbClient) => {
folderId folderId
})); }));
const secretResults = secretReferences.map(({ envName, envSlug, secretId, folderName, folderId }) => ({ const secretResults = secretReferences.map(
envName, ({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({
envSlug, envName,
secretId, envSlug,
folderName, secretId,
folderId folderName,
})); folderId,
referencedSecretKey
})
);
type ResultItem = FolderResult | SecretResult; type ResultItem = FolderResult | SecretResult;
const allResults: ResultItem[] = [...folderResults, ...secretResults]; const allResults: ResultItem[] = [...folderResults, ...secretResults];
@@ -229,7 +232,10 @@ export const secretImportDALFactory = (db: TDbClient) => {
envSlug: string; envSlug: string;
folders: { folders: {
[folderName: string]: { [folderName: string]: {
secrets: string[]; secrets: {
secretId: string;
referencedSecretKey: string;
}[];
folderId: string; folderId: string;
folderImported: boolean; folderImported: boolean;
}; };
@@ -256,7 +262,10 @@ export const secretImportDALFactory = (db: TDbClient) => {
} }
if ("secretId" in item && item.secretId) { if ("secretId" in item && item.secretId) {
updatedAcc[env].folders[folder].secrets = [...updatedAcc[env].folders[folder].secrets, item.secretId]; updatedAcc[env].folders[folder].secrets = [
...updatedAcc[env].folders[folder].secrets,
{ secretId: item.secretId, referencedSecretKey: item.referencedSecretKey }
];
} else { } else {
updatedAcc[env].folders[folder].folderImported = true; updatedAcc[env].folders[folder].folderImported = true;
} }
@@ -264,7 +273,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
return updatedAcc; return updatedAcc;
}, {}); }, {});
const formattedResult: EnvInfo[] = Object.keys(groupedByEnv).map((envName) => { const formattedResult: EnvironmentInfo[] = Object.keys(groupedByEnv).map((envName) => {
const envData = groupedByEnv[envName]; const envData = groupedByEnv[envName];
const folders: FolderInfo[] = Object.keys(envData.folders).map((folderName) => { const folders: FolderInfo[] = Object.keys(envData.folders).map((folderName) => {
@@ -288,7 +297,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
return formattedResult; return formattedResult;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "get secret imports and references" }); throw new DatabaseError({ error, name: "GetSecretImportsAndReferences" });
} }
}; };
@@ -801,8 +801,11 @@ export const secretImportServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId,
}: TGetSecretImportsDTO) => { secrets
}: TGetSecretImportsDTO & {
secrets: { secretKey: string; secretValue: string }[] | undefined;
}) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
actorId, actorId,
@@ -823,7 +826,6 @@ export const secretImportServiceFactory = ({
}); });
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId); const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId);
console.log({ importedBy });
const deepPaths: { path: string; folderId: string }[] = []; const deepPaths: { path: string; folderId: string }[] = [];
await Promise.all( await Promise.all(
@@ -839,15 +841,90 @@ export const secretImportServiceFactory = ({
}) })
); );
return importedBy.map((el) => ({ const result = importedBy.map((el) => ({
...el, environment: {
name: el.envName,
slug: el.envSlug
},
folders: el.folders.map((folderItem) => ({ folders: el.folders.map((folderItem) => ({
folderId: folderItem.folderId, folderId: folderItem.folderId,
folderImported: folderItem.folderImported, isImported: folderItem.folderImported,
secrets: folderItem.secrets, secrets: folderItem.secrets,
folderName: deepPaths.find((p) => p.folderId === folderItem.folderId)?.path || `...${folderItem.folderName}` name: deepPaths.find((p) => p.folderId === folderItem.folderId)?.path || `...${folderItem.folderName}`
})) }))
})); }));
// Special case for same folder references as these do not have an entry on the references table
const locallyReferenced =
secrets
?.filter((secret) => {
return secrets.some(
(otherSecret) =>
otherSecret.secretKey !== secret.secretKey && secret.secretValue.includes(`\${${otherSecret.secretKey}}`)
);
})
.flatMap((secret) => {
return secrets
.filter(
(otherSecret) =>
otherSecret.secretKey !== secret.secretKey &&
secret.secretValue.includes(`\${${otherSecret.secretKey}}`)
)
.map((otherSecret) => ({
secretId: secret.secretKey,
referencedSecretKey: otherSecret.secretKey
}));
}) || [];
if (locallyReferenced.length > 0) {
const existingEnvIndex = result.findIndex((item) => item.environment.slug === environment);
if (existingEnvIndex >= 0) {
const existingFolderIndex = result[existingEnvIndex].folders.findIndex(
(folderItem) => folderItem.name === secretPath
);
if (existingFolderIndex >= 0) {
if (!result[existingEnvIndex].folders[existingFolderIndex].secrets) {
result[existingEnvIndex].folders[existingFolderIndex].secrets = [];
}
const existingSecrets = result[existingEnvIndex].folders[existingFolderIndex].secrets || [];
locallyReferenced.forEach((ref) => {
if (
!existingSecrets.some(
(s) => s.secretId === ref.secretId && s.referencedSecretKey === ref.referencedSecretKey
)
) {
existingSecrets.push(ref);
}
});
} else {
result[existingEnvIndex].folders.push({
folderId: folder.id,
isImported: false,
secrets: locallyReferenced,
name: secretPath
});
}
} else {
result.push({
environment: {
slug: environment,
name: environment
},
folders: [
{
folderId: folder.id,
isImported: false,
secrets: locallyReferenced,
name: secretPath
}
]
});
}
}
return result;
}; };
return { return {
@@ -54,22 +54,19 @@ export type FolderResult = {
}; };
export type SecretResult = { export type SecretResult = {
envName: string;
secretId: string; secretId: string;
folderName: string; referencedSecretKey: string;
folderId: string; } & FolderResult;
envSlug: string;
};
export type FolderInfo = { export type FolderInfo = {
folderName: string; folderName: string;
secrets?: string[]; secrets?: { secretId: string; referencedSecretKey: string }[];
folderId: string; folderId: string;
folderImported: boolean; folderImported: boolean;
envSlug?: string; envSlug?: string;
}; };
export type EnvInfo = { export type EnvironmentInfo = {
envName: string; envName: string;
envSlug: string; envSlug: string;
folders: FolderInfo[]; folders: FolderInfo[];
+6 -3
View File
@@ -42,9 +42,12 @@ export type DashboardProjectSecretsDetailsResponse = {
totalSecretRotationCount?: number; totalSecretRotationCount?: number;
totalCount: number; totalCount: number;
importedBy?: { importedBy?: {
envName: string; environment: { name: string; slug: string };
envSlug: string; folders: {
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; name: string;
secrets?: { secretId: string; referencedSecretKey: string }[];
isImported: boolean;
}[];
}[]; }[];
}; };
@@ -5,26 +5,35 @@ 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";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
enum ItemType {
Folder = "Folder",
Secret = "Secret"
}
interface FlatItem { interface FlatItem {
type: "folder" | "secret"; type: ItemType;
path: string; path: string;
secretKey?: string; secretKey?: string;
reference: string; reference: string;
id: string; id: string;
envName: string; environment: { name: string; slug: string };
envSlug: string;
} }
interface CollapsibleSecretImportsProps { interface CollapsibleSecretImportsProps {
importedBy?: { importedBy?: {
envName: string; environment: { name: string; slug: string };
envSlug: string; folders: {
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; name: string;
secrets?: { secretId: string; referencedSecretKey: string }[];
isImported: boolean;
}[];
}[]; }[];
secretToDelete: string;
} }
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
importedBy = [] importedBy = [],
secretToDelete
}) => { }) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@@ -43,7 +52,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
const handlePathClick = (item: FlatItem) => { const handlePathClick = (item: FlatItem) => {
let pathToNavigate; let pathToNavigate;
if (item.type === "folder") { if (item.type === ItemType.Folder) {
pathToNavigate = item.path; pathToNavigate = item.path;
} else { } else {
const lastSlashIndex = item.path.lastIndexOf("/"); const lastSlashIndex = item.path.lastIndexOf("/");
@@ -51,7 +60,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
} }
const encodedPath = encodeURIComponent(pathToNavigate); const encodedPath = encodeURIComponent(pathToNavigate);
window.open( window.open(
`/secret-manager/${currentWorkspace.id}/secrets/${item.envSlug}?secretPath=${encodedPath}`, `/secret-manager/${currentWorkspace.id}/secrets/${item.environment.slug}?secretPath=${encodedPath}`,
"_blank" "_blank"
); );
}; };
@@ -61,38 +70,36 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
importedBy.forEach((env) => { importedBy.forEach((env) => {
env.folders.forEach((folder) => { env.folders.forEach((folder) => {
if (folder.folderImported) { if (folder.isImported) {
items.push({ items.push({
type: "folder", type: ItemType.Folder,
path: folder.folderName, path: folder.name,
id: `folder-${env.envName}-${folder.folderName}`, id: `folder-${env.environment.name}-${folder.name}`,
reference: "Imported", reference: "Imported",
envName: env.envName, environment: env.environment
envSlug: env.envSlug
}); });
} }
if (folder.secrets?.length) { if (folder.secrets?.length) {
folder.secrets.forEach((secret) => { folder.secrets.forEach(({ secretId: secret, referencedSecretKey }) => {
const secretPath = const secretPath = folder.name === "/" ? `/${secret}` : `${folder.name}/${secret}`;
folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`; if (referencedSecretKey === secretToDelete) {
items.push({
items.push({ type: ItemType.Secret,
type: "secret", path: secretPath,
path: secretPath, secretKey: referencedSecretKey,
secretKey: secret, id: `secret-${env.environment.name}-${secretPath}`,
id: `secret-${env.envName}-${secretPath}`, reference: "Referenced",
reference: "Referenced", environment: env.environment
envName: env.envName, });
envSlug: env.envSlug }
});
}); });
} }
}); });
}); });
return items.sort((a, b) => { return items.sort((a, b) => {
const envCompare = a.envName.localeCompare(b.envName); const envCompare = a.environment.name.localeCompare(b.environment.name);
if (envCompare !== 0) return envCompare; if (envCompare !== 0) return envCompare;
const aPath = a.path.startsWith("/") ? a.path : `/${a.path}`; const aPath = a.path.startsWith("/") ? a.path : `/${a.path}`;
@@ -142,12 +149,12 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
> >
<Td> <Td>
<FontAwesomeIcon <FontAwesomeIcon
icon={item.type === "secret" ? faKey : faFileImport} icon={item.type === ItemType.Secret ? faKey : faFileImport}
className={`h-4 w-4 ${item.type === "secret" ? "text-gray-400" : "text-green-700"}`} className={`h-4 w-4 ${item.type === ItemType.Secret ? "text-gray-400" : "text-green-700"}`}
aria-hidden="true" aria-hidden="true"
/> />
</Td> </Td>
<Td className="px-4">{item.envName}</Td> <Td className="px-4">{item.environment.name}</Td>
<Td className="truncate px-4">{truncatePath(item.path)}</Td> <Td className="truncate px-4">{truncatePath(item.path)}</Td>
<Td className="px-4">{item.reference}</Td> <Td className="px-4">{item.reference}</Td>
</Tr> </Tr>
@@ -30,9 +30,12 @@ type Props = {
isVisible?: boolean; isVisible?: boolean;
isProtectedBranch?: boolean; isProtectedBranch?: boolean;
importedBy?: { importedBy?: {
envName: string; environment: { name: string; slug: string };
envSlug: string; folders: {
folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; name: string;
secrets?: { secretId: string; referencedSecretKey: string }[];
isImported: boolean;
}[];
}[]; }[];
}; };
@@ -366,7 +369,12 @@ export const SecretListView = ({
buttonText="Delete Secret" buttonText="Delete Secret"
formContent={ formContent={
importedBy && importedBy &&
importedBy.length > 0 && <CollapsibleSecretImports importedBy={importedBy} /> importedBy.length > 0 && (
<CollapsibleSecretImports
importedBy={importedBy}
secretToDelete={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
/>
)
} }
deletionMessage={ deletionMessage={
<> <>
@@ -374,7 +382,7 @@ export const SecretListView = ({
<span className="font-bold"> <span className="font-bold">
&quot;{(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}&quot; &quot;{(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}&quot;
</span>{" "} </span>{" "}
below to perform this action to perform this action
</> </>
} }
/> />