command/kv: Add a "kv" subcommand for using the key-value store (#4168)

* Add more cli subcommands

* Add metadata commands

* Add more subcommands

* Update cli

* Move archive commands to delete

* Add helpers for making http calls to the kv backend

* rename cli header

* Format the various maps from kv

* Add list command

* Update help text

* Add a command to enable versioning on a backend

* Rename enable-versions command

* Some review feedback

* Fix listing of top level keys

* Fix issue when metadata is nil

* Add test for lising top level keys

* Fix some typos

* Add a note about deleting all versions
This commit is contained in:
Brian Kassouf 2018-03-21 15:02:41 -07:00 committed by GitHub
parent 695eae6ede
commit 5c84c36915
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1661 additions and 3 deletions

View File

@ -675,6 +675,90 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
},
}, nil
},
"kv": func() (cli.Command, error) {
return &KVCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv put": func() (cli.Command, error) {
return &KVPutCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv get": func() (cli.Command, error) {
return &KVGetCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv delete": func() (cli.Command, error) {
return &KVDeleteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv list": func() (cli.Command, error) {
return &KVListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv destroy": func() (cli.Command, error) {
return &KVDestroyCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv undelete": func() (cli.Command, error) {
return &KVUndeleteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv enable-versioning": func() (cli.Command, error) {
return &KVEnableVersioningCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv metadata": func() (cli.Command, error) {
return &KVMetadataCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv metadata put": func() (cli.Command, error) {
return &KVMetadataPutCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv metadata get": func() (cli.Command, error) {
return &KVMetadataGetCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"kv metadata delete": func() (cli.Command, error) {
return &KVMetadataDeleteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
}
// Deprecated commands

View File

@ -134,6 +134,9 @@ func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{})
return t.OutputList(ui, secret, data)
case []string:
return t.OutputList(ui, nil, data)
case map[string]interface{}:
t.OutputMap(ui, data.(map[string]interface{}))
return nil
default:
return errors.New("Cannot use the table formatter for this type")
}
@ -261,6 +264,34 @@ func (t TableFormatter) OutputSecret(ui cli.Ui, secret *api.Secret) error {
return nil
}
func (t TableFormatter) OutputMap(ui cli.Ui, data map[string]interface{}) {
out := make([]string, 0, len(data)+1)
if len(data) > 0 {
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, data[k]))
}
}
// If we got this far and still don't have any data, there's nothing to print,
// sorry.
if len(out) == 0 {
return
}
// Prepend the header
out = append([]string{"Key" + hopeDelim + "Value"}, out...)
ui.Output(tableOutput(out, &columnize.Config{
Delim: hopeDelim,
}))
}
// OutputSealStatus will print *api.SealStatusResponse in the CLI according to the format provided
func OutputSealStatus(ui cli.Ui, client *api.Client, status *api.SealStatusResponse) int {
switch Format(ui) {

52
command/kv.go Normal file
View File

@ -0,0 +1,52 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*KVCommand)(nil)
type KVCommand struct {
*BaseCommand
}
func (c *KVCommand) Synopsis() string {
return "Interact with Vault's Key-Value storage"
}
func (c *KVCommand) Help() string {
helpText := `
Usage: vault kv <subcommand> [options] [args]
This command has subcommands for interacting with Vault's key-value
store. Here are some simple examples, and more detailed examples are
available in the subcommands or the documentation.
Create or update the key named "foo" in the "secret" mount with the value
"bar=baz":
$ vault kv put secret/foo bar=baz
Read this value back:
$ vault kv get secret/foo
Get metadata for the key:
$ vault kv metadata get secret/foo
Get a specific version of the key:
$ vault kv get -version=1 secret/foo
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *KVCommand) Run(args []string) int {
return cli.RunResultHelp
}

141
command/kv_delete.go Normal file
View File

@ -0,0 +1,141 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVDeleteCommand)(nil)
var _ cli.CommandAutocomplete = (*KVDeleteCommand)(nil)
type KVDeleteCommand struct {
*BaseCommand
flagVersions []string
}
func (c *KVDeleteCommand) Synopsis() string {
return "Deletes versions in the KV store"
}
func (c *KVDeleteCommand) Help() string {
helpText := `
Usage: vault kv delete [options] PATH
Deletes the data for the provided version and path in the key-value store. The
versioned data will not be fully removed, but marked as deleted and will no
longer be returned in normal get requests.
To delete the latest version of the key "foo":
$ vault kv delete secret/foo
To delete version 3 of key foo:
$ vault kv delete -versions=3 secret/foo
To delete all versions and metadata, see the "vault kv metadata" subcommand.
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVDeleteCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
// Common Options
f := set.NewFlagSet("Common Options")
f.StringSliceVar(&StringSliceVar{
Name: "versions",
Target: &c.flagVersions,
Default: nil,
Usage: `Specifies the version numbers to delete.`,
})
return set
}
func (c *KVDeleteCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultFiles()
}
func (c *KVDeleteCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVDeleteCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
path := sanitizePath(args[0])
var err error
if len(c.flagVersions) > 0 {
err = c.deleteVersions(path, c.flagVersions)
} else {
err = c.deleteLatest(path)
}
if err != nil {
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err))
return 2
}
c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", path))
return 0
}
func (c *KVDeleteCommand) deleteLatest(path string) error {
var err error
path, err = addPrefixToVKVPath(path, "data")
if err != nil {
return err
}
client, err := c.Client()
if err != nil {
return err
}
_, err = kvDeleteRequest(client, path)
return err
}
func (c *KVDeleteCommand) deleteVersions(path string, versions []string) error {
var err error
path, err = addPrefixToVKVPath(path, "delete")
if err != nil {
return err
}
data := map[string]interface{}{
"versions": c.flagVersions,
}
client, err := c.Client()
if err != nil {
return err
}
_, err = kvWriteRequest(client, path, data)
return err
}

119
command/kv_destroy.go Normal file
View File

@ -0,0 +1,119 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVDestroyCommand)(nil)
var _ cli.CommandAutocomplete = (*KVDestroyCommand)(nil)
type KVDestroyCommand struct {
*BaseCommand
flagVersions []string
}
func (c *KVDestroyCommand) Synopsis() string {
return "Permanently removes one or more versions in the KV store"
}
func (c *KVDestroyCommand) Help() string {
helpText := `
Usage: vault kv destroy [options] KEY
Permanently removes the specified versions' data from the key-value store. If
no key exists at the path, no action is taken.
To destroy version 3 of key foo:
$ vault kv destroy -versions=3 secret/foo
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVDestroyCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.StringSliceVar(&StringSliceVar{
Name: "versions",
Target: &c.flagVersions,
Default: nil,
Usage: `Specifies the version numbers to destroy.`,
})
return set
}
func (c *KVDestroyCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *KVDestroyCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVDestroyCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
if len(c.flagVersions) == 0 {
c.UI.Error("No versions provided, use the \"-versions\" flag to specify the version to destroy.")
return 1
}
var err error
path := sanitizePath(args[0])
path, err = addPrefixToVKVPath(path, "destroy")
if err != nil {
c.UI.Error(err.Error())
return 2
}
data := map[string]interface{}{
"versions": c.flagVersions,
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
secret, err := kvWriteRequest(client, path, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
return 2
}
if secret == nil {
// Don't output anything unless using the "table" format
if Format(c.UI) == "table" {
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path))
}
return 0
}
return OutputSecret(c.UI, secret)
}

View File

@ -0,0 +1,89 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVEnableVersioningCommand)(nil)
var _ cli.CommandAutocomplete = (*KVEnableVersioningCommand)(nil)
type KVEnableVersioningCommand struct {
*BaseCommand
}
func (c *KVEnableVersioningCommand) Synopsis() string {
return "Turns on versioning for a KV store"
}
func (c *KVEnableVersioningCommand) Help() string {
helpText := `
Usage: vault kv enable-versions [options] KEY
This command turns on versioning for the backend at the provided path.
$ vault kv enable-versions secret
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVEnableVersioningCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
return set
}
func (c *KVEnableVersioningCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *KVEnableVersioningCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVEnableVersioningCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
// Append a trailing slash to indicate it's a path in output
mountPath := ensureTrailingSlash(sanitizePath(args[0]))
if err := client.Sys().TuneMount(mountPath, api.MountConfigInput{
Options: map[string]string{
"versioned": "true",
},
}); err != nil {
c.UI.Error(fmt.Sprintf("Error tuning secrets engine %s: %s", mountPath, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Tuned the secrets engine at: %s", mountPath))
return 0
}

137
command/kv_get.go Normal file
View File

@ -0,0 +1,137 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVGetCommand)(nil)
var _ cli.CommandAutocomplete = (*KVGetCommand)(nil)
type KVGetCommand struct {
*BaseCommand
flagVersion int
}
func (c *KVGetCommand) Synopsis() string {
return "Retrieves data from the KV store"
}
func (c *KVGetCommand) Help() string {
helpText := `
Usage: vault kv get [options] KEY
Retrieves the value from Vault's key-value store at the given key name. If no
key exists with that name, an error is returned. If a key exists with that
name but has no data, nothing is returned.
$ vault kv get secret/foo
To view the given key name at a specific version in time, specify the "-version"
flag:
$ vault kv get -version=1 secret/foo
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVGetCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.IntVar(&IntVar{
Name: "version",
Target: &c.flagVersion,
Default: 0,
Usage: `If passed, the value at the version number will be returned.`,
})
return set
}
func (c *KVGetCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *KVGetCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVGetCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
path := sanitizePath(args[0])
path, err = addPrefixToVKVPath(path, "data")
if err != nil {
c.UI.Error(err.Error())
return 2
}
var versionParam map[string]string
if c.flagVersion > 0 {
versionParam = map[string]string{
"version": fmt.Sprintf("%d", c.flagVersion),
}
}
secret, err := kvReadRequest(client, path, versionParam)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err))
return 2
}
if secret == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
return 2
}
if c.flagField != "" {
return PrintRawField(c.UI, secret, c.flagField)
}
// If we have wrap info print the secret normally.
if secret.WrapInfo != nil || c.flagFormat != "table" {
return OutputSecret(c.UI, secret)
}
if metadata, ok := secret.Data["metadata"]; ok && metadata != nil {
c.UI.Info(getHeaderForMap("Metadata", metadata.(map[string]interface{})))
OutputData(c.UI, metadata)
c.UI.Info("")
}
if data, ok := secret.Data["data"]; ok && data != nil {
c.UI.Info(getHeaderForMap("Data", data.(map[string]interface{})))
OutputData(c.UI, data)
}
return 0
}

143
command/kv_helpers.go Normal file
View File

@ -0,0 +1,143 @@
package command
import (
"errors"
"fmt"
"net/http"
"path"
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/consts"
)
func kvReadRequest(client *api.Client, path string, params map[string]string) (*api.Secret, error) {
r := client.NewRequest("GET", "/v1/"+path)
if r.Headers == nil {
r.Headers = http.Header{}
}
r.Headers.Add(consts.VaultKVCLIClientHeader, "v1")
for k, v := range params {
r.Params.Set(k, v)
}
resp, err := client.RawRequest(r)
if resp != nil {
defer resp.Body.Close()
}
if resp != nil && resp.StatusCode == 404 {
return nil, nil
}
if err != nil {
return nil, err
}
return api.ParseSecret(resp.Body)
}
func kvListRequest(client *api.Client, path string) (*api.Secret, error) {
r := client.NewRequest("LIST", "/v1/"+path)
if r.Headers == nil {
r.Headers = http.Header{}
}
r.Headers.Add(consts.VaultKVCLIClientHeader, "v1")
// Set this for broader compatibility, but we use LIST above to be able to
// handle the wrapping lookup function
r.Method = "GET"
r.Params.Set("list", "true")
resp, err := client.RawRequest(r)
if resp != nil {
defer resp.Body.Close()
}
if resp != nil && resp.StatusCode == 404 {
return nil, nil
}
if err != nil {
return nil, err
}
return api.ParseSecret(resp.Body)
}
func kvWriteRequest(client *api.Client, path string, data map[string]interface{}) (*api.Secret, error) {
r := client.NewRequest("PUT", "/v1/"+path)
if r.Headers == nil {
r.Headers = http.Header{}
}
r.Headers.Add(consts.VaultKVCLIClientHeader, "v1")
if err := r.SetJSONBody(data); err != nil {
return nil, err
}
resp, err := client.RawRequest(r)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return nil, err
}
if resp.StatusCode == 200 {
return api.ParseSecret(resp.Body)
}
return nil, nil
}
func kvDeleteRequest(client *api.Client, path string) (*api.Secret, error) {
r := client.NewRequest("DELETE", "/v1/"+path)
if r.Headers == nil {
r.Headers = http.Header{}
}
r.Headers.Add(consts.VaultKVCLIClientHeader, "v1")
resp, err := client.RawRequest(r)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return nil, err
}
if resp.StatusCode == 200 {
return api.ParseSecret(resp.Body)
}
return nil, nil
}
func addPrefixToVKVPath(p, apiPrefix string) (string, error) {
parts := strings.SplitN(p, "/", 2)
if len(parts) != 2 {
return "", errors.New("Invalid path")
}
return path.Join(parts[0], apiPrefix, parts[1]), nil
}
func getHeaderForMap(header string, data map[string]interface{}) string {
maxKey := 0
for k := range data {
if len(k) > maxKey {
maxKey = len(k)
}
}
// 4 for the column spaces and 5 for the len("value")
totalLen := maxKey + 4 + 5
equalSigns := totalLen - (len(header) + 2)
// If we have zero or fewer equal signs bump it back up to two on either
// side of the header.
if equalSigns <= 0 {
equalSigns = 4
}
// If the number of equal signs is not divisible by two add a sign.
if equalSigns%2 != 0 {
equalSigns = equalSigns + 1
}
return fmt.Sprintf("%s %s %s", strings.Repeat("=", equalSigns/2), header, strings.Repeat("=", equalSigns/2))
}

104
command/kv_list.go Normal file
View File

@ -0,0 +1,104 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVListCommand)(nil)
var _ cli.CommandAutocomplete = (*KVListCommand)(nil)
type KVListCommand struct {
*BaseCommand
}
func (c *KVListCommand) Synopsis() string {
return "List data or secrets"
}
func (c *KVListCommand) Help() string {
helpText := `
Usage: vault kv list [options] PATH
Lists data from Vault's key-value store at the given path.
List values under the "my-app" folder of the key-value store:
$ vault kv list secret/my-app/
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVListCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
}
func (c *KVListCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultFolders()
}
func (c *KVListCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVListCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
path := ensureTrailingSlash(sanitizePath(args[0]))
path, err = addPrefixToVKVPath(path, "metadata")
if err != nil {
c.UI.Error(err.Error())
return 2
}
secret, err := kvListRequest(client, path)
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err))
return 2
}
if secret == nil || secret.Data == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
return 2
}
// If the secret is wrapped, return the wrapped response.
if secret.WrapInfo != nil && secret.WrapInfo.TTL != 0 {
return OutputSecret(c.UI, secret)
}
if _, ok := extractListData(secret); !ok {
c.UI.Error(fmt.Sprintf("No entries found at %s", path))
return 2
}
return OutputList(c.UI, secret)
}

48
command/kv_metadata.go Normal file
View File

@ -0,0 +1,48 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*KVMetadataCommand)(nil)
type KVMetadataCommand struct {
*BaseCommand
}
func (c *KVMetadataCommand) Synopsis() string {
return "Interact with Vault's Key-Value storage"
}
func (c *KVMetadataCommand) Help() string {
helpText := `
Usage: vault kv metadata <subcommand> [options] [args]
This command has subcommands for interacting with the metadata endpoint in
Vault's key-value store. Here are some simple examples, and more detailed
examples are available in the subcommands or the documentation.
Create or update a metadata entry for a key:
$ vault kv metadata put -max-versions=5 secret/foo
Get the metadata for a key, this provides information about each existing
version:
$ vault kv metadata get secret/foo
Delete a key and all existing versions:
$ vault kv metadata delete secret/foo
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *KVMetadataCommand) Run(args []string) int {
return cli.RunResultHelp
}

View File

@ -0,0 +1,87 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVMetadataDeleteCommand)(nil)
var _ cli.CommandAutocomplete = (*KVMetadataDeleteCommand)(nil)
type KVMetadataDeleteCommand struct {
*BaseCommand
}
func (c *KVMetadataDeleteCommand) Synopsis() string {
return "Deletes all versions and metadata for a key in the KV store"
}
func (c *KVMetadataDeleteCommand) Help() string {
helpText := `
Usage: vault kv metadata delete [options] PATH
Deletes all versions and metadata for the provided key.
$ vault kv metadata delete secret/foo
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVMetadataDeleteCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *KVMetadataDeleteCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultFiles()
}
func (c *KVMetadataDeleteCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVMetadataDeleteCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
path := sanitizePath(args[0])
path, err = addPrefixToVKVPath(path, "metadata")
if err != nil {
c.UI.Error(err.Error())
return 2
}
if _, err := kvDeleteRequest(client, path); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err))
return 2
}
c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", path))
return 0
}

129
command/kv_metadata_get.go Normal file
View File

@ -0,0 +1,129 @@
package command
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVMetadataGetCommand)(nil)
var _ cli.CommandAutocomplete = (*KVMetadataGetCommand)(nil)
type KVMetadataGetCommand struct {
*BaseCommand
}
func (c *KVMetadataGetCommand) Synopsis() string {
return "Retrieves key metadata from the KV store"
}
func (c *KVMetadataGetCommand) Help() string {
helpText := `
Usage: vault kv metadata get [options] KEY
Retrieves the metadata from Vault's key-value store at the given key name. If no
key exists with that name, an error is returned.
$ vault kv metadata get secret/foo
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVMetadataGetCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
return set
}
func (c *KVMetadataGetCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *KVMetadataGetCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVMetadataGetCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
path := sanitizePath(args[0])
path, err = addPrefixToVKVPath(path, "metadata")
if err != nil {
c.UI.Error(err.Error())
return 2
}
secret, err := kvReadRequest(client, path, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err))
return 2
}
if secret == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
return 2
}
if c.flagField != "" {
return PrintRawField(c.UI, secret, c.flagField)
}
// If we have wrap info print the secret normally.
if secret.WrapInfo != nil || c.flagFormat != "table" {
return OutputSecret(c.UI, secret)
}
versions := secret.Data["versions"].(map[string]interface{})
delete(secret.Data, "versions")
c.UI.Info(getHeaderForMap("Metadata", secret.Data))
OutputSecret(c.UI, secret)
versionKeys := []int{}
for k := range versions {
i, err := strconv.Atoi(k)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing version %s", k))
return 2
}
versionKeys = append(versionKeys, i)
}
sort.Ints(versionKeys)
for _, v := range versionKeys {
c.UI.Info("\n" + getHeaderForMap(fmt.Sprintf("Version %d", v), versions[strconv.Itoa(v)].(map[string]interface{})))
OutputData(c.UI, versions[strconv.Itoa(v)])
}
return 0
}

135
command/kv_metadata_put.go Normal file
View File

@ -0,0 +1,135 @@
package command
import (
"fmt"
"io"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVMetadataPutCommand)(nil)
var _ cli.CommandAutocomplete = (*KVMetadataPutCommand)(nil)
type KVMetadataPutCommand struct {
*BaseCommand
flagMaxVersions int
flagCASRequired bool
testStdin io.Reader // for tests
}
func (c *KVMetadataPutCommand) Synopsis() string {
return "Sets or updates key settings in the KV store"
}
func (c *KVMetadataPutCommand) Help() string {
helpText := `
Usage: vault metadata kv put [options] KEY [DATA]
This command can be used to create a blank key in the key-value store or to
update key configuration for a specified key.
Create a key in the key-value store with no data:
$ vault kv metadata put secret/foo
Set a max versions setting on the key:
$ vault kv metadata put -max-versions=5 secret/foo
Require Check-and-Set for this key:
$ vault kv metadata put -require-cas secret/foo
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVMetadataPutCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.IntVar(&IntVar{
Name: "max-versions",
Target: &c.flagMaxVersions,
Default: 0,
Usage: `The number of versions to keep. If not set, the backends configured max version is used.`,
})
f.BoolVar(&BoolVar{
Name: "cas-required",
Target: &c.flagCASRequired,
Default: false,
Usage: `If true the key will require the cas parameter to be set on all write requests. If false, the backends configuration will be used.`,
})
return set
}
func (c *KVMetadataPutCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *KVMetadataPutCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVMetadataPutCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
var err error
path := sanitizePath(args[0])
path, err = addPrefixToVKVPath(path, "metadata")
if err != nil {
c.UI.Error(err.Error())
return 2
}
data := map[string]interface{}{
"max_versions": c.flagMaxVersions,
"cas_required": c.flagCASRequired,
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
secret, err := kvWriteRequest(client, path, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
return 2
}
if secret == nil {
// Don't output anything unless using the "table" format
if Format(c.UI) == "table" {
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path))
}
return 0
}
return OutputSecret(c.UI, secret)
}

143
command/kv_put.go Normal file
View File

@ -0,0 +1,143 @@
package command
import (
"fmt"
"io"
"os"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVPutCommand)(nil)
var _ cli.CommandAutocomplete = (*KVPutCommand)(nil)
type KVPutCommand struct {
*BaseCommand
flagCAS int
testStdin io.Reader // for tests
}
func (c *KVPutCommand) Synopsis() string {
return "Sets or updates data in the KV store"
}
func (c *KVPutCommand) Help() string {
helpText := `
Usage: vault kv put [options] KEY [DATA]
Writes the data to the given path in the key-value store. The data can be of
any type.
$ vault kv put secret/foo bar=baz
The data can also be consumed from a file on disk by prefixing with the "@"
symbol. For example:
$ vault kv put secret/foo @data.json
Or it can be read from stdin using the "-" symbol:
$ echo "abcd1234" | vault kv put secret/foo bar=-
To perform a Check-And-Set operation, specify the -cas flag with the
appropriate version number corresponding to the key you want to perform
the CAS operation on:
$ vault kv put -cas=1 secret/foo bar=baz
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVPutCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.IntVar(&IntVar{
Name: "cas",
Target: &c.flagCAS,
Default: -1,
Usage: `Specifies to use a Check-And-Set operation. If not set the write
will be allowed. If set to 0 a write will only be allowed if the key
doesnt exist. If the index is non-zero the write will only be allowed
if the keys current version matches the version specified in the cas
parameter.`,
})
return set
}
func (c *KVPutCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *KVPutCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVPutCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
// Pull our fake stdin if needed
stdin := (io.Reader)(os.Stdin)
if c.testStdin != nil {
stdin = c.testStdin
}
var err error
path := sanitizePath(args[0])
path, err = addPrefixToVKVPath(path, "data")
if err != nil {
c.UI.Error(err.Error())
return 2
}
data, err := parseArgsData(stdin, args[1:])
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err))
return 1
}
data = map[string]interface{}{
"data": data,
"options": map[string]interface{}{},
}
if c.flagCAS > -1 {
data["options"].(map[string]interface{})["cas"] = c.flagCAS
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
secret, err := kvWriteRequest(client, path, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
return 2
}
if secret == nil {
// Don't output anything unless using the "table" format
if Format(c.UI) == "table" {
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path))
}
return 0
}
return OutputSecret(c.UI, secret)
}

119
command/kv_undelete.go Normal file
View File

@ -0,0 +1,119 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*KVUndeleteCommand)(nil)
var _ cli.CommandAutocomplete = (*KVUndeleteCommand)(nil)
type KVUndeleteCommand struct {
*BaseCommand
flagVersions []string
}
func (c *KVUndeleteCommand) Synopsis() string {
return "Undeletes versions in the KV store"
}
func (c *KVUndeleteCommand) Help() string {
helpText := `
Usage: vault kv undelete [options] KEY
Undeletes the data for the provided version and path in the key-value store.
This restores the data, allowing it to be returned on get requests.
To undelete version 3 of key "foo":
$ vault kv undelete -versions=3 secret/foo
Additional flags and more advanced use cases are detailed below.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *KVUndeleteCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.StringSliceVar(&StringSliceVar{
Name: "versions",
Target: &c.flagVersions,
Default: nil,
Usage: `Specifies the version numbers to undelete.`,
})
return set
}
func (c *KVUndeleteCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *KVUndeleteCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *KVUndeleteCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
if len(c.flagVersions) == 0 {
c.UI.Error("No versions provided, use the \"-versions\" flag to specify the version to undelete.")
return 1
}
var err error
path := sanitizePath(args[0])
path, err = addPrefixToVKVPath(path, "undelete")
if err != nil {
c.UI.Error(err.Error())
return 2
}
data := map[string]interface{}{
"versions": c.flagVersions,
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
secret, err := kvWriteRequest(client, path, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
return 2
}
if secret == nil {
// Don't output anything unless using the "table" format
if Format(c.UI) == "table" {
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path))
}
return 0
}
return OutputSecret(c.UI, secret)
}

View File

@ -134,6 +134,13 @@ type encryptedKeyStorage struct {
prefix string
}
func ensureTailingSlash(path string) string {
if !strings.HasSuffix(path, "/") {
return path + "/"
}
return path
}
// List implements the logical.Storage List method, and decrypts all the items
// in a path prefix. This can only operate on full folder structures so the
// prefix should end in a "/".
@ -143,7 +150,7 @@ func (s *encryptedKeyStorage) List(ctx context.Context, prefix string) ([]string
return nil, err
}
keys, err := s.s.List(ctx, encPrefix+"/")
keys, err := s.s.List(ctx, ensureTailingSlash(encPrefix))
if err != nil {
return keys, err
}
@ -244,6 +251,10 @@ func (s *encryptedKeyStorage) Delete(ctx context.Context, path string) error {
// by "/") with the object's key policy. The context for each encryption is the
// plaintext path prefix for the key.
func (s *encryptedKeyStorage) encryptPath(path string) (string, error) {
if path == "" || path == "/" {
return s.prefix, nil
}
path = paths.Clean(path)
// Trim the prefix if it starts with a "/"
@ -252,7 +263,7 @@ func (s *encryptedKeyStorage) encryptPath(path string) (string, error) {
parts := strings.Split(path, "/")
encPath := s.prefix
context := s.prefix
context := strings.TrimSuffix(s.prefix, "/")
for _, p := range parts {
encoded := base64.StdEncoding.EncodeToString([]byte(p))
ciphertext, err := s.policy.Encrypt(0, []byte(context), nil, encoded)

View File

@ -160,7 +160,91 @@ func TestEncrytedKeysStorage_BadPolicy(t *testing.T) {
}
}
func TestEncrytedKeysStorage_CRUD(t *testing.T) {
func TestEncryptedKeysStorage_List(t *testing.T) {
s := &logical.InmemStorage{}
policy := &Policy{
Name: "metadata",
Type: KeyType_AES256_GCM96,
Derived: true,
KDF: Kdf_hkdf_sha256,
ConvergentEncryption: true,
ConvergentVersion: 2,
VersionTemplate: EncryptedKeyPolicyVersionTpl,
versionPrefixCache: &sync.Map{},
}
ctx := context.Background()
err := policy.Rotate(ctx, s)
if err != nil {
t.Fatal(err)
}
es, err := NewEncryptedKeyStorageWrapper(EncryptedKeyStorageConfig{
Policy: policy,
Prefix: "prefix",
})
if err != nil {
t.Fatal(err)
}
err = es.Wrap(s).Put(ctx, &logical.StorageEntry{
Key: "test",
Value: []byte("test"),
})
if err != nil {
t.Fatal(err)
}
err = es.Wrap(s).Put(ctx, &logical.StorageEntry{
Key: "test/foo",
Value: []byte("test"),
})
if err != nil {
t.Fatal(err)
}
err = es.Wrap(s).Put(ctx, &logical.StorageEntry{
Key: "test/foo1/test",
Value: []byte("test"),
})
if err != nil {
t.Fatal(err)
}
keys, err := es.Wrap(s).List(ctx, "test/")
if err != nil {
t.Fatal(err)
}
// Test prefixed with "/"
keys, err = es.Wrap(s).List(ctx, "/test/")
if err != nil {
t.Fatal(err)
}
if len(keys) != 2 || keys[0] != "foo1/" || keys[1] != "foo" {
t.Fatalf("bad keys: %#v", keys)
}
keys, err = es.Wrap(s).List(ctx, "/")
if err != nil {
t.Fatal(err)
}
if len(keys) != 2 || keys[0] != "test" || keys[1] != "test/" {
t.Fatalf("bad keys: %#v", keys)
}
keys, err = es.Wrap(s).List(ctx, "")
if err != nil {
t.Fatal(err)
}
if len(keys) != 2 || keys[0] != "test" || keys[1] != "test/" {
t.Fatalf("bad keys: %#v", keys)
}
}
func TestEncryptedKeysStorage_CRUD(t *testing.T) {
s := &logical.InmemStorage{}
policy := &Policy{
Name: "metadata",

View File

@ -7,6 +7,7 @@ import (
"sync"
"sync/atomic"
"github.com/hashicorp/vault/helper/consts"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
)
@ -26,6 +27,7 @@ var StdAllowedHeaders = []string{
"X-Vault-Wrap-Format",
"X-Vault-Wrap-TTL",
"X-Vault-Policy-Override",
consts.VaultKVCLIClientHeader,
}
// CORSConfig stores the state of the CORS configuration.