Feat: Recursively get all secrets from inside path

This commit is contained in:
Daniel Hougaard
2024-03-22 16:12:22 +01:00
parent 9ff3210ed6
commit 566f7e4c61
3 changed files with 166 additions and 100 deletions

View File

@ -157,11 +157,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
recursive: z
deep: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.recursive),
.describe(RAW_SECRETS.LIST.deep),
include_imports: z
.enum(["true", "false"])
.default("false")
@ -230,7 +230,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
projectId: workspaceId,
path: secretPath,
includeImports: req.query.include_imports,
recursive: req.query.recursive
deep: req.query.deep
});
await server.services.auditLog.createAuditLog({
@ -608,6 +608,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
deep: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true"),
include_imports: z
.enum(["true", "false"])
.default("false")
@ -621,6 +625,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
_id: z.string(),
workspace: z.string(),
environment: z.string(),
secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({
id: true,
slug: true,
@ -660,7 +665,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
environment: req.query.environment,
projectId: req.query.workspaceId,
path: req.query.secretPath,
includeImports: req.query.include_imports
includeImports: req.query.include_imports,
deep: req.query.deep
});
await server.services.auditLog.createAuditLog({

View File

@ -1,4 +1,5 @@
/* eslint-disable no-await-in-loop */
import { subject } from "@casl/ability";
import path from "path";
import {
@ -10,6 +11,7 @@ import {
TSecrets
} from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import {
buildSecretBlindIndexFromName,
@ -20,6 +22,7 @@ import { BadRequestError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
@ -54,8 +57,25 @@ type TRecursivelyFetchSecretsFromFoldersArg = {
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "find">;
};
export const recursivelyGetSecretPaths = ({ folderDAL, projectEnvDAL }: TRecursivelyFetchSecretsFromFoldersArg) => {
const getPaths = async (projectId: string, environment: string, currentPath: string) => {
type TGetPathsDTO = {
projectId: string;
environment: string;
currentPath: string;
auth: {
actor: ActorType;
actorId: string;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string | undefined;
};
};
export const recursivelyGetSecretPaths = ({
folderDAL,
projectEnvDAL,
permissionService
}: TRecursivelyFetchSecretsFromFoldersArg) => {
const getPaths = async ({ projectId, environment, currentPath }: Omit<TGetPathsDTO, "auth">) => {
let secretPaths: string[] = [];
// Get secrets in the current folder.
@ -93,7 +113,7 @@ export const recursivelyGetSecretPaths = ({ folderDAL, projectEnvDAL }: TRecursi
// Ensure the path is correctly formatted for the next level.
const subFolderPath = `${currentPath}${currentPath !== "/" ? "/" : ""}${folder.name}`;
return getPaths(projectId, environment, subFolderPath);
return getPaths({ projectId, environment, currentPath: subFolderPath });
})
);
@ -107,7 +127,32 @@ export const recursivelyGetSecretPaths = ({ folderDAL, projectEnvDAL }: TRecursi
return secretPaths;
};
return getPaths;
return async ({ projectId, environment, currentPath, auth }: TGetPathsDTO) => {
const paths = await getPaths({ projectId, environment, currentPath });
const { permission } = await permissionService.getProjectPermission(
auth.actor,
auth.actorId,
projectId,
auth.actorAuthMethod,
auth.actorOrgId
);
const allowedPaths = paths.filter((p) =>
// if its service token allow full access over imported one
auth.actor === ActorType.SERVICE
? true
: permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: p
})
)
);
return allowedPaths;
};
};
type TInterpolateSecretArg = {
@ -275,7 +320,10 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
return expandSecrets;
};
export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environment: string }, key: string) => {
export const decryptSecretRaw = (
secret: TSecrets & { workspace: string; environment: string; secretPath?: string },
key: string
) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
@ -303,6 +351,7 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ
return {
secretKey,
secretPath: secret.secretPath,
workspace: secret.workspace,
environment: secret.environment,
secretValue,

View File

@ -1,3 +1,5 @@
/* eslint-disable no-unreachable-loop */
/* eslint-disable no-await-in-loop */
import { ForbiddenError, subject } from "@casl/ability";
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas";
@ -437,51 +439,98 @@ export const secretServiceFactory = ({
actor,
actorOrgId,
actorAuthMethod,
includeImports
includeImports,
deep
}: TGetSecretsDTO) => {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const importsArray: Awaited<ReturnType<typeof fnSecretsFromImports>> = [];
const secretsArray: (Awaited<ReturnType<typeof secretDAL.findByFolderId>>[number] & {
secretPath: string;
environment: string;
workspace: string;
})[] = [];
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] };
const folderId = folder.id;
let paths = [path];
const secrets = await secretDAL.findByFolderId(folderId, actorId);
if (deep) {
const getPaths = recursivelyGetSecretPaths({
permissionService,
folderDAL,
projectEnvDAL
});
const deepPaths = await getPaths({
projectId,
environment,
currentPath: path,
auth: {
actor,
actorId,
actorAuthMethod,
actorOrgId
}
});
paths = deepPaths.length === 0 ? paths : deepPaths;
}
for (const currentPath of paths) {
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: currentPath })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, currentPath);
if (!folder) return { secrets: [], imports: [] };
const folderId = folder.id;
const secrets = await secretDAL.findByFolderId(folderId, actorId);
if (includeImports) {
const secretImports = await secretImportDAL.find({ folderId });
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
// if its service token allow full access over imported one
actor === ActorType.SERVICE
? true
: permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug,
secretPath: importPath
})
)
);
const importedSecrets = await fnSecretsFromImports({
allowedImports,
secretDAL,
folderDAL
});
secretsArray.push(
...secrets.map((secret) => ({ ...secret, secretPath: currentPath, workspace: projectId, environment }))
);
importsArray.push(...importedSecrets);
}
secretsArray.push(
...secrets.map((secret) => ({ ...secret, secretPath: currentPath, workspace: projectId, environment }))
);
}
if (includeImports) {
const secretImports = await secretImportDAL.find({ folderId });
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
// if its service token allow full access over imported one
actor === ActorType.SERVICE
? true
: permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importEnv.slug,
secretPath: importPath
})
)
);
const importedSecrets = await fnSecretsFromImports({
allowedImports,
secretDAL,
folderDAL
});
return {
secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })),
imports: importedSecrets
secrets: secretsArray,
imports: importsArray
};
}
return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) };
return {
secrets: secretsArray
};
};
const getSecretByName = async ({
@ -802,70 +851,32 @@ export const secretServiceFactory = ({
actorAuthMethod,
environment,
includeImports,
recursive
deep
}: TGetSecretsRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
let secrets: Awaited<ReturnType<typeof getSecrets>>["secrets"];
let imports: Awaited<ReturnType<typeof getSecrets>>["imports"];
if (recursive) {
const getPaths = recursivelyGetSecretPaths({
permissionService,
folderDAL,
projectEnvDAL
});
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;
}
const { secrets, imports } = await getSecrets({
actorId,
projectId,
environment,
actor,
actorOrgId,
actorAuthMethod,
path,
includeImports,
deep
});
return {
secrets: secrets.map((el) => decryptSecretRaw(el, botKey)),
imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({
...el,
secrets: importedSecrets.map((sec) =>
decryptSecretRaw({ ...sec, environment: el.environment, workspace: projectId }, botKey)
decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath },
botKey
)
)
}))
};