From b4703c2e67fbdd8f8e900eea2d5a58a8462ea48b Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Fri, 7 Apr 2023 11:27:42 -0700 Subject: [PATCH] allow to query for folders via path in backend, allow to filter by path in cli --- .../controllers/v1/secretsFolderController.ts | 21 ++++- .../src/controllers/v2/secretsController.ts | 24 ++---- backend/src/models/folder.ts | 8 ++ backend/src/utils/folder.ts | 76 ++++++++++++++++++- cli/packages/api/api.go | 1 + cli/packages/api/model.go | 68 ++++++++++------- cli/packages/cmd/export.go | 8 +- cli/packages/cmd/run.go | 8 +- cli/packages/cmd/secrets.go | 36 ++++++--- cli/packages/models/cli.go | 1 + cli/packages/util/helper.go | 33 ++++++++ cli/packages/util/secrets.go | 46 +++++------ cli/packages/visualize/secrets.go | 16 +++- cli/packages/visualize/visualize.go | 30 +++++++- 14 files changed, 297 insertions(+), 79 deletions(-) diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 896ff13e92..b3bed29066 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -2,21 +2,38 @@ import { Request, Response } from 'express'; import { Secret } from '../../models'; import Folder from '../../models/folder'; import { BadRequestError } from '../../utils/errors'; +import { ROOT_FOLDER_PATH, getFolderPath, getParentPath, normalizePath, validateFolderName } from '../../utils/folder'; +// TODO +// verify workspace id/environment export const createFolder = async (req: Request, res: Response) => { const { workspaceId, environment, folderName, parentFolderId } = req.body + if (!validateFolderName(folderName)) { + throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" }) + } + if (parentFolderId) { - const parentFolder = await Folder.findById(parentFolderId); + const parentFolder = await Folder.find({ environment: environment, workspace: workspaceId, id: parentFolderId }); if (!parentFolder) { throw BadRequestError({ message: "The parent folder doesn't exist" }) } } + let completePath = await getFolderPath(parentFolderId) + if (completePath == ROOT_FOLDER_PATH) { + completePath = "" + } + + const currentFolderPath = completePath + "/" + folderName // construct new path with current folder to be created + const normalizedCurrentPath = normalizePath(currentFolderPath) + const normalizedParentPath = getParentPath(normalizedCurrentPath) + const existingFolder = await Folder.findOne({ name: folderName, workspace: workspaceId, environment: environment, parent: parentFolderId, + path: normalizedCurrentPath }); if (existingFolder) { @@ -28,6 +45,8 @@ export const createFolder = async (req: Request, res: Response) => { workspace: workspaceId, environment: environment, parent: parentFolderId, + path: normalizedCurrentPath, + parentPath: normalizedParentPath }); await newFolder.save(); diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 6b31d26385..845794a92b 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -25,7 +25,7 @@ import { BatchSecretRequest, BatchSecret } from '../../types/secret'; -import { getFolderPath } from '../../utils/folder'; +import { getFolderPath, getFoldersInDirectory, normalizePath } from '../../utils/folder'; import Folder from '../../models/folder'; /** @@ -507,7 +507,7 @@ export const getSecrets = async (req: Request, res: Response) => { #swagger.security = [{ "apiKeyAuth": [] - }] + }] #swagger.parameters['workspaceId'] = { "description": "ID of project", @@ -543,8 +543,9 @@ export const getSecrets = async (req: Request, res: Response) => { const postHogClient = getPostHogClient(); - const { workspaceId, environment, tagSlugs } = req.query; - const { folderId } = req.query + const { workspaceId, environment, tagSlugs, secretsPath } = req.query; + + const normalizedPath = normalizePath(secretsPath as string) const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : []; let userId = "" // used for getting personal secrets for user let userEmail = "" // used for posthog @@ -602,10 +603,8 @@ export const getSecrets = async (req: Request, res: Response) => { } } - // query for secrets at root folder - if (folderId != undefined) { - secretQuery.folder = folderId == "" ? undefined : folderId - } + // Add path to secrets query + secretQuery.path = normalizedPath if (hasWriteOnlyAccess) { secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag") @@ -630,14 +629,7 @@ export const getSecrets = async (req: Request, res: Response) => { ipAddress: req.ip }); - let folders: any[] = [] - if (folderId != undefined) { - folders = await Folder.find({ - workspace: workspaceId, - environment: environment, - parent: folderId == "" ? undefined : folderId // undefined means root - }) - } + const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath) if (postHogClient) { postHogClient.capture({ diff --git a/backend/src/models/folder.ts b/backend/src/models/folder.ts index aa40a7847e..e4248657f1 100644 --- a/backend/src/models/folder.ts +++ b/backend/src/models/folder.ts @@ -19,6 +19,14 @@ const folderSchema = new Schema({ ref: 'Folder', required: false, // optional for root folders }, + path: { + type: String, + required: true + }, + parentPath: { + type: String, + required: true, + }, }, { timestamps: true }); diff --git a/backend/src/utils/folder.ts b/backend/src/utils/folder.ts index b26c29cac2..f128453394 100644 --- a/backend/src/utils/folder.ts +++ b/backend/src/utils/folder.ts @@ -1,5 +1,7 @@ import Folder from "../models/folder"; +export const ROOT_FOLDER_PATH = "/" + export const getFolderPath = async (folderId: string) => { let currentFolder = await Folder.findById(folderId); const pathSegments = []; @@ -10,4 +12,76 @@ export const getFolderPath = async (folderId: string) => { } return '/' + pathSegments.join('/'); -}; \ No newline at end of file +}; + +/** + Returns the folder ID associated with the specified secret path in the given workspace and environment. + @param workspaceId - The ID of the workspace to search in. + @param environment - The environment to search in. + @param secretPath - The secret path to search for. + @returns The folder ID associated with the specified secret path, or undefined if the path is at the root folder level. + @throws Error if the specified secret path is not found. +*/ +export const getFolderIdFromPath = async (workspaceId: string, environment: string, secretPath: string) => { + const secretPathParts = secretPath.split("/").filter(path => path != "") + if (secretPathParts.length <= 1) { + return undefined // root folder, so no folder id + } + + const folderId = await Folder.find({ path: secretPath, workspace: workspaceId, environment: environment }) + if (!folderId) { + throw Error("Secret path not found") + } + + return folderId +} + +/** + * Cleans up a path by removing empty parts, duplicate slashes, + * and ensuring it starts with ROOT_FOLDER_PATH. + * @param path - The input path to clean up. + * @returns The cleaned-up path string. + */ +export const normalizePath = (path: string) => { + if (path == undefined || path == "" || path == ROOT_FOLDER_PATH) { + return ROOT_FOLDER_PATH + } + + const pathParts = path.split("/").filter(part => part != "") + const cleanPathString = ROOT_FOLDER_PATH + pathParts.join("/") + + return cleanPathString +} + +export const getFoldersInDirectory = async (workspaceId: string, environment: string, pathString: string) => { + const normalizedPath = normalizePath(pathString) + const foldersInDirectory = await Folder.find({ + workspace: workspaceId, + environment: environment, + parentPath: normalizedPath, + }); + + return foldersInDirectory; +} + +/** + * Returns the parent path of the given path. + * @param path - The input path. + * @returns The parent path string. + */ +export const getParentPath = (path: string) => { + const normalizedPath = normalizePath(path); + const folderParts = normalizedPath.split('/').filter(part => part !== ''); + + let folderParent = ROOT_FOLDER_PATH; + if (folderParts.length > 1) { + folderParent = ROOT_FOLDER_PATH + folderParts.slice(0, folderParts.length - 1).join('/'); + } + + return folderParent; +} + +export const validateFolderName = (folderName: string) => { + const validNameRegex = /^[a-zA-Z0-9-_]+$/; + return validNameRegex.test(folderName); +} \ No newline at end of file diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index f0a347800b..30fbde5e59 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -115,6 +115,7 @@ func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Req SetQueryParam("environment", request.Environment). SetQueryParam("workspaceId", request.WorkspaceId). SetQueryParam("tagSlugs", request.TagSlugs). + SetQueryParam("secretsPath", request.SecretPath). Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)) if err != nil { diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index af8dbc5b47..a48e89c895 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -198,35 +198,51 @@ type GetEncryptedSecretsV2Request struct { Environment string `json:"environment"` WorkspaceId string `json:"workspaceId"` TagSlugs string `json:"tagSlugs"` + SecretPath string `json:"secretPath"` + FolderId string `json:"folderId"` +} + +type Folders struct { + ID string `json:"_id"` + Name string `json:"name"` + Workspace string `json:"workspace"` + Environment string `json:"environment"` + Parent string `json:"parent"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type Secrets struct { + ID string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + User string `json:"user,omitempty"` + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } `json:"tags"` } type GetEncryptedSecretsV2Response struct { - Secrets []struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - User string `json:"user,omitempty"` - Tags []struct { - ID string `json:"_id"` - Name string `json:"name"` - Slug string `json:"slug"` - Workspace string `json:"workspace"` - } `json:"tags"` - } `json:"secrets"` + Secrets []Secrets `json:"secrets"` + + Folders []Folders `json:"folders"` } type GetServiceTokenDetailsResponse struct { diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index 07512798b3..fd7d2b4913 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -74,7 +74,12 @@ var exportCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, WorkspaceId: projectId}) + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, _, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, WorkspaceId: projectId, Path: secretsPath}) if err != nil { util.HandleError(err, "Unable to fetch secrets") } @@ -112,6 +117,7 @@ func init() { exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") exportCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs") exportCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets from") + exportCmd.Flags().String("path", "/", "The path to the folder where secrets are located. Defaults to root folder") } // Format according to the format flag diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index ba60c9fec0..226e002c7a 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -82,7 +82,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, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, _, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, Path: secretsPath}) 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") @@ -182,6 +187,7 @@ func init() { 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 ") + runCmd.Flags().String("path", "/", "The path to the folder where secrets are located. Defaults to root folder") } // Will execute a single command and pass in the given secrets into the process diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 69a4539a67..8795785d41 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -44,6 +44,11 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") if err != nil { util.HandleError(err) @@ -54,7 +59,8 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + normalizedPath := util.NormalizePath(secretsPath) + secrets, folders, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, Path: normalizedPath}) if err != nil { util.HandleError(err) } @@ -63,6 +69,10 @@ var secretsCmd = &cobra.Command{ secrets = util.SubstituteSecrets(secrets) } + if len(folders) > 0 { + visualize.PrintSecretFolders(folders) + } + visualize.PrintAllSecretDetails(secrets) }, } @@ -142,7 +152,7 @@ var secretsSetCmd = &cobra.Command{ plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) // pull current secrets - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) + secrets, _, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) if err != nil { util.HandleError(err, "unable to retrieve secrets") } @@ -263,13 +273,14 @@ var secretsSetCmd = &cobra.Command{ } // Print secret operations - headers := [...]string{"SECRET NAME", "SECRET VALUE", "STATUS"} - rows := [][3]string{} + secretHeaders := [...]string{"SECRET NAME", "SECRET VALUE", "STATUS"} + secretRows := [][3]string{} for _, secretOperation := range secretOperations { - rows = append(rows, [...]string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation}) + secretRows = append(secretRows, [...]string{secretOperation.SecretKey, secretOperation.SecretValue, secretOperation.SecretOperation}) } - visualize.Table(headers, rows) + // visualize.PrintSecretFolders() + visualize.SecretsTable(secretHeaders, secretRows) }, } @@ -299,11 +310,13 @@ var secretsDeleteCmd = &cobra.Command{ util.HandleError(err, "Unable to get local project details") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) + secrets, folders, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) if err != nil { util.HandleError(err, "Unable to fetch secrets") } + fmt.Println("folders===>", folders) + secretByKey := getSecretsByKeys(secrets) validSecretIdsToDelete := []string{} invalidSecretNamesThatDoNotExist := []string{} @@ -360,11 +373,13 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + secrets, folders, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) if err != nil { util.HandleError(err, "To fetch all secrets") } + fmt.Println("folders===>", folders) + requestedSecrets := []models.SingleEnvironmentVariable{} secretsMap := getSecretsByKeys(secrets) @@ -403,11 +418,13 @@ func generateExampleEnv(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + secrets, folders, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) if err != nil { util.HandleError(err, "To fetch all secrets") } + fmt.Println("folders===>", folders) + tagsHashToSecretKey := make(map[string]int) slugsToFilerBy := make(map[string]int) @@ -602,6 +619,7 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod func init() { secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + secretsCmd.Flags().String("path", "/", "The path to the folder where secrets are located. Defaults to root folder") secretsCmd.AddCommand(secretsGenerateExampleEnvCmd) secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 5e969a9c00..d5682850ad 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -56,4 +56,5 @@ type GetAllSecretsParameters struct { InfisicalToken string TagSlugs string WorkspaceId string + Path string } diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 76b6e3e464..6f602b9b5d 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -137,3 +137,36 @@ func getCurrentBranch() (string, error) { } return path.Base(strings.TrimSpace(out.String())), nil } + +func GetSplitPathByDash(path string) []string { + pathParts := strings.Split(path, "/") + var filteredPathParts []string + for _, s := range pathParts { + if s != "" { + filteredPathParts = append(filteredPathParts, s) + } + } + + return filteredPathParts +} + +// NormalizePath cleans up a path by removing empty parts, duplicate slashes, +// and ensuring it starts with ROOT_FOLDER_PATH. +func NormalizePath(path string) string { + ROOT_FOLDER_PATH := "/" + + if path == "" || path == ROOT_FOLDER_PATH { + return ROOT_FOLDER_PATH + } + + pathParts := strings.Split(path, "/") + nonEmptyParts := []string{} + for _, part := range pathParts { + if part != "" { + nonEmptyParts = append(nonEmptyParts, part) + } + } + + cleanPathString := ROOT_FOLDER_PATH + strings.Join(nonEmptyParts, "/") + return cleanPathString +} diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index c25c07efd1..f8e14d3a2e 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -17,10 +17,10 @@ import ( "github.com/go-resty/resty/v2" ) -func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.SingleEnvironmentVariable, []api.Folders, error) { serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) if len(serviceTokenParts) < 4 { - return nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") + return nil, nil, fmt.Errorf("invalid service token entered. Please double check your service token and try again") } serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) @@ -32,7 +32,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient) if err != nil { - return nil, fmt.Errorf("unable to get service token details. [err=%v]", err) + return nil, nil, fmt.Errorf("unable to get service token details. [err=%v]", err) } encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ @@ -41,28 +41,28 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl }) if err != nil { - return nil, err + return nil, nil, err } decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag) if err != nil { - return nil, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) + return nil, nil, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) } plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) if err != nil { - return nil, fmt.Errorf("unable to decrypt the required workspace key") + return nil, nil, fmt.Errorf("unable to decrypt the required workspace key") } plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) if err != nil { - return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) + return nil, nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) } - return plainTextSecrets, nil + return plainTextSecrets, encryptedSecrets.Folders, nil } -func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretPath string) ([]models.SingleEnvironmentVariable, []api.Folders, error) { httpClient := resty.New() httpClient.SetAuthToken(JTWToken). SetHeader("Accept", "application/json") @@ -73,7 +73,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request) if err != nil { - return nil, fmt.Errorf("unable to get your encrypted workspace key. [err=%v]", err) + return nil, nil, fmt.Errorf("unable to get your encrypted workspace key. [err=%v]", err) } encryptedWorkspaceKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey) @@ -103,25 +103,26 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) - encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ + encryptedSecretsAndFolders, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ WorkspaceId: workspaceId, Environment: environmentName, TagSlugs: tagSlugs, + SecretPath: secretPath, }) if err != nil { - return nil, err + return nil, nil, err } - plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecrets) + plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsAndFolders) if err != nil { - return nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) + return nil, nil, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) } - return plainTextSecrets, nil + return plainTextSecrets, encryptedSecretsAndFolders.Folders, nil } -func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, error) { +func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models.SingleEnvironmentVariable, []api.Folders, error) { var infisicalToken string if params.InfisicalToken == "" { infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) @@ -132,6 +133,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models isConnected := CheckIsConnectedToInternet() var secretsToReturn []models.SingleEnvironmentVariable var errorToReturn error + var folders []api.Folders if infisicalToken == "" { if isConnected { @@ -144,12 +146,12 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models loggedInUserDetails, err := GetCurrentLoggedInUserDetails() if err != nil { - return nil, err + return nil, nil, err } workspaceFile, err := GetWorkSpaceFromFile() if err != nil { - return nil, err + return nil, nil, err } if params.WorkspaceId != "" { @@ -159,10 +161,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models // Verify environment err = ValidateEnvironmentName(params.Environment, workspaceFile.WorkspaceId, loggedInUserDetails.UserCredentials) if err != nil { - return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) + return nil, nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) } - secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs) + secretsToReturn, folders, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.Path) log.Debugf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] @@ -182,10 +184,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models } else { log.Debug("Trying to fetch secrets using service token") - secretsToReturn, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken) + secretsToReturn, folders, errorToReturn = GetPlainTextSecretsViaServiceToken(infisicalToken) } - return secretsToReturn, errorToReturn + return secretsToReturn, folders, errorToReturn } func ValidateEnvironmentName(environmentName string, workspaceId string, userLoggedInDetails models.UserCredentials) error { diff --git a/cli/packages/visualize/secrets.go b/cli/packages/visualize/secrets.go index 7be41020db..ab4c8296df 100644 --- a/cli/packages/visualize/secrets.go +++ b/cli/packages/visualize/secrets.go @@ -1,6 +1,9 @@ package visualize -import "github.com/Infisical/infisical-merge/packages/models" +import ( + "github.com/Infisical/infisical-merge/packages/api" + "github.com/Infisical/infisical-merge/packages/models" +) func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { rows := [][3]string{} @@ -10,5 +13,16 @@ func PrintAllSecretDetails(secrets []models.SingleEnvironmentVariable) { headers := [...]string{"SECRET NAME", "SECRET VALUE", "SECRET TYPE"} + SecretsTable(headers, rows) +} + +func PrintSecretFolders(folders []api.Folders) { + rows := [][]string{} + for _, folder := range folders { + rows = append(rows, []string{folder.Name}) + } + + headers := []string{"FOLDER NAME(S)"} + Table(headers, rows) } diff --git a/cli/packages/visualize/visualize.go b/cli/packages/visualize/visualize.go index 8599f24056..915adbe395 100644 --- a/cli/packages/visualize/visualize.go +++ b/cli/packages/visualize/visualize.go @@ -29,8 +29,36 @@ const ( ellipsis = "…" ) +// Given any number of headers and rows, this function will print out a table +func Table(headers []string, rows [][]string) { + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.SetStyle(table.StyleLight) + + // t.SetTitle("Title") + t.Style().Options.DrawBorder = true + t.Style().Options.SeparateHeader = true + t.Style().Options.SeparateColumns = true + + tableHeaders := table.Row{} + for _, header := range headers { + tableHeaders = append(tableHeaders, header) + } + + t.AppendHeader(tableHeaders) + for _, row := range rows { + tableRow := table.Row{} + for _, val := range row { + tableRow = append(tableRow, val) + } + t.AppendRow(tableRow) + } + + t.Render() +} + // Given headers and rows, this function will print out a table -func Table(headers [3]string, rows [][3]string) { +func SecretsTable(headers [3]string, rows [][3]string) { // if we're not in a terminal or cygwin terminal, don't truncate the secret value shouldTruncate := isatty.IsTerminal(os.Stdout.Fd())