VAULT-19233 First part of caching static secrets work (#23054)

* VAULT-19233 First part of caching static secrets work

* VAULT-19233 update godoc

* VAULT-19233 invalidate cache on non-GET

* VAULT-19233 add locking to proxy cache writes

* VAULT-19233 update locking, future-proof

* VAULT-19233 fix mutex

* VAULT-19233 Use ParseSecret
This commit is contained in:
Violet Hynes
2023-09-22 10:57:38 -04:00
committed by GitHub
parent c93137d9a3
commit 54c84decfd
7 changed files with 525 additions and 55 deletions

View File

@@ -39,6 +39,9 @@ const (
// TokenType - Bucket/type for auto-auth tokens
TokenType = "token"
// StaticSecretType - Bucket/type for static secrets
StaticSecretType = "static-secret"
// LeaseType - v2 Bucket/type for auth AND secret leases.
//
// This bucket stores keys in the same order they were created using

View File

@@ -7,6 +7,7 @@ import (
"context"
"encoding/json"
"net/http"
"sync"
"time"
)
@@ -22,6 +23,12 @@ type Index struct {
// Required: true, Unique: true
Token string
// Tokens is a list of tokens that can access this cached response,
// which is used for static secret caching, and enabling multiple
// tokens to be able to access the same cache entry for static secrets.
// Required: false, Unique: false
Tokens []string
// TokenParent is the parent token of the token held by this index
// Required: false, Unique: false
TokenParent string
@@ -71,6 +78,10 @@ type Index struct {
// Type is the index type (token, auth-lease, secret-lease)
Type string
// IndexLock is a lock held for some indexes to prevent data
// races upon update.
IndexLock sync.Mutex
}
type IndexName uint32

View File

@@ -17,6 +17,7 @@ func TestSerializeDeserialize(t *testing.T) {
testIndex := &Index{
ID: "testid",
Token: "testtoken",
Tokens: []string{"token1", "token2"},
TokenParent: "parent token",
TokenAccessor: "test accessor",
Namespace: "test namespace",

View File

@@ -12,9 +12,9 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"slices"
"strings"
"sync"
"time"
@@ -99,16 +99,21 @@ type LeaseCache struct {
// shuttingDown is used to determine if cache needs to be evicted or not
// when the context is cancelled
shuttingDown atomic.Bool
// cacheStaticSecrets is used to determine if the cache should also
// cache static secrets, as well as dynamic secrets.
cacheStaticSecrets bool
}
// LeaseCacheConfig is the configuration for initializing a new
// Lease.
// LeaseCache.
type LeaseCacheConfig struct {
Client *api.Client
BaseContext context.Context
Proxier Proxier
Logger hclog.Logger
Storage *cacheboltdb.BoltStorage
Client *api.Client
BaseContext context.Context
Proxier Proxier
Logger hclog.Logger
Storage *cacheboltdb.BoltStorage
CacheStaticSecrets bool
}
type inflightRequest struct {
@@ -151,15 +156,16 @@ func NewLeaseCache(conf *LeaseCacheConfig) (*LeaseCache, error) {
baseCtxInfo := cachememdb.NewContextInfo(conf.BaseContext)
return &LeaseCache{
client: conf.Client,
proxier: conf.Proxier,
logger: conf.Logger,
db: db,
baseCtxInfo: baseCtxInfo,
l: &sync.RWMutex{},
idLocks: locksutil.CreateLocks(),
inflightCache: gocache.New(gocache.NoExpiration, gocache.NoExpiration),
ps: conf.Storage,
client: conf.Client,
proxier: conf.Proxier,
logger: conf.Logger,
db: db,
baseCtxInfo: baseCtxInfo,
l: &sync.RWMutex{},
idLocks: locksutil.CreateLocks(),
inflightCache: gocache.New(gocache.NoExpiration, gocache.NoExpiration),
ps: conf.Storage,
cacheStaticSecrets: conf.CacheStaticSecrets,
}, nil
}
@@ -180,9 +186,43 @@ func (c *LeaseCache) PersistentStorage() *cacheboltdb.BoltStorage {
return c.ps
}
// checkCacheForDynamicSecretRequest checks the cache for a particular request based on its
// computed ID. It returns a non-nil *SendResponse if an entry is found.
func (c *LeaseCache) checkCacheForDynamicSecretRequest(id string) (*SendResponse, error) {
return c.checkCacheForRequest(id, nil)
}
// checkCacheForStaticSecretRequest checks the cache for a particular request based on its
// computed ID. It returns a non-nil *SendResponse if an entry is found.
// If a request is provided, it will validate that the token is allowed to retrieve this
// cache entry, and return nil if it isn't. It will also evict the cache if this is a non-GET
// request.
func (c *LeaseCache) checkCacheForStaticSecretRequest(id string, req *SendRequest) (*SendResponse, error) {
return c.checkCacheForRequest(id, req)
}
// checkCacheForRequest checks the cache for a particular request based on its
// computed ID. It returns a non-nil *SendResponse if an entry is found.
func (c *LeaseCache) checkCacheForRequest(id string) (*SendResponse, error) {
// computed ID. It returns a non-nil *SendResponse if an entry is found.
// If a token is provided, it will validate that the token is allowed to retrieve this
// cache entry, and return nil if it isn't.
func (c *LeaseCache) checkCacheForRequest(id string, req *SendRequest) (*SendResponse, error) {
var token string
if req != nil {
token = req.Token
// HEAD and OPTIONS are included as future-proofing, since neither of those modify the resource either.
if req.Request.Method != http.MethodGet && req.Request.Method != http.MethodHead && req.Request.Method != http.MethodOptions {
// This must be an update to the resource, so we should short-circuit and invalidate the cache
// as we know the cache is now stale.
c.logger.Debug("evicting index from cache, as non-GET received", "id", id, "method", req.Request.Method, "path", req.Request.URL.Path)
err := c.db.Evict(cachememdb.IndexNameID, id)
if err != nil {
return nil, err
}
return nil, nil
}
}
index, err := c.db.Get(cachememdb.IndexNameID, id)
if err != nil {
return nil, err
@@ -192,6 +232,16 @@ func (c *LeaseCache) checkCacheForRequest(id string) (*SendResponse, error) {
return nil, nil
}
if token != "" {
// This is a static secret check. We need to ensure that this token
// has previously demonstrated access to this static secret.
if !slices.Contains(index.Tokens, token) {
// We don't have access to this static secret, so
// we do not return the cached response.
return nil, nil
}
}
// Cached request is found, deserialize the response
reader := bufio.NewReader(bytes.NewReader(index.Response))
resp, err := http.ReadResponse(reader, nil)
@@ -221,36 +271,42 @@ func (c *LeaseCache) checkCacheForRequest(id string) (*SendResponse, error) {
// it will return the cached response, otherwise it will delegate to the
// underlying Proxier and cache the received response.
func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse, error) {
// Compute the index ID
id, err := computeIndexID(req)
// Compute the index ID for both static and dynamic secrets.
// The primary difference is that for dynamic secrets, the
// Vault token forms part of the index.
dynamicSecretCacheId, err := computeIndexID(req)
if err != nil {
c.logger.Error("failed to compute cache key", "error", err)
return nil, err
}
staticSecretCacheId := computeStaticSecretCacheIndex(req)
// Check the inflight cache to see if there are other inflight requests
// of the same kind, based on the computed ID. If so, we increment a counter
// Note: we lock both the dynamic secret cache ID and the static secret cache ID
// as at this stage, we don't know what kind of secret it is.
var inflight *inflightRequest
defer func() {
// Cleanup on the cache if there are no remaining inflight requests.
// This is the last step, so we defer the call first
if inflight != nil && inflight.remaining.Load() == 0 {
c.inflightCache.Delete(id)
c.inflightCache.Delete(dynamicSecretCacheId)
c.inflightCache.Delete(staticSecretCacheId)
}
}()
idLock := locksutil.LockForKey(c.idLocks, id)
idLockDynamicSecret := locksutil.LockForKey(c.idLocks, dynamicSecretCacheId)
// Briefly grab an ID-based lock in here to emulate a load-or-store behavior
// and prevent concurrent cacheable requests from being proxied twice if
// they both miss the cache due to it being clean when peeking the cache
// entry.
idLock.Lock()
inflightRaw, found := c.inflightCache.Get(id)
idLockDynamicSecret.Lock()
inflightRaw, found := c.inflightCache.Get(dynamicSecretCacheId)
if found {
idLock.Unlock()
idLockDynamicSecret.Unlock()
inflight = inflightRaw.(*inflightRequest)
inflight.remaining.Inc()
defer inflight.remaining.Dec()
@@ -263,19 +319,52 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
case <-inflight.ch:
}
} else {
inflight = newInflightRequest()
if inflight == nil {
inflight = newInflightRequest()
inflight.remaining.Inc()
defer inflight.remaining.Dec()
defer close(inflight.ch)
}
c.inflightCache.Set(dynamicSecretCacheId, inflight, gocache.NoExpiration)
idLockDynamicSecret.Unlock()
}
idLockStaticSecret := locksutil.LockForKey(c.idLocks, staticSecretCacheId)
// Briefly grab an ID-based lock in here to emulate a load-or-store behavior
// and prevent concurrent cacheable requests from being proxied twice if
// they both miss the cache due to it being clean when peeking the cache
// entry.
idLockStaticSecret.Lock()
inflightRaw, found = c.inflightCache.Get(staticSecretCacheId)
if found {
idLockStaticSecret.Unlock()
inflight = inflightRaw.(*inflightRequest)
inflight.remaining.Inc()
defer inflight.remaining.Dec()
c.inflightCache.Set(id, inflight, gocache.NoExpiration)
idLock.Unlock()
// If found it means that there's an inflight request being processed.
// We wait until that's finished before proceeding further.
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-inflight.ch:
}
} else {
if inflight == nil {
inflight = newInflightRequest()
inflight.remaining.Inc()
defer inflight.remaining.Dec()
defer close(inflight.ch)
}
// Signal that the processing request is done
defer close(inflight.ch)
c.inflightCache.Set(staticSecretCacheId, inflight, gocache.NoExpiration)
idLockStaticSecret.Unlock()
}
// Check if the response for this request is already in the cache
cachedResp, err := c.checkCacheForRequest(id)
// Check if the response for this request is already in the dynamic secret cache
cachedResp, err := c.checkCacheForDynamicSecretRequest(dynamicSecretCacheId)
if err != nil {
return nil, err
}
@@ -284,6 +373,16 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
return cachedResp, nil
}
// Check if the response for this request is already in the static secret cache
cachedResp, err = c.checkCacheForStaticSecretRequest(staticSecretCacheId, req)
if err != nil {
return nil, err
}
if cachedResp != nil {
c.logger.Debug("returning cached response", "id", staticSecretCacheId, "path", req.Request.URL.Path)
return cachedResp, nil
}
c.logger.Debug("forwarding request from cache", "method", req.Request.Method, "path", req.Request.URL.Path)
// Pass the request down and get a response
@@ -308,7 +407,6 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
// Build the index to cache based on the response received
index := &cachememdb.Index{
ID: id,
Namespace: namespace,
RequestPath: req.Request.URL.Path,
LastRenewed: time.Now().UTC(),
@@ -337,6 +435,20 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
return resp, nil
}
// TODO: if secret.MountType == "kvv1" || secret.MountType == "kvv2"
if c.cacheStaticSecrets && secret != nil {
index.Type = cacheboltdb.StaticSecretType
index.ID = staticSecretCacheId
err := c.cacheStaticSecret(ctx, req, resp, index)
if err != nil {
return nil, err
}
return resp, nil
} else {
// Since it's not a static secret, set the ID to be the dynamic id
index.ID = dynamicSecretCacheId
}
// Short-circuit if the secret is not renewable
tokenRenewable, err := secret.TokenIsRenewable()
if err != nil {
@@ -420,7 +532,7 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
if resp.Response.Body != nil {
resp.Response.Body.Close()
}
resp.Response.Body = ioutil.NopCloser(bytes.NewReader(resp.ResponseBody))
resp.Response.Body = io.NopCloser(bytes.NewReader(resp.ResponseBody))
// Set the index's Response
index.Response = respBytes.Bytes()
@@ -440,20 +552,86 @@ func (c *LeaseCache) Send(ctx context.Context, req *SendRequest) (*SendResponse,
index.RequestToken = req.Token
index.RequestHeader = req.Request.Header
// Store the index in the cache
c.logger.Debug("storing response into the cache", "method", req.Request.Method, "path", req.Request.URL.Path)
err = c.Set(ctx, index)
if err != nil {
c.logger.Error("failed to cache the proxied response", "error", err)
return nil, err
if index.Type != cacheboltdb.StaticSecretType {
// Store the index in the cache
c.logger.Debug("storing response into the cache", "method", req.Request.Method, "path", req.Request.URL.Path)
err = c.Set(ctx, index)
if err != nil {
c.logger.Error("failed to cache the proxied response", "error", err)
return nil, err
}
// Start renewing the secret in the response
go c.startRenewing(renewCtx, index, req, secret)
}
// Start renewing the secret in the response
go c.startRenewing(renewCtx, index, req, secret)
return resp, nil
}
func (c *LeaseCache) cacheStaticSecret(ctx context.Context, req *SendRequest, resp *SendResponse, index *cachememdb.Index) error {
// If a cached version of this secret exists, we now have access, so
// we don't need to re-cache, just update index.Tokens
indexFromCache, err := c.db.Get(cachememdb.IndexNameID, index.ID)
if err != nil {
return err
}
// We must hold a lock for the index while it's being updated.
// We keep the two locking mechanisms distinct, so that it's only writes
// that have to be serial.
index.IndexLock.Lock()
defer index.IndexLock.Unlock()
// The index already exists, so all we need to do is add our token
// to the index's allowed token list, then re-store it
if indexFromCache != nil {
indexFromCache.Tokens = append(indexFromCache.Tokens, req.Token)
return c.storeStaticSecretIndex(ctx, req, indexFromCache)
}
// Serialize the response to store it in the cached index
var respBytes bytes.Buffer
err = resp.Response.Write(&respBytes)
if err != nil {
c.logger.Error("failed to serialize response", "error", err)
return err
}
// Reset the response body for upper layers to read
if resp.Response.Body != nil {
resp.Response.Body.Close()
}
resp.Response.Body = io.NopCloser(bytes.NewReader(resp.ResponseBody))
// Set the index's Response
index.Response = respBytes.Bytes()
// Set the index's tokens
index.Tokens = []string{req.Token}
// Set the index type
index.Type = cacheboltdb.StaticSecretType
return c.storeStaticSecretIndex(ctx, req, index)
}
func (c *LeaseCache) storeStaticSecretIndex(ctx context.Context, req *SendRequest, index *cachememdb.Index) error {
// Store the index in the cache
c.logger.Debug("storing response into the cache", "method", req.Request.Method, "path", req.Request.URL.Path)
err := c.Set(ctx, index)
if err != nil {
c.logger.Error("failed to cache the proxied response", "error", err)
return err
}
// TODO: We need to also update the cache for the token's permission capabilities.
// TODO: for this we'll need: req.Token, req.URL.Path
// TODO: we need to build a NEW index, with a hash of the token as the ID
return nil
}
func (c *LeaseCache) createCtxInfo(ctx context.Context) *cachememdb.ContextInfo {
if ctx == nil {
c.l.RLock()
@@ -575,7 +753,7 @@ func computeIndexID(req *SendRequest) (string, error) {
}
// Reset the request body after it has been closed by Write
req.Request.Body = ioutil.NopCloser(bytes.NewReader(req.RequestBody))
req.Request.Body = io.NopCloser(bytes.NewReader(req.RequestBody))
// Append req.Token into the byte slice. This is needed since auto-auth'ed
// requests sets the token directly into SendRequest.Token
@@ -586,6 +764,14 @@ func computeIndexID(req *SendRequest) (string, error) {
return hex.EncodeToString(cryptoutil.Blake2b256Hash(string(b.Bytes()))), nil
}
// computeStaticSecretCacheIndex results in a value that uniquely identifies a static
// secret's cached ID. Notably, we intentionally ignore headers (for example,
// the X-Vault-Token header) to remain agnostic to which token is being
// used in the request. We care only about the path.
func computeStaticSecretCacheIndex(req *SendRequest) string {
return hex.EncodeToString(cryptoutil.Blake2b256Hash(req.Request.URL.Path))
}
// HandleCacheClear returns a handlerFunc that can perform cache clearing operations.
func (c *LeaseCache) HandleCacheClear(ctx context.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -662,7 +848,11 @@ func (c *LeaseCache) handleCacheClear(ctx context.Context, in *cacheClearInput)
return err
}
for _, index := range indexes {
index.RenewCtxInfo.CancelFunc()
if index.RenewCtxInfo != nil {
if index.RenewCtxInfo.CancelFunc != nil {
index.RenewCtxInfo.CancelFunc()
}
}
}
case "token":
@@ -684,7 +874,7 @@ func (c *LeaseCache) handleCacheClear(ctx context.Context, in *cacheClearInput)
index.RenewCtxInfo.CancelFunc()
case "token_accessor":
if in.TokenAccessor == "" {
if in.TokenAccessor == "" && in.Type != cacheboltdb.StaticSecretType {
return errors.New("token accessor not provided")
}
@@ -1123,7 +1313,9 @@ func (c *LeaseCache) restoreLeaseRenewCtx(index *cachememdb.Index) error {
}
renewCtxInfo = c.createCtxInfo(parentCtx)
default:
return fmt.Errorf("unknown cached index item: %s", index.ID)
// This isn't a renewable cache entry, i.e. a static secret cache entry.
// We return, because there's nothing to do.
return nil
}
renewCtx := context.WithValue(renewCtxInfo.Ctx, contextIndexID, index.ID)

View File

@@ -440,10 +440,11 @@ func (c *ProxyCommand) Run(args []string) int {
// Create the lease cache proxier and set its underlying proxier to
// the API proxier.
leaseCache, err = cache.NewLeaseCache(&cache.LeaseCacheConfig{
Client: proxyClient,
BaseContext: ctx,
Proxier: apiProxy,
Logger: cacheLogger.Named("leasecache"),
Client: proxyClient,
BaseContext: ctx,
Proxier: apiProxy,
Logger: cacheLogger.Named("leasecache"),
CacheStaticSecrets: config.Cache.CacheStaticSecrets,
})
if err != nil {
c.UI.Error(fmt.Sprintf("Error creating lease cache: %v", err))

View File

@@ -101,8 +101,9 @@ type APIProxy struct {
// Cache contains any configuration needed for Cache mode
type Cache struct {
Persist *agentproxyshared.PersistConfig `hcl:"persist"`
InProcDialer transportDialer `hcl:"-"`
Persist *agentproxyshared.PersistConfig `hcl:"persist"`
InProcDialer transportDialer `hcl:"-"`
CacheStaticSecrets bool `hcl:"cache_static_secrets"`
}
// AutoAuth is the configured authentication method and sinks

View File

@@ -4,6 +4,7 @@
package command
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
@@ -578,8 +579,8 @@ vault {
wg.Wait()
}
// TestProxy_Cache_DynamicSecret Tests that the cache successfully caches a dynamic secret
// going through the Proxy,
// TestProxy_Cache_DynamicSecret tests that the cache successfully caches a dynamic secret
// going through the Proxy, and that a subsequent request will be served from the cache.
func TestProxy_Cache_DynamicSecret(t *testing.T) {
logger := logging.NewVaultLogger(hclog.Trace)
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
@@ -685,6 +686,266 @@ vault {
wg.Wait()
}
// TestProxy_Cache_StaticSecret Tests that the cache successfully caches a static secret
// going through the Proxy,
func TestProxy_Cache_StaticSecret(t *testing.T) {
logger := logging.NewVaultLogger(hclog.Trace)
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
serverClient := cluster.Cores[0].Client
// Unset the environment variable so that proxy picks up the right test
// cluster address
defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress))
os.Unsetenv(api.EnvVaultAddress)
cacheConfig := `
cache {
cache_static_secrets = true
}
`
listenAddr := generateListenerAddress(t)
listenConfig := fmt.Sprintf(`
listener "tcp" {
address = "%s"
tls_disable = true
}
`, listenAddr)
config := fmt.Sprintf(`
vault {
address = "%s"
tls_skip_verify = true
}
%s
%s
log_level = "trace"
`, serverClient.Address(), cacheConfig, listenConfig)
configPath := makeTempFile(t, "config.hcl", config)
defer os.Remove(configPath)
// Start proxy
_, cmd := testProxyCommand(t, logger)
cmd.startedCh = make(chan struct{})
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
cmd.Run([]string{"-config", configPath})
wg.Done()
}()
select {
case <-cmd.startedCh:
case <-time.After(5 * time.Second):
t.Errorf("timeout")
}
proxyClient, err := api.NewClient(api.DefaultConfig())
if err != nil {
t.Fatal(err)
}
proxyClient.SetToken(serverClient.Token())
proxyClient.SetMaxRetries(0)
err = proxyClient.SetAddress("http://" + listenAddr)
if err != nil {
t.Fatal(err)
}
secretData := map[string]interface{}{
"foo": "bar",
}
// Create kvv1 secret
err = serverClient.KVv1("secret").Put(context.Background(), "my-secret", secretData)
if err != nil {
t.Fatal(err)
}
// We use raw requests so we can check the headers for cache hit/miss.
// We expect the first to miss, and the second to hit.
req := proxyClient.NewRequest(http.MethodGet, "/v1/secret/my-secret")
resp1, err := proxyClient.RawRequest(req)
if err != nil {
t.Fatal(err)
}
cacheValue := resp1.Header.Get("X-Cache")
require.Equal(t, "MISS", cacheValue)
req = proxyClient.NewRequest(http.MethodGet, "/v1/secret/my-secret")
resp2, err := proxyClient.RawRequest(req)
if err != nil {
t.Fatal(err)
}
cacheValue = resp2.Header.Get("X-Cache")
require.Equal(t, "HIT", cacheValue)
// Lastly, we check to make sure the actual data we received is
// as we expect. We must use ParseSecret due to the raw requests.
secret1, err := api.ParseSecret(resp1.Body)
if err != nil {
t.Fatal(err)
}
require.Equal(t, secretData, secret1.Data)
secret2, err := api.ParseSecret(resp2.Body)
if err != nil {
t.Fatal(err)
}
require.Equal(t, secret1.Data, secret2.Data)
close(cmd.ShutdownCh)
wg.Wait()
}
// TestProxy_Cache_StaticSecretInvalidation Tests that the cache successfully caches a static secret
// going through the Proxy, and that it gets invalidated by a POST.
func TestProxy_Cache_StaticSecretInvalidation(t *testing.T) {
logger := logging.NewVaultLogger(hclog.Trace)
cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
serverClient := cluster.Cores[0].Client
// Unset the environment variable so that proxy picks up the right test
// cluster address
defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress))
os.Unsetenv(api.EnvVaultAddress)
cacheConfig := `
cache {
cache_static_secrets = true
}
`
listenAddr := generateListenerAddress(t)
listenConfig := fmt.Sprintf(`
listener "tcp" {
address = "%s"
tls_disable = true
}
`, listenAddr)
config := fmt.Sprintf(`
vault {
address = "%s"
tls_skip_verify = true
}
%s
%s
log_level = "trace"
`, serverClient.Address(), cacheConfig, listenConfig)
configPath := makeTempFile(t, "config.hcl", config)
defer os.Remove(configPath)
// Start proxy
_, cmd := testProxyCommand(t, logger)
cmd.startedCh = make(chan struct{})
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
cmd.Run([]string{"-config", configPath})
wg.Done()
}()
select {
case <-cmd.startedCh:
case <-time.After(5 * time.Second):
t.Errorf("timeout")
}
proxyClient, err := api.NewClient(api.DefaultConfig())
if err != nil {
t.Fatal(err)
}
proxyClient.SetToken(serverClient.Token())
proxyClient.SetMaxRetries(0)
err = proxyClient.SetAddress("http://" + listenAddr)
if err != nil {
t.Fatal(err)
}
secretData := map[string]interface{}{
"foo": "bar",
}
secretData2 := map[string]interface{}{
"bar": "baz",
}
// Create kvv1 secret
err = serverClient.KVv1("secret").Put(context.Background(), "my-secret", secretData)
if err != nil {
t.Fatal(err)
}
// We use raw requests so we can check the headers for cache hit/miss.
req := proxyClient.NewRequest(http.MethodGet, "/v1/secret/my-secret")
resp1, err := proxyClient.RawRequest(req)
if err != nil {
t.Fatal(err)
}
cacheValue := resp1.Header.Get("X-Cache")
require.Equal(t, "MISS", cacheValue)
// Update the secret using the proxy client
err = proxyClient.KVv1("secret").Put(context.Background(), "my-secret", secretData2)
if err != nil {
t.Fatal(err)
}
resp2, err := proxyClient.RawRequest(req)
if err != nil {
t.Fatal(err)
}
cacheValue = resp2.Header.Get("X-Cache")
// This should miss too, as we just updated it
require.Equal(t, "MISS", cacheValue)
resp3, err := proxyClient.RawRequest(req)
if err != nil {
t.Fatal(err)
}
cacheValue = resp3.Header.Get("X-Cache")
// This should hit, as the third request should get the cached value
require.Equal(t, "HIT", cacheValue)
// Lastly, we check to make sure the actual data we received is
// as we expect. We must use ParseSecret due to the raw requests.
secret1, err := api.ParseSecret(resp1.Body)
if err != nil {
t.Fatal(err)
}
require.Equal(t, secretData, secret1.Data)
secret2, err := api.ParseSecret(resp2.Body)
if err != nil {
t.Fatal(err)
}
require.Equal(t, secretData2, secret2.Data)
secret3, err := api.ParseSecret(resp3.Body)
if err != nil {
t.Fatal(err)
}
require.Equal(t, secret2.Data, secret3.Data)
close(cmd.ShutdownCh)
wg.Wait()
}
// TestProxy_ApiProxy_Retry Tests the retry functionalities of Vault Proxy's API Proxy
func TestProxy_ApiProxy_Retry(t *testing.T) {
//----------------------------------------------------