Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions packages/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1364,3 +1364,42 @@ func CallGetCertificateRequest(httpClient *resty.Client, certificateRequestId st

return &resBody, nil
}

func GetTagBySlug(httpClient *resty.Client, projectId string, tagSlug string) (SecretTag, error) {
var resBody GetTagBySlugResponse
response, err := httpClient.
R().
SetResult(&resBody).
SetHeader("User-Agent", USER_AGENT).
Get(fmt.Sprintf("%v/v1/projects/%s/tags/slug/%s", config.INFISICAL_URL, url.PathEscape(projectId), url.PathEscape(tagSlug)))

if err != nil {
return SecretTag{}, NewGenericRequestError("GetTagBySlug", err)
}

if response.IsError() {
return SecretTag{}, NewAPIErrorWithResponse("GetTagBySlug", response, nil)
}

return resBody.Tag, nil
}

func CreateTag(httpClient *resty.Client, projectId string, request CreateTagRequest) (SecretTag, error) {
var resBody CreateTagResponse
response, err := httpClient.
R().
SetResult(&resBody).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v1/projects/%s/tags", config.INFISICAL_URL, url.PathEscape(projectId)))

if err != nil {
return SecretTag{}, NewGenericRequestError("CreateTag", err)
}

if response.IsError() {
return SecretTag{}, NewAPIErrorWithResponse("CreateTag", response, nil)
}

return resBody.Tag, nil
}
60 changes: 41 additions & 19 deletions packages/api/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,12 @@ type Project struct {
}

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"`
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"`
TagIDs []string `json:"tagIds,omitempty"`
}

type GetEncryptedWorkspaceKeyRequest struct {
Expand Down Expand Up @@ -487,14 +488,15 @@ type CreateSecretV3Request struct {
}

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"`
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"`
TagIDs []string `json:"tagIds,omitempty"`
}

type DeleteSecretV3Request struct {
Expand All @@ -516,12 +518,13 @@ type UpdateSecretByNameV3Request struct {
}

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"`
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"`
TagIDs []string `json:"tagIds,omitempty"`
}

type GetSingleSecretByNameV3Request struct {
Expand Down Expand Up @@ -1124,3 +1127,22 @@ type GetCertificateRequestResponse struct {
CertificateID *string `json:"certificateId,omitempty"`
ErrorMessage *string `json:"errorMessage,omitempty"`
}

type SecretTag struct {
ID string `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
}

type GetTagBySlugResponse struct {
Tag SecretTag `json:"tag"`
}

type CreateTagRequest struct {
Slug string `json:"slug"`
Color string `json:"color"`
}

type CreateTagResponse struct {
Tag SecretTag `json:"tag"`
}
10 changes: 8 additions & 2 deletions packages/cmd/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ var secretsSetCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag")
}

tags, err := cmd.Flags().GetStringArray("tag")
if err != nil {
util.HandleError(err, `Unable to parse "tag" flag`)
}

processedArgs := []string{}
for _, arg := range args {
splitKeyValue := strings.SplitN(arg, "=", 2)
Expand Down Expand Up @@ -253,7 +258,7 @@ var secretsSetCmd = &cobra.Command{
util.PrintErrorMessageAndExit("When using service tokens or machine identities, you must set the --projectId flag")
}

secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file)
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, token, file, tags)

if err != nil {
util.HandleError(err, "Unable to set secrets")
Expand All @@ -280,7 +285,7 @@ var secretsSetCmd = &cobra.Command{
secretOperations, err = util.SetRawSecrets(processedArgs, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
Type: "",
Token: loggedInUserDetails.UserCredentials.JTWToken,
}, file)
}, file, tags)

if err != nil {
util.HandleError(err, "Unable to set secrets")
Expand Down Expand Up @@ -835,6 +840,7 @@ func init() {
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")
secretsSetCmd.Flags().String("file", "", "Load secrets from the specified file. File format: .env or YAML (comments: # or //). This option is mutually exclusive with command-line secrets arguments.")
secretsSetCmd.Flags().StringArray("tag", []string{}, "Tags to associate with the secret. Can be specified multiple times (e.g. --tag backend --tag production). When updating an existing secret, the provided tags will replace any existing tags")
util.AddOutputFlagsToCmd(secretsSetCmd, "The output to format the secrets in.")

secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)")
Expand Down
2 changes: 1 addition & 1 deletion packages/models/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type LoggedInUser struct {
}

type Tag struct {
ID string `json:"_id"`
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Color string `json:"color"`
Expand Down
81 changes: 68 additions & 13 deletions packages/util/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"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"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -376,16 +378,6 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo
return secretsToReturn, errorToReturn
}

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 OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType string) []models.SingleEnvironmentVariable {
personalSecrets := make(map[string]models.SingleEnvironmentVariable)
sharedSecrets := make(map[string]models.SingleEnvironmentVariable)
Expand Down Expand Up @@ -631,7 +623,7 @@ func validateSecretKey(key string) error {
return nil
}

func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string) ([]models.SecretSetOperation, error) {
func SetRawSecrets(secretArgs []string, secretType string, environmentName string, secretsPath string, projectId string, tokenDetails *models.TokenDetails, file string, tagSlugs []string) ([]models.SecretSetOperation, error) {
if file != "" {
content, err := os.ReadFile(file)
if err != nil {
Expand Down Expand Up @@ -688,6 +680,20 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
return nil, fmt.Errorf("unable to retrieve secrets [err=%v]", err)
}

uniqueSlugs := make(map[string]struct{}, len(tagSlugs))
var cliProvidedTagIds []string
for _, slug := range tagSlugs {
if _, seen := uniqueSlugs[slug]; seen {
continue
}
uniqueSlugs[slug] = struct{}{}
tag, err := GetOrCreateTag(httpClient, projectId, slug)
if err != nil {
return nil, err
}
cliProvidedTagIds = append(cliProvidedTagIds, tag.ID)
}
Comment thread
mathnogueira marked this conversation as resolved.

secretsToCreate := []api.RawSecret{}
secretsToModify := []api.RawSecret{}
secretOperations := []models.SecretSetOperation{}
Expand Down Expand Up @@ -733,15 +739,37 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
SecretValue: value,
SecretKey: key,
Type: existingSecret.Type,
TagIDs: cliProvidedTagIds,
}

existingTagIds := make(map[string]struct{}, len(existingSecret.Tags))
for _, tag := range existingSecret.Tags {
existingTagIds[tag.ID] = struct{}{}
}

tagsChanged := len(cliProvidedTagIds) > 0 && len(cliProvidedTagIds) != len(existingTagIds)
if !tagsChanged {
for _, id := range cliProvidedTagIds {
if _, found := existingTagIds[id]; !found {
tagsChanged = true
break
}
}
}

// Only add to modifications if the value is different
if existingSecret.Value != value {
if existingSecret.Value != value || tagsChanged {
secretsToModify = append(secretsToModify, encryptedSecretDetails)
message := "SECRET VALUE MODIFIED"
if existingSecret.Value == value {
// We only display SECRET TAGS UPDATED if the value has not changed
// otherwise value changes should take precedence
message = "SECRET TAGS MODIFIED"
}
secretOperations = append(secretOperations, models.SecretSetOperation{
SecretKey: key,
SecretValue: value,
SecretOperation: "SECRET VALUE MODIFIED",
SecretOperation: message,
})
} else {
// Current value is same as existing so no change
Expand All @@ -758,6 +786,7 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
SecretKey: key,
SecretValue: value,
Type: secretType,
TagIDs: cliProvidedTagIds,
}
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
secretOperations = append(secretOperations, models.SecretSetOperation{
Expand All @@ -776,6 +805,7 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
SecretPath: secretsPath,
WorkspaceID: projectId,
Environment: environmentName,
TagIDs: secret.TagIDs,
}

err = api.CallCreateRawSecretsV3(httpClient, createSecretRequest)
Expand All @@ -792,6 +822,7 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
WorkspaceID: projectId,
Environment: environmentName,
Type: secret.Type,
TagIDs: secret.TagIDs,
}

err = api.CallUpdateRawSecretsV3(httpClient, updateSecretRequest)
Expand All @@ -803,3 +834,27 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
return secretOperations, nil

}

func GetOrCreateTag(client *resty.Client, projectId string, slug string) (api.SecretTag, error) {
tag, err := api.GetTagBySlug(client, projectId, slug)
if err == nil {
return tag, nil
}

var apiErr *api.APIError
if errors.As(err, &apiErr) {
if apiErr.StatusCode == http.StatusNotFound {
newTag, createErr := api.CreateTag(client, projectId, api.CreateTagRequest{
Slug: slug,
Color: "",
})
if createErr != nil {
return api.SecretTag{}, fmt.Errorf("could not create tag %q: [err=%v]", slug, createErr)
}

return newTag, nil
}
}

return api.SecretTag{}, fmt.Errorf("unable to resolve tag slug %q [err=%v]", slug, err)
}
Comment on lines 836 to +860
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent tag auto-creation on slug mismatch

GetOrCreateTag silently creates a new project tag whenever the given slug is not found. A single character typo (e.g. --tag producton) will permanently add a new tag to the project with an empty color string, with no warning to the user. Consider logging an informational message when a tag is created, or returning an error and asking the user to use infisical tags create explicitly.

Loading