From b0c541f8dc07d518065e194b0c1478e79c11ddee Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Wed, 8 Feb 2023 13:46:57 -0800 Subject: [PATCH] generate example .env file command --- cli/packages/api/model.go | 39 +++++--- cli/packages/cmd/secrets.go | 183 ++++++++++++++++++++++++++++++++++- cli/packages/models/cli.go | 11 ++- cli/packages/util/helper.go | 12 +++ cli/packages/util/log.go | 2 +- cli/packages/util/secrets.go | 31 +++++- 6 files changed, 256 insertions(+), 22 deletions(-) diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 698d414084..8f929bf697 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -201,21 +201,30 @@ type GetEncryptedSecretsV2Request struct { 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"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - User string `json:"user,omitempty"` + 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"` } diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index c5fb11c1eb..a1a8916fd9 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -6,6 +6,8 @@ package cmd import ( "encoding/base64" "fmt" + "regexp" + "sort" "strings" "unicode" @@ -22,7 +24,7 @@ import ( ) var secretsCmd = &cobra.Command{ - Example: `infisical secrets"`, + Example: `infisical secrets`, Short: "Used to create, read update and delete secrets", Use: "secrets", DisableFlagsInUseLine: true, @@ -67,6 +69,16 @@ var secretsGetCmd = &cobra.Command{ Run: getSecretsByNames, } +var secretsGenerateExampleEnvCmd = &cobra.Command{ + Example: `secrets generate-example-env > .example-env`, + Short: "Used to generate a example .env file", + Use: "generate-example-env", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + PreRun: toggleDebug, + Run: generateExampleEnv, +} + var secretsSetCmd = &cobra.Command{ Example: `secrets set ..."`, Short: "Used set secrets", @@ -357,6 +369,171 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { visualize.PrintAllSecretDetails(requestedSecrets) } +func generateExampleEnv(cmd *cobra.Command, args []string) { + environmentName, err := cmd.Flags().GetString("env") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + workspaceFileExists := util.WorkspaceConfigFileExistsInCurrentPath() + if !workspaceFileExists { + util.HandleError(err, "Unable to parse flag") + } + + infisicalToken, err := cmd.Flags().GetString("token") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken}) + if err != nil { + util.HandleError(err, "To fetch all secrets") + } + + tagsHashToSecretKey := make(map[string]int) + + type TagsAndSecrets struct { + Secrets []models.SingleEnvironmentVariable + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } + } + + // sort secrets by associated tags (most number of tags to least tags) + sort.Slice(secrets, func(i, j int) bool { + return len(secrets[i].Tags) > len(secrets[j].Tags) + }) + + for _, secret := range secrets { + listOfTagSlugs := []string{} + + for _, tag := range secret.Tags { + listOfTagSlugs = append(listOfTagSlugs, tag.Slug) + } + sort.Strings(listOfTagSlugs) + + tagsHash := util.GetHashFromStringList(listOfTagSlugs) + + tagsHashToSecretKey[tagsHash] += 1 + } + + finalTagHashToSecretKey := make(map[string]TagsAndSecrets) + + for _, secret := range secrets { + listOfTagSlugs := []string{} + for _, tag := range secret.Tags { + listOfTagSlugs = append(listOfTagSlugs, tag.Slug) + } + + // sort the slug so we get the same hash each time + sort.Strings(listOfTagSlugs) + + tagsHash := util.GetHashFromStringList(listOfTagSlugs) + occurrence, exists := tagsHashToSecretKey[tagsHash] + if exists && occurrence > 0 { + + value, exists2 := finalTagHashToSecretKey[tagsHash] + allSecretsForTags := append(value.Secrets, secret) + + // sort the the secrets by keys so that they can later be sorted by the first item in the secrets array + sort.Slice(allSecretsForTags, func(i, j int) bool { + return allSecretsForTags[i].Key < allSecretsForTags[j].Key + }) + + if exists2 { + finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{ + Tags: secret.Tags, + Secrets: allSecretsForTags, + } + } else { + finalTagHashToSecretKey[tagsHash] = TagsAndSecrets{ + Tags: secret.Tags, + Secrets: []models.SingleEnvironmentVariable{secret}, + } + } + + tagsHashToSecretKey[tagsHash] -= 1 + } + } + + // sort the fianl result by secret key fo consistent print order + listOfsecretDetails := make([]TagsAndSecrets, 0, len(finalTagHashToSecretKey)) + for _, secretDetails := range finalTagHashToSecretKey { + listOfsecretDetails = append(listOfsecretDetails, secretDetails) + } + + // sort the order of the headings by the order of the secrets + sort.Slice(listOfsecretDetails, func(i, j int) bool { + return len(listOfsecretDetails[i].Tags) < len(listOfsecretDetails[j].Tags) + }) + + for _, secretDetails := range listOfsecretDetails { + listOfKeyValue := []string{} + + for _, secret := range secretDetails.Secrets { + re := regexp.MustCompile(`(.*)DEFAULT:(.*)`) + match := re.FindStringSubmatch(secret.Comment) + defaultValue := "" + comment := secret.Comment + + // Case: Only has default value + if len(match) == 2 { + defaultValue = strings.TrimSpace(match[1]) + } + + // Case: has a comment and a default value + if len(match) == 3 { + comment = match[1] + defaultValue = match[2] + } + + row := "" + if comment != "" { + comment = addHash(comment) + row = fmt.Sprintf("%s \n%s=%s", strings.TrimSpace(comment), strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue)) + } else { + row = fmt.Sprintf("%s=%s", strings.TrimSpace(secret.Key), strings.TrimSpace(defaultValue)) + } + + // each secret row to be added to the file + listOfKeyValue = append(listOfKeyValue, row) + } + + listOfTagNames := []string{} + for _, tag := range secretDetails.Tags { + listOfTagNames = append(listOfTagNames, tag.Name) + } + + heading := CenterString(strings.Join(listOfTagNames, " & "), 80) + + if len(listOfTagNames) == 0 { + fmt.Printf("\n%s \n", strings.Join(listOfKeyValue, "\n \n")) + } else { + fmt.Printf("\n\n\n%s\n \n%s \n", heading, strings.Join(listOfKeyValue, "\n \n")) + } + } +} + +func CenterString(s string, numStars int) string { + stars := strings.Repeat("*", numStars) + padding := (numStars - len(s)) / 2 + cenetredTextWithStar := stars[:padding] + " " + strings.ToUpper(s) + " " + stars[padding:] + + hashes := strings.Repeat("#", len(cenetredTextWithStar)+2) + return fmt.Sprintf("%s \n# %s \n%s", hashes, cenetredTextWithStar, hashes) +} + +func addHash(input string) string { + lines := strings.Split(input, "\n") + for i, line := range lines { + lines[i] = "# " + line + } + return strings.Join(lines, "\n") +} + func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { secretMapByName := make(map[string]models.SingleEnvironmentVariable) @@ -368,6 +545,10 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod } func init() { + + secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + secretsCmd.AddCommand(secretsGenerateExampleEnvCmd) + secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") secretsCmd.AddCommand(secretsGetCmd) diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 67b89b27fe..3ffc1cc20f 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -1,6 +1,8 @@ package models -import "github.com/99designs/keyring" +import ( + "github.com/99designs/keyring" +) type UserCredentials struct { Email string `json:"email"` @@ -19,6 +21,13 @@ type SingleEnvironmentVariable struct { Value string `json:"value"` Type string `json:"type"` ID string `json:"_id"` + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } `json:"tags"` + Comment string `json:"comment"` } type Workspace struct { diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index a6e7e35fc1..3fd8853c4a 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -1,6 +1,7 @@ package util import ( + "crypto/sha256" "encoding/base64" "fmt" "os" @@ -98,3 +99,14 @@ func RequireLocalWorkspaceFile() { PrintMessageAndExit("Your project id is missing in your local config file. Please add it or run again [infisical init]") } } + +func GetHashFromStringList(list []string) string { + hash := sha256.New() + + for _, item := range list { + hash.Write([]byte(item)) + } + + sum := sha256.Sum256(hash.Sum(nil)) + return fmt.Sprintf("%x", sum) +} diff --git a/cli/packages/util/log.go b/cli/packages/util/log.go index a701987fc2..b191f33c82 100644 --- a/cli/packages/util/log.go +++ b/cli/packages/util/log.go @@ -24,7 +24,7 @@ func PrintErrorAndExit(exitCode int, err error, messages ...string) { } func PrintWarning(message string) { - color.Yellow("Warning: %v", message) + color.New(color.FgYellow).Fprintf(os.Stderr, "Warning: %v \n", message) } func PrintMessageAndExit(messages ...string) { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 86f4a5e05a..7a4bc09dbc 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -315,11 +315,34 @@ func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2R return nil, fmt.Errorf("unable to symmetrically decrypt secret value") } + // Decrypt comment + comment_iv, err := base64.StdEncoding.DecodeString(secret.SecretCommentIV) + if err != nil { + return nil, fmt.Errorf("unable to decode secret IV for secret value") + } + + comment_tag, err := base64.StdEncoding.DecodeString(secret.SecretCommentTag) + if err != nil { + return nil, fmt.Errorf("unable to decode secret authentication tag for secret value") + } + + comment_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretCommentCiphertext) + if err != nil { + return nil, fmt.Errorf("unable to decode secret cipher text for secret key") + } + + plainTextComment, err := crypto.DecryptSymmetric(key, comment_ciphertext, comment_tag, comment_iv) + if err != nil { + return nil, fmt.Errorf("unable to symmetrically decrypt secret comment") + } + plainTextSecret := models.SingleEnvironmentVariable{ - Key: string(plainTextKey), - Value: string(plainTextValue), - Type: string(secret.Type), - ID: secret.ID, + Key: string(plainTextKey), + Value: string(plainTextValue), + Type: string(secret.Type), + ID: secret.ID, + Tags: secret.Tags, + Comment: string(plainTextComment), } plainTextSecrets = append(plainTextSecrets, plainTextSecret)