Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom metadata to data responses #51

Merged
merged 7 commits into from
Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 28 additions & 18 deletions path_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
}
Expand Down Expand Up @@ -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,
},
}

Expand Down Expand Up @@ -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,
},
}

Expand Down Expand Up @@ -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,
},
}

Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand Down
178 changes: 157 additions & 21 deletions path_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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{}{
Expand All @@ -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{
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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",
Expand All @@ -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{}{
Expand All @@ -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{
Expand All @@ -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{}{
Expand Down