diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3ee5f6aee..fd51119f1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -215,6 +215,7 @@ export const SECRETS = { export const RAW_SECRETS = { 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.", 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.", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 7745a8a04..1bfa1a8d3 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -490,6 +490,7 @@ export const registerRoutes = async ( snapshotService, secretQueueService, secretImportDAL, + projectEnvDAL, projectBotService }); const sarService = secretApprovalRequestServiceFactory({ diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index f69466328..5344be43d 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -157,6 +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 + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.LIST.recursive), include_imports: z .enum(["true", "false"]) .default("false") @@ -165,7 +170,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secrets: secretRawSchema.array(), + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional() + }) + .array(), imports: z .object({ secretPath: z.string(), @@ -218,7 +227,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, projectId: workspaceId, path: secretPath, - includeImports: req.query.include_imports + includeImports: req.query.include_imports, + recursive: req.query.recursive }); await server.services.auditLog.createAuditLog({ @@ -596,6 +606,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + recursive: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true"), include_imports: z .enum(["true", "false"]) .default("false") @@ -604,19 +618,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ secrets: SecretsSchema.omit({ secretBlindIndex: true }) - .merge( - z.object({ - _id: z.string(), - workspace: z.string(), - environment: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - name: true, - color: true - }).array() - }) - ) + .extend({ + _id: z.string(), + workspace: z.string(), + environment: z.string(), + secretPath: z.string().optional(), + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + name: true, + color: true + }).array() + }) .array(), imports: z .object({ @@ -648,7 +661,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, + recursive: req.query.recursive }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index f9c6f1be7..cbc8936e1 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -70,9 +70,31 @@ export const secretImportDALFactory = (db: TDbClient) => { } }; + const findByFolderIds = async (folderIds: string[], tx?: Knex) => { + try { + const docs = await (tx || db)(TableName.SecretImport) + .whereIn("folderId", folderIds) + .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`) + .select( + db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports, + db.ref("slug").withSchema(TableName.Environment), + db.ref("name").withSchema(TableName.Environment), + db.ref("id").withSchema(TableName.Environment).as("envId") + ) + .orderBy("position", "asc"); + return docs.map(({ envId, slug, name, ...el }) => ({ + ...el, + importEnv: { id: envId, slug, name } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find secret imports" }); + } + }; + return { ...secretImportOrm, find, + findByFolderIds, findLastImportPosition, updateAllPosition }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index 504174765..8a5970b83 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -171,6 +171,50 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { + try { + // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) + if (userId && !uuidValidate(userId)) { + // eslint-disable-next-line no-param-reassign + userId = undefined; + } + + const secs = await (tx || db)(TableName.Secret) + .whereIn("folderId", folderIds) + .where((bd) => { + void bd.whereNull("userId").orWhere({ userId: userId || null }); + }) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")) + .orderBy("id", "asc"); + const data = sqlNestRelationships({ + data: secs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + id, + color, + slug, + name + }) + } + ] + }); + return data; + } catch (error) { + throw new DatabaseError({ error, name: "get all secret" }); + } + }; + const findByBlindIndexes = async ( folderId: string, blindIndexes: Array<{ blindIndex: string; type: SecretType }>, @@ -207,6 +251,7 @@ export const secretDALFactory = (db: TDbClient) => { bulkUpdateNoVersionIncrement, getSecretTags, findByFolderId, + findByFolderIds, findByBlindIndexes }; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 212bb01f0..24c2bd811 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 { @@ -7,8 +8,11 @@ import { SecretType, TableName, TSecretBlindIndexes, + TSecretFolders, 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, @@ -18,7 +22,9 @@ import { import { BadRequestError } from "@app/lib/errors"; import { groupBy, unique } from "@app/lib/fn"; +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"; import { TSecretDALFactory } from "./secret-dal"; import { @@ -45,6 +51,133 @@ export const generateSecretBlindIndexBySalt = async (secretName: string, secretB return secretBlindIndex; }; +type TRecursivelyFetchSecretsFromFoldersArg = { + permissionService: Pick; + folderDAL: Pick; + projectEnvDAL: Pick; +}; + +type TGetPathsDTO = { + projectId: string; + environment: string; + currentPath: string; + + auth: { + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string | undefined; + }; +}; + +// Introduce a new interface for mapping parent IDs to their children +interface FolderMap { + [parentId: string]: TSecretFolders[]; +} +const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { + const map: FolderMap = {}; + map.null = []; // Initialize mapping for root directory + + folders.forEach((folder) => { + const parentId = folder.parentId || "null"; + if (!map[parentId]) { + map[parentId] = []; + } + map[parentId].push(folder); + }); + + return map; +}; + +const generatePaths = ( + map: FolderMap, + parentId: string = "null", + basePath: string = "" +): { path: string; folderId: string }[] => { + const children = map[parentId || "null"] || []; + let paths: { path: string; folderId: string }[] = []; + + children.forEach((child) => { + // Determine if this is the root folder of the environment. If no parentId is present and the name is root, it's the root folder + const isRootFolder = child.name === "root" && !child.parentId; + + // Form the current path based on the base path and the current child + // eslint-disable-next-line no-nested-ternary + const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`; + + paths.push({ + path: currPath, + folderId: child.id + }); // Add the current path + + // Recursively generate paths for children, passing down the formatted pathh + const childPaths = generatePaths(map, child.id, currPath); + paths = paths.concat( + childPaths.map((p) => ({ + path: p.path, + folderId: p.folderId + })) + ); + }); + + return paths; +}; + +export const recursivelyGetSecretPaths = ({ + folderDAL, + projectEnvDAL, + permissionService +}: TRecursivelyFetchSecretsFromFoldersArg) => { + const getPaths = async ({ projectId, environment, currentPath, auth }: TGetPathsDTO) => { + const env = await projectEnvDAL.findOne({ + projectId, + slug: environment + }); + + if (!env) { + throw new Error(`'${environment}' environment not found in project with ID ${projectId}`); + } + + // Fetch all folders in env once with a single query + const folders = await folderDAL.find({ + envId: env.id + }); + + // Build the folder hierarchy map + const folderMap = buildHierarchy(folders); + + // Generate the paths paths and normalize the root path to / + const paths = generatePaths(folderMap).map((p) => ({ + path: p.path === "/" ? p.path : p.path.substring(1), + folderId: p.folderId + })); + + const { permission } = await permissionService.getProjectPermission( + auth.actor, + auth.actorId, + projectId, + auth.actorAuthMethod, + auth.actorOrgId + ); + + // Filter out paths that the user does not have permission to access, and paths that are not in the current path + const allowedPaths = paths.filter( + (folder) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: folder.path + }) + ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) + ); + + return allowedPaths; + }; + + return getPaths; +}; + type TInterpolateSecretArg = { projectId: string; secretEncKey: string; @@ -202,9 +335,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD ); // eslint-disable-next-line - secrets[key].value = secrets[key].skipMultilineEncoding - ? expandedVal - : formatMultiValueEnv(expandedVal); + secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal); } return secrets; @@ -212,7 +343,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, @@ -240,6 +374,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 ed96edb41..3b504fbf4 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"; @@ -13,13 +15,20 @@ import { logger } from "@app/lib/logger"; import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; 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 { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-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 { TAttachSecretTagsDTO, @@ -47,20 +56,25 @@ type TSecretServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; - folderDAL: Pick; projectDAL: Pick; + projectEnvDAL: Pick; + folderDAL: Pick< + TSecretFolderDALFactory, + "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" + >; secretBlindIndexDAL: TSecretBlindIndexDALFactory; permissionService: Pick; snapshotService: Pick; secretQueueService: Pick; projectBotService: Pick; - secretImportDAL: Pick; + secretImportDAL: Pick; secretVersionTagDAL: Pick; }; export type TSecretServiceFactory = ReturnType; export const secretServiceFactory = ({ secretDAL, + projectEnvDAL, secretTagDAL, secretVersionDAL, folderDAL, @@ -425,7 +439,8 @@ export const secretServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - includeImports + includeImports, + recursive }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission( actor, @@ -434,19 +449,52 @@ export const secretServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + + let paths: { folderId: string; path: string }[] = []; + + if (recursive) { + const getPaths = recursivelyGetSecretPaths({ + permissionService, + folderDAL, + projectEnvDAL + }); + + const deepPaths = await getPaths({ + projectId, + environment, + currentPath: path, + auth: { + actor, + actorId, + actorAuthMethod, + actorOrgId + } + }); + + if (!deepPaths) return { secrets: [], imports: [] }; + + paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + const folder = await folderDAL.findBySecretPath(projectId, environment, path); + if (!folder) return { secrets: [], imports: [] }; + + paths = [{ folderId: folder.id, path }]; + } + + const groupedPaths = groupBy(paths, (p) => p.folderId); + + const secrets = await secretDAL.findByFolderIds( + paths.map((p) => p.folderId), + actorId ); - const folder = await folderDAL.findBySecretPath(projectId, environment, path); - 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 secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ importEnv, importPath }) => // if its service token allow full access over imported one actor === ActorType.SERVICE @@ -464,12 +512,26 @@ export const secretServiceFactory = ({ secretDAL, folderDAL }); + return { - secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })), + secrets: secrets.map((secret) => ({ + ...secret, + workspace: projectId, + environment, + secretPath: groupedPaths[secret.folderId][0].path + })), imports: importedSecrets }; } - return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) }; + + return { + secrets: secrets.map((secret) => ({ + ...secret, + workspace: projectId, + environment, + secretPath: groupedPaths[secret.folderId][0].path + })) + }; }; const getSecretByName = async ({ @@ -789,7 +851,8 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, environment, - includeImports + includeImports, + recursive }: TGetSecretsRawDTO) => { const botKey = await projectBotService.getBotKey(projectId); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); @@ -802,7 +865,8 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, path, - includeImports + includeImports, + recursive }); return { @@ -810,7 +874,10 @@ export const secretServiceFactory = ({ 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 + ) ) })) }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index efd4f0f8b..22347de4e 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -74,6 +74,7 @@ export type TGetSecretsDTO = { path: string; environment: string; includeImports?: boolean; + recursive?: boolean; } & TProjectPermission; export type TGetASecretDTO = { @@ -140,6 +141,7 @@ export type TGetSecretsRawDTO = { path: string; environment: string; includeImports?: boolean; + recursive?: boolean; } & TProjectPermission; export type TGetASecretRawDTO = { diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 38d82a0a5..d45a42db4 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -277,6 +277,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req SetQueryParam("environment", request.Environment). SetQueryParam("workspaceId", request.WorkspaceId) + if request.Recursive { + httpRequest.SetQueryParam("recursive", "true") + } + if request.IncludeImport { httpRequest.SetQueryParam("include_imports", "true") } diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index b49cb1581..ed9a1d629 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -291,6 +291,7 @@ type GetEncryptedSecretsV3Request struct { WorkspaceId string `json:"workspaceId"` SecretPath string `json:"secretPath"` IncludeImport bool `json:"include_imports"` + Recursive bool `json:"recursive"` } type GetFoldersV1Request struct { @@ -510,7 +511,7 @@ type CreateDynamicSecretLeaseV1Request struct { type CreateDynamicSecretLeaseV1Response struct { Lease struct { - Id string `json:"id"` + Id string `json:"id"` ExpireAt time.Time `json:"expireAt"` } `json:"lease"` DynamicSecret struct { diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index ce3f7a8b0..80f321396 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -332,7 +332,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) { func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) { return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) { - res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false) + res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false, false) if err != nil { return nil, err } diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index d008af1ec..cb44e3d7e 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -98,7 +98,12 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, projectConfigDir) + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, projectConfigDir) if err != nil { util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") @@ -202,6 +207,7 @@ func init() { runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("include-imports", true, "Import linked secrets ") + runCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index cf9c89b47..bbee424c4 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -63,6 +63,11 @@ var secretsCmd = &cobra.Command{ util.HandleError(err) } + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err) + } + tagSlugs, err := cmd.Flags().GetString("tags") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -73,7 +78,7 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, "") + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, "") if err != nil { util.HandleError(err) } @@ -413,12 +418,17 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse path flag") } + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err, "Unable to parse recursive flag") + } + showOnlyValue, err := cmd.Flags().GetBool("raw-value") if err != nil { util.HandleError(err, "Unable to parse path flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "") + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true, Recursive: recursive}, "") if err != nil { util.HandleError(err, "To fetch all secrets") } @@ -727,6 +737,7 @@ func init() { secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") + secretsCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.Flags().String("path", "/", "get secrets within a folder path") rootCmd.AddCommand(secretsCmd) diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 4a7dc782a..e197218e0 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -93,6 +93,7 @@ type GetAllSecretsParameters struct { WorkspaceId string SecretsPath string IncludeImport bool + Recursive bool } type GetAllFoldersParameters struct { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 6f56bc84a..23e95db64 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -17,7 +17,7 @@ import ( "github.com/rs/zerolog/log" ) -func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { +func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) if len(serviceTokenParts) < 4 { return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") @@ -49,6 +49,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str Environment: environment, SecretPath: secretPath, IncludeImport: includeImports, + Recursive: recursive, }) if err != nil { @@ -80,7 +81,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str return plainTextSecrets, serviceTokenDetails, nil } -func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) { httpClient := resty.New() httpClient.SetAuthToken(JTWToken). SetHeader("Accept", "application/json") @@ -125,6 +126,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work WorkspaceId: workspaceId, Environment: environmentName, IncludeImport: includeImports, + Recursive: recursive, // TagSlugs: tagSlugs, } @@ -152,7 +154,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work return plainTextSecrets, nil } -func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool) (models.PlaintextSecretResult, error) { +func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -161,6 +163,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin WorkspaceId: workspaceId, Environment: environmentName, IncludeImport: includeImports, + Recursive: recursive, // TagSlugs: tagSlugs, } @@ -329,7 +332,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId, - params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport) + params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport, params.Recursive) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] @@ -350,10 +353,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } else { if params.InfisicalToken != "" { log.Debug().Msg("Trying to fetch secrets using service token") - secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport) + secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) } else if params.UniversalAuthAccessToken != "" { log.Debug().Msg("Trying to fetch secrets using universal auth") - res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport) + res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) errorToReturn = err secretsToReturn = res.Secrets