diff --git a/go.mod b/go.mod index 592eb72cb..689eb7a58 100644 --- a/go.mod +++ b/go.mod @@ -112,7 +112,7 @@ require ( github.com/hashicorp/vault-plugin-secrets-azure v0.11.4 github.com/hashicorp/vault-plugin-secrets-gcp v0.11.2 github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0 - github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6 + github.com/hashicorp/vault-plugin-secrets-kv v0.11.0 github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1 github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0 github.com/hashicorp/vault-plugin-secrets-terraform v0.3.0 diff --git a/go.sum b/go.sum index 8488e3387..7c092fb21 100644 --- a/go.sum +++ b/go.sum @@ -992,8 +992,8 @@ github.com/hashicorp/vault-plugin-secrets-gcp v0.11.2 h1:IsNnBsat7/AsiVKSrlAHlIN github.com/hashicorp/vault-plugin-secrets-gcp v0.11.2/go.mod h1:ndpmRkIPHW5UYqv2nn2AJNVZsucJ8lY2bp5i5Ngvhuc= github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0 h1:0Vi5WEIpZctk/ZoRClodV9WCnM/lCzw9XekMhRZdo8k= github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0/go.mod h1:6DPwGu8oGR1sZRpjwkcAnrQZWQuAJ/Ph+rQHfUo1Yf4= -github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6 h1:Z3NnaIBragxW6iTW7OnvklRzZSZdaidxjs/vkCneGAg= -github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6/go.mod h1:9V2Ecim3m/qw+YAQelUeFADqZ1GVo8xwoLqfKsqh9pI= +github.com/hashicorp/vault-plugin-secrets-kv v0.11.0 h1:xSvgh7ObLo3MhVzmGhOxiWND++cTP/QM2LxTmpYim/Q= +github.com/hashicorp/vault-plugin-secrets-kv v0.11.0/go.mod h1:9V2Ecim3m/qw+YAQelUeFADqZ1GVo8xwoLqfKsqh9pI= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1 h1:Maewon4nu0KL1ALBOvL6Rsj+Qyr9hdULWflyMz7+9nk= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1/go.mod h1:PLx2vxXukfsKsDRo/PlG4fxmJ1d+H2h82wT3vf4buuI= github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0 h1:d6N/aMlklMfEacyiIuu5ZnTlADhGkGZkDrOtQXBRuhI= diff --git a/vault/external_tests/kv/kv_subkeys_test.go b/vault/external_tests/kv/kv_subkeys_test.go new file mode 100644 index 000000000..866abc528 --- /dev/null +++ b/vault/external_tests/kv/kv_subkeys_test.go @@ -0,0 +1,328 @@ +package kv + +import ( + "context" + "net/http" + "testing" + + "github.com/go-test/deep" + logicalKv "github.com/hashicorp/vault-plugin-secrets-kv" + "github.com/hashicorp/vault/api" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault" +) + +// TestKV_Subkeys_NotFound issues a read to the subkeys endpoint for a path +// that does not exist. A 400 status should be returned. +func TestKV_Subkeys_NotFound(t *testing.T) { + coreConfig := &vault.CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "kv": logicalKv.VersionedKVFactory, + }, + } + + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + + cluster.Start() + defer cluster.Cleanup() + + cores := cluster.Cores + + core := cores[0].Core + c := cluster.Cores[0].Client + vault.TestWaitActive(t, core) + + // Mount a KVv2 backend + err := c.Sys().Mount("kv", &api.MountInput{ + Type: "kv-v2", + }) + if err != nil { + t.Fatal(err) + } + + req := c.NewRequest("GET", "/v1/kv/subkeys/foo") + apiResp, err := c.RawRequestWithContext(context.Background(), req) + + if err == nil || apiResp == nil { + t.Fatalf("expected subkeys request to fail, err :%v, resp: %#v", err, apiResp) + } + + if apiResp.StatusCode != http.StatusNotFound { + t.Fatalf("expected subkeys request to fail with %d status code, resp: %#v", http.StatusNotFound, apiResp) + } +} + +// TestKV_Subkeys_Deleted writes a single version of a secret to the KVv2 +// secret engine. The secret is subsequently deleted. A read to the subkeys +// endpoint should return a 400 status with a nil "subkeys" value and the +// "deletion_time" key in the "metadata" key should be not be empty. +func TestKV_Subkeys_Deleted(t *testing.T) { + coreConfig := &vault.CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "kv": logicalKv.VersionedKVFactory, + }, + } + + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + + cluster.Start() + defer cluster.Cleanup() + + cores := cluster.Cores + + core := cores[0].Core + c := cluster.Cores[0].Client + vault.TestWaitActive(t, core) + + // Mount a KVv2 backend + err := c.Sys().Mount("kv", &api.MountInput{ + Type: "kv-v2", + }) + if err != nil { + t.Fatal(err) + } + + kvData := map[string]interface{}{ + "data": map[string]interface{}{ + "bar": "a", + }, + } + + resp, err := c.Logical().Write("kv/data/foo", kvData) + if err != nil { + t.Fatalf("write failed, err :%v, resp: %#v", err, resp) + } + + resp, err = c.Logical().Delete("kv/data/foo") + if err != nil { + t.Fatalf("delete failed, err :%v, resp: %#v", err, resp) + } + + req := c.NewRequest("GET", "/v1/kv/subkeys/foo") + apiResp, err := c.RawRequestWithContext(context.Background(), req) + if resp != nil { + defer apiResp.Body.Close() + } + + if err == nil || apiResp == nil { + t.Fatalf("expected subkeys request to fail, err :%v, resp: %#v", err, apiResp) + } + + if apiResp.StatusCode != http.StatusNotFound { + t.Fatalf("expected subkeys request to fail with %d status code, resp: %#v", http.StatusNotFound, apiResp) + } + + secret, err := api.ParseSecret(apiResp.Body) + if err != nil { + t.Fatalf("failed to parse resp body, err: %v", err) + } + + subkeys, ok := secret.Data["subkeys"] + if !ok { + t.Fatalf("key \"subkeys\" not found in response") + } + + if subkeys != nil { + t.Fatalf("expected nil subkeys, actual: %#v", subkeys) + } + + metadata, ok := secret.Data["metadata"].(map[string]interface{}) + + if !ok { + t.Fatalf("metadata not present in response or invalid, metadata: %#v", secret.Data["metadata"]) + } + + if deletionTime, ok := metadata["deletion_time"].(string); !ok || deletionTime == "" { + t.Fatalf("metadata does not contain deletion time, metadata: %#v", metadata) + } +} + +// TestKV_Subkeys_Destroyed writes a single version of a secret to the KVv2 +// secret engine. The secret is subsequently destroyed. A read to the subkeys +// endpoint should return a 400 status with a nil "subkeys" value and the +// "destroyed" key in the "metadata" key should be set to true. +func TestKV_Subkeys_Destroyed(t *testing.T) { + coreConfig := &vault.CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "kv": logicalKv.VersionedKVFactory, + }, + } + + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + + cluster.Start() + defer cluster.Cleanup() + + cores := cluster.Cores + + core := cores[0].Core + c := cluster.Cores[0].Client + vault.TestWaitActive(t, core) + + // Mount a KVv2 backend + err := c.Sys().Mount("kv", &api.MountInput{ + Type: "kv-v2", + }) + if err != nil { + t.Fatal(err) + } + + kvData := map[string]interface{}{ + "data": map[string]interface{}{ + "bar": "a", + }, + } + + resp, err := c.Logical().Write("kv/data/foo", kvData) + if err != nil { + t.Fatalf("write failed, err :%v, resp: %#v", err, resp) + } + + destroyVersions := map[string]interface{}{ + "versions": []int{1}, + } + + resp, err = c.Logical().Write("kv/destroy/foo", destroyVersions) + if err != nil { + t.Fatalf("destroy failed, err :%v, resp: %#v", err, resp) + } + + req := c.NewRequest("GET", "/v1/kv/subkeys/foo") + apiResp, err := c.RawRequestWithContext(context.Background(), req) + if apiResp != nil { + defer apiResp.Body.Close() + } + + if err == nil || apiResp == nil { + t.Fatalf("expected subkeys request to fail, err :%v, resp: %#v", err, apiResp) + } + + if apiResp.StatusCode != http.StatusNotFound { + t.Fatalf("expected subkeys request to fail with %d status code, resp: %#v", http.StatusNotFound, apiResp) + } + + secret, err := api.ParseSecret(apiResp.Body) + if err != nil { + t.Fatalf("failed to parse resp body, err: %v", err) + } + + subkeys, ok := secret.Data["subkeys"] + if !ok { + t.Fatalf("key \"subkeys\" not found in response") + } + + if subkeys != nil { + t.Fatalf("expected nil subkeys, actual: %#v", subkeys) + } + + metadata, ok := secret.Data["metadata"].(map[string]interface{}) + + if !ok { + t.Fatalf("metadata not present in response or invalid, metadata: %#v", secret.Data["metadata"]) + } + + if destroyed, ok := metadata["destroyed"].(bool); !ok || !destroyed { + t.Fatalf("expected destroyed to be true, metadata: %#v", metadata) + } +} + +// TestKV_Subkeys_CurrentVersion writes multiples versions of a secret to the +// KVv2 secret engine. It ensures that the subkeys endpoint returns a 200 status +// and current version of the secret. +func TestKV_Subkeys_CurrentVersion(t *testing.T) { + coreConfig := &vault.CoreConfig{ + LogicalBackends: map[string]logical.Factory{ + "kv": logicalKv.VersionedKVFactory, + }, + } + + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + + cluster.Start() + defer cluster.Cleanup() + + cores := cluster.Cores + + core := cores[0].Core + c := cluster.Cores[0].Client + vault.TestWaitActive(t, core) + + // Mount a KVv2 backend + err := c.Sys().Mount("kv", &api.MountInput{ + Type: "kv-v2", + }) + if err != nil { + t.Fatal(err) + } + + kvData := map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "does-not-matter", + "bar": map[string]interface{}{ + "a": map[string]interface{}{ + "c": "does-not-matter", + }, + "b": map[string]interface{}{}, + }, + }, + } + + resp, err := c.Logical().Write("kv/data/foo", kvData) + if err != nil { + t.Fatalf("write failed, err :%v, resp: %#v", err, resp) + } + + kvData = map[string]interface{}{ + "data": map[string]interface{}{ + "baz": "does-not-matter", + }, + } + + resp, err = c.Logical().JSONMergePatch(context.Background(), "kv/data/foo", kvData) + if err != nil { + t.Fatalf("patch failed, err :%v, resp: %#v", err, resp) + } + + req := c.NewRequest("GET", "/v1/kv/subkeys/foo") + apiResp, err := c.RawRequestWithContext(context.Background(), req) + if resp != nil { + defer apiResp.Body.Close() + } + + if err != nil || apiResp == nil { + t.Fatalf("subkeys request failed, err :%v, resp: %#v", err, apiResp) + } + + if apiResp.StatusCode != http.StatusOK { + t.Fatalf("expected subkeys request to succeed with %d status code, resp: %#v", http.StatusOK, apiResp) + } + + secret, err := api.ParseSecret(apiResp.Body) + if err != nil { + t.Fatalf("failed to parse resp body, err: %v", err) + } + + expectedSubkeys := map[string]interface{}{ + "foo": nil, + "bar": map[string]interface{}{ + "a": map[string]interface{}{ + "c": nil, + }, + "b": nil, + }, + "baz": nil, + } + + if diff := deep.Equal(secret.Data["subkeys"], expectedSubkeys); len(diff) > 0 { + t.Fatalf("resp and expected data mismatch, diff: %#v", diff) + } +}