vault/command/agentproxyshared/cache/lease_cache_test.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

1233 lines
38 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cache
import (
"context"
"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/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"),
})
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"),
})
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,
})
require.NoError(t, err)
return lc
}
func TestCache_ComputeIndexID(t *testing.T) {
type args struct {
req *http.Request
}
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))
}
})
}
}
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 an 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)
}
}
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)
}
idx, err := lc.db.Get(cachememdb.IndexNameRequestPath, "root/", urlPath)
if err != nil {
t.Fatal(err)
}
if idx != nil {
t.Fatalf("expected nil entry, got: %#v", idx)
}
// 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)
}
idx, err = lc.db.Get(cachememdb.IndexNameRequestPath, "root/", urlPath)
if err != nil {
t.Fatal(err)
}
if idx != nil {
t.Fatalf("expected nil entry, got: %#v", idx)
}
}
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}`),
}
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)
}