From 64323ca64c62b16b288e800e080c8f3b4cd2e849 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard <daniel@infisical.com>
Date: Sat, 2 Nov 2024 04:24:00 +0400
Subject: [PATCH] feat: list secrets with etag support

---
 packages/api/secrets/list_secrets.go | 58 +++++++++++++++++++++++++++-
 packages/api/secrets/models.go       | 19 +++++++++
 packages/models/secrets.go           |  6 +++
 secrets.go                           | 47 ++++++++++++++++++++++
 4 files changed, 129 insertions(+), 1 deletion(-)

diff --git a/packages/api/secrets/list_secrets.go b/packages/api/secrets/list_secrets.go
index d6aa98e..e7def70 100644
--- a/packages/api/secrets/list_secrets.go
+++ b/packages/api/secrets/list_secrets.go
@@ -2,15 +2,71 @@ package api
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/go-resty/resty/v2"
 	"github.com/infisical/go-sdk/packages/errors"
 )
 
 const callListSecretsV3RawOperation = "CallListSecretsV3Raw"
+const callListSecretsWithETagV3RawOperation = "CallListSecretsWithETagV3Raw"
 
-func CallListSecretsV3(httpClient *resty.Client, request ListSecretsV3RawRequest) (ListSecretsV3RawResponse, error) {
+func CallListSecretsWithETagV3(httpClient *resty.Client, request ListSecretsV3RawWithETagRequest) (response ListSecretsV3RawResponse, serverETag string, isModified bool, err error) {
+
+	secretsResponse := ListSecretsV3RawResponse{}
+
+	if request.SecretPath == "" {
+		request.SecretPath = "/"
+	}
+
+	if request.CurrentETag != "" {
+
+		isWeakETag := strings.HasPrefix(request.CurrentETag, "W/")
+		if isWeakETag {
+			request.CurrentETag = strings.TrimPrefix(request.CurrentETag, "W/")
+		}
+
+		request.CurrentETag = strings.TrimPrefix(request.CurrentETag, "\"")
+		request.CurrentETag = strings.TrimSuffix(request.CurrentETag, "\"")
+		request.CurrentETag = fmt.Sprintf("\"%s\"", request.CurrentETag)
+
+		if isWeakETag {
+			request.CurrentETag = fmt.Sprintf("W/%s", request.CurrentETag)
+		}
+	}
+
+	fmt.Printf("ETAG: %s\n", request.CurrentETag)
+
+	res, err := httpClient.R().
+		SetResult(&secretsResponse).
+		SetHeader("if-none-match", request.CurrentETag).
+		SetQueryParams(map[string]string{
+			"workspaceId":            request.ProjectID,
+			"workspaceSlug":          request.ProjectSlug,
+			"environment":            request.Environment,
+			"secretPath":             request.SecretPath,
+			"expandSecretReferences": fmt.Sprintf("%t", request.ExpandSecretReferences),
+			"include_imports":        fmt.Sprintf("%t", request.IncludeImports),
+			"recursive":              fmt.Sprintf("%t", request.Recursive),
+		}).Get("/v3/secrets/raw")
+
+	if err != nil {
+		return ListSecretsV3RawResponse{}, "", false, errors.NewRequestError(callListSecretsWithETagV3RawOperation, err)
+	}
 
+	if res.IsError() {
+		return ListSecretsV3RawResponse{}, "", false, errors.NewAPIErrorWithResponse(callListSecretsWithETagV3RawOperation, res)
+	}
+
+	var modified = true
+	if res.StatusCode() == 304 || (res.Header().Get("etag") == request.CurrentETag && request.CurrentETag != "") {
+		modified = false
+	}
+
+	return secretsResponse, res.Header().Get("etag"), modified, nil
+}
+
+func CallListSecretsV3(httpClient *resty.Client, request ListSecretsV3RawRequest) (ListSecretsV3RawResponse, error) {
 	secretsResponse := ListSecretsV3RawResponse{}
 
 	if request.SecretPath == "" {
diff --git a/packages/api/secrets/models.go b/packages/api/secrets/models.go
index 6c257b9..b381f0e 100644
--- a/packages/api/secrets/models.go
+++ b/packages/api/secrets/models.go
@@ -16,6 +16,19 @@ type ListSecretsV3RawRequest struct {
 	SecretPath             string `json:"secretPath,omitempty"`
 }
 
+type ListSecretsV3RawWithETagRequest struct {
+	AttachToProcessEnv bool   `json:"-"`
+	CurrentETag        string `json:"-"`
+	// ProjectId and ProjectSlug are used to fetch secrets from the project. Only one of them is required.
+	ProjectID              string `json:"workspaceId,omitempty"`
+	ProjectSlug            string `json:"workspaceSlug,omitempty"`
+	Environment            string `json:"environment"`
+	ExpandSecretReferences bool   `json:"expandSecretReferences"`
+	IncludeImports         bool   `json:"include_imports"`
+	Recursive              bool   `json:"recursive"`
+	SecretPath             string `json:"secretPath,omitempty"`
+}
+
 type ListSecretsV3RawResponse struct {
 	Secrets []models.Secret       `json:"secrets"`
 	Imports []models.SecretImport `json:"imports"`
@@ -84,3 +97,9 @@ type DeleteSecretV3RawRequest struct {
 type DeleteSecretV3RawResponse struct {
 	Secret models.Secret `json:"secret"`
 }
+
+type ListSecretsWithETagResponse struct {
+	Secrets    []models.Secret
+	ETag       string
+	IsModified bool
+}
diff --git a/packages/models/secrets.go b/packages/models/secrets.go
index ee7228c..6a9a821 100644
--- a/packages/models/secrets.go
+++ b/packages/models/secrets.go
@@ -12,6 +12,12 @@ type Secret struct {
 	SecretPath    string `json:"secretPath,omitempty"`
 }
 
+type ListSecretsWithETagResult struct {
+	Secrets    []Secret
+	ETag       string
+	IsModified bool
+}
+
 type SecretImport struct {
 	SecretPath  string   `json:"secretPath"`
 	Environment string   `json:"environment"`
diff --git a/secrets.go b/secrets.go
index 372dfbd..8936baa 100644
--- a/secrets.go
+++ b/secrets.go
@@ -9,6 +9,7 @@ import (
 )
 
 type ListSecretsOptions = api.ListSecretsV3RawRequest
+type ListSecretsWithETagOptions = api.ListSecretsV3RawWithETagRequest
 type RetrieveSecretOptions = api.RetrieveSecretV3RawRequest
 type UpdateSecretOptions = api.UpdateSecretV3RawRequest
 type CreateSecretOptions = api.CreateSecretV3RawRequest
@@ -16,6 +17,7 @@ type DeleteSecretOptions = api.DeleteSecretV3RawRequest
 
 type SecretsInterface interface {
 	List(options ListSecretsOptions) ([]models.Secret, error)
+	ListWithETag(options ListSecretsWithETagOptions) (models.ListSecretsWithETagResult, error)
 	Retrieve(options RetrieveSecretOptions) (models.Secret, error)
 	Update(options UpdateSecretOptions) (models.Secret, error)
 	Create(options CreateSecretOptions) (models.Secret, error)
@@ -64,6 +66,51 @@ func (s *Secrets) List(options ListSecretsOptions) ([]models.Secret, error) {
 	return util.SortSecretsByKeys(secrets), nil
 }
 
+func (s *Secrets) ListWithETag(options ListSecretsWithETagOptions) (models.ListSecretsWithETagResult, error) {
+	res, etag, isModified, err := api.CallListSecretsWithETagV3(s.client.httpClient, options)
+
+	if err != nil {
+		return models.ListSecretsWithETagResult{}, err
+	}
+
+	if options.Recursive {
+		util.EnsureUniqueSecretsByKey(&res.Secrets)
+	}
+
+	secrets := append([]models.Secret(nil), res.Secrets...) // Clone main secrets slice, we will modify this if imports are enabled
+	if options.IncludeImports {
+
+		// Append secrets from imports
+		for _, importBlock := range res.Imports {
+			for _, importSecret := range importBlock.Secrets {
+				// Only append the secret if it is not already in the list, imports take precedence
+				if !util.ContainsSecret(secrets, importSecret.SecretKey) {
+					secrets = append(secrets, importSecret)
+				}
+			}
+		}
+	}
+
+	if options.AttachToProcessEnv {
+		for _, secret := range secrets {
+			// Only set the environment variable if it is not already set
+			if os.Getenv(secret.SecretKey) == "" {
+				os.Setenv(secret.SecretKey, secret.SecretValue)
+			}
+		}
+
+	}
+
+	sortedSecrets := util.SortSecretsByKeys(secrets)
+
+	return models.ListSecretsWithETagResult{
+		Secrets:    sortedSecrets,
+		ETag:       etag,
+		IsModified: isModified,
+	}, nil
+
+}
+
 func (s *Secrets) Retrieve(options RetrieveSecretOptions) (models.Secret, error) {
 	res, err := api.CallRetrieveSecretV3(s.client.httpClient, options)