vault/command/agentproxyshared/cache/lease_cache_test.go
Violet Hynes 1e132479f0
VAULT-19233 Second part of caching static secrets work (#23177)
* VAULT-19237 Add mount_type to secret response

* VAULT-19237 changelog

* VAULT-19237 make MountType generic

* VAULT-19237 clean up comment

* VAULT-19237 update changelog

* VAULT-19237 update test, remove mounttype from wrapped responses

* VAULT-19237 fix a lot of tests

* VAULT-19237 standby test

* ensure -log-level is added to core config (#23017)

* Feature/document tls servername (#22714)

* Add Raft TLS Helm examples

Co-authored-by: Pascal Reeb <pascal.reeb@adfinis.com>
---------

* Clean up unused CRL entries when issuer is removed (#23007)

* Clean up unused CRL entries when issuer is removed

When a issuer is removed, the space utilized by its CRL was not freed,
both from the CRL config mapping issuer IDs to CRL IDs and from the
CRL storage entry. We thus implement a two step cleanup, wherein
orphaned CRL IDs are removed from the config and any remaining full
CRL entries are removed from disk.

This relates to a Consul<->Vault interop issue (#22980), wherein Consul
creates a new issuer on every leadership election, causing this config
to grow. Deleting issuers manually does not entirely solve this problem
as the config does not fully reclaim space used in this entry.

Notably, an observation that when deleting issuers, the CRL was rebuilt
on secondary clusters (due to the invalidation not caring about type of
the operation); for consistency and to clean up the unified CRLs, we
also need to run the rebuild on the active primary cluster that deleted
the issuer as well.

This approach does allow cleanup on existing impacted clusters by simply
rebuilding the CRL.

Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add test case on CRL removal

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog entry

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
Co-authored-by: Steven Clark <steven.clark@hashicorp.com>

* UI: Handle control group error on SSH (#23025)

* Handle control group error on SSH

* Add changelog

* Fix enterprise failure of TestCRLIssuerRemoval (#23038)

This fixes the enterprise failure of the test
 ```
  === FAIL: builtin/logical/pki TestCRLIssuerRemoval (0.00s)
     crl_test.go:1456:
         	Error Trace:	/home/runner/actions-runner/_work/vault-enterprise/vault-enterprise/builtin/logical/pki/crl_test.go:1456
         	Error:      	Received unexpected error:
         	            	Global, cross-cluster revocation queue cannot be enabled when auto rebuilding is disabled as the local cluster may not have the certificate entry!
         	Test:       	TestCRLIssuerRemoval
         	Messages:   	failed enabling unified CRLs on enterprise

 ```

* fix LDAP auto auth changelog (#23027)

* 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 add caching of capabilities map, and some additional test coverage

* VAULT-19233 Additional testing

* VAULT-19233 namespaces for cache ids

* VAULT-19233 cache-clear testing and implementation

* VAULT-19233 adjust format, add more tests

* VAULT-19233 some more docs

* VAULT-19233 Add RLock holding for map access

* VAULT-19233 PR comments

* VAULT-19233 Different table for capabilities indexes

* VAULT-19233 keep unique for request path

* VAULT-19233 passthrough for non-v1 requests

* VAULT-19233 some renames/PR comment updates

* VAULT-19233 remove type from capabilities index

* VAULT-19233 remove obsolete capabilities

* VAULT-19233 remove erroneous capabilities

* VAULT-19233 woops, missed a test

* VAULT-19233 typo

* VAULT-19233 add custom error for cachememdb

* VAULT-19233 fix cachememdb test

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
Co-authored-by: Chris Capurso <1036769+ccapurso@users.noreply.github.com>
Co-authored-by: Andreas Gruhler <andreas.gruhler@adfinis.com>
Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
2023-10-06 14:44:43 -04:00

1666 lines
51 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cache
import (
"context"
"encoding/hex"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
"github.com/go-test/deep"
hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agentproxyshared/cache/cacheboltdb"
"github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb"
"github.com/hashicorp/vault/command/agentproxyshared/cache/keymanager"
"github.com/hashicorp/vault/helper/useragent"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/cryptoutil"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
)
func testNewLeaseCache(t *testing.T, responses []*SendResponse) *LeaseCache {
t.Helper()
client, err := api.NewClient(api.DefaultConfig())
if err != nil {
t.Fatal(err)
}
lc, err := NewLeaseCache(&LeaseCacheConfig{
Client: client,
BaseContext: context.Background(),
Proxier: NewMockProxier(responses),
Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"),
CacheStaticSecrets: true,
})
if err != nil {
t.Fatal(err)
}
return lc
}
func testNewLeaseCacheWithDelay(t *testing.T, cacheable bool, delay int) *LeaseCache {
t.Helper()
client, err := api.NewClient(api.DefaultConfig())
if err != nil {
t.Fatal(err)
}
lc, err := NewLeaseCache(&LeaseCacheConfig{
Client: client,
BaseContext: context.Background(),
Proxier: &mockDelayProxier{cacheable, delay},
Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"),
CacheStaticSecrets: true,
})
if err != nil {
t.Fatal(err)
}
return lc
}
func testNewLeaseCacheWithPersistence(t *testing.T, responses []*SendResponse, storage *cacheboltdb.BoltStorage) *LeaseCache {
t.Helper()
client, err := api.NewClient(api.DefaultConfig())
require.NoError(t, err)
lc, err := NewLeaseCache(&LeaseCacheConfig{
Client: client,
BaseContext: context.Background(),
Proxier: NewMockProxier(responses),
Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"),
Storage: storage,
CacheStaticSecrets: true,
})
require.NoError(t, err)
return lc
}
func TestCache_ComputeIndexID(t *testing.T) {
tests := []struct {
name string
req *SendRequest
want string
wantErr bool
}{
{
"basic",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "test",
},
},
},
"7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717",
false,
},
{
"ignore consistency headers",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "test",
},
Header: http.Header{
vaulthttp.VaultIndexHeaderName: []string{"foo"},
vaulthttp.VaultInconsistentHeaderName: []string{"foo"},
vaulthttp.VaultForwardHeaderName: []string{"foo"},
},
},
},
"7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717",
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := computeIndexID(tt.req)
if (err != nil) != tt.wantErr {
t.Errorf("actual_error: %v, expected_error: %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, string(tt.want)) {
t.Errorf("bad: index id; actual: %q, expected: %q", got, string(tt.want))
}
})
}
}
// TestCache_ComputeStaticSecretIndexID ensures that
// computeStaticSecretCacheIndex works correctly. If this test breaks, then our
// hashing algorithm has changed, and we risk breaking backwards compatibility.
func TestCache_ComputeStaticSecretIndexID(t *testing.T) {
req := &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/foo/bar",
},
},
}
index := computeStaticSecretCacheIndex(req)
// We expect this to be "", as it doesn't start with /v1
expectedIndex := ""
require.Equal(t, expectedIndex, index)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/foo/bar",
},
},
}
expectedIndex = "b117a962f19f17fa372c8681cadcd6fd370d28ee6e0a7012196b780bef601b53"
index2 := computeStaticSecretCacheIndex(req)
require.Equal(t, expectedIndex, index2)
}
// Test_GetStaticSecretPathFromRequestNoNamespaces tests that getStaticSecretPathFromRequest
// behaves as expected when no namespaces are involved.
func Test_GetStaticSecretPathFromRequestNoNamespaces(t *testing.T) {
req := &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/foo/bar",
},
},
}
path := getStaticSecretPathFromRequest(req)
require.Equal(t, "foo/bar", path)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
// Paths like this are not static secrets, so we should return ""
Path: "foo/bar",
},
},
}
path = getStaticSecretPathFromRequest(req)
require.Equal(t, "", path)
}
// Test_GetStaticSecretPathFromRequestNamespaces tests that getStaticSecretPathFromRequest
// behaves as expected when namespaces are involved.
func Test_GetStaticSecretPathFromRequestNamespaces(t *testing.T) {
req := &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/foo/bar",
},
Header: map[string][]string{api.NamespaceHeaderName: {"ns1"}},
},
}
path := getStaticSecretPathFromRequest(req)
require.Equal(t, "ns1/foo/bar", path)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/ns1/foo/bar",
},
},
}
path = getStaticSecretPathFromRequest(req)
require.Equal(t, "ns1/foo/bar", path)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
// Paths like this are not static secrets, so we should return ""
Path: "ns1/foo/bar",
},
},
}
path = getStaticSecretPathFromRequest(req)
require.Equal(t, "", path)
}
// TestCache_CanonicalizeStaticSecretPath ensures that
// canonicalizeStaticSecretPath works as expected with all kinds of inputs.
func TestCache_CanonicalizeStaticSecretPath(t *testing.T) {
expected := "foo/bar"
actual := canonicalizeStaticSecretPath("/v1/foo/bar", "")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("foo/bar", "")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("/foo/bar", "")
require.Equal(t, expected, actual)
expected = "ns1/foo/bar"
actual = canonicalizeStaticSecretPath("/v1/ns1/foo/bar", "")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("ns1/foo/bar", "")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("/ns1/foo/bar", "")
require.Equal(t, expected, actual)
expected = "ns1/foo/bar"
actual = canonicalizeStaticSecretPath("/v1/foo/bar", "ns1")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("/foo/bar", "ns1")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("foo/bar", "ns1")
require.Equal(t, expected, actual)
expected = "ns1/foo/bar"
actual = canonicalizeStaticSecretPath("/v1/foo/bar", "ns1/")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("/foo/bar", "ns1/")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("foo/bar", "ns1/")
require.Equal(t, expected, actual)
expected = "ns1/foo/bar"
actual = canonicalizeStaticSecretPath("/v1/foo/bar", "/ns1/")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("/foo/bar", "/ns1/")
require.Equal(t, expected, actual)
actual = canonicalizeStaticSecretPath("foo/bar", "/ns1/")
require.Equal(t, expected, actual)
}
// TestCache_ComputeStaticSecretIndexIDNamespaces ensures that
// computeStaticSecretCacheIndex correctly identifies that a request
// with a namespace header and a request specifying the namespace in the path
// are equivalent.
func TestCache_ComputeStaticSecretIndexIDNamespaces(t *testing.T) {
req := &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "foo/bar",
},
Header: map[string][]string{api.NamespaceHeaderName: {"ns1"}},
},
}
index := computeStaticSecretCacheIndex(req)
// Paths like this are not static secrets, so we should expect ""
require.Equal(t, "", index)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "ns1/foo/bar",
},
},
}
// Paths like this are not static secrets, so we should expect ""
index2 := computeStaticSecretCacheIndex(req)
require.Equal(t, "", index2)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/ns1/foo/bar",
},
},
}
expectedIndex := "a4605679d269aa1bebac7079a471a33403413f388f63bf0da3c771b225857932"
// We expect that computeStaticSecretCacheIndex will compute the same index
index3 := computeStaticSecretCacheIndex(req)
require.Equal(t, expectedIndex, index3)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/foo/bar",
},
Header: map[string][]string{api.NamespaceHeaderName: {"ns1"}},
},
}
index4 := computeStaticSecretCacheIndex(req)
require.Equal(t, expectedIndex, index4)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/foo/bar",
},
Header: map[string][]string{api.NamespaceHeaderName: {"ns1/"}},
},
}
// Paths like this are not static secrets, so we should expect ""
index5 := computeStaticSecretCacheIndex(req)
require.Equal(t, "", index5)
req = &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/foo/bar",
},
Header: map[string][]string{api.NamespaceHeaderName: {"ns1/"}},
},
}
index6 := computeStaticSecretCacheIndex(req)
require.Equal(t, expectedIndex, index6)
}
func TestLeaseCache_EmptyToken(t *testing.T) {
responses := []*SendResponse{
newTestSendResponse(http.StatusCreated, `{"value": "invalid", "auth": {"client_token": "testtoken"}}`),
}
lc := testNewLeaseCache(t, responses)
// Even if the send request doesn't have a token on it, a successful
// cacheable response should result in the index properly getting populated
// with a token and memdb shouldn't complain while inserting the index.
urlPath := "http://example.com/v1/sample/api"
sendReq := &SendRequest{
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
}
resp, err := lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatalf("expected a non empty response")
}
}
func TestLeaseCache_SendCacheable(t *testing.T) {
// Emulate 2 responses from the api proxy. One returns a new token and the
// other returns a lease.
responses := []*SendResponse{
newTestSendResponse(http.StatusCreated, `{"auth": {"client_token": "testtoken", "renewable": true}}`),
newTestSendResponse(http.StatusOK, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}}`),
}
lc := testNewLeaseCache(t, responses)
// Register a token so that the token and lease requests are cached
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
// Make a request. A response with a new token is returned to the lease
// cache and that will be cached.
urlPath := "http://example.com/v1/sample/api"
sendReq := &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
}
resp, err := lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response.StatusCode, responses[0].Response.StatusCode); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Send the same request again to get the cached response
sendReq = &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response.StatusCode, responses[0].Response.StatusCode); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Check TokenParent
cachedItem, err := lc.db.Get(cachememdb.IndexNameToken, "testtoken")
if err != nil {
t.Fatal(err)
}
if cachedItem == nil {
t.Fatalf("expected token entry from cache")
}
if cachedItem.TokenParent != "autoauthtoken" {
t.Fatalf("unexpected value for tokenparent: %s", cachedItem.TokenParent)
}
// Modify the request a little bit to ensure the second response is
// returned to the lease cache.
sendReq = &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input_changed"}`)),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response.StatusCode, responses[1].Response.StatusCode); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Make the same request again and ensure that the same response is returned
// again.
sendReq = &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input_changed"}`)),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response.StatusCode, responses[1].Response.StatusCode); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
}
// TestLeaseCache_StoreCacheableStaticSecret tests that cacheStaticSecret works
// as expected, creating the two expected cache entries, and also ensures
// that we can evict the cache entry with the cache clear API afterwards.
func TestLeaseCache_StoreCacheableStaticSecret(t *testing.T) {
request := &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/secrets/foo/bar",
},
},
Token: "token",
}
response := newTestSendResponse(http.StatusCreated, `{"data": {"foo": "bar"}, "mount_type": "kvv2"}`)
responses := []*SendResponse{
response,
}
index := &cachememdb.Index{
Type: cacheboltdb.StaticSecretType,
RequestPath: request.Request.URL.Path,
Namespace: "root/",
Token: "token",
ID: computeStaticSecretCacheIndex(request),
}
lc := testNewLeaseCache(t, responses)
// We expect two entries to be stored by this:
// 1. The actual static secret
// 2. The capabilities index
err := lc.cacheStaticSecret(context.Background(), request, response, index)
if err != nil {
return
}
indexFromDB, err := lc.db.Get(cachememdb.IndexNameID, index.ID)
if err != nil {
return
}
require.NotNil(t, indexFromDB)
require.Equal(t, "token", indexFromDB.Token)
require.Equal(t, map[string]struct{}{"token": {}}, indexFromDB.Tokens)
require.Equal(t, cacheboltdb.StaticSecretType, indexFromDB.Type)
require.Equal(t, request.Request.URL.Path, indexFromDB.RequestPath)
require.Equal(t, "root/", indexFromDB.Namespace)
capabilitiesIndexFromDB, err := lc.db.GetCapabilitiesIndex(cachememdb.IndexNameID, hex.EncodeToString(cryptoutil.Blake2b256Hash(index.Token)))
if err != nil {
return
}
require.NotNil(t, capabilitiesIndexFromDB)
require.Equal(t, "token", capabilitiesIndexFromDB.Token)
require.Equal(t, map[string]struct{}{"secrets/foo/bar": {}}, capabilitiesIndexFromDB.ReadablePaths)
err = lc.handleCacheClear(context.Background(), &cacheClearInput{
Type: "request_path",
RequestPath: request.Request.URL.Path,
})
require.NoError(t, err)
expectedClearedIndex, err := lc.db.Get(cachememdb.IndexNameID, index.ID)
require.Equal(t, cachememdb.ErrCacheItemNotFound, err)
require.Nil(t, expectedClearedIndex)
}
// TestLeaseCache_StaticSecret_CacheClear_All tests that static secrets are
// stored correctly, as well as removed from the cache by a cache clear with
// "all" specified as the type.
func TestLeaseCache_StaticSecret_CacheClear_All(t *testing.T) {
request := &SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/secrets/foo/bar",
},
},
Token: "token",
}
response := newTestSendResponse(http.StatusCreated, `{"data": {"foo": "bar"}, "mount_type": "kvv2"}`)
responses := []*SendResponse{
response,
}
index := &cachememdb.Index{
Type: cacheboltdb.StaticSecretType,
RequestPath: request.Request.URL.Path,
Namespace: "root/",
Token: "token",
ID: computeStaticSecretCacheIndex(request),
}
lc := testNewLeaseCache(t, responses)
// We expect two entries to be stored by this:
// 1. The actual static secret
// 2. The capabilities index
err := lc.cacheStaticSecret(context.Background(), request, response, index)
if err != nil {
return
}
indexFromDB, err := lc.db.Get(cachememdb.IndexNameID, index.ID)
if err != nil {
return
}
require.NotNil(t, indexFromDB)
require.Equal(t, "token", indexFromDB.Token)
require.Equal(t, map[string]struct{}{"token": {}}, indexFromDB.Tokens)
require.Equal(t, cacheboltdb.StaticSecretType, indexFromDB.Type)
require.Equal(t, request.Request.URL.Path, indexFromDB.RequestPath)
require.Equal(t, "root/", indexFromDB.Namespace)
capabilitiesIndexFromDB, err := lc.db.GetCapabilitiesIndex(cachememdb.IndexNameID, hex.EncodeToString(cryptoutil.Blake2b256Hash(index.Token)))
if err != nil {
t.Fatal(err)
}
require.NotNil(t, capabilitiesIndexFromDB)
require.Equal(t, "token", capabilitiesIndexFromDB.Token)
require.Equal(t, map[string]struct{}{"secrets/foo/bar": {}}, capabilitiesIndexFromDB.ReadablePaths)
err = lc.handleCacheClear(context.Background(), &cacheClearInput{
Type: "all",
})
require.NoError(t, err)
expectedClearedIndex, err := lc.db.Get(cachememdb.IndexNameID, index.ID)
require.Equal(t, cachememdb.ErrCacheItemNotFound, err)
require.Nil(t, expectedClearedIndex)
expectedClearedCapabilitiesIndex, err := lc.db.GetCapabilitiesIndex(cachememdb.IndexNameID, capabilitiesIndexFromDB.ID)
require.Equal(t, cachememdb.ErrCacheItemNotFound, err)
require.Nil(t, expectedClearedCapabilitiesIndex)
}
// TestLeaseCache_SendCacheableStaticSecret tests that the cache has no issue returning
// static secret style responses. It's similar to TestLeaseCache_SendCacheable in that it
// only tests the surface level of the functionality, but there are other tests that
// test the rest.
func TestLeaseCache_SendCacheableStaticSecret(t *testing.T) {
response := newTestSendResponse(http.StatusCreated, `{"data": {"foo": "bar"}, "mount_type": "kvv2"}`)
responses := []*SendResponse{
response,
response,
response,
response,
}
lc := testNewLeaseCache(t, responses)
// Register a token
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
// Make a request. A response with a new token is returned to the lease
// cache and that will be cached.
urlPath := "http://example.com/v1/sample/api"
sendReq := &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
}
resp, err := lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response.StatusCode, response.Response.StatusCode); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Send the same request again to get the cached response
sendReq = &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response.StatusCode, responses[0].Response.StatusCode); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Modify the request a little to ensure the second response is
// returned to the lease cache.
sendReq = &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input_changed"}`)),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response.StatusCode, response.Response.StatusCode); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Make the same request again and ensure that the same response is returned
// again.
sendReq = &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input_changed"}`)),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response.StatusCode, response.Response.StatusCode); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
}
func TestLeaseCache_SendNonCacheable(t *testing.T) {
responses := []*SendResponse{
newTestSendResponse(http.StatusOK, `{"value": "output"}`),
newTestSendResponse(http.StatusNotFound, `{"value": "invalid"}`),
newTestSendResponse(http.StatusOK, `<html>Hello</html>`),
newTestSendResponse(http.StatusTemporaryRedirect, ""),
}
lc := testNewLeaseCache(t, responses)
// Send a request through the lease cache which is not cacheable (there is
// no lease information or auth information in the response)
sendReq := &SendRequest{
Request: httptest.NewRequest("GET", "http://example.com", strings.NewReader(`{"value": "input"}`)),
}
resp, err := lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response, responses[0].Response); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Since the response is non-cacheable, the second response will be
// returned.
sendReq = &SendRequest{
Token: "foo",
Request: httptest.NewRequest("GET", "http://example.com", strings.NewReader(`{"value": "input"}`)),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response, responses[1].Response); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Since the response is non-cacheable, the third response will be
// returned.
sendReq = &SendRequest{
Token: "foo",
Request: httptest.NewRequest("GET", "http://example.com", nil),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response, responses[2].Response); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
// Since the response is non-cacheable, the fourth response will be
// returned.
sendReq = &SendRequest{
Token: "foo",
Request: httptest.NewRequest("GET", "http://example.com", nil),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response, responses[3].Response); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
}
func TestLeaseCache_SendNonCacheableNonTokenLease(t *testing.T) {
// Create the cache
responses := []*SendResponse{
newTestSendResponse(http.StatusOK, `{"value": "output", "lease_id": "foo"}`),
newTestSendResponse(http.StatusCreated, `{"value": "invalid", "auth": {"client_token": "testtoken"}}`),
}
lc := testNewLeaseCache(t, responses)
// Send a request through lease cache which returns a response containing
// lease_id. Response will not be cached because it doesn't belong to a
// token that is managed by the lease cache.
urlPath := "http://example.com/v1/sample/api"
sendReq := &SendRequest{
Token: "foo",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
}
resp, err := lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response, responses[0].Response); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
_, err = lc.db.Get(cachememdb.IndexNameRequestPath, "root/", urlPath)
if err != cachememdb.ErrCacheItemNotFound {
t.Fatal("expected entry to be nil, got", err)
}
// Verify that the response is not cached by sending the same request and
// by expecting a different response.
sendReq = &SendRequest{
Token: "foo",
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
}
resp, err = lc.Send(context.Background(), sendReq)
if err != nil {
t.Fatal(err)
}
if diff := deep.Equal(resp.Response, responses[1].Response); diff != nil {
t.Fatalf("expected getting proxied response: got %v", diff)
}
_, err = lc.db.Get(cachememdb.IndexNameRequestPath, "root/", urlPath)
if err != cachememdb.ErrCacheItemNotFound {
t.Fatal("expected entry to be nil, got", err)
}
}
func TestLeaseCache_HandleCacheClear(t *testing.T) {
lc := testNewLeaseCache(t, nil)
handler := lc.HandleCacheClear(context.Background())
ts := httptest.NewServer(handler)
defer ts.Close()
// Test missing body, should return 400
resp, err := http.Post(ts.URL, "application/json", nil)
if err != nil {
t.Fatal()
}
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("status code mismatch: expected = %v, got = %v", http.StatusBadRequest, resp.StatusCode)
}
testCases := []struct {
name string
reqType string
reqValue string
expectedStatusCode int
}{
{
"invalid_type",
"foo",
"",
http.StatusBadRequest,
},
{
"invalid_value",
"",
"bar",
http.StatusBadRequest,
},
{
"all",
"all",
"",
http.StatusOK,
},
{
"by_request_path",
"request_path",
"foo",
http.StatusOK,
},
{
"by_token",
"token",
"foo",
http.StatusOK,
},
{
"by_lease",
"lease",
"foo",
http.StatusOK,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
reqBody := fmt.Sprintf("{\"type\": \"%s\", \"value\": \"%s\"}", tc.reqType, tc.reqValue)
resp, err := http.Post(ts.URL, "application/json", strings.NewReader(reqBody))
if err != nil {
t.Fatal(err)
}
if tc.expectedStatusCode != resp.StatusCode {
t.Fatalf("status code mismatch: expected = %v, got = %v", tc.expectedStatusCode, resp.StatusCode)
}
})
}
}
func TestCache_DeriveNamespaceAndRevocationPath(t *testing.T) {
tests := []struct {
name string
req *SendRequest
wantNamespace string
wantRelativePath string
}{
{
"non_revocation_full_path",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/ns1/sys/mounts",
},
},
},
"root/",
"/v1/ns1/sys/mounts",
},
{
"non_revocation_relative_path",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/sys/mounts",
},
Header: http.Header{
consts.NamespaceHeaderName: []string{"ns1/"},
},
},
},
"ns1/",
"/v1/sys/mounts",
},
{
"non_revocation_relative_path",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/ns2/sys/mounts",
},
Header: http.Header{
consts.NamespaceHeaderName: []string{"ns1/"},
},
},
},
"ns1/",
"/v1/ns2/sys/mounts",
},
{
"revocation_full_path",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/ns1/sys/leases/revoke",
},
},
},
"ns1/",
"/v1/sys/leases/revoke",
},
{
"revocation_relative_path",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/sys/leases/revoke",
},
Header: http.Header{
consts.NamespaceHeaderName: []string{"ns1/"},
},
},
},
"ns1/",
"/v1/sys/leases/revoke",
},
{
"revocation_relative_partial_ns",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/ns2/sys/leases/revoke",
},
Header: http.Header{
consts.NamespaceHeaderName: []string{"ns1/"},
},
},
},
"ns1/ns2/",
"/v1/sys/leases/revoke",
},
{
"revocation_prefix_full_path",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/ns1/sys/leases/revoke-prefix/foo",
},
},
},
"ns1/",
"/v1/sys/leases/revoke-prefix/foo",
},
{
"revocation_prefix_relative_path",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/sys/leases/revoke-prefix/foo",
},
Header: http.Header{
consts.NamespaceHeaderName: []string{"ns1/"},
},
},
},
"ns1/",
"/v1/sys/leases/revoke-prefix/foo",
},
{
"revocation_prefix_partial_ns",
&SendRequest{
Request: &http.Request{
URL: &url.URL{
Path: "/v1/ns2/sys/leases/revoke-prefix/foo",
},
Header: http.Header{
consts.NamespaceHeaderName: []string{"ns1/"},
},
},
},
"ns1/ns2/",
"/v1/sys/leases/revoke-prefix/foo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotNamespace, gotRelativePath := deriveNamespaceAndRevocationPath(tt.req)
if gotNamespace != tt.wantNamespace {
t.Errorf("deriveNamespaceAndRevocationPath() gotNamespace = %v, want %v", gotNamespace, tt.wantNamespace)
}
if gotRelativePath != tt.wantRelativePath {
t.Errorf("deriveNamespaceAndRevocationPath() gotRelativePath = %v, want %v", gotRelativePath, tt.wantRelativePath)
}
})
}
}
func TestLeaseCache_Concurrent_NonCacheable(t *testing.T) {
lc := testNewLeaseCacheWithDelay(t, false, 50)
// We are going to send 100 requests, each taking 50ms to process. If these
// requests are processed serially, it will take ~5seconds to finish. we
// use a ContextWithTimeout to tell us if this is the case by giving ample
// time for it process them concurrently but time out if they get processed
// serially.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
wgDoneCh := make(chan struct{})
errCh := make(chan error)
go func() {
var wg sync.WaitGroup
// 100 concurrent requests
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Send a request through the lease cache which is not cacheable (there is
// no lease information or auth information in the response)
sendReq := &SendRequest{
Request: httptest.NewRequest("GET", "http://example.com", nil),
}
_, err := lc.Send(ctx, sendReq)
if err != nil {
errCh <- err
}
}()
}
wg.Wait()
close(wgDoneCh)
}()
select {
case <-ctx.Done():
t.Fatalf("request timed out: %s", ctx.Err())
case <-wgDoneCh:
case err := <-errCh:
t.Fatal(err)
}
}
func TestLeaseCache_Concurrent_Cacheable(t *testing.T) {
lc := testNewLeaseCacheWithDelay(t, true, 50)
if err := lc.RegisterAutoAuthToken("autoauthtoken"); err != nil {
t.Fatal(err)
}
// We are going to send 100 requests, each taking 50ms to process. If these
// requests are processed serially, it will take ~5seconds to finish, so we
// use a ContextWithTimeout to tell us if this is the case.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
var cacheCount atomic.Uint32
wgDoneCh := make(chan struct{})
errCh := make(chan error)
go func() {
var wg sync.WaitGroup
// Start 100 concurrent requests
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
sendReq := &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", "http://example.com/v1/sample/api", nil),
}
resp, err := lc.Send(ctx, sendReq)
if err != nil {
errCh <- err
}
if resp.CacheMeta != nil && resp.CacheMeta.Hit {
cacheCount.Inc()
}
}()
}
wg.Wait()
close(wgDoneCh)
}()
select {
case <-ctx.Done():
t.Fatalf("request timed out: %s", ctx.Err())
case <-wgDoneCh:
case err := <-errCh:
t.Fatal(err)
}
// Ensure that all but one request got proxied. The other 99 should be
// returned from the cache.
if cacheCount.Load() != 99 {
t.Fatalf("Should have returned a cached response 99 times, got %d", cacheCount.Load())
}
}
func setupBoltStorage(t *testing.T) (tempCacheDir string, boltStorage *cacheboltdb.BoltStorage) {
t.Helper()
km, err := keymanager.NewPassthroughKeyManager(context.Background(), nil)
require.NoError(t, err)
tempCacheDir, err = ioutil.TempDir("", "agent-cache-test")
require.NoError(t, err)
boltStorage, err = cacheboltdb.NewBoltStorage(&cacheboltdb.BoltStorageConfig{
Path: tempCacheDir,
Logger: hclog.Default(),
Wrapper: km.Wrapper(),
})
require.NoError(t, err)
require.NotNil(t, boltStorage)
// The calling function should `defer boltStorage.Close()` and `defer os.RemoveAll(tempCacheDir)`
return tempCacheDir, boltStorage
}
func compareBeforeAndAfter(t *testing.T, before, after *LeaseCache, beforeLen, afterLen int) {
beforeDB, err := before.db.GetByPrefix(cachememdb.IndexNameID)
require.NoError(t, err)
assert.Len(t, beforeDB, beforeLen)
afterDB, err := after.db.GetByPrefix(cachememdb.IndexNameID)
require.NoError(t, err)
assert.Len(t, afterDB, afterLen)
for _, cachedItem := range beforeDB {
if strings.Contains(cachedItem.RequestPath, "expect-missing") {
continue
}
restoredItem, err := after.db.Get(cachememdb.IndexNameID, cachedItem.ID)
require.NoError(t, err)
assert.NoError(t, err)
assert.Equal(t, cachedItem.ID, restoredItem.ID)
assert.Equal(t, cachedItem.Lease, restoredItem.Lease)
assert.Equal(t, cachedItem.LeaseToken, restoredItem.LeaseToken)
assert.Equal(t, cachedItem.Namespace, restoredItem.Namespace)
assert.EqualValues(t, cachedItem.RequestHeader, restoredItem.RequestHeader)
assert.Equal(t, cachedItem.RequestMethod, restoredItem.RequestMethod)
assert.Equal(t, cachedItem.RequestPath, restoredItem.RequestPath)
assert.Equal(t, cachedItem.RequestToken, restoredItem.RequestToken)
assert.Equal(t, cachedItem.Response, restoredItem.Response)
assert.Equal(t, cachedItem.Token, restoredItem.Token)
assert.Equal(t, cachedItem.TokenAccessor, restoredItem.TokenAccessor)
assert.Equal(t, cachedItem.TokenParent, restoredItem.TokenParent)
// check what we can in the renewal context
assert.NotEmpty(t, restoredItem.RenewCtxInfo.CancelFunc)
assert.NotZero(t, restoredItem.RenewCtxInfo.DoneCh)
require.NotEmpty(t, restoredItem.RenewCtxInfo.Ctx)
assert.Equal(t,
cachedItem.RenewCtxInfo.Ctx.Value(contextIndexID),
restoredItem.RenewCtxInfo.Ctx.Value(contextIndexID),
)
}
}
func TestLeaseCache_PersistAndRestore(t *testing.T) {
// Emulate responses from the api proxy. The first two use the auto-auth
// token, and the others use another token.
// The test re-sends each request to ensure that the response is cached
// so the number of responses and cacheTests specified should always be equal.
responses := []*SendResponse{
newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": 600}}`),
newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": 600}`),
// The auth token will get manually deleted from the bolt DB storage, causing both of the following two responses
// to be missing from the cache after a restore, because the lease is a child of the auth token.
newTestSendResponse(202, `{"auth": {"client_token": "testtoken2", "renewable": true, "orphan": true, "lease_duration": 600}}`),
newTestSendResponse(203, `{"lease_id": "secret2-lease", "renewable": true, "data": {"number": "two"}, "lease_duration": 600}`),
// 204 No content gets special handling - avoid.
newTestSendResponse(250, `{"auth": {"client_token": "testtoken3", "renewable": true, "orphan": true, "lease_duration": 600}}`),
newTestSendResponse(251, `{"lease_id": "secret3-lease", "renewable": true, "data": {"number": "three"}, "lease_duration": 600}`),
newTestSendResponse(http.StatusCreated, `{"data": {"foo": "bar"}, "mount_type": "kvv2"}`),
}
tempDir, boltStorage := setupBoltStorage(t)
defer os.RemoveAll(tempDir)
defer boltStorage.Close()
lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage)
// Register an auto-auth token so that the token and lease requests are cached
err := lc.RegisterAutoAuthToken("autoauthtoken")
require.NoError(t, err)
cacheTests := []struct {
token string
method string
urlPath string
body string
deleteFromPersistentStore bool // If true, will be deleted from bolt DB to induce an error on restore
expectMissingAfterRestore bool // If true, the response is not expected to be present in the restored cache
}{
{
// Make a request. A response with a new token is returned to the
// lease cache and that will be cached.
token: "autoauthtoken",
method: "GET",
urlPath: "http://example.com/v1/sample/api",
body: `{"value": "input"}`,
},
{
// Modify the request a little bit to ensure the second response is
// returned to the lease cache.
token: "autoauthtoken",
method: "GET",
urlPath: "http://example.com/v1/sample/api",
body: `{"value": "input_changed"}`,
},
{
// Simulate an approle login to get another token
method: "PUT",
urlPath: "http://example.com/v1/auth/approle-expect-missing/login",
body: `{"role_id": "my role", "secret_id": "my secret"}`,
deleteFromPersistentStore: true,
expectMissingAfterRestore: true,
},
{
// Test caching with the token acquired from the approle login
token: "testtoken2",
method: "GET",
urlPath: "http://example.com/v1/sample-expect-missing/api",
body: `{"second": "input"}`,
// This will be missing from the restored cache because its parent token was deleted
expectMissingAfterRestore: true,
},
{
// Simulate another approle login to get another token
method: "PUT",
urlPath: "http://example.com/v1/auth/approle/login",
body: `{"role_id": "my role", "secret_id": "my secret"}`,
},
{
// Test caching with the token acquired from the latest approle login
token: "testtoken3",
method: "GET",
urlPath: "http://example.com/v1/sample3/api",
body: `{"third": "input"}`,
},
}
var deleteIDs []string
for i, ct := range cacheTests {
// Send once to cache
req := httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body))
req.Header.Set("User-Agent", useragent.AgentProxyString())
sendReq := &SendRequest{
Token: ct.token,
Request: req,
}
if ct.deleteFromPersistentStore {
deleteID, err := computeIndexID(sendReq)
require.NoError(t, err)
deleteIDs = append(deleteIDs, deleteID)
// Now reset the body after calculating the index
req = httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body))
req.Header.Set("User-Agent", useragent.AgentProxyString())
sendReq.Request = req
}
resp, err := lc.Send(context.Background(), sendReq)
require.NoError(t, err)
assert.Equal(t, responses[i].Response.StatusCode, resp.Response.StatusCode, "expected proxied response")
assert.Nil(t, resp.CacheMeta)
// Send again to test cache. If this isn't cached, the response returned
// will be the next in the list and the status code will not match.
req = httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body))
req.Header.Set("User-Agent", useragent.AgentProxyString())
sendCacheReq := &SendRequest{
Token: ct.token,
Request: req,
}
respCached, err := lc.Send(context.Background(), sendCacheReq)
require.NoError(t, err, "failed to send request %+v", ct)
assert.Equal(t, responses[i].Response.StatusCode, respCached.Response.StatusCode, "expected proxied response")
require.NotNil(t, respCached.CacheMeta)
assert.True(t, respCached.CacheMeta.Hit)
}
require.NotEmpty(t, deleteIDs)
for _, deleteID := range deleteIDs {
err = boltStorage.Delete(deleteID, cacheboltdb.LeaseType)
require.NoError(t, err)
}
// Now we know the cache is working, so try restoring from the persisted
// cache's storage. Responses 3 and 4 have been cleared from the cache, so
// re-send those.
restoredCache := testNewLeaseCache(t, responses[2:4])
err = restoredCache.Restore(context.Background(), boltStorage)
errors, ok := err.(*multierror.Error)
require.True(t, ok)
assert.Len(t, errors.Errors, 1)
assert.Contains(t, errors.Error(), "could not find parent Token testtoken2")
// Now compare the cache contents before and after
compareBeforeAndAfter(t, lc, restoredCache, 7, 5)
// And finally send the cache requests once to make sure they're all being
// served from the restoredCache unless they were intended to be missing after restore.
for i, ct := range cacheTests {
req := httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body))
req.Header.Set("User-Agent", useragent.AgentProxyString())
sendCacheReq := &SendRequest{
Token: ct.token,
Request: req,
}
respCached, err := restoredCache.Send(context.Background(), sendCacheReq)
require.NoError(t, err, "failed to send request %+v", ct)
assert.Equal(t, responses[i].Response.StatusCode, respCached.Response.StatusCode, "expected proxied response")
if ct.expectMissingAfterRestore {
require.Nil(t, respCached.CacheMeta)
} else {
require.NotNil(t, respCached.CacheMeta)
assert.True(t, respCached.CacheMeta.Hit)
}
}
}
func TestLeaseCache_PersistAndRestore_WithManyDependencies(t *testing.T) {
tempDir, boltStorage := setupBoltStorage(t)
defer os.RemoveAll(tempDir)
defer boltStorage.Close()
var requests []*SendRequest
var responses []*SendResponse
var orderedRequestPaths []string
// helper func to generate new auth leases with a child secret lease attached
authAndSecretLease := func(id int, parentToken, newToken string) {
t.Helper()
path := fmt.Sprintf("/v1/auth/approle-%d/login", id)
orderedRequestPaths = append(orderedRequestPaths, path)
requests = append(requests, &SendRequest{
Token: parentToken,
Request: httptest.NewRequest("PUT", "http://example.com"+path, strings.NewReader("")),
})
responses = append(responses, newTestSendResponse(200, fmt.Sprintf(`{"auth": {"client_token": "%s", "renewable": true, "lease_duration": 600}}`, newToken)))
// Fetch a leased secret using the new token
path = fmt.Sprintf("/v1/kv/%d", id)
orderedRequestPaths = append(orderedRequestPaths, path)
requests = append(requests, &SendRequest{
Token: newToken,
Request: httptest.NewRequest("GET", "http://example.com"+path, strings.NewReader("")),
})
responses = append(responses, newTestSendResponse(200, fmt.Sprintf(`{"lease_id": "secret-%d-lease", "renewable": true, "data": {"number": %d}, "lease_duration": 600}`, id, id)))
}
// Pathological case: a long chain of child tokens
authAndSecretLease(0, "autoauthtoken", "many-ancestors-token;0")
for i := 1; i <= 50; i++ {
// Create a new generation of child token
authAndSecretLease(i, fmt.Sprintf("many-ancestors-token;%d", i-1), fmt.Sprintf("many-ancestors-token;%d", i))
}
// Lots of sibling tokens with auto auth token as their parent
for i := 51; i <= 100; i++ {
authAndSecretLease(i, "autoauthtoken", fmt.Sprintf("many-siblings-token;%d", i))
}
// Also create some extra siblings for an auth token further down the chain
for i := 101; i <= 110; i++ {
authAndSecretLease(i, "many-ancestors-token;25", fmt.Sprintf("many-siblings-for-ancestor-token;%d", i))
}
lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage)
// Register an auto-auth token so that the token and lease requests are cached
err := lc.RegisterAutoAuthToken("autoauthtoken")
require.NoError(t, err)
for _, req := range requests {
// Send once to cache
resp, err := lc.Send(context.Background(), req)
require.NoError(t, err)
assert.Equal(t, 200, resp.Response.StatusCode, "expected success")
assert.Nil(t, resp.CacheMeta)
}
// Ensure leases are retrieved in the correct order
var processed int
leases, err := boltStorage.GetByType(context.Background(), cacheboltdb.LeaseType)
require.NoError(t, err)
for _, lease := range leases {
index, err := cachememdb.Deserialize(lease)
require.NoError(t, err)
require.Equal(t, orderedRequestPaths[processed], index.RequestPath)
processed++
}
assert.Equal(t, len(orderedRequestPaths), processed)
restoredCache := testNewLeaseCache(t, nil)
err = restoredCache.Restore(context.Background(), boltStorage)
require.NoError(t, err)
// Now compare the cache contents before and after
compareBeforeAndAfter(t, lc, restoredCache, 223, 223)
}
func TestEvictPersistent(t *testing.T) {
ctx := context.Background()
responses := []*SendResponse{
newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}}`),
}
tempDir, boltStorage := setupBoltStorage(t)
defer os.RemoveAll(tempDir)
defer boltStorage.Close()
lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage)
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
// populate cache by sending request through
sendReq := &SendRequest{
Token: "autoauthtoken",
Request: httptest.NewRequest("GET", "http://example.com/v1/sample/api", strings.NewReader(`{"value": "some_input"}`)),
}
resp, err := lc.Send(context.Background(), sendReq)
require.NoError(t, err)
assert.Equal(t, resp.Response.StatusCode, 201, "expected proxied response")
assert.Nil(t, resp.CacheMeta)
// Check bolt for the cached lease
secrets, err := lc.ps.GetByType(ctx, cacheboltdb.LeaseType)
require.NoError(t, err)
assert.Len(t, secrets, 1)
// Call clear for the request path
err = lc.handleCacheClear(context.Background(), &cacheClearInput{
Type: "request_path",
RequestPath: "/v1/sample/api",
})
require.NoError(t, err)
time.Sleep(2 * time.Second)
// Check that cached item is gone
secrets, err = lc.ps.GetByType(ctx, cacheboltdb.LeaseType)
require.NoError(t, err)
assert.Len(t, secrets, 0)
}
func TestRegisterAutoAuth_sameToken(t *testing.T) {
// If the auto-auth token already exists in the cache, it should not be
// stored again in a new index.
lc := testNewLeaseCache(t, nil)
err := lc.RegisterAutoAuthToken("autoauthtoken")
assert.NoError(t, err)
oldTokenIndex, err := lc.db.Get(cachememdb.IndexNameToken, "autoauthtoken")
assert.NoError(t, err)
oldTokenID := oldTokenIndex.ID
// register the same token again
err = lc.RegisterAutoAuthToken("autoauthtoken")
assert.NoError(t, err)
// check that there's only one index for autoauthtoken
entries, err := lc.db.GetByPrefix(cachememdb.IndexNameToken, "autoauthtoken")
assert.NoError(t, err)
assert.Len(t, entries, 1)
newTokenIndex, err := lc.db.Get(cachememdb.IndexNameToken, "autoauthtoken")
assert.NoError(t, err)
// compare the ID's since those are randomly generated when an index for a
// token is added to the cache, so if a new token was added, the id's will
// not match.
assert.Equal(t, oldTokenID, newTokenIndex.ID)
}
func Test_hasExpired(t *testing.T) {
responses := []*SendResponse{
newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": 60}}`),
newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": 60}`),
}
lc := testNewLeaseCache(t, responses)
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
cacheTests := []struct {
token string
urlPath string
leaseType string
wantStatusCode int
}{
{
// auth lease
token: "autoauthtoken",
urlPath: "/v1/sample/auth",
leaseType: cacheboltdb.LeaseType,
wantStatusCode: responses[0].Response.StatusCode,
},
{
// secret lease
token: "autoauthtoken",
urlPath: "/v1/sample/secret",
leaseType: cacheboltdb.LeaseType,
wantStatusCode: responses[1].Response.StatusCode,
},
}
for _, ct := range cacheTests {
// Send once to cache
urlPath := "http://example.com" + ct.urlPath
sendReq := &SendRequest{
Token: ct.token,
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
}
resp, err := lc.Send(context.Background(), sendReq)
require.NoError(t, err)
assert.Equal(t, resp.Response.StatusCode, ct.wantStatusCode, "expected proxied response")
assert.Nil(t, resp.CacheMeta)
// get the Index out of the mem cache
index, err := lc.db.Get(cachememdb.IndexNameRequestPath, "root/", ct.urlPath)
require.NoError(t, err)
assert.Equal(t, ct.leaseType, index.Type)
// The lease duration is 60 seconds, so time.Now() should be within that
notExpired, err := lc.hasExpired(time.Now().UTC(), index)
require.NoError(t, err)
assert.False(t, notExpired)
// In 90 seconds the index should be "expired"
futureTime := time.Now().UTC().Add(time.Second * 90)
expired, err := lc.hasExpired(futureTime, index)
require.NoError(t, err)
assert.True(t, expired)
}
}
func TestLeaseCache_hasExpired_wrong_type(t *testing.T) {
index := &cachememdb.Index{
Type: cacheboltdb.TokenType,
Response: []byte(`HTTP/0.0 200 OK
Content-Type: application/json
Date: Tue, 02 Mar 2021 17:54:16 GMT
{}`),
}
lc := testNewLeaseCache(t, nil)
expired, err := lc.hasExpired(time.Now().UTC(), index)
assert.False(t, expired)
assert.EqualError(t, err, `secret without lease encountered in expiration check`)
}
func TestLeaseCacheRestore_expired(t *testing.T) {
// Emulate 2 responses from the api proxy, both expired
responses := []*SendResponse{
newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": -600}}`),
newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": -600}`),
}
tempDir, boltStorage := setupBoltStorage(t)
defer os.RemoveAll(tempDir)
defer boltStorage.Close()
lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage)
// Register an auto-auth token so that the token and lease requests are cached in mem
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
cacheTests := []struct {
token string
method string
urlPath string
body string
wantStatusCode int
}{
{
// Make a request. A response with a new token is returned to the
// lease cache and that will be cached.
token: "autoauthtoken",
method: "GET",
urlPath: "http://example.com/v1/sample/api",
body: `{"value": "input"}`,
wantStatusCode: responses[0].Response.StatusCode,
},
{
// Modify the request a little bit to ensure the second response is
// returned to the lease cache.
token: "autoauthtoken",
method: "GET",
urlPath: "http://example.com/v1/sample/api",
body: `{"value": "input_changed"}`,
wantStatusCode: responses[1].Response.StatusCode,
},
}
for _, ct := range cacheTests {
// Send once to cache
sendReq := &SendRequest{
Token: ct.token,
Request: httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body)),
}
resp, err := lc.Send(context.Background(), sendReq)
require.NoError(t, err)
assert.Equal(t, resp.Response.StatusCode, ct.wantStatusCode, "expected proxied response")
assert.Nil(t, resp.CacheMeta)
}
// Restore from the persisted cache's storage
restoredCache := testNewLeaseCache(t, nil)
err := restoredCache.Restore(context.Background(), boltStorage)
assert.NoError(t, err)
// The original mem cache should have all three items
beforeDB, err := lc.db.GetByPrefix(cachememdb.IndexNameID)
require.NoError(t, err)
assert.Len(t, beforeDB, 3)
// There should only be one item in the restored cache: the autoauth token
afterDB, err := restoredCache.db.GetByPrefix(cachememdb.IndexNameID)
require.NoError(t, err)
assert.Len(t, afterDB, 1)
// Just verify that the one item in the restored mem cache matches one in the original mem cache, and that it's the auto-auth token
beforeItem, err := lc.db.Get(cachememdb.IndexNameID, afterDB[0].ID)
require.NoError(t, err)
assert.NotNil(t, beforeItem)
assert.Equal(t, "autoauthtoken", afterDB[0].Token)
assert.Equal(t, cacheboltdb.TokenType, afterDB[0].Type)
}