diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 8d3f08b61..07dff0703 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -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({ diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index b499f9365..961f3fef6 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -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; }; -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) => { 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, diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 6b71f7988..57ef22c56 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -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> = []; + const secretsArray: (Awaited>[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>["secrets"]; - let imports: Awaited>["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 + ) ) })) };