diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index a56f4c24e..fd1dcc574 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -391,6 +391,7 @@ func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request } func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response response, err := httpClient. R(). @@ -566,3 +567,39 @@ func CallCreateDynamicSecretLeaseV1(httpClient *resty.Client, request CreateDyna return createDynamicSecretLeaseResponse, nil } + +func CallCreateRawSecretsV3(httpClient *resty.Client, request CreateRawSecretV3Request) error { + response, err := httpClient. + R(). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallCreateRawSecretsV3: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return fmt.Errorf("CallCreateRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return nil +} + +func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByNameV3Request) error { + response, err := httpClient. + R(). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch(fmt.Sprintf("%v/v3/secrets/raw/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallUpdateRawSecretsV3: Unable to complete api request [err=%w]", err) + } + + if response.IsError() { + return fmt.Errorf("CallUpdateRawSecretsV3: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String()) + } + + return nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 0fffe0520..e09f2275f 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -161,6 +161,14 @@ type Secret struct { PlainTextKey string `json:"plainTextKey"` } +type RawSecret struct { + SecretKey string `json:"secretKey,omitempty"` + SecretValue string `json:"secretValue,omitempty"` + Type string `json:"type,omitempty"` + SecretComment string `json:"secretComment,omitempty"` + ID string `json:"id,omitempty"` +} + type GetEncryptedWorkspaceKeyRequest struct { WorkspaceId string `json:"workspaceId"` } @@ -410,12 +418,23 @@ type CreateSecretV3Request struct { SecretPath string `json:"secretPath"` } +type CreateRawSecretV3Request struct { + SecretName string `json:"-"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type,omitempty"` + Environment string `json:"environment"` + SecretPath string `json:"secretPath,omitempty"` + SecretValue string `json:"secretValue"` + SecretComment string `json:"secretComment,omitempty"` + SkipMultilineEncoding bool `json:"skipMultilineEncoding,omitempty"` +} + type DeleteSecretV3Request struct { SecretName string `json:"secretName"` WorkspaceId string `json:"workspaceId"` Environment string `json:"environment"` - Type string `json:"type"` - SecretPath string `json:"secretPath"` + Type string `json:"type,omitempty"` + SecretPath string `json:"secretPath,omitempty"` } type UpdateSecretByNameV3Request struct { @@ -428,6 +447,15 @@ type UpdateSecretByNameV3Request struct { SecretValueTag string `json:"secretValueTag"` } +type UpdateRawSecretByNameV3Request struct { + SecretName string `json:"-"` + WorkspaceID string `json:"workspaceId"` + Environment string `json:"environment"` + SecretPath string `json:"secretPath,omitempty"` + SecretValue string `json:"secretValue"` + Type string `json:"type,omitempty"` +} + type GetSingleSecretByNameV3Request struct { SecretName string `json:"secretName"` WorkspaceId string `json:"workspaceId"` diff --git a/cli/packages/cmd/export.go b/cli/packages/cmd/export.go index 983c19255..c0fea738e 100644 --- a/cli/packages/cmd/export.go +++ b/cli/packages/cmd/export.go @@ -55,6 +55,11 @@ var exportCmd = &cobra.Command{ util.HandleError(err) } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + format, err := cmd.Flags().GetString("format") if err != nil { util.HandleError(err) @@ -70,11 +75,6 @@ var exportCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - token, err := util.GetInfisicalToken(cmd) - if err != nil { - util.HandleError(err, "Unable to parse flag") - } - tagSlugs, err := cmd.Flags().GetString("tags") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -169,9 +169,9 @@ func init() { exportCmd.Flags().StringP("format", "f", "dotenv", "Set the format of the output file (dotenv, json, csv)") exportCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") exportCmd.Flags().Bool("include-imports", true, "Imported linked secrets") - exportCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") + exportCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access 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("projectId", "", "manually set the projectId to export secrets from") exportCmd.Flags().String("path", "/", "get secrets within a folder path") exportCmd.Flags().String("template", "", "The path to the template file used to render secrets") } diff --git a/cli/packages/cmd/folder.go b/cli/packages/cmd/folder.go index 9cb76a312..538f1e2dc 100644 --- a/cli/packages/cmd/folder.go +++ b/cli/packages/cmd/folder.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "github.com/Infisical/infisical-merge/packages/models" @@ -71,10 +72,6 @@ var getCmd = &cobra.Command{ var createCmd = &cobra.Command{ Use: "create", Short: "Create a folder", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - }, Run: func(cmd *cobra.Command, args []string) { environmentName, _ := cmd.Flags().GetString("env") if !cmd.Flags().Changed("env") { @@ -84,6 +81,16 @@ var createCmd = &cobra.Command{ } } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + folderPath, err := cmd.Flags().GetString("path") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -95,19 +102,31 @@ var createCmd = &cobra.Command{ } if folderName == "" { - util.HandleError(fmt.Errorf("Invalid folder name"), "Folder name cannot be empty") + util.HandleError(errors.New("invalid folder name, folder name cannot be empty")) } - workspaceFile, err := util.GetWorkSpaceFromFile() if err != nil { util.HandleError(err, "Unable to get workspace file") } + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get workspace file") + } + + projectId = workspaceFile.WorkspaceId + } + params := models.CreateFolderParameters{ FolderName: folderName, - WorkspaceId: workspaceFile.WorkspaceId, Environment: environmentName, FolderPath: folderPath, + WorkspaceId: projectId, + } + + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + params.InfisicalToken = token.Token } _, err = util.CreateFolder(params) @@ -124,10 +143,6 @@ var createCmd = &cobra.Command{ var deleteCmd = &cobra.Command{ Use: "delete", Short: "Delete a folder", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - }, Run: func(cmd *cobra.Command, args []string) { environmentName, _ := cmd.Flags().GetString("env") @@ -138,6 +153,16 @@ var deleteCmd = &cobra.Command{ } } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + folderPath, err := cmd.Flags().GetString("path") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -149,21 +174,29 @@ var deleteCmd = &cobra.Command{ } if folderName == "" { - util.HandleError(fmt.Errorf("Invalid folder name"), "Folder name cannot be empty") + util.HandleError(errors.New("invalid folder name, folder name cannot be empty")) } - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.HandleError(err, "Unable to get workspace file") + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get workspace file") + } + + projectId = workspaceFile.WorkspaceId } params := models.DeleteFolderParameters{ FolderName: folderName, - WorkspaceId: workspaceFile.WorkspaceId, + WorkspaceId: projectId, Environment: environmentName, FolderPath: folderPath, } + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + params.InfisicalToken = token.Token + } + _, err = util.DeleteFolder(params) if err != nil { util.HandleError(err, "Unable to delete folder") diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index 04fe2588b..22a4ca65b 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -237,8 +237,8 @@ func filterReservedEnvVars(env map[string]models.SingleEnvironmentVariable) { func init() { rootCmd.AddCommand(runCmd) - runCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - runCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + runCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + runCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") 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 ") diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 423fe1657..43c159c34 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -4,23 +4,17 @@ Copyright (c) 2023 Infisical Inc. package cmd import ( - "crypto/sha256" - "encoding/base64" "fmt" - "os" "regexp" "sort" "strings" - "unicode" "github.com/Infisical/infisical-merge/packages/api" - "github.com/Infisical/infisical-merge/packages/crypto" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/visualize" "github.com/go-resty/resty/v2" "github.com/posthog/posthog-go" - "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -160,14 +154,19 @@ var secretsSetCmd = &cobra.Command{ } } - secretsPath, err := cmd.Flags().GetString("path") + token, err := util.GetInfisicalToken(cmd) if err != nil { util.HandleError(err, "Unable to parse flag") } - workspaceFile, err := util.GetWorkSpaceFromFile() + projectId, err := cmd.Flags().GetString("projectId") if err != nil { - util.HandleError(err, "Unable to get your local config details") + util.HandleError(err, "Unable to parse flag") + } + + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") } secretType, err := cmd.Flags().GetString("type") @@ -175,196 +174,18 @@ var secretsSetCmd = &cobra.Command{ util.HandleError(err, "Unable to parse secret type") } - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + var secretOperations []models.SecretSetOperation + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token) + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + secretOperations, err = util.SetEncryptedSecrets(args, secretType, environmentName, secretsPath) + } + if err != nil { - util.HandleError(err, "Unable to authenticate") - } - - if loggedInUserDetails.LoginExpired { - util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") - } - - - httpClient := resty.New(). - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). - SetHeader("Accept", "application/json") - - request := api.GetEncryptedWorkspaceKeyRequest{ - WorkspaceId: workspaceFile.WorkspaceId, - } - - workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request) - if err != nil { - util.HandleError(err, "unable to get your encrypted workspace key") - } - - encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey) - encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey) - encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce) - currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey) - - if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 { - log.Debug().Msgf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey) - util.PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again") - } - - // decrypt workspace key - plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) - - infisicalTokenEnv := os.Getenv(util.INFISICAL_TOKEN_NAME) - - // pull current secrets - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, InfisicalToken: infisicalTokenEnv}, "") - if err != nil { - util.HandleError(err, "unable to retrieve secrets") - } - - type SecretSetOperation struct { - SecretKey string - SecretValue string - SecretOperation string - } - - secretsToCreate := []api.Secret{} - secretsToModify := []api.Secret{} - secretOperations := []SecretSetOperation{} - - sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) - - for _, secret := range secrets { - if secret.Type == util.SECRET_TYPE_PERSONAL { - personalSecretMapByName[secret.Key] = secret - } else { - sharedSecretMapByName[secret.Key] = secret - } - } - - for _, arg := range args { - splitKeyValueFromArg := strings.SplitN(arg, "=", 2) - if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { - util.PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again") - } - - if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { - util.PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again") - } - - // Key and value from argument - key := splitKeyValueFromArg[0] - value := splitKeyValueFromArg[1] - - hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) - encryptedKey, err := crypto.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) - if err != nil { - util.HandleError(err, "unable to encrypt your secrets") - } - - hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) - encryptedValue, err := crypto.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) - if err != nil { - util.HandleError(err, "unable to encrypt your secrets") - } - - var existingSecret models.SingleEnvironmentVariable - var doesSecretExist bool - - if secretType == util.SECRET_TYPE_SHARED { - existingSecret, doesSecretExist = sharedSecretMapByName[key] - } else { - existingSecret, doesSecretExist = personalSecretMapByName[key] - } - - if doesSecretExist { - // case: secret exists in project so it needs to be modified - encryptedSecretDetails := api.Secret{ - ID: existingSecret.ID, - SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), - SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), - SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), - SecretValueHash: hashedValue, - PlainTextKey: key, - Type: existingSecret.Type, - } - - // Only add to modifications if the value is different - if existingSecret.Value != value { - secretsToModify = append(secretsToModify, encryptedSecretDetails) - secretOperations = append(secretOperations, SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET VALUE MODIFIED", - }) - } else { - // Current value is same as exisitng so no change - secretOperations = append(secretOperations, SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET VALUE UNCHANGED", - }) - } - - } else { - // case: secret doesn't exist in project so it needs to be created - encryptedSecretDetails := api.Secret{ - SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), - SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), - SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), - SecretKeyHash: hashedKey, - SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), - SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), - SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), - SecretValueHash: hashedValue, - Type: secretType, - PlainTextKey: key, - } - secretsToCreate = append(secretsToCreate, encryptedSecretDetails) - secretOperations = append(secretOperations, SecretSetOperation{ - SecretKey: key, - SecretValue: value, - SecretOperation: "SECRET CREATED", - }) - } - } - - for _, secret := range secretsToCreate { - createSecretRequest := api.CreateSecretV3Request{ - WorkspaceID: workspaceFile.WorkspaceId, - Environment: environmentName, - SecretName: secret.PlainTextKey, - SecretKeyCiphertext: secret.SecretKeyCiphertext, - SecretKeyIV: secret.SecretKeyIV, - SecretKeyTag: secret.SecretKeyTag, - SecretValueCiphertext: secret.SecretValueCiphertext, - SecretValueIV: secret.SecretValueIV, - SecretValueTag: secret.SecretValueTag, - Type: secret.Type, - SecretPath: secretsPath, - } - - err = api.CallCreateSecretsV3(httpClient, createSecretRequest) - if err != nil { - util.HandleError(err, "Unable to process new secret creations") - return - } - } - - for _, secret := range secretsToModify { - updateSecretRequest := api.UpdateSecretByNameV3Request{ - WorkspaceID: workspaceFile.WorkspaceId, - Environment: environmentName, - SecretValueCiphertext: secret.SecretValueCiphertext, - SecretValueIV: secret.SecretValueIV, - SecretValueTag: secret.SecretValueTag, - Type: secret.Type, - SecretPath: secretsPath, - } - - err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey) - if err != nil { - util.HandleError(err, "Unable to process secret update request") - return - } + util.HandleError(err, "Unable to set secrets") } // Print secret operations @@ -395,6 +216,16 @@ var secretsDeleteCmd = &cobra.Command{ } } + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + secretsPath, err := cmd.Flags().GetString("path") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -405,33 +236,44 @@ var secretsDeleteCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() - if err != nil { - util.HandleError(err, "Unable to authenticate") + httpClient := resty.New(). + SetHeader("Accept", "application/json") + + if projectId == "" { + workspaceFile, err := util.GetWorkSpaceFromFile() + if err != nil { + util.HandleError(err, "Unable to get local project details") + } + projectId = workspaceFile.WorkspaceId } - if loggedInUserDetails.LoginExpired { - util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") - } + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + httpClient.SetAuthToken(token.Token) + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() - workspaceFile, err := util.GetWorkSpaceFromFile() - if err != nil { - util.HandleError(err, "Unable to get local project details") + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken) } for _, secretName := range args { request := api.DeleteSecretV3Request{ - WorkspaceId: workspaceFile.WorkspaceId, + WorkspaceId: projectId, Environment: environmentName, SecretName: secretName, Type: secretType, SecretPath: secretsPath, } - httpClient := resty.New(). - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). - SetHeader("Accept", "application/json") - err = api.CallDeleteSecretsV3(httpClient, request) if err != nil { util.HandleError(err, "Unable to complete your delete request") @@ -789,13 +631,13 @@ func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]mod } func init() { - secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - secretsGenerateExampleEnvCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + secretsGenerateExampleEnvCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsGenerateExampleEnvCmd.Flags().String("projectId", "", "manually set the projectId when using machine identity based auth") secretsGenerateExampleEnvCmd.Flags().String("path", "/", "Fetch secrets from within a folder path") secretsCmd.AddCommand(secretsGenerateExampleEnvCmd) - secretsGetCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - secretsGetCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + secretsGetCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsGetCmd.Flags().String("projectId", "", "manually set the project ID to fetch secrets from when using machine identity based auth") secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") secretsGetCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret") @@ -804,41 +646,37 @@ func init() { secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) + secretsSetCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsSetCmd.Flags().String("projectId", "", "manually set the project ID to for setting secrets when using machine identity based auth") secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path") secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared") - // Only supports logged in users (JWT auth) - secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - } - secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") + secretsDeleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsDeleteCmd.Flags().String("projectId", "", "manually set the projectId to delete secrets from when using machine identity based auth") secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.AddCommand(secretsDeleteCmd) - // Only supports logged in users (JWT auth) - secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { - util.RequireLogin() - util.RequireLocalWorkspaceFile() - } - // *** Folders sub command *** folderCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") // Add getCmd, createCmd and deleteCmd flags here getCmd.Flags().StringP("path", "p", "/", "The path from where folders should be fetched from") - getCmd.Flags().String("token", "", "Fetch folders using the infisical token") - getCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + getCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + getCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from when using machine identity based auth") folderCmd.AddCommand(getCmd) // Add createCmd flags here createCmd.Flags().StringP("path", "p", "/", "Path to where the folder should be created") createCmd.Flags().StringP("name", "n", "", "Name of the folder to be created in selected `--path`") + createCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + createCmd.Flags().String("projectId", "", "manually set the project ID for creating folders in when using machine identity based auth") folderCmd.AddCommand(createCmd) // Add deleteCmd flags here deleteCmd.Flags().StringP("path", "p", "/", "Path to the folder to be deleted") + deleteCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + deleteCmd.Flags().String("projectId", "", "manually set the projectId to delete folders when using machine identity based auth") deleteCmd.Flags().StringP("name", "n", "", "Name of the folder to be deleted within selected `--path`") folderCmd.AddCommand(deleteCmd) @@ -846,8 +684,8 @@ func init() { // ** End of folders sub command - secretsCmd.Flags().String("token", "", "Fetch secrets using the Infisical Token") - secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch folders from for machine identity") + secretsCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token") + secretsCmd.Flags().String("projectId", "", "manually set the projectId to fetch secrets when using machine identity based auth") 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 ") diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 68527c469..6f996d667 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -134,3 +134,9 @@ type MachineIdentityCredentials struct { ClientId string ClientSecret string } + +type SecretSetOperation struct { + SecretKey string + SecretValue string + SecretOperation string +} diff --git a/cli/packages/util/folders.go b/cli/packages/util/folders.go index 18fe5c888..c7f6de630 100644 --- a/cli/packages/util/folders.go +++ b/cli/packages/util/folders.go @@ -172,19 +172,28 @@ func GetFoldersViaMachineIdentity(accessToken string, workspaceId string, envSlu // CreateFolder creates a folder in Infisical func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, error) { - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() - if err != nil { - return models.SingleFolder{}, err - } - if loggedInUserDetails.LoginExpired { - PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + // If no token is provided, we will try to get the token from the current logged in user + if params.InfisicalToken == "" { + RequireLogin() + RequireLocalWorkspaceFile() + loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + + if err != nil { + return models.SingleFolder{}, err + } + + if loggedInUserDetails.LoginExpired { + PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken } // set up resty client httpClient := resty.New() httpClient. - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetAuthToken(params.InfisicalToken). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json") @@ -209,19 +218,29 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er } func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder, error) { - loggedInUserDetails, err := GetCurrentLoggedInUserDetails() - if err != nil { - return nil, err - } - if loggedInUserDetails.LoginExpired { - PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + // If no token is provided, we will try to get the token from the current logged in user + if params.InfisicalToken == "" { + RequireLogin() + RequireLocalWorkspaceFile() + + loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + + if err != nil { + return nil, err + } + + if loggedInUserDetails.LoginExpired { + PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + params.InfisicalToken = loggedInUserDetails.UserCredentials.JTWToken } // set up resty client httpClient := resty.New() httpClient. - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetAuthToken(params.InfisicalToken). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json") diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 02c2704bd..08e6b563f 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -1,6 +1,7 @@ package util import ( + "crypto/sha256" "encoding/base64" "encoding/json" "errors" @@ -9,6 +10,7 @@ import ( "path" "regexp" "strings" + "unicode" "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/crypto" @@ -806,3 +808,336 @@ func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey str return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil } + +func SetEncryptedSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string) ([]models.SecretSetOperation, error) { + + workspaceFile, err := GetWorkSpaceFromFile() + if err != nil { + return nil, fmt.Errorf("unable to get your local config details [err=%v]", err) + } + + loggedInUserDetails, err := GetCurrentLoggedInUserDetails() + if err != nil { + return nil, fmt.Errorf("unable to authenticate [err=%v]", err) + } + + if loggedInUserDetails.LoginExpired { + PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") + } + + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") + + request := api.GetEncryptedWorkspaceKeyRequest{ + WorkspaceId: workspaceFile.WorkspaceId, + } + + workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request) + if err != nil { + return nil, fmt.Errorf("unable to get your encrypted workspace key [err=%v]", err) + } + + encryptedWorkspaceKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey) + encryptedWorkspaceKeySenderPublicKey, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey) + encryptedWorkspaceKeyNonce, _ := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce) + currentUsersPrivateKey, _ := base64.StdEncoding.DecodeString(loggedInUserDetails.UserCredentials.PrivateKey) + + if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 { + log.Debug().Msgf("Missing credentials for generating plainTextEncryptionKey: [currentUsersPrivateKey=%s] [encryptedWorkspaceKeySenderPublicKey=%s]", currentUsersPrivateKey, encryptedWorkspaceKeySenderPublicKey) + PrintErrorMessageAndExit("Some required user credentials are missing to generate your [plainTextEncryptionKey]. Please run [infisical login] then try again") + } + + // decrypt workspace key + plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) + + infisicalTokenEnv := os.Getenv(INFISICAL_TOKEN_NAME) + + // pull current secrets + secrets, err := GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, InfisicalToken: infisicalTokenEnv}, "") + if err != nil { + return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err) + } + + secretsToCreate := []api.Secret{} + secretsToModify := []api.Secret{} + secretOperations := []models.SecretSetOperation{} + + sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) + personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) + + for _, secret := range secrets { + if secret.Type == SECRET_TYPE_PERSONAL { + personalSecretMapByName[secret.Key] = secret + } else { + sharedSecretMapByName[secret.Key] = secret + } + } + + for _, arg := range secretArgs { + splitKeyValueFromArg := strings.SplitN(arg, "=", 2) + if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { + PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again") + } + + if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { + PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again") + } + + // Key and value from argument + key := splitKeyValueFromArg[0] + value := splitKeyValueFromArg[1] + + hashedKey := fmt.Sprintf("%x", sha256.Sum256([]byte(key))) + encryptedKey, err := crypto.EncryptSymmetric([]byte(key), []byte(plainTextEncryptionKey)) + if err != nil { + return nil, fmt.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + hashedValue := fmt.Sprintf("%x", sha256.Sum256([]byte(value))) + encryptedValue, err := crypto.EncryptSymmetric([]byte(value), []byte(plainTextEncryptionKey)) + if err != nil { + return nil, fmt.Errorf("unable to encrypt your secrets [err=%v]", err) + } + + var existingSecret models.SingleEnvironmentVariable + var doesSecretExist bool + + if secretType == SECRET_TYPE_SHARED { + existingSecret, doesSecretExist = sharedSecretMapByName[key] + } else { + existingSecret, doesSecretExist = personalSecretMapByName[key] + } + + if doesSecretExist { + // case: secret exists in project so it needs to be modified + encryptedSecretDetails := api.Secret{ + ID: existingSecret.ID, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + PlainTextKey: key, + Type: existingSecret.Type, + } + + // Only add to modifications if the value is different + if existingSecret.Value != value { + secretsToModify = append(secretsToModify, encryptedSecretDetails) + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE MODIFIED", + }) + } else { + // Current value is same as exisitng so no change + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE UNCHANGED", + }) + } + + } else { + // case: secret doesn't exist in project so it needs to be created + encryptedSecretDetails := api.Secret{ + SecretKeyCiphertext: base64.StdEncoding.EncodeToString(encryptedKey.CipherText), + SecretKeyIV: base64.StdEncoding.EncodeToString(encryptedKey.Nonce), + SecretKeyTag: base64.StdEncoding.EncodeToString(encryptedKey.AuthTag), + SecretKeyHash: hashedKey, + SecretValueCiphertext: base64.StdEncoding.EncodeToString(encryptedValue.CipherText), + SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), + SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), + SecretValueHash: hashedValue, + Type: secretType, + PlainTextKey: key, + } + secretsToCreate = append(secretsToCreate, encryptedSecretDetails) + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET CREATED", + }) + } + } + + for _, secret := range secretsToCreate { + createSecretRequest := api.CreateSecretV3Request{ + WorkspaceID: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretName: secret.PlainTextKey, + SecretKeyCiphertext: secret.SecretKeyCiphertext, + SecretKeyIV: secret.SecretKeyIV, + SecretKeyTag: secret.SecretKeyTag, + SecretValueCiphertext: secret.SecretValueCiphertext, + SecretValueIV: secret.SecretValueIV, + SecretValueTag: secret.SecretValueTag, + Type: secret.Type, + SecretPath: secretsPath, + } + + err = api.CallCreateSecretsV3(httpClient, createSecretRequest) + if err != nil { + return nil, fmt.Errorf("unable to process new secret creations [err=%v]", err) + } + } + + for _, secret := range secretsToModify { + updateSecretRequest := api.UpdateSecretByNameV3Request{ + WorkspaceID: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretValueCiphertext: secret.SecretValueCiphertext, + SecretValueIV: secret.SecretValueIV, + SecretValueTag: secret.SecretValueTag, + Type: secret.Type, + SecretPath: secretsPath, + } + + err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey) + if err != nil { + return nil, fmt.Errorf("unable to process secret update request [err=%v]", err) + } + } + + return secretOperations, nil + +} + +func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails) ([]models.SecretSetOperation, error) { + + if tokenDetails == nil { + return nil, fmt.Errorf("unable to process set secret operations, token details are missing") + } + + getAllEnvironmentVariablesRequest := models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath, WorkspaceId: projectId} + if tokenDetails.Type == UNIVERSAL_AUTH_TOKEN_IDENTIFIER { + getAllEnvironmentVariablesRequest.UniversalAuthAccessToken = tokenDetails.Token + } else { + getAllEnvironmentVariablesRequest.InfisicalToken = tokenDetails.Token + } + + httpClient := resty.New(). + SetAuthToken(tokenDetails.Token). + SetHeader("Accept", "application/json") + + // pull current secrets + secrets, err := GetAllEnvironmentVariables(getAllEnvironmentVariablesRequest, "") + if err != nil { + return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err) + } + + secretsToCreate := []api.RawSecret{} + secretsToModify := []api.RawSecret{} + secretOperations := []models.SecretSetOperation{} + + sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) + personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) + + for _, secret := range secrets { + if secret.Type == SECRET_TYPE_PERSONAL { + personalSecretMapByName[secret.Key] = secret + } else { + sharedSecretMapByName[secret.Key] = secret + } + } + + for _, arg := range secretArgs { + splitKeyValueFromArg := strings.SplitN(arg, "=", 2) + if splitKeyValueFromArg[0] == "" || splitKeyValueFromArg[1] == "" { + PrintErrorMessageAndExit("ensure that each secret has a none empty key and value. Modify the input and try again") + } + + if unicode.IsNumber(rune(splitKeyValueFromArg[0][0])) { + PrintErrorMessageAndExit("keys of secrets cannot start with a number. Modify the key name(s) and try again") + } + + // Key and value from argument + key := splitKeyValueFromArg[0] + value := splitKeyValueFromArg[1] + + var existingSecret models.SingleEnvironmentVariable + var doesSecretExist bool + + if secretType == SECRET_TYPE_SHARED { + existingSecret, doesSecretExist = sharedSecretMapByName[key] + } else { + existingSecret, doesSecretExist = personalSecretMapByName[key] + } + + if doesSecretExist { + // case: secret exists in project so it needs to be modified + encryptedSecretDetails := api.RawSecret{ + ID: existingSecret.ID, + SecretValue: value, + SecretKey: key, + Type: existingSecret.Type, + } + + // Only add to modifications if the value is different + if existingSecret.Value != value { + secretsToModify = append(secretsToModify, encryptedSecretDetails) + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE MODIFIED", + }) + } else { + // Current value is same as existing so no change + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET VALUE UNCHANGED", + }) + } + + } else { + // case: secret doesn't exist in project so it needs to be created + encryptedSecretDetails := api.RawSecret{ + SecretKey: key, + SecretValue: value, + Type: secretType, + } + secretsToCreate = append(secretsToCreate, encryptedSecretDetails) + secretOperations = append(secretOperations, models.SecretSetOperation{ + SecretKey: key, + SecretValue: value, + SecretOperation: "SECRET CREATED", + }) + } + } + + for _, secret := range secretsToCreate { + createSecretRequest := api.CreateRawSecretV3Request{ + SecretName: secret.SecretKey, + SecretValue: secret.SecretValue, + Type: secret.Type, + SecretPath: secretsPath, + WorkspaceID: projectId, + Environment: environmentName, + } + + err = api.CallCreateRawSecretsV3(httpClient, createSecretRequest) + if err != nil { + return nil, fmt.Errorf("unable to process new secret creations [err=%v]", err) + } + } + + for _, secret := range secretsToModify { + updateSecretRequest := api.UpdateRawSecretByNameV3Request{ + SecretName: secret.SecretKey, + SecretValue: secret.SecretValue, + SecretPath: secretsPath, + WorkspaceID: projectId, + Environment: environmentName, + Type: secret.Type, + } + + err = api.CallUpdateRawSecretsV3(httpClient, updateSecretRequest) + if err != nil { + return nil, fmt.Errorf("unable to process secret update request [err=%v]", err) + } + } + + return secretOperations, nil + +}