diff --git a/command/agent/cache/api_proxy_test.go b/command/agent/cache/api_proxy_test.go index d8b9987ce..e2f6e0611 100644 --- a/command/agent/cache/api_proxy_test.go +++ b/command/agent/cache/api_proxy_test.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/vault/helper/namespace" ) -func TestCache_APIProxy(t *testing.T) { +func TestAPIProxy(t *testing.T) { cleanup, client, _, _ := setupClusterAndAgent(namespace.RootContext(nil), t, nil) defer cleanup() diff --git a/command/agent/cache/cache_test.go b/command/agent/cache/cache_test.go index 169cb69fe..6f1e7eff8 100644 --- a/command/agent/cache/cache_test.go +++ b/command/agent/cache/cache_test.go @@ -3,6 +3,7 @@ package cache import ( "context" "fmt" + "io" "io/ioutil" "math/rand" "net" @@ -18,6 +19,7 @@ import ( kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/command/agent/cache/cachememdb" "github.com/hashicorp/vault/command/agent/sink/mock" "github.com/hashicorp/vault/helper/consts" "github.com/hashicorp/vault/helper/logging" @@ -50,13 +52,12 @@ func setupClusterAndAgent(ctx context.Context, t *testing.T, coreConfig *vault.C DisableMlock: true, DisableCache: true, Logger: logging.NewVaultLogger(hclog.Trace), - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, } } - if coreConfig.CredentialBackends == nil { + // Always set up the userpass backend since we use that to generate an admin + // token for the client that will make proxied requests to through the agent. + if coreConfig.CredentialBackends == nil || coreConfig.CredentialBackends["userpass"] == nil { coreConfig.CredentialBackends = map[string]logical.Factory{ "userpass": userpass.Factory, } @@ -935,7 +936,7 @@ func TestCache_NonCacheable(t *testing.T) { } } -func TestCache_AuthResponse(t *testing.T) { +func TestCache_Caching_AuthResponse(t *testing.T) { cleanup, _, testClient, _ := setupClusterAndAgent(namespace.RootContext(nil), t, nil) defer cleanup() @@ -985,7 +986,7 @@ func TestCache_AuthResponse(t *testing.T) { } } -func TestCache_LeaseResponse(t *testing.T) { +func TestCache_Caching_LeaseResponse(t *testing.T) { coreConfig := &vault.CoreConfig{ DisableMlock: true, DisableCache: true, @@ -1064,3 +1065,132 @@ func TestCache_LeaseResponse(t *testing.T) { } } } + +func TestCache_Caching_CacheClear(t *testing.T) { + t.Run("request_path", func(t *testing.T) { + testCachingCacheClearCommon(t, "request_path") + }) + + t.Run("lease", func(t *testing.T) { + testCachingCacheClearCommon(t, "lease") + }) + + t.Run("token", func(t *testing.T) { + testCachingCacheClearCommon(t, "token") + }) + + t.Run("token_accessor", func(t *testing.T) { + testCachingCacheClearCommon(t, "token_accessor") + }) + + t.Run("all", func(t *testing.T) { + testCachingCacheClearCommon(t, "all") + }) +} + +func testCachingCacheClearCommon(t *testing.T, clearType string) { + coreConfig := &vault.CoreConfig{ + DisableMlock: true, + DisableCache: true, + Logger: hclog.NewNullLogger(), + LogicalBackends: map[string]logical.Factory{ + "kv": vault.LeasedPassthroughBackendFactory, + }, + } + + cleanup, client, testClient, leaseCache := setupClusterAndAgent(namespace.RootContext(nil), t, coreConfig) + defer cleanup() + + err := client.Sys().Mount("kv", &api.MountInput{ + Type: "kv", + }) + if err != nil { + t.Fatal(err) + } + + // Write data to the lease-kv backend + _, err = testClient.Logical().Write("kv/foo", map[string]interface{}{ + "value": "bar", + "ttl": "1h", + }) + if err != nil { + t.Fatal(err) + } + + // Proxy this request, agent should cache the response + resp, err := testClient.Logical().Read("kv/foo") + if err != nil { + t.Fatal(err) + } + gotLeaseID := resp.LeaseID + + // Verify the entry exists + idx, err := leaseCache.db.Get(cachememdb.IndexNameLease, gotLeaseID) + if err != nil { + t.Fatal(err) + } + + if idx == nil { + t.Fatalf("expected cached entry, got: %v", idx) + } + + data := map[string]interface{}{ + "type": clearType, + } + + // We need to set the value here depending on what we're trying to test. + // Some values are be static, but others are dynamically generated at runtime. + switch clearType { + case "request_path": + data["value"] = "/v1/kv/foo" + case "lease": + data["value"] = resp.LeaseID + case "token": + data["value"] = testClient.Token() + case "token_accessor": + lookupResp, err := client.Auth().Token().Lookup(testClient.Token()) + if err != nil { + t.Fatal(err) + } + data["value"] = lookupResp.Data["accessor"] + case "all": + default: + t.Fatalf("invalid type provided: %v", clearType) + } + + r := testClient.NewRequest("PUT", consts.AgentPathCacheClear) + if err := r.SetJSONBody(data); err != nil { + t.Fatal(err) + } + + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + apiResp, err := testClient.RawRequestWithContext(ctx, r) + if resp != nil { + defer apiResp.Body.Close() + } + if apiResp != nil && apiResp.StatusCode == 404 { + _, parseErr := api.ParseSecret(apiResp.Body) + switch parseErr { + case nil: + case io.EOF: + default: + t.Fatal(err) + } + } + if err != nil { + t.Fatal(err) + } + + time.Sleep(100 * time.Millisecond) + + // Verify the entry is cleared + idx, err = leaseCache.db.Get(cachememdb.IndexNameLease, gotLeaseID) + if err != nil { + t.Fatal(err) + } + + if idx != nil { + t.Fatalf("expected entry to be nil, got: %v", idx) + } +} diff --git a/command/agent/cache/lease_cache.go b/command/agent/cache/lease_cache.go index a39f5739a..0705171d2 100644 --- a/command/agent/cache/lease_cache.go +++ b/command/agent/cache/lease_cache.go @@ -411,6 +411,14 @@ func computeIndexID(req *SendRequest) (string, error) { // HandleCacheClear returns a handlerFunc that can perform cache clearing operations. func (c *LeaseCache) HandleCacheClear(ctx context.Context) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Only handle POST/PUT requests + switch r.Method { + case http.MethodPost: + case http.MethodPut: + default: + return + } + req := new(cacheClearRequest) if err := jsonutil.DecodeJSONFromReader(r.Body, req); err != nil { if err == io.EOF { diff --git a/command/agent/cache/lease_cache_test.go b/command/agent/cache/lease_cache_test.go index 6c1e9d361..6c854cc68 100644 --- a/command/agent/cache/lease_cache_test.go +++ b/command/agent/cache/lease_cache_test.go @@ -76,7 +76,7 @@ func TestCache_ComputeIndexID(t *testing.T) { } } -func TestCache_LeaseCache_EmptyToken(t *testing.T) { +func TestLeaseCache_EmptyToken(t *testing.T) { responses := []*SendResponse{ &SendResponse{ Response: &api.Response{ @@ -106,7 +106,7 @@ func TestCache_LeaseCache_EmptyToken(t *testing.T) { } } -func TestCache_LeaseCache_SendCacheable(t *testing.T) { +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{ @@ -187,7 +187,7 @@ func TestCache_LeaseCache_SendCacheable(t *testing.T) { } } -func TestCache_LeaseCache_SendNonCacheable(t *testing.T) { +func TesteaseCache_SendNonCacheable(t *testing.T) { responses := []*SendResponse{ &SendResponse{ Response: &api.Response{ @@ -236,7 +236,7 @@ func TestCache_LeaseCache_SendNonCacheable(t *testing.T) { } } -func TestCache_LeaseCache_SendNonCacheableNonTokenLease(t *testing.T) { +func TestLeaseCache_SendNonCacheableNonTokenLease(t *testing.T) { // Create the cache responses := []*SendResponse{ &SendResponse{ @@ -291,7 +291,7 @@ func TestCache_LeaseCache_SendNonCacheableNonTokenLease(t *testing.T) { } } -func TestCache_LeaseCache_HandleCacheClear(t *testing.T) { +func TestLeaseCache_HandleCacheClear(t *testing.T) { lc := testNewLeaseCache(t, nil) handler := lc.HandleCacheClear(context.Background())