mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
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:
@@ -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) +
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user