diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index a23aa49e1..8ea4fb48d 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -577,13 +577,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { totalSecretCount: z.number().optional(), importedBy: z .object({ - envName: z.string(), - envSlug: z.string(), + environment: z.object({ + name: z.string(), + slug: z.string() + }), folders: z .object({ - folderName: z.string(), - folderImported: z.boolean(), - secrets: z.array(z.string()).optional() + name: z.string(), + isImported: z.boolean(), + secrets: z + .object({ + secretId: z.string(), + referencedSecretKey: z.string() + }) + .array() + .optional() }) .array() }) @@ -856,7 +864,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, - actorOrgId: req.permission.orgId + actorOrgId: req.permission.orgId, + secrets }); if (secrets?.length || secretRotations?.length) { diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index f045df664..1a171aa2e 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -5,7 +5,7 @@ import { TableName, TSecretImports } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; 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; @@ -179,31 +179,31 @@ export const secretImportDALFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const knexInstance = tx || db.replicaNode(); - - const folderImports = await knexInstance(TableName.SecretImport) + const folderImports = await (tx || db.replicaNode())(TableName.SecretImport) .where({ importPath: secretPath, importEnv: environmentId }) .join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .select( - knexInstance.ref("name").withSchema(TableName.Environment).as("envName"), - knexInstance.ref("slug").withSchema(TableName.Environment).as("envSlug"), - knexInstance.ref("name").withSchema(TableName.SecretFolder).as("folderName"), - knexInstance.ref("id").withSchema(TableName.SecretFolder).as("folderId") + db.ref("name").withSchema(TableName.Environment).as("envName"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.SecretFolder).as("folderName"), + 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 }) .join(TableName.SecretV2, `${TableName.SecretReferenceV2}.secretId`, `${TableName.SecretV2}.id`) .join(TableName.SecretFolder, `${TableName.SecretV2}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) .where(`${TableName.Environment}.projectId`, projectId) + .where(`${TableName.SecretFolder}.isReserved`, false) .select( - knexInstance.ref("key").withSchema(TableName.SecretV2).as("secretId"), - knexInstance.ref("name").withSchema(TableName.SecretFolder).as("folderName"), - knexInstance.ref("name").withSchema(TableName.Environment).as("envName"), - knexInstance.ref("slug").withSchema(TableName.Environment).as("envSlug"), - knexInstance.ref("id").withSchema(TableName.SecretFolder).as("folderId") + db.ref("key").withSchema(TableName.SecretV2).as("secretId"), + db.ref("name").withSchema(TableName.SecretFolder).as("folderName"), + db.ref("name").withSchema(TableName.Environment).as("envName"), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("id").withSchema(TableName.SecretFolder).as("folderId"), + db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey") ); const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({ @@ -213,13 +213,16 @@ export const secretImportDALFactory = (db: TDbClient) => { folderId })); - const secretResults = secretReferences.map(({ envName, envSlug, secretId, folderName, folderId }) => ({ - envName, - envSlug, - secretId, - folderName, - folderId - })); + const secretResults = secretReferences.map( + ({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({ + envName, + envSlug, + secretId, + folderName, + folderId, + referencedSecretKey + }) + ); type ResultItem = FolderResult | SecretResult; const allResults: ResultItem[] = [...folderResults, ...secretResults]; @@ -229,7 +232,10 @@ export const secretImportDALFactory = (db: TDbClient) => { envSlug: string; folders: { [folderName: string]: { - secrets: string[]; + secrets: { + secretId: string; + referencedSecretKey: string; + }[]; folderId: string; folderImported: boolean; }; @@ -256,7 +262,10 @@ export const secretImportDALFactory = (db: TDbClient) => { } 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 { updatedAcc[env].folders[folder].folderImported = true; } @@ -264,7 +273,7 @@ export const secretImportDALFactory = (db: TDbClient) => { return updatedAcc; }, {}); - const formattedResult: EnvInfo[] = Object.keys(groupedByEnv).map((envName) => { + const formattedResult: EnvironmentInfo[] = Object.keys(groupedByEnv).map((envName) => { const envData = groupedByEnv[envName]; const folders: FolderInfo[] = Object.keys(envData.folders).map((folderName) => { @@ -288,7 +297,7 @@ export const secretImportDALFactory = (db: TDbClient) => { return formattedResult; } catch (error) { - throw new DatabaseError({ error, name: "get secret imports and references" }); + throw new DatabaseError({ error, name: "GetSecretImportsAndReferences" }); } }; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index cc966e943..154b4a77f 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -801,8 +801,11 @@ export const secretImportServiceFactory = ({ actor, actorId, actorAuthMethod, - actorOrgId - }: TGetSecretImportsDTO) => { + actorOrgId, + secrets + }: TGetSecretImportsDTO & { + secrets: { secretKey: string; secretValue: string }[] | undefined; + }) => { const { permission } = await permissionService.getProjectPermission({ actor, actorId, @@ -823,7 +826,6 @@ export const secretImportServiceFactory = ({ }); const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId); - console.log({ importedBy }); const deepPaths: { path: string; folderId: string }[] = []; await Promise.all( @@ -839,15 +841,90 @@ export const secretImportServiceFactory = ({ }) ); - return importedBy.map((el) => ({ - ...el, + const result = importedBy.map((el) => ({ + environment: { + name: el.envName, + slug: el.envSlug + }, folders: el.folders.map((folderItem) => ({ folderId: folderItem.folderId, - folderImported: folderItem.folderImported, + isImported: folderItem.folderImported, 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 { diff --git a/backend/src/services/secret-import/secret-import-types.ts b/backend/src/services/secret-import/secret-import-types.ts index 217750e87..e4490e715 100644 --- a/backend/src/services/secret-import/secret-import-types.ts +++ b/backend/src/services/secret-import/secret-import-types.ts @@ -54,22 +54,19 @@ export type FolderResult = { }; export type SecretResult = { - envName: string; secretId: string; - folderName: string; - folderId: string; - envSlug: string; -}; + referencedSecretKey: string; +} & FolderResult; export type FolderInfo = { folderName: string; - secrets?: string[]; + secrets?: { secretId: string; referencedSecretKey: string }[]; folderId: string; folderImported: boolean; envSlug?: string; }; -export type EnvInfo = { +export type EnvironmentInfo = { envName: string; envSlug: string; folders: FolderInfo[]; diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 750bfc826..c88760edc 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -42,9 +42,12 @@ export type DashboardProjectSecretsDetailsResponse = { totalSecretRotationCount?: number; totalCount: number; importedBy?: { - envName: string; - envSlug: string; - folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; + environment: { name: string; slug: string }; + folders: { + name: string; + secrets?: { secretId: string; referencedSecretKey: string }[]; + isImported: boolean; + }[]; }[]; }; 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 6be011587..3c39d9a99 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx @@ -5,26 +5,35 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2"; import { useWorkspace } from "@app/context"; +enum ItemType { + Folder = "Folder", + Secret = "Secret" +} + interface FlatItem { - type: "folder" | "secret"; + type: ItemType; path: string; secretKey?: string; reference: string; id: string; - envName: string; - envSlug: string; + environment: { name: string; slug: string }; } interface CollapsibleSecretImportsProps { importedBy?: { - envName: string; - envSlug: string; - folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; + environment: { name: string; slug: string }; + folders: { + name: string; + secrets?: { secretId: string; referencedSecretKey: string }[]; + isImported: boolean; + }[]; }[]; + secretToDelete: string; } export const CollapsibleSecretImports: React.FC = ({ - importedBy = [] + importedBy = [], + secretToDelete }) => { const { currentWorkspace } = useWorkspace(); @@ -43,7 +52,7 @@ export const CollapsibleSecretImports: React.FC = const handlePathClick = (item: FlatItem) => { let pathToNavigate; - if (item.type === "folder") { + if (item.type === ItemType.Folder) { pathToNavigate = item.path; } else { const lastSlashIndex = item.path.lastIndexOf("/"); @@ -51,7 +60,7 @@ export const CollapsibleSecretImports: React.FC = } const encodedPath = encodeURIComponent(pathToNavigate); window.open( - `/secret-manager/${currentWorkspace.id}/secrets/${item.envSlug}?secretPath=${encodedPath}`, + `/secret-manager/${currentWorkspace.id}/secrets/${item.environment.slug}?secretPath=${encodedPath}`, "_blank" ); }; @@ -61,38 +70,36 @@ export const CollapsibleSecretImports: React.FC = importedBy.forEach((env) => { env.folders.forEach((folder) => { - if (folder.folderImported) { + if (folder.isImported) { items.push({ - type: "folder", - path: folder.folderName, - id: `folder-${env.envName}-${folder.folderName}`, + type: ItemType.Folder, + path: folder.name, + id: `folder-${env.environment.name}-${folder.name}`, reference: "Imported", - envName: env.envName, - envSlug: env.envSlug + environment: env.environment }); } if (folder.secrets?.length) { - folder.secrets.forEach((secret) => { - const secretPath = - folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`; - - items.push({ - type: "secret", - path: secretPath, - secretKey: secret, - id: `secret-${env.envName}-${secretPath}`, - reference: "Referenced", - envName: env.envName, - envSlug: env.envSlug - }); + folder.secrets.forEach(({ secretId: secret, referencedSecretKey }) => { + const secretPath = folder.name === "/" ? `/${secret}` : `${folder.name}/${secret}`; + if (referencedSecretKey === secretToDelete) { + items.push({ + type: ItemType.Secret, + path: secretPath, + secretKey: referencedSecretKey, + id: `secret-${env.environment.name}-${secretPath}`, + reference: "Referenced", + environment: env.environment + }); + } }); } }); }); 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; const aPath = a.path.startsWith("/") ? a.path : `/${a.path}`; @@ -142,12 +149,12 @@ export const CollapsibleSecretImports: React.FC = >