diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 996ef670a..ec855168e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -631,7 +631,8 @@ export const FOLDERS = { workspaceId: "The ID of the project to list folders from.", environment: "The slug of the environment to list folders from.", path: "The path to list folders from.", - directory: "The directory to list folders from. (Deprecated in favor of path)" + directory: "The directory to list folders from. (Deprecated in favor of path)", + recursive: "Whether or not to fetch all folders from the specified base path, and all of its subdirectories." }, GET_BY_ID: { folderId: "The ID of the folder to get details." diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 77149eb1b..67bd26552 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -70,6 +70,19 @@ export const DefaultResponseErrorsSchema = { }) }; +export const booleanSchema = z + .union([z.boolean(), z.string().trim()]) + .transform((value) => { + if (typeof value === "string") { + // ie if not empty, 0 or false, return true + return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false"; + } + + return value; + }) + .optional() + .default(true); + export const sapPubSchema = SecretApprovalPoliciesSchema.merge( z.object({ environment: z.object({ diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index d976cbc1b..901723647 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -16,7 +16,12 @@ import { secretsLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; +import { + booleanSchema, + SanitizedDynamicSecretSchema, + SanitizedTagSchema, + secretRawSchema +} from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; @@ -24,20 +29,6 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; const MAX_DEEP_SEARCH_LIMIT = 500; // arbitrary limit to prevent excessive results -// handle querystring boolean values -const booleanSchema = z - .union([z.boolean(), z.string().trim()]) - .transform((value) => { - if (typeof value === "string") { - // ie if not empty, 0 or false, return true - return Boolean(value) && Number(value) !== 0 && value.toLowerCase() !== "false"; - } - - return value; - }) - .optional() - .default(true); - const parseSecretPathSearch = (search?: string) => { if (!search) return { diff --git a/backend/src/server/routes/v1/secret-folder-router.ts b/backend/src/server/routes/v1/secret-folder-router.ts index b0a22548a..5f8b553f5 100644 --- a/backend/src/server/routes/v1/secret-folder-router.ts +++ b/backend/src/server/routes/v1/secret-folder-router.ts @@ -9,6 +9,8 @@ import { readLimit, secretsLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { booleanSchema } from "../sanitizedSchemas"; + export const registerSecretFolderRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", @@ -347,11 +349,14 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) => .default("/") .transform(prefixWithSlash) .transform(removeTrailingSlash) - .describe(FOLDERS.LIST.directory) + .describe(FOLDERS.LIST.directory), + recursive: booleanSchema.default(false).describe(FOLDERS.LIST.recursive) }), response: { 200: z.object({ - folders: SecretFoldersSchema.array() + folders: SecretFoldersSchema.extend({ + relativePath: z.string().optional() + }).array() }) } }, diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 7afbb290f..d4f58a1d4 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -401,7 +401,8 @@ export const secretFolderServiceFactory = ({ orderBy, orderDirection, limit, - offset + offset, + recursive }: TGetFolderDTO) => { // folder list is allowed to be read by anyone // permission to check does user has access @@ -420,6 +421,17 @@ export const secretFolderServiceFactory = ({ const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!parentFolder) return []; + if (recursive) { + const recursiveFolders = await folderDAL.findByEnvsDeep({ parentIds: [parentFolder.id] }); + // remove the parent folder + return recursiveFolders + .filter((folder) => folder.id !== parentFolder.id) + .map((folder) => ({ + ...folder, + relativePath: folder.path + })); + } + const folders = await folderDAL.find( { envId: env.id, diff --git a/backend/src/services/secret-folder/secret-folder-types.ts b/backend/src/services/secret-folder/secret-folder-types.ts index bbe4c7223..2a5681b5b 100644 --- a/backend/src/services/secret-folder/secret-folder-types.ts +++ b/backend/src/services/secret-folder/secret-folder-types.ts @@ -45,6 +45,7 @@ export type TGetFolderDTO = { orderDirection?: OrderByDirection; limit?: number; offset?: number; + recursive?: boolean; } & TProjectPermission; export type TGetFolderByIdDTO = {