package util import ( "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path" "regexp" "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/go-resty/resty/v2" "github.com/rs/zerolog/log" "github.com/zalando/go-keyring" ) func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool, tagSlugs string) ([]models.SingleEnvironmentVariable, 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") } serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) httpClient := resty.New() httpClient.SetAuthToken(serviceToken). SetHeader("Accept", "application/json") serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient) if err != nil { return nil, fmt.Errorf("unable to get service token details. [err=%v]", err) } // if multiple scopes are there then user needs to specify which environment and secret path if environment == "" { if len(serviceTokenDetails.Scopes) != 1 { return nil, fmt.Errorf("you need to provide the --env for multiple environment scoped token") } else { environment = serviceTokenDetails.Scopes[0].Environment } } rawSecrets, err := api.CallGetRawSecretsV3(httpClient, api.GetRawSecretsV3Request{ WorkspaceId: serviceTokenDetails.Workspace, Environment: environment, SecretPath: secretPath, IncludeImport: includeImports, Recursive: recursive, TagSlugs: tagSlugs, }) if err != nil { return nil, err } plainTextSecrets := []models.SingleEnvironmentVariable{} for _, secret := range rawSecrets.Secrets { plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace}) } if includeImports { plainTextSecrets, err = InjectRawImportedSecret(plainTextSecrets, rawSecrets.Imports) if err != nil { return nil, err } } return plainTextSecrets, nil } func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string) (models.PlaintextSecretResult, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") getSecretsRequest := api.GetRawSecretsV3Request{ WorkspaceId: workspaceId, Environment: environmentName, IncludeImport: includeImports, Recursive: recursive, TagSlugs: tagSlugs, } if secretsPath != "" { getSecretsRequest.SecretPath = secretsPath } rawSecrets, err := api.CallGetRawSecretsV3(httpClient, getSecretsRequest) if err != nil { return models.PlaintextSecretResult{}, err } plainTextSecrets := []models.SingleEnvironmentVariable{} for _, secret := range rawSecrets.Secrets { plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace}) } if includeImports { plainTextSecrets, err = InjectRawImportedSecret(plainTextSecrets, rawSecrets.Imports) if err != nil { return models.PlaintextSecretResult{}, err } } return models.PlaintextSecretResult{ Secrets: plainTextSecrets, Etag: rawSecrets.ETag, }, nil } func GetSinglePlainTextSecretByNameV3(accessToken string, workspaceId string, environmentName string, secretsPath string, secretName string) (models.SingleEnvironmentVariable, string, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") getSecretsRequest := api.GetRawSecretV3ByNameRequest{ WorkspaceID: workspaceId, Environment: environmentName, SecretName: secretName, SecretPath: secretsPath, } rawSecret, err := api.CallFetchSingleSecretByName(httpClient, getSecretsRequest) if err != nil { return models.SingleEnvironmentVariable{}, "", err } formattedSecrets := models.SingleEnvironmentVariable{ Key: rawSecret.Secret.SecretKey, WorkspaceId: rawSecret.Secret.Workspace, Value: rawSecret.Secret.SecretValue, Type: rawSecret.Secret.Type, ID: rawSecret.Secret.ID, Comment: rawSecret.Secret.SecretComment, } return formattedSecrets, rawSecret.ETag, nil } func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string, ttl string) (models.DynamicSecretLease, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") dynamicSecretRequest := api.CreateDynamicSecretLeaseV1Request{ ProjectSlug: projectSlug, Environment: environmentName, SecretPath: secretsPath, Slug: slug, TTL: ttl, } dynamicSecret, err := api.CallCreateDynamicSecretLeaseV1(httpClient, dynamicSecretRequest) if err != nil { return models.DynamicSecretLease{}, err } return models.DynamicSecretLease{ Lease: dynamicSecret.Lease, Data: dynamicSecret.Data, DynamicSecret: dynamicSecret.DynamicSecret, }, nil } func InjectRawImportedSecret(secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedRawSecretV3) ([]models.SingleEnvironmentVariable, error) { if importedSecrets == nil { return secrets, nil } hasOverriden := make(map[string]bool) for _, sec := range secrets { hasOverriden[sec.Key] = true } for i := len(importedSecrets) - 1; i >= 0; i-- { importSec := importedSecrets[i] plainTextImportedSecrets := importSec.Secrets for _, sec := range plainTextImportedSecrets { if _, ok := hasOverriden[sec.SecretKey]; !ok { secrets = append(secrets, models.SingleEnvironmentVariable{ Key: sec.SecretKey, WorkspaceId: sec.Workspace, Value: sec.SecretValue, Type: sec.Type, ID: sec.ID, }) hasOverriden[sec.SecretKey] = true } } } return secrets, nil } func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tagSlugs string) []models.SingleEnvironmentVariable { if tagSlugs == "" { return plainTextSecrets } tagSlugsMap := make(map[string]bool) tagSlugsList := strings.Split(tagSlugs, ",") for _, slug := range tagSlugsList { tagSlugsMap[slug] = true } filteredSecrets := []models.SingleEnvironmentVariable{} for _, secret := range plainTextSecrets { for _, tag := range secret.Tags { if tagSlugsMap[tag.Slug] { filteredSecrets = append(filteredSecrets, secret) break } } } return filteredSecrets } func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectConfigFilePath string) ([]models.SingleEnvironmentVariable, error) { var secretsToReturn []models.SingleEnvironmentVariable // var serviceTokenDetails api.GetServiceTokenDetailsResponse var errorToReturn error if params.InfisicalToken == "" && params.UniversalAuthAccessToken == "" { if projectConfigFilePath == "" { RequireLocalWorkspaceFile() } else { ValidateWorkspaceFile(projectConfigFilePath) } RequireLogin() log.Debug().Msg("GetAllEnvironmentVariables: Trying to fetch secrets using logged in details") loggedInUserDetails, err := GetCurrentLoggedInUserDetails() isConnected := ValidateInfisicalAPIConnection() if isConnected { log.Debug().Msg("GetAllEnvironmentVariables: Connected to Infisical instance, checking logged in creds") } if err != nil { return nil, err } if isConnected && loggedInUserDetails.LoginExpired { PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again") } var infisicalDotJson models.WorkspaceConfigFile if projectConfigFilePath == "" { projectConfig, err := GetWorkSpaceFromFile() if err != nil { return nil, err } infisicalDotJson = projectConfig } else { projectConfig, err := GetWorkSpaceFromFilePath(projectConfigFilePath) if err != nil { return nil, err } infisicalDotJson = projectConfig } if params.WorkspaceId != "" { infisicalDotJson.WorkspaceId = params.WorkspaceId } res, err := GetPlainTextSecretsV3(loggedInUserDetails.UserCredentials.JTWToken, infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", err) if err == nil { backupEncryptionKey, err := GetBackupEncryptionKey() if err != nil { return nil, err } WriteBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey, res.Secrets) } secretsToReturn = res.Secrets errorToReturn = err // only attempt to serve cached secrets if no internet connection and if at least one secret cached if !isConnected { backupEncryptionKey, _ := GetBackupEncryptionKey() if backupEncryptionKey != nil { backedUpSecrets, err := ReadBackupSecrets(infisicalDotJson.WorkspaceId, params.Environment, params.SecretsPath, backupEncryptionKey) if len(backedUpSecrets) > 0 { PrintWarning("Unable to fetch the latest secret(s) due to connection error, serving secrets from last successful fetch. For more info, run with --debug") secretsToReturn = backedUpSecrets errorToReturn = err } } } } 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, params.Recursive, params.TagSlugs) } else if params.UniversalAuthAccessToken != "" { if params.WorkspaceId == "" { PrintErrorMessageAndExit("Project ID is required when using machine identity") } log.Debug().Msg("Trying to fetch secrets using universal auth") res, err := GetPlainTextSecretsV3(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive, params.TagSlugs) errorToReturn = err secretsToReturn = res.Secrets } } return secretsToReturn, errorToReturn } var secRefRegex = regexp.MustCompile(`\${([^\}]*)}`) func recursivelyExpandSecret(expandedSecs map[string]string, interpolatedSecs map[string]string, crossSecRefFetch func(env string, path []string, key string) string, key string) string { if v, ok := expandedSecs[key]; ok { return v } interpolatedVal, ok := interpolatedSecs[key] if !ok { HandleError(fmt.Errorf("could not find refered secret - %s", key), "Kindly check whether its provided") } refs := secRefRegex.FindAllStringSubmatch(interpolatedVal, -1) for _, val := range refs { // key: "${something}" val: [${something},something] interpolatedExp, interpolationKey := val[0], val[1] ref := strings.Split(interpolationKey, ".") // ${KEY1} => [key1] if len(ref) == 1 { val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey) interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val) continue } // cross board reference ${env.folder.key1} => [env folder key1] if len(ref) > 1 { secEnv, tmpSecPath, secKey := ref[0], ref[1:len(ref)-1], ref[len(ref)-1] interpolatedSecs[interpolationKey] = crossSecRefFetch(secEnv, tmpSecPath, secKey) // get the reference value val := recursivelyExpandSecret(expandedSecs, interpolatedSecs, crossSecRefFetch, interpolationKey) interpolatedVal = strings.ReplaceAll(interpolatedVal, interpolatedExp, val) } } expandedSecs[key] = interpolatedVal return interpolatedVal } func getSecretsByKeys(secrets []models.SingleEnvironmentVariable) map[string]models.SingleEnvironmentVariable { secretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets)) for _, secret := range secrets { secretMapByName[secret.Key] = secret } return secretMapByName } func ExpandSecrets(secrets []models.SingleEnvironmentVariable, auth models.ExpandSecretsAuthentication, projectConfigPathDir string) []models.SingleEnvironmentVariable { expandedSecs := make(map[string]string) interpolatedSecs := make(map[string]string) // map[env.secret-path][keyname]Secret crossEnvRefSecs := make(map[string]map[string]models.SingleEnvironmentVariable) // a cache to hold all cross board reference secrets for _, sec := range secrets { // get all references in a secret refs := secRefRegex.FindAllStringSubmatch(sec.Value, -1) // nil means its a secret without reference if refs == nil { expandedSecs[sec.Key] = sec.Value // atomic secrets without any interpolation } else { interpolatedSecs[sec.Key] = sec.Value } } for i, sec := range secrets { // already present pick that up if expandedVal, ok := expandedSecs[sec.Key]; ok { secrets[i].Value = expandedVal continue } expandedVal := recursivelyExpandSecret(expandedSecs, interpolatedSecs, func(env string, secPaths []string, secKey string) string { secPaths = append([]string{"/"}, secPaths...) secPath := path.Join(secPaths...) secPathDot := strings.Join(secPaths, ".") uniqKey := fmt.Sprintf("%s.%s", env, secPathDot) if crossRefSec, ok := crossEnvRefSecs[uniqKey]; !ok { var refSecs []models.SingleEnvironmentVariable var err error // if not in cross reference cache, fetch it from server if auth.InfisicalToken != "" { refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, InfisicalToken: auth.InfisicalToken, SecretsPath: secPath}, projectConfigPathDir) } else if auth.UniversalAuthAccessToken != "" { refSecs, err = GetAllEnvironmentVariables((models.GetAllSecretsParameters{Environment: env, UniversalAuthAccessToken: auth.UniversalAuthAccessToken, SecretsPath: secPath, WorkspaceId: sec.WorkspaceId}), projectConfigPathDir) } else if IsLoggedIn() { refSecs, err = GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: env, SecretsPath: secPath}, projectConfigPathDir) } else { HandleError(errors.New("no authentication provided"), "Please provide authentication to fetch secrets") } if err != nil { HandleError(err, fmt.Sprintf("Could not fetch secrets in environment: %s secret-path: %s", env, secPath), "If you are using a service token to fetch secrets, please ensure it is valid") } refSecsByKey := getSecretsByKeys(refSecs) // save it to avoid calling api again for same environment and folder path crossEnvRefSecs[uniqKey] = refSecsByKey return refSecsByKey[secKey].Value } else { return crossRefSec[secKey].Value } }, sec.Key) secrets[i].Value = expandedVal } return secrets } func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable { personalSecrets := make(map[string]models.SingleEnvironmentVariable) sharedSecrets := make(map[string]models.SingleEnvironmentVariable) secretsToReturn := []models.SingleEnvironmentVariable{} secretsToReturnMap := make(map[string]models.SingleEnvironmentVariable) for _, secret := range secrets { if secret.Type == PERSONAL_SECRET_TYPE_NAME { personalSecrets[secret.Key] = secret } if secret.Type == SHARED_SECRET_TYPE_NAME { sharedSecrets[secret.Key] = secret } } if secretType == PERSONAL_SECRET_TYPE_NAME { for _, secret := range secrets { if personalSecret, exists := personalSecrets[secret.Key]; exists { secretsToReturnMap[secret.Key] = personalSecret } else { if _, exists = secretsToReturnMap[secret.Key]; !exists { secretsToReturnMap[secret.Key] = secret } } } } else if secretType == SHARED_SECRET_TYPE_NAME { for _, secret := range secrets { if sharedSecret, exists := sharedSecrets[secret.Key]; exists { secretsToReturnMap[secret.Key] = sharedSecret } else { if _, exists := secretsToReturnMap[secret.Key]; !exists { secretsToReturnMap[secret.Key] = secret } } } } for _, secret := range secretsToReturnMap { secretsToReturn = append(secretsToReturn, secret) } return secretsToReturn } func GetBackupEncryptionKey() ([]byte, error) { encryptionKey, err := GetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY) if err != nil { if err == keyring.ErrUnsupportedPlatform { return nil, errors.New("your OS does not support keyring. Consider using a service token https://infisical.com/docs/documentation/platform/token") } else if err == keyring.ErrNotFound { // generate a new key randomizedKey := make([]byte, 16) rand.Read(randomizedKey) encryptionKey = hex.EncodeToString(randomizedKey) if err := SetValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY, encryptionKey); err != nil { return nil, err } return []byte(encryptionKey), nil } else { return nil, fmt.Errorf("something went wrong, failed to retrieve value from system keyring [error=%v]", err) } } return []byte(encryptionKey), nil } func WriteBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte, secrets []models.SingleEnvironmentVariable) error { formattedPath := strings.ReplaceAll(secretsPath, "/", "-") fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath) secrets_backup_folder_name := "secrets-backup" _, fullConfigFileDirPath, err := GetFullConfigFilePath() if err != nil { return fmt.Errorf("WriteBackupSecrets: unable to get full config folder path [err=%s]", err) } // create secrets backup directory fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) { err := os.Mkdir(fullPathToSecretsBackupFolder, os.ModePerm) if err != nil { return err } } marshaledSecrets, _ := json.Marshal(secrets) result, err := crypto.EncryptSymmetric(marshaledSecrets, encryptionKey) if err != nil { return fmt.Errorf("WriteBackupSecrets: Unable to encrypt local secret backup to file [err=%s]", err) } listOfSecretsMarshalled, _ := json.Marshal(result) err = os.WriteFile(fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName), listOfSecretsMarshalled, 0600) if err != nil { return fmt.Errorf("WriteBackupSecrets: Unable to write backup secrets to file [err=%s]", err) } return nil } func ReadBackupSecrets(workspace string, environment string, secretsPath string, encryptionKey []byte) ([]models.SingleEnvironmentVariable, error) { formattedPath := strings.ReplaceAll(secretsPath, "/", "-") fileName := fmt.Sprintf("project_secrets_%s_%s_%s.json", workspace, environment, formattedPath) secrets_backup_folder_name := "secrets-backup" _, fullConfigFileDirPath, err := GetFullConfigFilePath() if err != nil { return nil, fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err) } fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) if _, err := os.Stat(fullPathToSecretsBackupFolder); errors.Is(err, os.ErrNotExist) { return nil, nil } encryptedBackupSecretsFilePath := fmt.Sprintf("%s/%s", fullPathToSecretsBackupFolder, fileName) encryptedBackupSecretsAsBytes, err := os.ReadFile(encryptedBackupSecretsFilePath) if err != nil { return nil, err } var encryptedBackUpSecrets models.SymmetricEncryptionResult err = json.Unmarshal(encryptedBackupSecretsAsBytes, &encryptedBackUpSecrets) if err != nil { return nil, fmt.Errorf("ReadBackupSecrets: unable to parse encrypted backup secrets. The secrets backup may be malformed [err=%s]", err) } result, err := crypto.DecryptSymmetric(encryptionKey, encryptedBackUpSecrets.CipherText, encryptedBackUpSecrets.AuthTag, encryptedBackUpSecrets.Nonce) if err != nil { return nil, fmt.Errorf("ReadBackupSecrets: unable to decrypt encrypted backup secrets [err=%s]", err) } var plainTextSecrets []models.SingleEnvironmentVariable _ = json.Unmarshal(result, &plainTextSecrets) return plainTextSecrets, nil } func DeleteBackupSecrets() error { secrets_backup_folder_name := "secrets-backup" _, fullConfigFileDirPath, err := GetFullConfigFilePath() if err != nil { return fmt.Errorf("ReadBackupSecrets: unable to write config file because an error occurred when getting config file path [err=%s]", err) } fullPathToSecretsBackupFolder := fmt.Sprintf("%s/%s", fullConfigFileDirPath, secrets_backup_folder_name) DeleteValueInKeyring(INFISICAL_BACKUP_SECRET) DeleteValueInKeyring(INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY) return os.RemoveAll(fullPathToSecretsBackupFolder) } func GetEnvFromWorkspaceFile() string { workspaceFile, err := GetWorkSpaceFromFile() if err != nil { log.Debug().Msgf("getEnvFromWorkspaceFile: [err=%s]", err) return "" } if env := GetEnvelopmentBasedOnGitBranch(workspaceFile); env != "" { return env } return workspaceFile.DefaultEnvironment } func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) string { branch, err := getCurrentBranch() if err != nil { log.Debug().Msgf("getEnvelopmentBasedOnGitBranch: [err=%s]", err) } envBasedOnGitBranch, ok := workspaceFile.GitBranchToEnvironmentMapping[branch] log.Debug().Msgf("GetEnvelopmentBasedOnGitBranch: [envBasedOnGitBranch=%s] [ok=%t]", envBasedOnGitBranch, ok) if err == nil && ok { return envBasedOnGitBranch } else { log.Debug().Msgf("getEnvelopmentBasedOnGitBranch: [err=%s]", err) return "" } } func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey string, workspaceId string) ([]byte, error) { httpClient := resty.New() httpClient.SetAuthToken(authenticationToken). SetHeader("Accept", "application/json") request := api.GetEncryptedWorkspaceKeyRequest{ WorkspaceId: workspaceId, } workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request) if err != nil { return nil, fmt.Errorf("GetPlainTextWorkspaceKey: unable to retrieve your encrypted workspace key. [err=%v]", err) } encryptedWorkspaceKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey) if err != nil { return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKey [err=%v]", err) } encryptedWorkspaceKeySenderPublicKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey) if err != nil { return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKeySenderPublicKey [err=%v]", err) } encryptedWorkspaceKeyNonce, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce) if err != nil { return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKeyNonce [err=%v]", err) } currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(receiverPrivateKey) if err != nil { return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for currentUsersPrivateKey [err=%v]", err) } if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 { return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Missing credentials for generating plainTextEncryptionKey") } return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), 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 } if tokenDetails.Type == SERVICE_TOKEN_IDENTIFIER { 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 }