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

KV helper methods for api package #15305

Merged
merged 22 commits into from
May 25, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
681faca
Add Read methods for KVClient
digivava May 4, 2022
377a224
Merge branch 'main' into VAULT-5973_api-kv-helpers
digivava May 4, 2022
275f67e
KV write helper
digivava May 5, 2022
8377269
Add changelog
digivava May 5, 2022
ce740af
Add Delete method
digivava May 6, 2022
56aa04b
Use extractVersionMetadata inside extractDataAndVersionMetadata
digivava May 6, 2022
24c6d83
Return nil, nil for v1 writes
digivava May 6, 2022
003a7f3
Add test for extracting version metadata
digivava May 6, 2022
7855382
Merge branch 'digivava/more-api-kv-helpers' into VAULT-5973_api-kv-he…
digivava May 9, 2022
53b2cc1
Merge branch 'main' into VAULT-5973_api-kv-helpers
digivava May 17, 2022
c271d46
Split kv client into v1 and v2-specific clients
digivava May 17, 2022
060a639
Add ability to set options on Put
digivava May 18, 2022
263ae37
Add test for KV helpers
digivava May 18, 2022
b8f93f0
Add custom metadata to top level and allow for getting versions as so…
digivava May 20, 2022
dd5d630
Update tests
digivava May 20, 2022
1df6a35
Separate KV v1 and v2 into different files
digivava May 20, 2022
09ce4df
Add test for GetVersionsAsList, rename Metadata key to VersionMetadat…
digivava May 20, 2022
e682c91
Move structs and godoc comments to more appropriate files
digivava May 20, 2022
4e88f89
Add more tests for extract methods
digivava May 23, 2022
38f94ca
Rework custom metadata helper to be more consistent with other helpers
digivava May 23, 2022
a049bfe
Remove KVSecret from custom metadata test now that we don't append to…
digivava May 23, 2022
ae82f4f
Return early for readability and make test value name less confusing
digivava May 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions api/kv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package api

// A KVSecret is a key-value secret returned by Vault's KV secrets engine,
// and is the most basic type of secret stored in Vault.
//
// Data contains the key-value pairs of the secret itself,
// while Metadata contains a subset of metadata describing
// this particular version of the secret.
// The Metadata field for a KV v1 secret will always be nil, as
// metadata is only supported starting in KV v2.
//
// The Raw field can be inspected for information about the lease,
// and passed to a LifetimeWatcher object for periodic renewal.
type KVSecret struct {
Data map[string]interface{}
VersionMetadata *KVVersionMetadata
CustomMetadata map[string]interface{}
Raw *Secret
}

// KVv1 is used to return a client for reads and writes against
// a KV v1 secrets engine in Vault.
//
// The mount path is the location where the target KV secrets engine resides
// in Vault.
//
// While v1 is not necessarily deprecated, Vault development servers tend to
// use v2 as the version of the KV secrets engine, as this is what's mounted
// by default when a server is started in -dev mode. See the kvv2 struct.
//
// Learn more about the KV secrets engine here:
// https://www.vaultproject.io/docs/secrets/kv
digivava marked this conversation as resolved.
Show resolved Hide resolved
func (c *Client) KVv1(mountPath string) *kvv1 {
return &kvv1{c: c, mountPath: mountPath}
}

// KVv2 is used to return a client for reads and writes against
// a KV v2 secrets engine in Vault.
//
// The mount path is the location where the target KV secrets engine resides
// in Vault.
//
// Vault development servers tend to have "secret" as the mount path,
// as these are the default settings when a server is started in -dev mode.
//
// Learn more about the KV secrets engine here:
// https://www.vaultproject.io/docs/secrets/kv
func (c *Client) KVv2(mountPath string) *kvv2 {
return &kvv2{c: c, mountPath: mountPath}
}
341 changes: 341 additions & 0 deletions api/kv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,341 @@
package api

import (
"reflect"
"testing"
"time"
)

func TestExtractVersionMetadata(t *testing.T) {
digivava marked this conversation as resolved.
Show resolved Hide resolved
t.Parallel()

inputCreatedTimeStr := "2022-05-06T23:02:04.865025Z"
inputDeletionTimeStr := "2022-06-17T01:15:03.279013Z"
expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr)
if err != nil {
t.Fatalf("unable to parse expected created time: %v", err)
}
expectedDeletionTimeParsed, err := time.Parse(time.RFC3339, inputDeletionTimeStr)
if err != nil {
t.Fatalf("unable to parse expected created time: %v", err)
}

testCases := []struct {
name string
input *Secret
expected *KVVersionMetadata
}{
{
name: "a secret",
input: &Secret{
Data: map[string]interface{}{
"data": map[string]interface{}{
"password": "Hashi123",
},
"metadata": map[string]interface{}{
"version": 10,
"created_time": inputCreatedTimeStr,
"deletion_time": "",
"destroyed": false,
"custom_metadata": nil,
},
},
},
expected: &KVVersionMetadata{
Version: 10,
CreatedTime: expectedCreatedTimeParsed,
DeletionTime: time.Time{},
Destroyed: false,
},
},
{
name: "a secret that has been deleted",
input: &Secret{
Data: map[string]interface{}{
"data": map[string]interface{}{
"password": "Hashi123",
},
"metadata": map[string]interface{}{
"version": 10,
"created_time": inputCreatedTimeStr,
"deletion_time": inputDeletionTimeStr,
"destroyed": false,
"custom_metadata": nil,
},
},
},
expected: &KVVersionMetadata{
Version: 10,
CreatedTime: expectedCreatedTimeParsed,
DeletionTime: expectedDeletionTimeParsed,
Destroyed: false,
},
},
{
name: "a response from a Write operation",
input: &Secret{
Data: map[string]interface{}{
"version": 10,
"created_time": inputCreatedTimeStr,
"deletion_time": "",
"destroyed": false,
"custom_metadata": nil,
},
},
expected: &KVVersionMetadata{
Version: 10,
CreatedTime: expectedCreatedTimeParsed,
DeletionTime: time.Time{},
Destroyed: false,
},
},
}

for _, tc := range testCases {
versionMetadata, err := extractVersionMetadata(tc.input)
if err != nil {
t.Fatalf("err: %s", err)
}

if !reflect.DeepEqual(versionMetadata, tc.expected) {
t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, versionMetadata, tc.expected)
}
}
}

func TestExtractDataAndVersionMetadata(t *testing.T) {
t.Parallel()

inputCreatedTimeStr := "2022-05-06T23:02:04.865025Z"
inputDeletionTimeStr := "2022-06-17T01:15:03.279013Z"
expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr)
if err != nil {
t.Fatalf("unable to parse expected created time: %v", err)
}
expectedDeletionTimeParsed, err := time.Parse(time.RFC3339, inputDeletionTimeStr)
if err != nil {
t.Fatalf("unable to parse expected created time: %v", err)
}

readResp := &Secret{
Data: map[string]interface{}{
"data": map[string]interface{}{
"password": "Hashi123",
},
"metadata": map[string]interface{}{
"version": 10,
"created_time": inputCreatedTimeStr,
"deletion_time": "",
"destroyed": false,
"custom_metadata": nil,
},
},
}

readRespDeleted := &Secret{
Data: map[string]interface{}{
"data": nil,
"metadata": map[string]interface{}{
"version": 10,
"created_time": inputCreatedTimeStr,
"deletion_time": inputDeletionTimeStr,
"destroyed": false,
"custom_metadata": nil,
},
},
}

testCases := []struct {
name string
input *Secret
expected *KVSecret
}{
{
name: "a response from a Read operation",
input: readResp,
expected: &KVSecret{
Data: map[string]interface{}{
"password": "Hashi123",
},
VersionMetadata: &KVVersionMetadata{
Version: 10,
CreatedTime: expectedCreatedTimeParsed,
DeletionTime: time.Time{},
Destroyed: false,
},
// it's tempting to test some Secrets with custom_metadata but
// we can't in this test because it isn't until we call the
// addCustomMetadata function that the custom metadata
// gets added onto the struct. See TestAddCustomMetadata.
CustomMetadata: nil,
Raw: readResp,
},
},
{
name: "a secret that has been deleted and thus has nil data",
input: readRespDeleted,
expected: &KVSecret{
Data: nil,
VersionMetadata: &KVVersionMetadata{
Version: 10,
CreatedTime: expectedCreatedTimeParsed,
DeletionTime: expectedDeletionTimeParsed,
Destroyed: false,
},
CustomMetadata: nil,
Raw: readRespDeleted,
},
},
}

for _, tc := range testCases {
dvm, err := extractDataAndVersionMetadata(tc.input)
if err != nil {
t.Fatalf("err: %s", err)
}

if !reflect.DeepEqual(dvm, tc.expected) {
t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, dvm, tc.expected)
}
}
}

func TestExtractFullMetadata(t *testing.T) {
inputCreatedTimeStr := "2022-05-20T00:51:49.419794Z"
expectedCreatedTimeParsed, err := time.Parse(time.RFC3339, inputCreatedTimeStr)
if err != nil {
t.Fatalf("unable to parse expected created time: %v", err)
}

inputUpdatedTimeStr := "2022-05-20T20:23:43.284488Z"
expectedUpdatedTimeParsed, err := time.Parse(time.RFC3339, inputUpdatedTimeStr)
if err != nil {
t.Fatalf("unable to parse expected updated time: %v", err)
}

inputDeletedTimeStr := "2022-05-21T00:05:49.521697Z"
expectedDeletedTimeParsed, err := time.Parse(time.RFC3339, inputDeletedTimeStr)
if err != nil {
t.Fatalf("unable to parse expected deletion time: %v", err)
}

metadataResp := &Secret{
Data: map[string]interface{}{
"cas_required": true,
"created_time": inputCreatedTimeStr,
"current_version": 2,
"custom_metadata": map[string]interface{}{
"org": "eng",
},
"delete_version_after": "200s",
"max_versions": 3,
"oldest_version": 1,
"updated_time": inputUpdatedTimeStr,
"versions": map[string]interface{}{
"2": map[string]interface{}{
"created_time": inputUpdatedTimeStr,
"deletion_time": "",
"destroyed": false,
},
"1": map[string]interface{}{
"created_time": inputCreatedTimeStr,
"deletion_time": inputDeletedTimeStr,
"destroyed": false,
},
},
},
}

testCases := []struct {
name string
input *Secret
expected *KVMetadata
}{
{
name: "a metadata response",
input: metadataResp,
expected: &KVMetadata{
CASRequired: true,
CreatedTime: expectedCreatedTimeParsed,
CurrentVersion: 2,
CustomMetadata: map[string]interface{}{
"org": "eng",
},
DeleteVersionAfter: time.Duration(200 * time.Second),
MaxVersions: 3,
OldestVersion: 1,
UpdatedTime: expectedUpdatedTimeParsed,
Versions: map[string]KVVersionMetadata{
"2": {
Version: 2,
CreatedTime: expectedUpdatedTimeParsed,
DeletionTime: time.Time{},
},
"1": {
Version: 1,
CreatedTime: expectedCreatedTimeParsed,
DeletionTime: expectedDeletedTimeParsed,
},
},
},
},
}

for _, tc := range testCases {
md, err := extractFullMetadata(tc.input)
if err != nil {
t.Fatalf("err: %s", err)
}

if !reflect.DeepEqual(md, tc.expected) {
t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, md, tc.expected)
}
}
}

func TestExtractCustomMetadata(t *testing.T) {
testCases := []struct {
name string
inputAPIResp *Secret
inputKVSecret *KVSecret
expected map[string]interface{}
}{
{
name: "a read response with some custom metadata",
inputAPIResp: &Secret{
Data: map[string]interface{}{
"metadata": map[string]interface{}{
"custom_metadata": map[string]interface{}{"org": "eng"},
},
},
},
inputKVSecret: &KVSecret{
CustomMetadata: nil, // starts with nothing, let's see if it has it after we call our addCustomMetadata method
},
expected: map[string]interface{}{"org": "eng"},
},
{
name: "a write response with some (pre-existing) custom metadata",
inputAPIResp: &Secret{
Data: map[string]interface{}{
"custom_metadata": map[string]interface{}{"org": "eng"},
},
},
inputKVSecret: &KVSecret{
CustomMetadata: nil, // starts with nothing, let's see if it has it after we call our addCustomMetadata method
},
expected: map[string]interface{}{"org": "eng"},
},
}

for _, tc := range testCases {
cm, err := extractCustomMetadata(tc.inputAPIResp)
if err != nil {
t.Fatalf("err: %s", err)
}

if !reflect.DeepEqual(cm, tc.expected) {
t.Fatalf("%s: got\n%#v\nexpected\n%#v\n", tc.name, cm, tc.expected)
}
}
}
Loading