Skip to content

Commit

Permalink
Merge pull request #4 from Lingrino/kv_list
Browse files Browse the repository at this point in the history
KV List
  • Loading branch information
lingrino authored May 13, 2018
2 parents 23dba85 + 35a3043 commit 84232f1
Show file tree
Hide file tree
Showing 7 changed files with 409 additions and 14 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @lingrino
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ test:
docker run -d --name=test-vault -p 8300:8300 \
-e VAULT_DEV_ROOT_TOKEN_ID=hunter2 \
-e VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8300 \
-e VAULT_LOG=debug \
vault:latest && sleep 8
export VAULT_ADDR=http://localhost:8300 && \
export VAULT_TOKEN=hunter2 && \
go test ./...
go test -v ./...
65 changes: 52 additions & 13 deletions vault/client.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package vault

import (
"fmt"

vapi "github.com/hashicorp/vault/api"
"github.com/pkg/errors"
)

// Client is a wrapper around a real Vault API client.
Expand All @@ -23,49 +22,89 @@ func (c *Client) simpleInit() error {

client, err := vapi.NewClient(vapi.DefaultConfig())
if err != nil {
return fmt.Errorf("[FATAL]: simpleInit: Failed to init the vault client: %s", err)
return errors.Wrap(err, "Failed to init the vault client")
}
c.client = client

return err
}

// seed uses a client to write dummy data used for teting to vault
// seed uses a client to write dummy data used for testing to vault
// strings generated here: https://www.random.org/strings
func (c *Client) seed() error {
var err error
var mountPath string

c.client.Sys().EnableAuditWithOptions("audit_stdout", &vapi.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": "stdout",
"log_raw": "true",
},
})

seeds := map[string]map[string]string{
"secret/data/test/foo": {
"test/foo": {
"value": "bar",
},
"secret/data/test/value": {
"test/value": {
"fizz": "buzz",
"foo": "bar",
},
"secret/data/test/fizz": {
"test/fizz": {
"fizz": "buzz",
"foo": "bar",
},
"secret/data/test/HToOeKKD": {
"test/HToOeKKD": {
"3zqxVbJY": "TvOjGxvC",
},
"secret/data/test/inner/WKNC3muM": {
"test/inner/WKNC3muM": {
"IY1C148K": "JxBfEt91",
"iwVzPqbY": "0NH9GlR1",
},
"secret/data/test/inner/A2xlzTfE": {
"test/inner/A2xlzTfE": {
"Eg5ljS7t": "BHRMKjj1",
"quqr32S5": "pcidzSMW",
},
"secret/data/test/inner/again/inner/UCrt6sZT": {
"test/inner/again/inner/UCrt6sZT": {
"Eg5ljS7t": "6F1B5nBg",
"quqr32S5": "81iY4HAN",
"r6R0JUzX": "rs1mCRB5",
},
}

// Seed v1 mount
mountPath = "secretv1/"
err = c.client.Sys().Mount(mountPath, &vapi.MountInput{
Type: "kv",
Options: map[string]string{
"version": "1",
},
})
for path, secret := range seeds {
writePath := mountPath + path
data := make(map[string]interface{})

for k, v := range secret {
data[k] = v
}

_, err = c.client.Logical().Write(writePath, data)
if err != nil {
return errors.Wrapf(err, "Failed to seed vault at path %s", writePath)
}
}

// Seed v2 mount
mountPath = "secretv2/"
c.client.Sys().Mount(mountPath, &vapi.MountInput{
Type: "kv",
Options: map[string]string{
"version": "2",
},
})
for path, secret := range seeds {
writePath := mountPath + "data/" + path
data := make(map[string]interface{})

for k, v := range secret {
Expand All @@ -78,9 +117,9 @@ func (c *Client) seed() error {
"data": data,
}

_, err = c.client.Logical().Write(path, data)
_, err = c.client.Logical().Write(writePath, data)
if err != nil {
return fmt.Errorf("[FATAL]: seed: Failed to seed vault at path %s: %s", path, err)
return errors.Wrapf(err, "Failed to seed vault at path %s", writePath)
}
}
return err
Expand Down
225 changes: 225 additions & 0 deletions vault/kv_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package vault

import (
"fmt"
"sort"
"strings"
"sync"

"github.com/pkg/errors"
)

// KVListInput is the required input for KVList
type KVListInput struct {
Path string
Recurse bool
TrimPathPrefix bool
MountPath string
MountVersion string
}

// NewKVListInput takes a path and returns a kvListInput struct with
// default values to produce similar to what is returned by Vault CLI
func NewKVListInput(p string) *KVListInput {
return &KVListInput{
Path: p,
Recurse: false,
TrimPathPrefix: true,
MountPath: "",
MountVersion: "",
}
}

// listWorkerResult holds the key and any errors from a job
type listWorkerResult struct {
key string
err error
}

// listWorker listens on the inputs channel for new paths to list and then does the
// work of listing those paths and returning the result. If recurse is true any listed keys
// that are folders will be added back to the inputs channel for further processing
func (c *Client) listWorker(inputs chan *KVListInput, results chan<- *listWorkerResult, inputsWG *sync.WaitGroup, resultsWG *sync.WaitGroup) {
// listen on the inputs channel until it is closed
for {
i, more := <-inputs
if more {
// This lets us only get mount info only once, by passing it in future inputs
if i.MountPath == "" {
mountPath, version, err := c.PathMountInfo(i.Path)
if err != nil {
fmt.Println("hi")
resultsWG.Add(1)
results <- &listWorkerResult{
key: "",
err: errors.Wrapf(err, "Failed to describe mount for path %s", i.Path),
}
inputsWG.Done()
continue
}
i.MountPath = mountPath
i.MountVersion = version
}

// For v2 mounts lists happen on mount/metadata/path instead of mount/path
var listPath string
if i.MountVersion == "2" {
listPath = c.PathJoin(i.MountPath, "metadata", strings.TrimPrefix(i.Path, i.MountPath))
} else {
listPath = i.Path
}

// Do the actual list
secret, err := c.client.Logical().List(listPath)
if err != nil {
resultsWG.Add(1)
results <- &listWorkerResult{
key: "",
err: errors.Wrapf(err, "Failed to list path at %s", listPath),
}
inputsWG.Done()
continue
}

// extract list data from the returned secret
if secret == nil || secret.Data == nil {
resultsWG.Add(1)
results <- &listWorkerResult{
key: "",
err: fmt.Errorf("Secret at %s was nil", listPath),
}
inputsWG.Done()
continue

}
keys, ok := secret.Data["keys"]
if !ok || keys == nil {
resultsWG.Add(1)
results <- &listWorkerResult{
key: "",
err: fmt.Errorf("No Data[\"keys\"] in secret at %s", listPath),
}
inputsWG.Done()
continue
}
list, ok := keys.([]interface{})
if !ok {
resultsWG.Add(1)
results <- &listWorkerResult{
key: "",
err: fmt.Errorf("Failed to convert keys to interface at %s", listPath),
}
inputsWG.Done()
continue
}

// For each key, either add it to results or add it back to inputs if recurse and folder
for _, v := range list {
key, ok := v.(string)
if !ok {
resultsWG.Add(1)
results <- &listWorkerResult{
key: "",
err: fmt.Errorf("Failed to assert %s as a string at %s", key, listPath),
}
inputsWG.Done()
continue
}
// If we're recursing and the key is a folder, add it back as an input to be listed
if c.PathIsFolder(key) && i.Recurse {
inputsWG.Add(1)
inputs <- &KVListInput{
Path: c.PathJoin(i.Path, key),
Recurse: i.Recurse,
TrimPathPrefix: i.TrimPathPrefix,
MountPath: i.MountPath,
MountVersion: i.MountVersion,
}
} else if c.PathIsFolder(key) {
resultsWG.Add(1)
results <- &listWorkerResult{
key: c.PathJoin(i.Path, key) + "/",
err: nil,
}

} else {
resultsWG.Add(1)
results <- &listWorkerResult{
key: c.PathJoin(i.Path, key),
err: nil,
}
}
}
inputsWG.Done()
} else {
return
}
}
}

// KVList takes a path and returns a slice of all values at that path
// If Recurse, also list all nested paths/folders
// If TrimPathPrefix, do not prefix keys with leading path
func (c *Client) KVList(i *KVListInput) ([]string, error) {
var err error
var output []string

if i.Path == "" {
return nil, errors.Wrap(err, "Path is not specified")
}

inputs := make(chan *KVListInput, 5)
results := make(chan *listWorkerResult, 5)

var inputsWG sync.WaitGroup
var resultsWG sync.WaitGroup

// Add our first input
inputsWG.Add(1)
inputs <- i

// Listen on results channel and add keys to an output list
go func() {
for {
o, more := <-results
if more {
if o.err != nil {
err = errors.Wrapf(err, "Failed to list path %s", i.Path)
} else {
output = append(output, o.key)
}

resultsWG.Done()
} else {
return
}
}
}()

// Spawn 5 workers if recursing, otherise just one
// TODO - read worker count from configuration
if i.Recurse {
for w := 1; w <= 5; w++ {
go c.listWorker(inputs, results, &inputsWG, &resultsWG)
}
} else {
go c.listWorker(inputs, results, &inputsWG, &resultsWG)
}

// Wait until all lists are complete
inputsWG.Wait()
resultsWG.Wait()
close(inputs)
close(results)

// Remove the prefix if it is not wanted
if i.TrimPathPrefix == true {
for idx, pth := range output {
output[idx] = strings.TrimPrefix(strings.TrimPrefix(pth, i.Path), "/")
}
}

sort.Strings(output)

return output, err
}
Loading

0 comments on commit 84232f1

Please sign in to comment.