Merge pull request #3356 from Infisical/feat/showWarningOnImportedSecretDeletion

Add warning on secret deletions where it's being imported by another folder
This commit is contained in:
carlosmonastyrski
2025-04-14 17:37:16 -03:00
committed by GitHub
13 changed files with 775 additions and 27 deletions

View File

@@ -142,6 +142,34 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
})
.array()
.optional(),
importedByEnvs: z
.object({
environment: z.string(),
importedBy: z
.object({
environment: z.object({
name: z.string(),
slug: z.string()
}),
folders: z
.object({
name: z.string(),
isImported: z.boolean(),
secrets: z
.object({
secretId: z.string(),
referencedSecretKey: z.string()
})
.array()
.optional()
})
.array()
})
.array()
.optional()
})
.array()
.optional(),
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
@@ -471,6 +499,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
const importedByEnvs = [];
for await (const environment of environments) {
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
path: secretPath,
environment,
projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secrets: secrets?.filter((s) => s.environment === environment)
});
if (importedBy) {
importedByEnvs.push({
environment,
importedBy
});
}
}
return {
folders,
dynamicSecrets,
@@ -482,6 +532,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalImportCount,
totalSecretCount,
totalSecretRotationCount,
importedByEnvs,
totalCount:
(totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) +
@@ -575,6 +626,28 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
importedBy: z
.object({
environment: z.object({
name: z.string(),
slug: z.string()
}),
folders: z
.object({
name: z.string(),
isImported: z.boolean(),
secrets: z
.object({
secretId: z.string(),
referencedSecretKey: z.string()
})
.array()
.optional()
})
.array()
})
.array()
.optional(),
totalSecretRotationCount: z.number().optional(),
totalCount: z.number()
})
@@ -835,6 +908,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
const importedBy = await server.services.secretImport.getFolderIsImportedBy({
path: secretPath,
environment,
projectId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
secrets
});
if (secrets?.length || secretRotations?.length) {
const secretCount =
(secrets?.length ?? 0) +
@@ -880,6 +964,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalDynamicSecretCount,
totalSecretCount,
totalSecretRotationCount,
importedBy,
totalCount:
(totalImportCount ?? 0) +
(totalFolderCount ?? 0) +

View File

@@ -5,6 +5,8 @@ import { TableName, TSecretImports } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify } from "@app/lib/knex";
import { EnvironmentInfo, FolderInfo, FolderResult, SecretResult } from "./secret-import-types";
export type TSecretImportDALFactory = ReturnType<typeof secretImportDALFactory>;
export const secretImportDALFactory = (db: TDbClient) => {
@@ -169,6 +171,136 @@ export const secretImportDALFactory = (db: TDbClient) => {
}
};
const getFolderIsImportedBy = async (
secretPath: string,
environmentId: string,
environment: string,
projectId: string,
tx?: Knex
) => {
try {
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(
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 (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(
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 }) => ({
envName,
envSlug,
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];
type EnvFolderMap = {
[envName: string]: {
envSlug: string;
folders: {
[folderName: string]: {
secrets: {
secretId: string;
referencedSecretKey: string;
}[];
folderId: string;
folderImported: boolean;
};
};
};
};
const groupedByEnv = allResults.reduce<EnvFolderMap>((acc, item) => {
const env = item.envName;
const folder = item.folderName;
const { envSlug } = item;
const updatedAcc = { ...acc };
if (!updatedAcc[env]) {
updatedAcc[env] = {
envSlug,
folders: {}
};
}
if (!updatedAcc[env].folders[folder]) {
updatedAcc[env].folders[folder] = { secrets: [], folderId: item.folderId, folderImported: false };
}
if ("secretId" in item && 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;
}
return updatedAcc;
}, {});
const formattedResult: EnvironmentInfo[] = Object.keys(groupedByEnv).map((envName) => {
const envData = groupedByEnv[envName];
const folders: FolderInfo[] = Object.keys(envData.folders).map((folderName) => {
const folderData = envData.folders[folderName];
const hasSecrets = folderData.secrets.length > 0;
return {
folderName,
folderId: folderData.folderId,
folderImported: folderData.folderImported,
...(hasSecrets && { secrets: folderData.secrets })
};
});
return {
envName,
envSlug: envData.envSlug,
folders
};
});
return formattedResult;
} catch (error) {
throw new DatabaseError({ error, name: "GetSecretImportsAndReferences" });
}
};
return {
...secretImportOrm,
find,
@@ -176,6 +308,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
findByFolderIds,
findLastImportPosition,
updateAllPosition,
getProjectImportCount
getProjectImportCount,
getFolderIsImportedBy
};
};

View File

@@ -27,6 +27,7 @@ import { decryptSecretRaw } from "../secret/secret-fns";
import { TSecretQueueFactory } from "../secret/secret-queue";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { recursivelyGetSecretPaths } from "../secret-v2-bridge/secret-v2-bridge-fns";
import { TSecretImportDALFactory } from "./secret-import-dal";
import { fnSecretsFromImports, fnSecretsV2FromImports } from "./secret-import-fns";
import {
@@ -798,6 +799,139 @@ export const secretImportServiceFactory = ({
return secImportsArrays.flat();
};
const getFolderIsImportedBy = async ({
path: secretPath,
environment,
projectId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
secrets
}: TGetSecretImportsDTO & {
secrets: { secretKey: string; secretValue: string }[] | undefined;
}) => {
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.SecretImports, { environment, secretPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder)
throw new NotFoundError({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
});
const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId);
const deepPaths: { path: string; folderId: string }[] = [];
await Promise.all(
importedBy.map(async (el) => {
const envDeepPaths = await recursivelyGetSecretPaths({
folderDAL,
projectEnvDAL,
projectId,
environment: el.envSlug,
currentPath: "/"
});
deepPaths.push(...envDeepPaths);
})
);
const result = importedBy.map((el) => ({
environment: {
name: el.envName,
slug: el.envSlug
},
folders: el.folders.map((folderItem) => ({
folderId: folderItem.folderId,
isImported: folderItem.folderImported,
secrets: folderItem.secrets,
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 {
createImport,
updateImport,
@@ -810,6 +944,7 @@ export const secretImportServiceFactory = ({
getProjectImportCount,
fnSecretsFromImports,
getProjectImportMultiEnvCount,
getImportsMultiEnv
getImportsMultiEnv,
getFolderIsImportedBy
};
};

View File

@@ -45,3 +45,29 @@ export type TGetSecretsFromImportDTO = {
environment: string;
path: string;
} & TProjectPermission;
export type FolderResult = {
envName: string;
folderName: string;
folderId: string;
envSlug: string;
};
export type SecretResult = {
secretId: string;
referencedSecretKey: string;
} & FolderResult;
export type FolderInfo = {
folderName: string;
secrets?: { secretId: string; referencedSecretKey: string }[];
folderId: string;
folderImported: boolean;
envSlug?: string;
};
export type EnvironmentInfo = {
envName: string;
envSlug: string;
folders: FolderInfo[];
};