diff --git a/path_data.go b/path_data.go index bb26a18d..0cfcd3fc 100644 --- a/path_data.go +++ b/path_data.go @@ -114,10 +114,11 @@ func (b *versionedKVBackend) pathDataRead() framework.OperationFunc { Data: map[string]interface{}{ "data": nil, "metadata": map[string]interface{}{ - "version": verNum, - "created_time": ptypesTimestampToString(vm.CreatedTime), - "deletion_time": ptypesTimestampToString(vm.DeletionTime), - "destroyed": vm.Destroyed, + "version": verNum, + "created_time": ptypesTimestampToString(vm.CreatedTime), + "deletion_time": ptypesTimestampToString(vm.DeletionTime), + "destroyed": vm.Destroyed, + "custom_metadata": meta.CustomMetadata, }, }, } @@ -340,10 +341,11 @@ func (b *versionedKVBackend) pathDataWrite() framework.OperationFunc { resp := &logical.Response{ Data: map[string]interface{}{ - "version": meta.CurrentVersion, - "created_time": ptypesTimestampToString(vm.CreatedTime), - "deletion_time": ptypesTimestampToString(vm.DeletionTime), - "destroyed": vm.Destroyed, + "version": meta.CurrentVersion, + "created_time": ptypesTimestampToString(vm.CreatedTime), + "deletion_time": ptypesTimestampToString(vm.DeletionTime), + "destroyed": vm.Destroyed, + "custom_metadata": meta.CustomMetadata, }, } @@ -433,10 +435,11 @@ func (b *versionedKVBackend) pathDataPatch() framework.OperationFunc { // or destroyed notFoundResp := &logical.Response{ Data: map[string]interface{}{ - "version": currentVersion, - "created_time": ptypesTimestampToString(versionMetadata.CreatedTime), - "deletion_time": ptypesTimestampToString(versionMetadata.DeletionTime), - "destroyed": versionMetadata.Destroyed, + "version": currentVersion, + "created_time": ptypesTimestampToString(versionMetadata.CreatedTime), + "deletion_time": ptypesTimestampToString(versionMetadata.DeletionTime), + "destroyed": versionMetadata.Destroyed, + "custom_metadata": meta.CustomMetadata, }, } @@ -535,10 +538,11 @@ func (b *versionedKVBackend) pathDataPatch() framework.OperationFunc { resp := &logical.Response{ Data: map[string]interface{}{ - "version": meta.CurrentVersion, - "created_time": ptypesTimestampToString(newVersionMetadata.CreatedTime), - "deletion_time": ptypesTimestampToString(newVersionMetadata.DeletionTime), - "destroyed": newVersionMetadata.Destroyed, + "version": meta.CurrentVersion, + "created_time": ptypesTimestampToString(newVersionMetadata.CreatedTime), + "deletion_time": ptypesTimestampToString(newVersionMetadata.DeletionTime), + "destroyed": newVersionMetadata.Destroyed, + "custom_metadata": meta.CustomMetadata, }, } @@ -650,9 +654,9 @@ func max(a, b uint32) uint32 { return a } -const dataHelpSyn = `Write, Read, and Delete data in the Key-Value Store.` +const dataHelpSyn = `Write, Patch, Read, and Delete data in the Key-Value Store.` const dataHelpDesc = ` -This path takes a key name and based on the opperation stores, retreives or +This path takes a key name and based on the operation stores, retrieves or deletes versions of data. If a write operation is used the endpoint takes an options object and a data @@ -661,6 +665,12 @@ the data object is encrypted and stored in the storage backend. Each write operation for a key creates a new version and does not overwrite the previous data. +A patch operation must be performed on an existing secret. The secret must neither +be deleted nor destroyed. Like a write operation, patch operations accept an +options object and data object. The options object is used to pass some options to +the patch command and the data object is used to perform a partial update on the +current version of the secret and store the encrypted result in the storage backend. + A read operation will return the latest version for a key unless the "version" parameter is set, then it returns the version at that number. diff --git a/path_data_test.go b/path_data_test.go index 4655bead..2101619d 100644 --- a/path_data_test.go +++ b/path_data_test.go @@ -34,29 +34,82 @@ func getBackend(t *testing.T) (logical.Backend, logical.Storage) { return b, config.StorageView } +func keys(m map[string]interface{}) map[string]struct{} { + set := make(map[string]struct{}) + + for k := range m { + set[k] = struct{}{} + } + + return set +} + +// expectedMetadataKeys produces a deterministic set of expected +// metadata fields to ensure consistent shape across all endpoints +func expectedMetadataKeys() map[string]struct{} { + return map[string]struct{}{ + "version": {}, + "created_time": {}, + "deletion_time": {}, + "destroyed": {}, + "custom_metadata": {}, + } +} + func TestVersionedKV_Data_Put(t *testing.T) { b, storage := getBackend(t) + customMetadata := map[string]string{ + "foo": "abc", + "bar": "def", + } + + metadata := map[string]interface{}{ + "custom_metadata": customMetadata, + } + + req := &logical.Request{ + Operation: logical.CreateOperation, + Path: "metadata/foo", + Storage: storage, + Data: metadata, + } + + resp, err := b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("metadata CreateOperation request failed, err: %s, resp %#v", err, resp) + } + data := map[string]interface{}{ "data": map[string]interface{}{ "bar": "baz", }, } - req := &logical.Request{ + req = &logical.Request{ Operation: logical.CreateOperation, Path: "data/foo", Storage: storage, Data: data, } - resp, err := b.HandleRequest(context.Background(), req) + resp, err = b.HandleRequest(context.Background(), req) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) + t.Fatalf("data CreateOperation request failed, err: %s, resp %#v", err, resp) + } + + actualKeys := keys(resp.Data) + + if diff := deep.Equal(actualKeys, expectedMetadataKeys()); len(diff) > 0 { + t.Fatalf("metadata map keys mismatch, diff: %#v", diff) } if resp.Data["version"] != uint64(1) { - t.Fatalf("Bad response: %#v", resp) + t.Fatalf("expected version to be 1, resp: %#v", resp) + } + + if diff := deep.Equal(resp.Data["custom_metadata"], customMetadata); len(diff) > 0 { + t.Fatalf("custom_metadata map mismatch, diff: %#v", diff) } data = map[string]interface{}{ @@ -77,11 +130,21 @@ func TestVersionedKV_Data_Put(t *testing.T) { resp, err = b.HandleRequest(context.Background(), req) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) + t.Fatalf("data CreateOperation request failed, err: %s, resp %#v", err, resp) + } + + actualKeys = keys(resp.Data) + + if diff := deep.Equal(actualKeys, expectedMetadataKeys()); len(diff) > 0 { + t.Fatalf("metadata map keys mismatch, diff: %#v", diff) } if resp.Data["version"] != uint64(2) { - t.Fatalf("Bad response: %#v", resp) + t.Fatalf("expected version to be 2, resp: %#v", resp) + } + + if diff := deep.Equal(resp.Data["custom_metadata"], customMetadata); len(diff) > 0 { + t.Fatalf("custom_metadata map mismatch, diff: %#v", diff) } } @@ -139,11 +202,32 @@ func TestVersionedKV_Data_Get(t *testing.T) { resp, err := b.HandleRequest(context.Background(), req) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) + t.Fatalf("data ReadOperation request failed, err: %s, resp %#v", err, resp) } if resp != nil { - t.Fatalf("Bad response: %#v", resp) + t.Fatalf("expected nil resp for data ReadOperation resp: %#v", resp) + } + + customMetadata := map[string]string{ + "foo": "abc", + "bar": "def", + } + + metadata := map[string]interface{}{ + "custom_metadata": customMetadata, + } + + req = &logical.Request{ + Operation: logical.CreateOperation, + Path: "metadata/foo", + Storage: storage, + Data: metadata, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("metadata CreateOperation request failed, err: %s, resp %#v", err, resp) } data := map[string]interface{}{ @@ -161,11 +245,11 @@ func TestVersionedKV_Data_Get(t *testing.T) { resp, err = b.HandleRequest(context.Background(), req) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("err:%s resp:%#v\n", err, resp) + t.Fatalf("data CreateOperation request failed, err: %s, resp %#v", err, resp) } if resp.Data["version"] != uint64(1) { - t.Fatalf("Bad response: %#v", resp) + t.Fatalf("epxected version to be 1, resp: %#v", resp) } req = &logical.Request{ @@ -183,17 +267,32 @@ func TestVersionedKV_Data_Get(t *testing.T) { t.Fatalf("Bad response: %#v", resp) } - if resp.Data["metadata"].(map[string]interface{})["version"].(uint64) != uint64(1) { - t.Fatalf("Bad response: %#v", resp) + if _, ok := resp.Data["metadata"]; !ok { + t.Fatalf("data ReadOperation resp did not include metadata field, resp: %#v", resp) } - parsed, err := time.Parse(time.RFC3339Nano, resp.Data["metadata"].(map[string]interface{})["created_time"].(string)) + respMetadata := resp.Data["metadata"].(map[string]interface{}) + actualMetadataKeys := keys(respMetadata) + + if diff := deep.Equal(actualMetadataKeys, expectedMetadataKeys()); len(diff) > 0 { + t.Fatalf("metadata map keys mismatch, diff: %#v\n", diff) + } + + if respMetadata["version"].(uint64) != uint64(1) { + t.Fatalf("expected version to be 1, resp: %#v", resp) + } + + parsed, err := time.Parse(time.RFC3339Nano, respMetadata["created_time"].(string)) if err != nil { - t.Fatal(err) + t.Fatalf("failed to parse created_time: %#v", respMetadata["created_time"]) } if !parsed.After(time.Now().Add(-1*time.Minute)) || !parsed.Before(time.Now()) { - t.Fatalf("Bad response: %#v", resp) + t.Fatalf("invalid created_time value: %#v", respMetadata["created_time"]) + } + + if diff := deep.Equal(respMetadata["custom_metadata"], customMetadata); len(diff) > 0 { + t.Fatalf("custom_metadata mismatch, diff: %#v\n", diff) } } @@ -698,6 +797,27 @@ func TestVersionedKV_Patch_NoData(t *testing.T) { func TestVersionedKV_Patch_Success(t *testing.T) { b, storage := getBackend(t) + customMetadata := map[string]string{ + "foo": "abc", + "bar": "def", + } + + metadata := map[string]interface{}{ + "custom_metadata": customMetadata, + } + + req := &logical.Request{ + Operation: logical.CreateOperation, + Path: "metadata/foo", + Storage: storage, + Data: metadata, + } + + resp, err := b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("metadata CreateOperation request failed, err: %s, resp %#v", err, resp) + } + data := map[string]interface{}{ "data": map[string]interface{}{ "bar": "baz", @@ -707,20 +827,26 @@ func TestVersionedKV_Patch_Success(t *testing.T) { }, } - req := &logical.Request{ + req = &logical.Request{ Operation: logical.CreateOperation, Path: "data/foo", Storage: storage, Data: data, } - resp, err := b.HandleRequest(context.Background(), req) + resp, err = b.HandleRequest(context.Background(), req) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("CreateOperation request failed - err:%s resp:%#v\n", err, resp) + t.Fatalf("data CreateOperation request failed - err:%s resp:%#v\n", err, resp) + } + + actualKeys := keys(resp.Data) + + if diff := deep.Equal(actualKeys, expectedMetadataKeys()); len(diff) > 0 { + t.Fatalf("metadata map keys mismatch, diff: %#v", diff) } if resp.Data["version"] != uint64(1) { - t.Fatalf("Bad response: %#v", resp) + t.Fatalf("expected version to be 1, resp: %#v", resp) } data = map[string]interface{}{ @@ -746,7 +872,17 @@ func TestVersionedKV_Patch_Success(t *testing.T) { resp, err = b.HandleRequest(context.Background(), req) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("PatchOperation request failed - err:%s resp:%#v\n", err, resp) + t.Fatalf("data PatchOperation request failed - err:%s resp:%#v\n", err, resp) + } + + actualKeys = keys(resp.Data) + + if diff := deep.Equal(actualKeys, expectedMetadataKeys()); len(diff) > 0 { + t.Fatalf("metadata map keys mismatch, diff: %#v", diff) + } + + if resp.Data["version"] != uint64(2) { + t.Fatalf("expected version to be 2, resp: %#v", resp) } req = &logical.Request{ @@ -758,7 +894,7 @@ func TestVersionedKV_Patch_Success(t *testing.T) { resp, err = b.HandleRequest(context.Background(), req) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("Readperation request failed - err:%s resp:%#v\n", err, resp) + t.Fatalf("data ReadOperation request failed - err:%s resp:%#v\n", err, resp) } expectedData := map[string]interface{}{