open-vault/vault/external_tests/api/kv_helpers_test.go

628 lines
19 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package api
import (
"context"
"errors"
"reflect"
"testing"
"time"
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"
)
const (
v1MountPath = "secret"
v2MountPath = "secret-v2"
secretPath = "my-secret"
)
var (
client *api.Client
secretData = map[string]interface{}{
"foo": "bar",
}
)
// setupKVv2Test creates the secret that will be used in each KV v2 subtest. It
// returns a function (that should be deferred whenever setupKVv2Test is called)
// which will perform the cleanup of all existing versions of the secret, as
// well as the secret that was written for comparison.
func setupKVv2Test(t *testing.T) (func(t *testing.T), *api.KVSecret) {
writtenSecret, err := client.KVv2(v2MountPath).Put(context.Background(), secretPath, secretData)
if err != nil {
t.Fatal(err)
}
if writtenSecret == nil || writtenSecret.VersionMetadata == nil {
t.Fatal("secret created during kv v2 subtest setup did not have expected contents")
}
return func(t *testing.T) {
err := client.KVv2(v2MountPath).DeleteMetadata(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
}, writtenSecret
}
func TestKVHelpers(t *testing.T) {
t.Parallel()
// initialize test cluster
coreConfig := &vault.CoreConfig{
LogicalBackends: map[string]logical.Factory{
"kv": logicalKv.Factory,
"kv-v2": logicalKv.VersionedKVFactory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
core := cores[0].Core
client = cluster.Cores[0].Client
vault.TestWaitActive(t, core)
// mount the KVv2 backend
// (the test cluster has already mounted the KVv1 backend at "secret")
err := client.Sys().MountWithContext(context.Background(), "secret-v2", &api.MountInput{
Type: "kv-v2",
})
if err != nil {
t.Fatal(err)
}
//// v1 ////
t.Run("kv v1: put, get, and delete data", func(t *testing.T) {
if err := client.KVv1(v1MountPath).Put(context.Background(), secretPath, secretData); err != nil {
t.Fatal(err)
}
secret, err := client.KVv1(v1MountPath).Get(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
if secret.Data["foo"] != "bar" {
t.Fatalf("kv v1 secret did not contain expected value")
}
if err := client.KVv1(v1MountPath).Delete(context.Background(), secretPath); err != nil {
t.Fatal(err)
}
_, err = client.KVv1(v1MountPath).Get(context.Background(), secretPath)
if !errors.Is(err, api.ErrSecretNotFound) {
t.Fatalf("KVv1.Get is expected to return an api.ErrSecretNotFound wrapped error after secret had been deleted; got %v", err)
}
})
t.Run("kv v1: get secret that does not exist", func(t *testing.T) {
_, err = client.KVv1(v1MountPath).Get(context.Background(), "does/not/exist")
if err == nil {
t.Fatalf("KVv1.Get is expected to return an error for a missing secret")
}
if !errors.Is(err, api.ErrSecretNotFound) {
t.Fatalf("KVv1.Get is expected to return an api.ErrSecretNotFound wrapped error for a missing secret; got %v", err)
}
})
//// v2 ////
t.Run("kv v2: get data and full metadata", func(t *testing.T) {
teardownTest, originalSecret := setupKVv2Test(t)
defer teardownTest(t)
secret, err := client.KVv2(v2MountPath).Get(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
if secret.Data["foo"] != "bar" {
t.Fatal("kv v2 secret did not contain expected value")
}
if secret.VersionMetadata.CreatedTime != originalSecret.VersionMetadata.CreatedTime {
t.Fatal("the created_time on the secret did not match the response from when it was created")
}
// get its full metadata
fullMetadata, err := client.KVv2(v2MountPath).GetMetadata(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(secret.CustomMetadata, fullMetadata.CustomMetadata) {
t.Fatalf("custom metadata on the secret does not match the custom metadata in the full metadata")
}
})
t.Run("kv v2: get secret that does not exist", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
_, err = client.KVv2(v1MountPath).Get(context.Background(), "does/not/exist")
if !errors.Is(err, api.ErrSecretNotFound) {
t.Fatalf("KVv2.Get is expected to return an api.ErrSecretNotFound wrapped error for a missing secret; got %v", err)
}
_, err = client.KVv2(v1MountPath).GetMetadata(context.Background(), "does/not/exist")
if !errors.Is(err, api.ErrSecretNotFound) {
t.Fatalf("KVv2.GetMetadata is expected to return an api.ErrSecretNotFound wrapped error for a missing secret; got %v", err)
}
_, err = client.KVv2(v1MountPath).GetVersion(context.Background(), secretPath, 99)
if !errors.Is(err, api.ErrSecretNotFound) {
t.Fatalf("KVv2.GetVersion is expected to return an api.ErrSecretNotFound wrapped error for a missing secret version; got %v", err)
}
})
t.Run("kv v2: multiple versions", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
// create a second version
_, err = client.KVv2(v2MountPath).Put(context.Background(), secretPath, map[string]interface{}{
"foo": "baz",
})
if err != nil {
t.Fatal(err)
}
s2, err := client.KVv2(v2MountPath).Get(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
if s2.Data["foo"] != "baz" {
t.Fatalf("second version of secret did not have expected contents")
}
if s2.VersionMetadata.Version != 2 {
t.Fatalf("wrong version of kv v2 secret was read, expected 2 but got %d", s2.VersionMetadata.Version)
}
})
t.Run("kv v2: delete and undelete", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
// create a second version
_, err = client.KVv2(v2MountPath).Put(context.Background(), secretPath, map[string]interface{}{
"foo": "baz",
})
if err != nil {
t.Fatal(err)
}
// get a specific past version
s1, err := client.KVv2(v2MountPath).GetVersion(context.Background(), secretPath, 1)
if err != nil {
t.Fatal(err)
}
if s1.VersionMetadata.Version != 1 {
t.Fatalf("wrong version of kv v2 secret was read, expected 1 but got %d", s1.VersionMetadata.Version)
}
// delete that version
if err = client.KVv2(v2MountPath).DeleteVersions(context.Background(), secretPath, []int{1}); err != nil {
t.Fatal(err)
}
s1AfterDelete, err := client.KVv2(v2MountPath).GetVersion(context.Background(), secretPath, 1)
if err != nil {
t.Fatal(err)
}
if s1AfterDelete.VersionMetadata.DeletionTime.IsZero() {
t.Fatalf("the deletion_time in the first version of the secret was not updated")
}
if s1AfterDelete.Data != nil {
t.Fatalf("data still exists on the first version of the secret despite this version being deleted")
}
// undelete it
err = client.KVv2(v2MountPath).Undelete(context.Background(), secretPath, []int{1})
if err != nil {
t.Fatal(err)
}
s1AfterUndelete, err := client.KVv2(v2MountPath).GetVersion(context.Background(), secretPath, 1)
if err != nil {
t.Fatal(err)
}
if s1AfterUndelete.Data == nil {
t.Fatalf("data is empty for the first version of the secret despite this version being undeleted")
}
})
t.Run("kv v2: destroy", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
err = client.KVv2(v2MountPath).Destroy(context.Background(), secretPath, []int{1})
if err != nil {
t.Fatal(err)
}
destroyedSecret, err := client.KVv2(v2MountPath).Get(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
if !destroyedSecret.VersionMetadata.Destroyed {
t.Fatalf("expected secret to be destroyed but it wasn't")
}
})
t.Run("kv v2: use named functional options and generic WithOption", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
// check that KVOption works
// WithCheckAndSet
_, err = client.KVv2(v2MountPath).Put(context.Background(), secretPath, map[string]interface{}{
"meow": "woof",
}, api.WithCheckAndSet(99))
// should fail
if err == nil {
t.Fatalf("expected error from trying to update different version from check-and-set value using WithCheckAndSet")
}
// WithOption (generic)
_, err = client.KVv2(v2MountPath).Put(context.Background(), secretPath, map[string]interface{}{
"bow": "wow",
}, api.WithOption("cas", 99))
// should fail
if err == nil {
t.Fatalf("expected error from trying to update different version from check-and-set value using generic WithOption")
}
})
t.Run("kv v2: patch", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
// WithMergeMethod Patch (implicit)
patch, err := client.KVv2(v2MountPath).Patch(context.Background(), secretPath, map[string]interface{}{
"dog": "cat",
})
if err != nil {
t.Fatal(err)
}
if patch.VersionMetadata.Version != 2 {
t.Fatalf("incorrect version %d, expected 2", patch.VersionMetadata.Version)
}
// WithMergeMethod Patch (explicit)
patchExp, err := client.KVv2(v2MountPath).Patch(context.Background(), secretPath, map[string]interface{}{
"rat": "mouse",
}, api.WithMergeMethod(api.KVMergeMethodPatch))
if err != nil {
t.Fatal(err)
}
if patchExp.VersionMetadata.Version != 3 {
t.Fatalf("incorrect version %d, expected 3", patchExp.VersionMetadata.Version)
}
// WithMergeMethod RW
patchRW, err := client.KVv2(v2MountPath).Patch(context.Background(), secretPath, map[string]interface{}{
"bird": "tweet",
}, api.WithMergeMethod(api.KVMergeMethodReadWrite))
if err != nil {
t.Fatal(err)
}
if patchRW.VersionMetadata.Version != 4 {
t.Fatalf("incorrect version %d, expected 4", patchRW.VersionMetadata.Version)
}
secretAfterPatches, err := client.KVv2(v2MountPath).Get(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
_, ok := secretAfterPatches.Data["dog"]
if !ok {
t.Fatalf("secret did not contain data patched with implicit Patch method")
}
_, ok = secretAfterPatches.Data["rat"]
if !ok {
t.Fatalf("secret did not contain data patched with explicit Patch method")
}
_, ok = secretAfterPatches.Data["bird"]
if !ok {
t.Fatalf("secret did not contain data patched with RW method")
}
value, ok := secretAfterPatches.Data["foo"]
if !ok || value != "bar" {
t.Fatalf("secret did not keep original data after patch")
}
// patch an existing field
_, err = client.KVv2(v2MountPath).Patch(context.Background(), secretPath, map[string]interface{}{
"dog": "pug",
})
if err != nil {
t.Fatal(err)
}
patchedFieldKV, err := client.KVv2(v2MountPath).Get(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
v, ok := patchedFieldKV.Data["dog"]
if !ok || v != "pug" {
t.Fatalf("secret's data was not replaced by patch")
}
// delete a key in a secret via patch
_, err = client.KVv2(v2MountPath).Patch(context.Background(), secretPath, map[string]interface{}{
"dog": nil,
})
if err != nil {
t.Fatal(err)
}
deletedFieldKV, err := client.KVv2(v2MountPath).Get(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
_, ok = deletedFieldKV.Data["dog"]
if ok {
t.Fatalf("secret key \"dog\" should have been removed by nil patch")
}
// set a key to an empty string via patch
_, err = client.KVv2(v2MountPath).Patch(context.Background(), secretPath, map[string]interface{}{
"dog": "",
})
if err != nil {
t.Fatal(err)
}
emptyValueKV, err := client.KVv2(v2MountPath).Get(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
v, ok = emptyValueKV.Data["dog"]
if !ok || v != "" {
t.Fatalf("secret key \"dog\" should have an empty string value")
}
})
t.Run("kv v2: patch a secret that does not exist", func(t *testing.T) {
for _, method := range [][]api.KVOption{
{},
{api.WithMergeMethod(api.KVMergeMethodPatch)},
{api.WithMergeMethod(api.KVMergeMethodReadWrite)},
} {
_, err = client.KVv2(v2MountPath).Patch(
context.Background(),
"does/not/exist",
map[string]interface{}{"no": "nope"},
method...,
)
if !errors.Is(err, api.ErrSecretNotFound) {
t.Fatalf("expected an api.ErrSecretNotFound wrapped error from trying to patch something that doesn't exist for %v method; got: %v", method, err)
}
}
})
t.Run("kv v2: roll back to an old version", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
// create a second version
_, err = client.KVv2(v2MountPath).Put(context.Background(), secretPath, map[string]interface{}{
"color": "yellow",
})
if err != nil {
t.Fatal(err)
}
// get versions as list
versions, err := client.KVv2(v2MountPath).GetVersionsAsList(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
expectedLength := 2
if len(versions) != expectedLength {
t.Fatalf("expected there to be %d versions of the secret but got %d", expectedLength, len(versions))
}
if versions[0].Version != 1 || versions[len(versions)-1].Version != expectedLength {
t.Fatalf("versions list is not ordered as expected")
}
// roll back to version 1
rb, err := client.KVv2(v2MountPath).Rollback(context.Background(), secretPath, 1)
if err != nil {
t.Fatal(err)
}
if rb.VersionMetadata.Version != 3 {
t.Fatalf("expected returned secret's version %d to be the latest version, which should be 3", rb.VersionMetadata.Version)
}
// destroy version 1
err = client.KVv2(v2MountPath).Destroy(context.Background(), secretPath, []int{1})
if err != nil {
t.Fatal(err)
}
// roll back but fail
_, err = client.KVv2(v2MountPath).Rollback(context.Background(), secretPath, 1)
if err == nil {
t.Fatalf("expected error from trying to rollback to destroyed version")
}
})
t.Run("kv v2: delete all versions of a secret", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
// create a second version
_, err = client.KVv2(v2MountPath).Put(context.Background(), secretPath, map[string]interface{}{
"color": "yellow",
})
if err != nil {
t.Fatal(err)
}
// delete it all
err = client.KVv2(v2MountPath).DeleteMetadata(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
versions, err := client.KVv2(v2MountPath).GetVersionsAsList(context.Background(), secretPath)
if err == nil {
t.Fatalf("expected to be unable to get list of versions since all metadata was destroyed")
}
if len(versions) > 0 {
t.Fatalf("expected no versions of secret after deleting all metadata")
}
})
t.Run("kv v2: create a secret with metadata but no data", func(t *testing.T) {
// put and patch metadata
////
noDataSecretPath := "empty"
// create a secret with metadata but no data
err = client.KVv2(v2MountPath).PutMetadata(context.Background(), noDataSecretPath, api.KVMetadataPutInput{
DeleteVersionAfter: 5 * time.Hour,
MaxVersions: 5,
CustomMetadata: map[string]interface{}{"ape": "gorilla"},
})
if err != nil {
t.Fatal(err)
}
// get its metadata to make sure it was created successfully
md, err := client.KVv2(v2MountPath).GetMetadata(context.Background(), noDataSecretPath)
if err != nil {
t.Fatal(err)
}
if md.CreatedTime.IsZero() {
t.Fatalf("secret metadata was not populated as expected: %v", err)
}
if len(md.Versions) > 0 {
t.Fatalf("no secret versions should have been created since only metadata was populated")
}
})
t.Run("kv v2: put and patch metadata", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
md, err := client.KVv2(v2MountPath).GetMetadata(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
// replace all modifiable metadata fields
err = client.KVv2(v2MountPath).PutMetadata(context.Background(), secretPath, api.KVMetadataPutInput{
CASRequired: true,
DeleteVersionAfter: 6 * time.Hour,
MaxVersions: 6,
CustomMetadata: map[string]interface{}{"foo": "fwah", "cat": "tabby"},
})
if err != nil {
t.Fatal(err)
}
// check that metadata was replaced
md2, err := client.KVv2(v2MountPath).GetMetadata(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
if md.CASRequired == md2.CASRequired || md.MaxVersions == md2.MaxVersions || md.DeleteVersionAfter == md2.DeleteVersionAfter || reflect.DeepEqual(md.CustomMetadata, md2.CustomMetadata) {
t.Fatalf("metadata fields should have been updated by PutMetadata")
}
// now let's try a patch
maxVersions := 7
err = client.KVv2(v2MountPath).PatchMetadata(context.Background(), secretPath, api.KVMetadataPatchInput{
MaxVersions: &maxVersions,
CustomMetadata: map[string]interface{}{"foo": nil, "rat": "brown"},
})
if err != nil {
t.Fatal(err)
}
// check that the metadata was only partially replaced
md3, err := client.KVv2(v2MountPath).GetMetadata(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
if md2.CASRequired != md3.CASRequired || md2.DeleteVersionAfter != md3.DeleteVersionAfter {
t.Fatalf("expected fields to remain unchanged but they were updated")
}
if md3.MaxVersions == 0 {
t.Fatalf("field was reset to its zero value when it should not have been")
}
if md2.MaxVersions == md3.MaxVersions {
t.Fatalf("expected field to be updated but it remained unchanged")
}
// let's check the custom metadata was updated correctly
if r, ok := md3.CustomMetadata["rat"]; ok {
if r != "brown" {
t.Fatalf("expected value to be \"brown\"")
}
} else {
t.Fatalf("expected there to be a new \"rat\" key")
}
if _, ok := md3.CustomMetadata["foo"]; ok {
t.Fatalf("expected \"foo\" key to be removed")
}
if _, ok := md3.CustomMetadata["cat"]; !ok {
t.Fatalf("did not expect \"cat\" key to be removed")
}
})
t.Run("kv v2: patch with explicit zero values", func(t *testing.T) {
teardownTest, _ := setupKVv2Test(t)
defer teardownTest(t)
// now let's do another patch to test the "explicit zero value" use case
var (
explicitFalse bool
explicitZero int
explicitTimeZero time.Duration
)
err = client.KVv2(v2MountPath).PatchMetadata(context.Background(), secretPath, api.KVMetadataPatchInput{
CASRequired: &explicitFalse,
MaxVersions: &explicitZero,
DeleteVersionAfter: &explicitTimeZero,
CustomMetadata: map[string]interface{}{},
})
if err != nil {
t.Fatal(err)
}
// check that those fields were reset to their zero value
md4, err := client.KVv2(v2MountPath).GetMetadata(context.Background(), secretPath)
if err != nil {
t.Fatal(err)
}
if len(md4.CustomMetadata) > 0 {
t.Fatalf("expected empty map to cause deletion of all custom metadata")
}
if md4.MaxVersions != 0 || md4.CASRequired != false {
t.Fatalf("expected fields to be reset to their zero values but they were %d and %t instead", md4.MaxVersions, md4.CASRequired)
}
if md4.DeleteVersionAfter.String() != "0s" {
t.Fatalf("expected delete-version-after to be reset to its zero value but instead it was %s", md4.DeleteVersionAfter.String())
}
})
}