mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-16 19:47:02 +02:00
* 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>
1233 lines
38 KiB
Go
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)
|
|
}
|