Feat: Recursively get secrets from all nested secret paths

This commit is contained in:
Daniel Hougaard
2024-03-26 19:35:12 +01:00
parent 7c623562e1
commit c29cb667d7
5 changed files with 150 additions and 18 deletions
+1
View File
@@ -215,6 +215,7 @@ export const SECRETS = {
export const RAW_SECRETS = { export const RAW_SECRETS = {
LIST: { LIST: {
recursive: "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories.",
workspaceId: "The ID of the project to list secrets from.", workspaceId: "The ID of the project to list secrets from.",
workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.", workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.",
environment: "The slug of the environment to list secrets from.", environment: "The slug of the environment to list secrets from.",
+14 -2
View File
@@ -157,6 +157,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
recursive: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.recursive),
include_imports: z include_imports: z
.enum(["true", "false"]) .enum(["true", "false"])
.default("false") .default("false")
@@ -165,7 +170,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secrets: secretRawSchema.array(), secrets: secretRawSchema
.merge(
z.object({
secretPath: z.string().optional()
})
)
.array(),
imports: z imports: z
.object({ .object({
secretPath: z.string(), secretPath: z.string(),
@@ -218,7 +229,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: workspaceId, projectId: workspaceId,
path: secretPath, path: secretPath,
includeImports: req.query.include_imports includeImports: req.query.include_imports,
recursive: req.query.recursive
}); });
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
+66 -3
View File
@@ -9,6 +9,7 @@ import {
TSecretBlindIndexes, TSecretBlindIndexes,
TSecrets TSecrets
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { import {
buildSecretBlindIndexFromName, buildSecretBlindIndexFromName,
@@ -17,8 +18,10 @@ import {
} from "@app/lib/crypto"; } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn"; import { groupBy, unique } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns"; import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { import {
@@ -45,6 +48,68 @@ export const generateSecretBlindIndexBySalt = async (secretName: string, secretB
return secretBlindIndex; return secretBlindIndex;
}; };
type TRecursivelyFetchSecretsFromFoldersArg = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "find">;
};
export const recursivelyGetSecretPaths = ({ folderDAL, projectEnvDAL }: TRecursivelyFetchSecretsFromFoldersArg) => {
const getPaths = async (projectId: string, environment: string, currentPath: string) => {
let secretPaths: string[] = [];
// Get secrets in the current folder.
try {
const folder = await folderDAL.findBySecretPath(projectId, environment, currentPath);
if (!folder) {
throw new Error(`Base directory '${currentPath}' not found.`);
}
secretPaths.push(currentPath);
} catch (error) {
logger.error(error, "Error fetching secrets from base directory");
throw error;
}
// List all subfolders in the current folder.
try {
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, currentPath);
if (!env) {
throw new Error(`Environment with not found`);
}
if (!parentFolder) {
throw new Error(`Parent folder not found`);
}
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id });
// Use Promise.all to handle recursive calls concurrently for efficiency.
const secretsFromSubFolders = await Promise.all(
folders.map(async (folder) => {
// Ensure the path is correctly formatted for the next level.
const subFolderPath = `${currentPath}${currentPath !== "/" ? "/" : ""}${folder.name}`;
return getPaths(projectId, environment, subFolderPath);
})
);
// Flatten the array of arrays and concatenate with the current secrets array.
secretPaths = secretPaths.concat(...secretsFromSubFolders);
} catch (error) {
logger.error(error, "Error fetching secrets from subdirectories");
throw error;
}
return secretPaths;
};
return getPaths;
};
type TInterpolateSecretArg = { type TInterpolateSecretArg = {
projectId: string; projectId: string;
secretEncKey: string; secretEncKey: string;
@@ -202,9 +267,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
); );
// eslint-disable-next-line // eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal);
? expandedVal
: formatMultiValueEnv(expandedVal);
} }
return secrets; return secrets;
+68 -13
View File
@@ -13,13 +13,20 @@ import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal"; import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns"; import {
decryptSecretRaw,
fnSecretBlindIndexCheck,
fnSecretBulkInsert,
fnSecretBulkUpdate,
recursivelyGetSecretPaths
} from "./secret-fns";
import { TSecretQueueFactory } from "./secret-queue"; import { TSecretQueueFactory } from "./secret-queue";
import { import {
TAttachSecretTagsDTO, TAttachSecretTagsDTO,
@@ -47,8 +54,12 @@ type TSecretServiceFactoryDep = {
secretDAL: TSecretDALFactory; secretDAL: TSecretDALFactory;
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
secretVersionDAL: TSecretVersionDALFactory; secretVersionDAL: TSecretVersionDALFactory;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick<
TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
>;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@@ -61,6 +72,7 @@ type TSecretServiceFactoryDep = {
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>; export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
export const secretServiceFactory = ({ export const secretServiceFactory = ({
secretDAL, secretDAL,
projectEnvDAL,
secretTagDAL, secretTagDAL,
secretVersionDAL, secretVersionDAL,
folderDAL, folderDAL,
@@ -789,21 +801,64 @@ export const secretServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
environment, environment,
includeImports includeImports,
recursive
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId); const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const { secrets, imports } = await getSecrets({ let secrets: Awaited<ReturnType<typeof getSecrets>>["secrets"];
actorId, let imports: Awaited<ReturnType<typeof getSecrets>>["imports"];
projectId,
environment, if (recursive) {
actor, const getPaths = recursivelyGetSecretPaths({
actorOrgId, permissionService,
actorAuthMethod, folderDAL,
path, projectEnvDAL
includeImports });
});
const paths = await getPaths(projectId, environment, path);
const result = await Promise.all(
paths.map(async (currentPath) => {
const secs = await getSecrets({
actorId,
projectId,
environment,
actor,
actorOrgId,
actorAuthMethod,
path: currentPath,
includeImports
});
return {
secrets: {
...secs.secrets,
secretPath: currentPath
},
imports: secs.imports
};
})
);
secrets = result.flatMap((el) => el.secrets);
imports = result.flatMap((el) => el.imports || []);
} else {
const result = await getSecrets({
actorId,
projectId,
environment,
actor,
actorOrgId,
actorAuthMethod,
path,
includeImports
});
secrets = result.secrets;
imports = result.imports;
}
return { return {
secrets: secrets.map((el) => decryptSecretRaw(el, botKey)), secrets: secrets.map((el) => decryptSecretRaw(el, botKey)),
@@ -140,6 +140,7 @@ export type TGetSecretsRawDTO = {
path: string; path: string;
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TGetASecretRawDTO = { export type TGetASecretRawDTO = {