Mount flag syntax to mitigate confusion from KV-v2 path discrepancies (#14807)

* Add explanation to help text and flag usage text

* KV get with new mount flag

* Clearer naming

* KV Put, Patch, Metadata Get + corresponding tests

* KV Delete, Destroy, Rollback, Undelete, MetadataDelete, MetadataPatch, MetadataPut

* Update KV-v2 docs to use mount flag syntax

* Add changelog

* Run make fmt

* Clarify deprecation message in help string

* Address style comments
This commit is contained in:
VAL 2022-04-06 13:58:06 -07:00 committed by GitHub
parent 7393bc173d
commit 2113ae1021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 826 additions and 256 deletions

3
changelog/14807.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
cli: Alternative flag-based syntax for KV to mitigate confusion from automatically appended /data
```

View File

@ -27,19 +27,25 @@ Usage: vault kv <subcommand> [options] [args]
Create or update the key named "foo" in the "secret" mount with the value
"bar=baz":
$ vault kv put secret/foo bar=baz
$ vault kv put -mount=secret foo bar=baz
Read this value back:
$ vault kv get secret/foo
$ vault kv get -mount=secret foo
Get metadata for the key:
$ vault kv metadata get secret/foo
$ vault kv metadata get -mount=secret foo
Get a specific version of the key:
$ vault kv get -version=1 secret/foo
$ vault kv get -mount=secret -version=1 foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/data/foo) can cause confusion:
$ vault kv get secret/foo
Please see the individual subcommand help for detailed usage information.
`

View File

@ -2,6 +2,7 @@ package command
import (
"fmt"
"path"
"strings"
"github.com/hashicorp/vault/api"
@ -18,6 +19,7 @@ type KVDeleteCommand struct {
*BaseCommand
flagVersions []string
flagMount string
}
func (c *KVDeleteCommand) Synopsis() string {
@ -34,11 +36,17 @@ Usage: vault kv delete [options] PATH
To delete the latest version of the key "foo":
$ vault kv delete -mount=secret foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/data/foo) can cause confusion:
$ vault kv delete secret/foo
To delete version 3 of key foo:
$ vault kv delete -versions=3 secret/foo
$ vault kv delete -mount=secret -versions=3 foo
To delete all versions and metadata, see the "vault kv metadata" subcommand.
@ -61,6 +69,17 @@ func (c *KVDeleteCommand) Flags() *FlagSets {
Usage: `Specifies the version numbers to delete.`,
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /data/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -96,22 +115,54 @@ func (c *KVDeleteCommand) Run(args []string) int {
return 2
}
path := sanitizePath(args[0])
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
var secret *api.Secret
var fullPath string
if v2 {
secret, err = c.deleteV2(path, mountPath, client)
secret, err = c.deleteV2(partialPath, mountPath, client)
fullPath = addPrefixToKVPath(partialPath, mountPath, "data")
} else {
secret, err = client.Logical().Delete(path)
// v1
if mountFlagSyntax {
fullPath = path.Join(mountPath, partialPath)
} else {
fullPath = partialPath
}
secret, err = client.Logical().Delete(fullPath)
}
if err != nil {
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", fullPath, err))
if secret != nil {
OutputSecret(c.UI, secret)
}
@ -121,7 +172,7 @@ func (c *KVDeleteCommand) Run(args []string) int {
if secret == nil {
// Don't output anything unless using the "table" format
if Format(c.UI) == "table" {
c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", path))
c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", fullPath))
}
return 0
}
@ -139,22 +190,12 @@ func (c *KVDeleteCommand) deleteV2(path, mountPath string, client *api.Client) (
switch {
case len(c.flagVersions) > 0:
path = addPrefixToKVPath(path, mountPath, "delete")
if err != nil {
return nil, err
}
data := map[string]interface{}{
"versions": kvParseVersionsFlags(c.flagVersions),
}
secret, err = client.Logical().Write(path, data)
default:
path = addPrefixToKVPath(path, mountPath, "data")
if err != nil {
return nil, err
}
secret, err = client.Logical().Delete(path)
}

View File

@ -17,6 +17,7 @@ type KVDestroyCommand struct {
*BaseCommand
flagVersions []string
flagMount string
}
func (c *KVDestroyCommand) Synopsis() string {
@ -32,6 +33,12 @@ Usage: vault kv destroy [options] KEY
To destroy version 3 of key foo:
$ vault kv destroy -mount=secret -versions=3 foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/data/foo) can cause confusion:
$ vault kv destroy -versions=3 secret/foo
Additional flags and more advanced use cases are detailed below.
@ -53,6 +60,17 @@ func (c *KVDestroyCommand) Flags() *FlagSets {
Usage: `Specifies the version numbers to destroy.`,
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /data/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -86,8 +104,8 @@ func (c *KVDestroyCommand) Run(args []string) int {
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])
client, err := c.Client()
if err != nil {
@ -95,16 +113,42 @@ func (c *KVDestroyCommand) Run(args []string) int {
return 2
}
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
if !v2 {
c.UI.Error("Destroy not supported on KV Version 1")
return 1
}
path = addPrefixToKVPath(path, mountPath, "destroy")
destroyPath := addPrefixToKVPath(partialPath, mountPath, "destroy")
if err != nil {
c.UI.Error(err.Error())
return 2
@ -114,9 +158,9 @@ func (c *KVDestroyCommand) Run(args []string) int {
"versions": kvParseVersionsFlags(c.flagVersions),
}
secret, err := client.Logical().Write(path, data)
secret, err := client.Logical().Write(destroyPath, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", destroyPath, err))
if secret != nil {
OutputSecret(c.UI, secret)
}
@ -125,7 +169,7 @@ func (c *KVDestroyCommand) Run(args []string) int {
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))
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", destroyPath))
}
return 0
}

View File

@ -2,6 +2,7 @@ package command
import (
"fmt"
"path"
"strings"
"github.com/mitchellh/cli"
@ -17,6 +18,7 @@ type KVGetCommand struct {
*BaseCommand
flagVersion int
flagMount string
}
func (c *KVGetCommand) Synopsis() string {
@ -31,12 +33,18 @@ Usage: vault kv get [options] KEY
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 -mount=secret foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/data/foo) can cause confusion:
$ 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
$ vault kv get -mount=secret -version=1 foo
Additional flags and more advanced use cases are detailed below.
@ -57,6 +65,17 @@ func (c *KVGetCommand) Flags() *FlagSets {
Usage: `If passed, the value at the version number will be returned.`,
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /data/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -92,35 +111,67 @@ func (c *KVGetCommand) Run(args []string) int {
return 2
}
path := sanitizePath(args[0])
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
var versionParam map[string]string
var fullPath string
// Add /data to v2 paths only
if v2 {
path = addPrefixToKVPath(path, mountPath, "data")
fullPath = addPrefixToKVPath(partialPath, mountPath, "data")
if c.flagVersion > 0 {
versionParam = map[string]string{
"version": fmt.Sprintf("%d", c.flagVersion),
}
}
} else {
// v1
if mountFlagSyntax {
fullPath = path.Join(mountPath, partialPath)
} else {
fullPath = partialPath
}
}
secret, err := kvReadRequest(client, path, versionParam)
secret, err := kvReadRequest(client, fullPath, versionParam)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error reading %s: %s", fullPath, err))
if secret != nil {
OutputSecret(c.UI, secret)
}
return 2
}
if secret == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
c.UI.Error(fmt.Sprintf("No value found at %s", fullPath))
return 2
}
@ -140,7 +191,7 @@ func (c *KVGetCommand) Run(args []string) int {
}
return PrintRawField(c.UI, data, c.flagField)
} else {
c.UI.Error(fmt.Sprintf("No data found at %s", path))
c.UI.Error(fmt.Sprintf("No data found at %s", fullPath))
return 2
}
} else {
@ -159,7 +210,7 @@ func (c *KVGetCommand) Run(args []string) int {
}
if v2 {
outputPath(c.UI, path, "Secret Path")
outputPath(c.UI, fullPath, "Secret Path")
}
if metadata, ok := secret.Data["metadata"]; ok && metadata != nil {

View File

@ -26,16 +26,22 @@ Usage: vault kv metadata <subcommand> [options] [args]
Create or update a metadata entry for a key:
$ vault kv metadata put -max-versions=5 -delete-version-after=3h25m19s secret/foo
$ vault kv metadata put -mount=secret -max-versions=5 -delete-version-after=3h25m19s foo
Get the metadata for a key, this provides information about each existing
version:
$ vault kv metadata get secret/foo
$ vault kv metadata get -mount=secret foo
Delete a key and all existing versions:
$ vault kv metadata delete secret/foo
$ vault kv metadata delete -mount=secret foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/metadata/foo) can cause confusion:
$ vault kv metadata get secret/foo
Please see the individual subcommand help for detailed usage information.
`

View File

@ -15,6 +15,7 @@ var (
type KVMetadataDeleteCommand struct {
*BaseCommand
flagMount string
}
func (c *KVMetadataDeleteCommand) Synopsis() string {
@ -27,6 +28,12 @@ Usage: vault kv metadata delete [options] PATH
Deletes all versions and metadata for the provided key.
$ vault kv metadata delete -mount=secret foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/metadata/foo) can cause confusion:
$ vault kv metadata delete secret/foo
Additional flags and more advanced use cases are detailed below.
@ -37,7 +44,23 @@ Usage: vault kv metadata delete [options] PATH
}
func (c *KVMetadataDeleteCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
set := c.flagSet(FlagSetHTTP)
// Common Options
f := set.NewFlagSet("Common Options")
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /metadata/ automatically appended between KV
v2 secrets.`,
})
return set
}
func (c *KVMetadataDeleteCommand) AutocompleteArgs() complete.Predictor {
@ -72,26 +95,51 @@ func (c *KVMetadataDeleteCommand) Run(args []string) int {
return 2
}
path := sanitizePath(args[0])
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
if !v2 {
c.UI.Error("Metadata not supported on KV Version 1")
return 1
}
path = addPrefixToKVPath(path, mountPath, "metadata")
if secret, err := client.Logical().Delete(path); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err))
fullPath := addPrefixToKVPath(partialPath, mountPath, "metadata")
if secret, err := client.Logical().Delete(fullPath); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", fullPath, err))
if secret != nil {
OutputSecret(c.UI, secret)
}
return 2
}
c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", path))
c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", fullPath))
return 0
}

View File

@ -17,6 +17,7 @@ var (
type KVMetadataGetCommand struct {
*BaseCommand
flagMount string
}
func (c *KVMetadataGetCommand) Synopsis() string {
@ -30,6 +31,12 @@ 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 -mount=secret foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/metadata/foo) can cause confusion:
$ vault kv metadata get secret/foo
Additional flags and more advanced use cases are detailed below.
@ -41,6 +48,20 @@ Usage: vault kv metadata get [options] KEY
func (c *KVMetadataGetCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /metadata/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -76,25 +97,50 @@ func (c *KVMetadataGetCommand) Run(args []string) int {
return 2
}
path := sanitizePath(args[0])
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
if !v2 {
c.UI.Error("Metadata not supported on KV Version 1")
return 1
}
path = addPrefixToKVPath(path, mountPath, "metadata")
secret, err := client.Logical().Read(path)
fullPath := addPrefixToKVPath(partialPath, mountPath, "metadata")
secret, err := client.Logical().Read(fullPath)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error reading %s: %s", fullPath, err))
return 2
}
if secret == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
c.UI.Error(fmt.Sprintf("No value found at %s", fullPath))
return 2
}
@ -109,7 +155,7 @@ func (c *KVMetadataGetCommand) Run(args []string) int {
versionsRaw, ok := secret.Data["versions"]
if !ok || versionsRaw == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
c.UI.Error(fmt.Sprintf("No value found at %s", fullPath))
OutputSecret(c.UI, secret)
return 2
}
@ -117,7 +163,7 @@ func (c *KVMetadataGetCommand) Run(args []string) int {
delete(secret.Data, "versions")
outputPath(c.UI, path, "Metadata Path")
outputPath(c.UI, fullPath, "Metadata Path")
c.UI.Info(getHeaderForMap("Metadata", secret.Data))
OutputSecret(c.UI, secret)

View File

@ -23,6 +23,7 @@ type KVMetadataPatchCommand struct {
flagCASRequired BoolPtr
flagDeleteVersionAfter time.Duration
flagCustomMetadata map[string]string
flagMount string
testStdin io.Reader // for tests
}
@ -39,23 +40,29 @@ Usage: vault metadata kv patch [options] KEY
Create a key in the key-value store with no data:
$ vault kv metadata patch -mount=secret foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/metadata/foo) can cause confusion:
$ vault kv metadata patch secret/foo
Set a max versions setting on the key:
$ vault kv metadata patch -max-versions=5 secret/foo
$ vault kv metadata patch -mount=secret -max-versions=5 foo
Set delete-version-after on the key:
$ vault kv metadata patch -delete-version-after=3h25m19s secret/foo
$ vault kv metadata patch -mount=secret -delete-version-after=3h25m19s foo
Require Check-and-Set for this key:
$ vault kv metadata patch -cas-required secret/foo
$ vault kv metadata patch -mount=secret -cas-required foo
Set custom metadata on the key:
$ vault kv metadata patch -custom-metadata=foo=abc -custom-metadata=bar=123 secret/foo
$ vault kv metadata patch -mount=secret -custom-metadata=foo=abc -custom-metadata=bar=123 foo
Additional flags and more advanced use cases are detailed below.
@ -103,6 +110,17 @@ func (c *KVMetadataPatchCommand) Flags() *FlagSets {
This can be specified multiple times to add multiple pieces of metadata.`,
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /metadata/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -139,19 +157,42 @@ func (c *KVMetadataPatchCommand) Run(args []string) int {
return 2
}
path := sanitizePath(args[0])
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
if !v2 {
c.UI.Error("Metadata not supported on KV Version 1")
return 1
}
path = addPrefixToKVPath(path, mountPath, "metadata")
fullPath := addPrefixToKVPath(partialPath, mountPath, "metadata")
data := map[string]interface{}{}
@ -171,9 +212,9 @@ func (c *KVMetadataPatchCommand) Run(args []string) int {
data["custom_metadata"] = c.flagCustomMetadata
}
secret, err := client.Logical().JSONMergePatch(context.Background(), path, data)
secret, err := client.Logical().JSONMergePatch(context.Background(), fullPath, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", fullPath, err))
if secret != nil {
OutputSecret(c.UI, secret)
@ -184,7 +225,7 @@ func (c *KVMetadataPatchCommand) Run(args []string) int {
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))
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", fullPath))
}
return 0
}

View File

@ -22,6 +22,7 @@ type KVMetadataPutCommand struct {
flagCASRequired BoolPtr
flagDeleteVersionAfter time.Duration
flagCustomMetadata map[string]string
flagMount string
testStdin io.Reader // for tests
}
@ -38,23 +39,29 @@ Usage: vault metadata kv put [options] KEY
Create a key in the key-value store with no data:
$ vault kv metadata put -mount=secret foo
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/metadata/foo) can cause confusion:
$ vault kv metadata put secret/foo
Set a max versions setting on the key:
$ vault kv metadata put -max-versions=5 secret/foo
$ vault kv metadata put -mount=secret -max-versions=5 foo
Set delete-version-after on the key:
$ vault kv metadata put -delete-version-after=3h25m19s secret/foo
$ vault kv metadata put -mount=secret -delete-version-after=3h25m19s foo
Require Check-and-Set for this key:
$ vault kv metadata put -cas-required secret/foo
$ vault kv metadata put -mount=secret -cas-required foo
Set custom metadata on the key:
$ vault kv metadata put -custom-metadata=foo=abc -custom-metadata=bar=123 secret/foo
$ vault kv metadata put -mount=secret -custom-metadata=foo=abc -custom-metadata=bar=123 foo
Additional flags and more advanced use cases are detailed below.
@ -102,6 +109,17 @@ func (c *KVMetadataPutCommand) Flags() *FlagSets {
"This can be specified multiple times to add multiple pieces of metadata.",
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /metadata/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -138,18 +156,43 @@ func (c *KVMetadataPutCommand) Run(args []string) int {
return 2
}
path := sanitizePath(args[0])
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
if !v2 {
c.UI.Error("Metadata not supported on KV Version 1")
return 1
}
path = addPrefixToKVPath(path, mountPath, "metadata")
fullPath := addPrefixToKVPath(partialPath, mountPath, "metadata")
data := map[string]interface{}{}
if c.flagMaxVersions >= 0 {
@ -168,9 +211,9 @@ func (c *KVMetadataPutCommand) Run(args []string) int {
data["custom_metadata"] = c.flagCustomMetadata
}
secret, err := client.Logical().Write(path, data)
secret, err := client.Logical().Write(fullPath, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", fullPath, err))
if secret != nil {
OutputSecret(c.UI, secret)
}
@ -179,7 +222,7 @@ func (c *KVMetadataPutCommand) Run(args []string) int {
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))
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", fullPath))
}
return 0
}

View File

@ -22,6 +22,7 @@ type KVPatchCommand struct {
flagCAS int
flagMethod string
flagMount string
testStdin io.Reader // for tests
}
@ -35,25 +36,31 @@ Usage: vault kv patch [options] KEY [DATA]
*NOTE*: This is only supported for KV v2 engine mounts.
Writes the data to the given path in the key-value store. The data can be of
Writes the data to the corresponding path in the key-value store. The data can be of
any type.
$ vault kv patch -mount=secret foo bar=baz
The deprecated path-like syntax can also be used, but this should be avoided,
as the fact that it is not actually the full API path to
the secret (secret/data/foo) can cause confusion:
$ vault kv patch secret/foo bar=baz
The data can also be consumed from a file on disk by prefixing with the "@"
symbol. For example:
$ vault kv patch secret/foo @data.json
$ vault kv patch -mount=secret foo @data.json
Or it can be read from stdin using the "-" symbol:
$ echo "abcd1234" | vault kv patch secret/foo bar=-
$ echo "abcd1234" | vault kv patch -mount=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 patch -cas=1 secret/foo bar=baz
$ vault kv patch -mount=secret -cas=1 foo bar=baz
By default, this operation will attempt an HTTP PATCH operation. If your
policy does not allow that, it will fall back to a read/local update/write approach.
@ -61,12 +68,12 @@ Usage: vault kv patch [options] KEY [DATA]
with the -method flag. When -method=patch is specified, only an HTTP PATCH
operation will be tried. If it fails, the entire command will fail.
$ vault kv patch -method=patch secret/foo bar=baz
$ vault kv patch -mount=secret -method=patch foo bar=baz
When -method=rw is specified, only a read/local update/write approach will be tried.
This was the default behavior previous to Vault 1.9.
$ vault kv patch -method=rw secret/foo bar=baz
$ vault kv patch -mount=secret -method=rw foo bar=baz
Additional flags and more advanced use cases are detailed below.
@ -98,6 +105,17 @@ func (c *KVPatchCommand) Flags() *FlagSets {
performed, then a local update, followed by a remote update.`,
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /data/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -134,7 +152,6 @@ func (c *KVPatchCommand) Run(args []string) int {
}
var err error
path := sanitizePath(args[0])
client, err := c.Client()
if err != nil {
@ -148,10 +165,35 @@ func (c *KVPatchCommand) Run(args []string) int {
return 1
}
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
if !v2 {
@ -159,7 +201,7 @@ func (c *KVPatchCommand) Run(args []string) int {
return 2
}
path = addPrefixToKVPath(path, mountPath, "data")
fullPath := addPrefixToKVPath(partialPath, mountPath, "data")
if err != nil {
c.UI.Error(err.Error())
return 2
@ -171,11 +213,11 @@ func (c *KVPatchCommand) Run(args []string) int {
switch c.flagMethod {
case "rw":
secret, code = c.readThenWrite(client, path, newData)
secret, code = c.readThenWrite(client, fullPath, newData)
case "patch":
secret, code = c.mergePatch(client, path, newData, false)
secret, code = c.mergePatch(client, fullPath, newData, false)
case "":
secret, code = c.mergePatch(client, path, newData, true)
secret, code = c.mergePatch(client, fullPath, newData, true)
default:
c.UI.Error(fmt.Sprintf("Unsupported method provided to -method flag: %s", c.flagMethod))
return 2
@ -186,7 +228,7 @@ func (c *KVPatchCommand) Run(args []string) int {
}
if Format(c.UI) == "table" {
outputPath(c.UI, path, "Secret Path")
outputPath(c.UI, fullPath, "Secret Path")
metadata := secret.Data
c.UI.Info(getHeaderForMap("Metadata", metadata))
return OutputData(c.UI, metadata)

View File

@ -4,6 +4,7 @@ import (
"fmt"
"io"
"os"
"path"
"strings"
"github.com/mitchellh/cli"
@ -19,6 +20,7 @@ type KVPutCommand struct {
*BaseCommand
flagCAS int
flagMount string
testStdin io.Reader // for tests
}
@ -33,22 +35,28 @@ 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 -mount=secret foo bar=baz
The deprecated path-like syntax can also be used, but this should be avoided
for KV v2, as the fact that it is not actually the full API path to
the secret (secret/data/foo) can cause confusion:
$ 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
$ vault kv put -mount=secret foo @data.json
Or it can be read from stdin using the "-" symbol:
$ echo "abcd1234" | vault kv put secret/foo bar=-
$ echo "abcd1234" | vault kv put -mount=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
$ vault kv put -mount=secret -cas=1 foo bar=baz
Additional flags and more advanced use cases are detailed below.
@ -73,6 +81,17 @@ func (c *KVPutCommand) Flags() *FlagSets {
parameter.`,
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /data/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -109,7 +128,6 @@ func (c *KVPutCommand) Run(args []string) int {
}
var err error
path := sanitizePath(args[0])
client, err := c.Client()
if err != nil {
@ -123,14 +141,41 @@ func (c *KVPutCommand) Run(args []string) int {
return 1
}
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
// Add /data to v2 paths only
var fullPath string
if v2 {
path = addPrefixToKVPath(path, mountPath, "data")
fullPath = addPrefixToKVPath(partialPath, mountPath, "data")
data = map[string]interface{}{
"data": data,
"options": map[string]interface{}{},
@ -139,11 +184,18 @@ func (c *KVPutCommand) Run(args []string) int {
if c.flagCAS > -1 {
data["options"].(map[string]interface{})["cas"] = c.flagCAS
}
} else {
// v1
if mountFlagSyntax {
fullPath = path.Join(mountPath, partialPath)
} else {
fullPath = partialPath
}
}
secret, err := client.Logical().Write(path, data)
secret, err := client.Logical().Write(fullPath, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", fullPath, err))
if secret != nil {
OutputSecret(c.UI, secret)
}
@ -152,7 +204,7 @@ func (c *KVPutCommand) Run(args []string) int {
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))
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", fullPath))
}
return 0
}
@ -162,7 +214,7 @@ func (c *KVPutCommand) Run(args []string) int {
}
if Format(c.UI) == "table" {
outputPath(c.UI, path, "Secret Path")
outputPath(c.UI, fullPath, "Secret Path")
metadata := secret.Data
c.UI.Info(getHeaderForMap("Metadata", metadata))
return OutputData(c.UI, metadata)

View File

@ -18,6 +18,7 @@ type KVRollbackCommand struct {
*BaseCommand
flagVersion int
flagMount string
}
func (c *KVRollbackCommand) Synopsis() string {
@ -35,6 +36,12 @@ Usage: vault kv rollback [options] KEY
is 5 and the rollback version is 2, the data from version 2 will become
version 6.
$ vault kv rollback -mount=secret -version=2 foo
The deprecated path-like syntax can also be used, but this should be avoided,
as the fact that it is not actually the full API path to
the secret (secret/data/foo) can cause confusion:
$ vault kv rollback -version=2 secret/foo
Additional flags and more advanced use cases are detailed below.
@ -55,6 +62,17 @@ func (c *KVRollbackCommand) Flags() *FlagSets {
Usage: `Specifies the version number that should be made current again.`,
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /data/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -96,7 +114,6 @@ func (c *KVRollbackCommand) Run(args []string) int {
}
var err error
path := sanitizePath(args[0])
client, err := c.Client()
if err != nil {
@ -104,10 +121,35 @@ func (c *KVRollbackCommand) Run(args []string) int {
return 2
}
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
if !v2 {
@ -115,7 +157,7 @@ func (c *KVRollbackCommand) Run(args []string) int {
return 2
}
path = addPrefixToKVPath(path, mountPath, "data")
fullPath := addPrefixToKVPath(partialPath, mountPath, "data")
if err != nil {
c.UI.Error(err.Error())
return 2
@ -124,31 +166,31 @@ func (c *KVRollbackCommand) Run(args []string) int {
// First, do a read to get the current version for check-and-set
var meta map[string]interface{}
{
secret, err := kvReadRequest(client, path, nil)
secret, err := kvReadRequest(client, fullPath, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", fullPath, err))
return 2
}
// Make sure a value already exists
if secret == nil || secret.Data == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
c.UI.Error(fmt.Sprintf("No value found at %s", fullPath))
return 2
}
// Verify metadata found
rawMeta, ok := secret.Data["metadata"]
if !ok || rawMeta == nil {
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", path))
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", fullPath))
return 2
}
meta, ok = rawMeta.(map[string]interface{})
if !ok {
c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", path))
c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", fullPath))
return 2
}
if meta == nil {
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", path))
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", fullPath))
return 2
}
}
@ -163,31 +205,31 @@ func (c *KVRollbackCommand) Run(args []string) int {
// Now run it again and read the version we want to roll back to
var data map[string]interface{}
{
secret, err := kvReadRequest(client, path, versionParam)
secret, err := kvReadRequest(client, fullPath, versionParam)
if err != nil {
c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", fullPath, err))
return 2
}
// Make sure a value already exists
if secret == nil || secret.Data == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
c.UI.Error(fmt.Sprintf("No value found at %s", fullPath))
return 2
}
// Verify metadata found
rawMeta, ok := secret.Data["metadata"]
if !ok || rawMeta == nil {
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", path))
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", fullPath))
return 2
}
meta, ok := rawMeta.(map[string]interface{})
if !ok {
c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", path))
c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", fullPath))
return 2
}
if meta == nil {
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", path))
c.UI.Error(fmt.Sprintf("No metadata found at %s; rollback only works on existing data", fullPath))
return 2
}
@ -205,34 +247,34 @@ func (c *KVRollbackCommand) Run(args []string) int {
// Verify old data found
rawData, ok := secret.Data["data"]
if !ok || rawData == nil {
c.UI.Error(fmt.Sprintf("No data found at %s; rollback only works on existing data", path))
c.UI.Error(fmt.Sprintf("No data found at %s; rollback only works on existing data", fullPath))
return 2
}
data, ok = rawData.(map[string]interface{})
if !ok {
c.UI.Error(fmt.Sprintf("Data found at %s is not the expected type (JSON object)", path))
c.UI.Error(fmt.Sprintf("Data found at %s is not the expected type (JSON object)", fullPath))
return 2
}
if data == nil {
c.UI.Error(fmt.Sprintf("No data found at %s; rollback only works on existing data", path))
c.UI.Error(fmt.Sprintf("No data found at %s; rollback only works on existing data", fullPath))
return 2
}
}
secret, err := client.Logical().Write(path, map[string]interface{}{
secret, err := client.Logical().Write(fullPath, map[string]interface{}{
"data": data,
"options": map[string]interface{}{
"cas": casVersion,
},
})
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", fullPath, 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))
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", fullPath))
}
return 0
}

View File

@ -123,6 +123,12 @@ func TestKVPutCommand(t *testing.T) {
[]string{"Success!"},
0,
},
{
"v1_mount_flag_syntax",
[]string{"-mount", "secret", "write/foo", "foo=bar"},
[]string{"Success!"},
0,
},
{
"v2_single_value",
[]string{"kv/write/foo", "foo=bar"},
@ -141,6 +147,12 @@ func TestKVPutCommand(t *testing.T) {
[]string{"== Secret Path ==", "kv/data/write/foo"},
0,
},
{
"v2_mount_flag_syntax",
[]string{"-mount", "kv", "write/foo", "foo=bar"},
v2ExpectedFields,
0,
},
{
"v2_single_value_backslash",
[]string{"kv/write/foo", "foo=\\"},
@ -428,20 +440,30 @@ func TestKVGetCommand(t *testing.T) {
[]string{"bar"},
0,
},
{
"v1_mount_flag_syntax",
[]string{"-mount", "secret", "read/foo"},
[]string{"foo"},
0,
},
{
"v2_field",
[]string{"-field", "foo", "kv/read/foo"},
[]string{"bar"},
0,
},
{
"v2_mount_flag_syntax",
[]string{"-mount", "kv", "read/foo"},
append(baseV2ExpectedFields, "foo"),
0,
},
{
"v2_not_found",
[]string{"kv/nope/not/once/never"},
[]string{"No value found at kv/data/nope/not/once/never"},
2,
},
{
"v2_read",
[]string{"kv/read/foo"},
@ -573,6 +595,12 @@ func TestKVMetadataGetCommand(t *testing.T) {
append(expectedTopLevelFields, expectedVersionFields[:]...),
0,
},
{
"mount_flag_syntax",
[]string{"-mount", "kv", "foo"},
expectedTopLevelFields,
0,
},
}
t.Run("validations", func(t *testing.T) {
@ -667,6 +695,12 @@ func TestKVPatchCommand_ArgValidation(t *testing.T) {
"Failed to parse K=V data",
1,
},
{
"mount_flag_syntax",
[]string{"-mount", "kv"},
"Not enough arguments",
1,
},
}
for _, tc := range cases {
@ -708,7 +742,7 @@ func expectedPatchFields() []string {
}
}
func TestKvPatchCommand_StdinFull(t *testing.T) {
func TestKVPatchCommand_StdinFull(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()
@ -726,52 +760,56 @@ func TestKvPatchCommand_StdinFull(t *testing.T) {
t.Fatalf("write failed, err: %#v\n", err)
}
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(`{"foo":"bar"}`))
stdinW.Close()
}()
cases := [][]string{
{"kv/patch/foo", "-"},
{"-mount", "kv", "patch/foo", "-"},
}
for i, args := range cases {
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(fmt.Sprintf(`{"foo%d":"bar%d"}`, i, i)))
stdinW.Close()
}()
code, combined := kvPatchWithRetry(t, client, args, stdinR)
args := []string{"kv/patch/foo", "-"}
code, combined := kvPatchWithRetry(t, client, args, stdinR)
for _, str := range expectedPatchFields() {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
for _, str := range expectedPatchFields() {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
}
}
if code != 0 {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args)
}
if code != 0 {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args)
}
secret, err := client.Logical().ReadWithContext(context.Background(), "kv/data/patch/foo")
if err != nil {
t.Fatalf("read failed, err: %#v\n", err)
}
secret, err := client.Logical().ReadWithContext(context.Background(), "kv/data/patch/foo")
if err != nil {
t.Fatalf("read failed, err: %#v\n", err)
}
if secret == nil || secret.Data == nil {
t.Fatal("expected secret to have data")
}
if secret == nil || secret.Data == nil {
t.Fatal("expected secret to have data")
}
secretDataRaw, ok := secret.Data["data"]
secretDataRaw, ok := secret.Data["data"]
if !ok {
t.Fatalf("expected secret to have nested data key, data: %#v", secret.Data)
}
if !ok {
t.Fatalf("expected secret to have nested data key, data: %#v", secret.Data)
}
secretData := secretDataRaw.(map[string]interface{})
foo, ok := secretData["foo"].(string)
if !ok {
t.Fatal("expected foo to be a string but it wasn't")
}
secretData := secretDataRaw.(map[string]interface{})
foo, ok := secretData[fmt.Sprintf("foo%d", i)].(string)
if !ok {
t.Fatal("expected foo to be a string but it wasn't")
}
if exp, act := "bar", foo; exp != act {
t.Fatalf("expected %q to be %q, data: %#v\n", act, exp, secret.Data)
if exp, act := fmt.Sprintf("bar%d", i), foo; exp != act {
t.Fatalf("expected %q to be %q, data: %#v\n", act, exp, secret.Data)
}
}
}
func TestKvPatchCommand_StdinValue(t *testing.T) {
func TestKVPatchCommand_StdinValue(t *testing.T) {
client, closer := testVaultServer(t)
defer closer()
@ -789,43 +827,49 @@ func TestKvPatchCommand_StdinValue(t *testing.T) {
t.Fatalf("write failed, err: %#v\n", err)
}
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte("bar"))
stdinW.Close()
}()
args := []string{"kv/patch/foo", "foo=-"}
code, combined := kvPatchWithRetry(t, client, args, stdinR)
if code != 0 {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args)
cases := [][]string{
{"kv/patch/foo", "foo=-"},
{"-mount", "kv", "patch/foo", "foo=-"},
}
for _, str := range expectedPatchFields() {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
for i, args := range cases {
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(fmt.Sprintf("bar%d", i)))
stdinW.Close()
}()
code, combined := kvPatchWithRetry(t, client, args, stdinR)
if code != 0 {
t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v\n", code, args)
}
}
secret, err := client.Logical().ReadWithContext(context.Background(), "kv/data/patch/foo")
if err != nil {
t.Fatalf("read failed, err: %#v\n", err)
}
for _, str := range expectedPatchFields() {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
}
if secret == nil || secret.Data == nil {
t.Fatal("expected secret to have data")
}
secret, err := client.Logical().ReadWithContext(context.Background(), "kv/data/patch/foo")
if err != nil {
t.Fatalf("read failed, err: %#v\n", err)
}
secretDataRaw, ok := secret.Data["data"]
if secret == nil || secret.Data == nil {
t.Fatal("expected secret to have data")
}
if !ok {
t.Fatalf("expected secret to have nested data key, data: %#v\n", secret.Data)
}
secretDataRaw, ok := secret.Data["data"]
secretData := secretDataRaw.(map[string]interface{})
if !ok {
t.Fatalf("expected secret to have nested data key, data: %#v\n", secret.Data)
}
if exp, act := "bar", secretData["foo"].(string); exp != act {
t.Fatalf("expected %q to be %q, data: %#v\n", act, exp, secret.Data)
secretData := secretDataRaw.(map[string]interface{})
if exp, act := fmt.Sprintf("bar%d", i), secretData["foo"].(string); exp != act {
t.Fatalf("expected %q to be %q, data: %#v\n", act, exp, secret.Data)
}
}
}
@ -839,16 +883,22 @@ func TestKVPatchCommand_RWMethodNotExists(t *testing.T) {
t.Fatalf("kv-v2 mount attempt failed - err: %#v\n", err)
}
args := []string{"-method", "rw", "kv/patch/foo", "foo=a"}
code, combined := kvPatchWithRetry(t, client, args, nil)
if code != 2 {
t.Fatalf("expected code to be 2 but was %d for patch cmd with args %#v\n", code, args)
cases := [][]string{
{"-method", "rw", "kv/patch/foo", "foo=a"},
{"-method", "rw", "-mount", "kv", "patch/foo", "foo=a"},
}
expectedOutputSubstr := "No value found"
if !strings.Contains(combined, expectedOutputSubstr) {
t.Fatalf("expected output %q to contain %q for patch cmd with args %#v\n", combined, expectedOutputSubstr, args)
for _, args := range cases {
code, combined := kvPatchWithRetry(t, client, args, nil)
if code != 2 {
t.Fatalf("expected code to be 2 but was %d for patch cmd with args %#v\n", code, args)
}
expectedOutputSubstr := "No value found"
if !strings.Contains(combined, expectedOutputSubstr) {
t.Fatalf("expected output %q to contain %q for patch cmd with args %#v\n", combined, expectedOutputSubstr, args)
}
}
}
@ -929,6 +979,13 @@ func TestKVPatchCommand_CAS(t *testing.T) {
[]string{"check-and-set parameter did not match the current version"},
2,
},
{
"mount_flag_syntax",
[]string{"-mount", "kv", "-cas", "1", "foo", "bar=quux"},
"quux",
expectedPatchFields(),
0,
},
}
for _, tc := range cases {

View File

@ -17,6 +17,7 @@ type KVUndeleteCommand struct {
*BaseCommand
flagVersions []string
flagMount string
}
func (c *KVUndeleteCommand) Synopsis() string {
@ -32,6 +33,12 @@ Usage: vault kv undelete [options] KEY
To undelete version 3 of key "foo":
$ vault kv undelete -mount=secret -versions=3 foo
The deprecated path-like syntax can also be used, but this should be avoided,
as the fact that it is not actually the full API path to
the secret (secret/data/foo) can cause confusion:
$ vault kv undelete -versions=3 secret/foo
Additional flags and more advanced use cases are detailed below.
@ -53,6 +60,17 @@ func (c *KVUndeleteCommand) Flags() *FlagSets {
Usage: `Specifies the version numbers to undelete.`,
})
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /data/ automatically appended between KV
v2 secrets.`,
})
return set
}
@ -93,25 +111,50 @@ func (c *KVUndeleteCommand) Run(args []string) int {
return 2
}
path := sanitizePath(args[0])
mountPath, v2, err := isKVv2(path, client)
if err != nil {
c.UI.Error(err.Error())
return 2
// If true, we're working with "-mount=secret foo" syntax.
// If false, we're using "secret/foo" syntax.
mountFlagSyntax := (c.flagMount != "")
var (
mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath = sanitizePath(c.flagMount)
_, v2, err = isKVv2(mountPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
}
if !v2 {
c.UI.Error("Undelete not supported on KV Version 1")
return 1
}
path = addPrefixToKVPath(path, mountPath, "undelete")
undeletePath := addPrefixToKVPath(partialPath, mountPath, "undelete")
data := map[string]interface{}{
"versions": kvParseVersionsFlags(c.flagVersions),
}
secret, err := client.Logical().Write(path, data)
secret, err := client.Logical().Write(undeletePath, data)
if err != nil {
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", undeletePath, err))
if secret != nil {
OutputSecret(c.UI, secret)
}
@ -120,7 +163,7 @@ func (c *KVUndeleteCommand) Run(args []string) int {
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))
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", undeletePath))
}
return 0
}

View File

@ -165,12 +165,17 @@ After the secrets engine is configured and a user/machine has a Vault token with
the proper permission, it can generate credentials. The `kv` secrets engine
allows for writing keys with arbitrary values.
The path-like KV-v1 syntax for referencing a secret (`secret/foo`) can still
be used in KV-v2, but we recommend using the `-mount=secret` flag syntax to
avoid mistaking it for the actual path to the secret (`secret/data/foo` is the
real path).
### Writing/Reading arbitrary data
1. Write arbitrary data:
```shell-session
$ vault kv put secret/my-secret foo=a bar=b
$ vault kv put -mount=secret my-secret foo=a bar=b
Key Value
--- -----
created_time 2019-06-19T17:20:22.985303Z
@ -183,7 +188,7 @@ allows for writing keys with arbitrary values.
1. Read arbitrary data:
```shell-session
$ vault kv get secret/my-secret
$ vault kv get -mount=secret my-secret
====== Metadata ======
Key Value
--- -----
@ -208,7 +213,7 @@ allows for writing keys with arbitrary values.
cas parameter.
```shell-session
$ vault kv put -cas=1 secret/my-secret foo=aa bar=bb
$ vault kv put -mount=secret -cas=1 my-secret foo=aa bar=bb
Key Value
--- -----
created_time 2019-06-19T17:22:23.369372Z
@ -221,7 +226,7 @@ allows for writing keys with arbitrary values.
1. Reading now will return the newest version of the data:
```shell-session
$ vault kv get secret/my-secret
$ vault kv get -mount=secret my-secret
====== Metadata ======
Key Value
--- -----
@ -251,7 +256,7 @@ allows for writing keys with arbitrary values.
the read to perform a check-and-set operation in the subsequent write.
```shell-session
$ vault kv patch -cas=2 secret/my-secret bar=bbb
$ vault kv patch -mount=secret -cas=2 my-secret bar=bbb
Key Value
--- -----
created_time 2019-06-19T17:23:49.199802Z
@ -268,7 +273,7 @@ allows for writing keys with arbitrary values.
Perform a patch using the `patch` method:
```shell-session
$ vault kv patch -method=patch -cas=2 secret/my-secret bar=bbb
$ vault kv patch -mount=secret -method=patch -cas=2 my-secret bar=bbb
Key Value
--- -----
created_time 2019-06-19T17:23:49.199802Z
@ -281,7 +286,7 @@ allows for writing keys with arbitrary values.
Perform a patch using the read-then-write method:
```shell-session
$ vault kv patch -method=rw secret/my-secret bar=bbb
$ vault kv patch -mount=secret -method=rw my-secret bar=bbb
Key Value
--- -----
created_time 2019-06-19T17:23:49.199802Z
@ -291,11 +296,11 @@ allows for writing keys with arbitrary values.
version 3
```
1. Reading after a patch will return the newest version of the data in which
2. Reading after a patch will return the newest version of the data in which
only the specified fields were updated:
```shell-session
$ vault kv get secret/my-secret
$ vault kv get -mount=secret my-secret
====== Metadata ======
Key Value
--- -----
@ -312,10 +317,10 @@ allows for writing keys with arbitrary values.
bar bbb
```
1. Previous versions can be accessed with the `-version` flag:
3. Previous versions can be accessed with the `-version` flag:
```shell-session
$ vault kv get -version=1 secret/my-secret
$ vault kv get -mount=secret -version=1 my-secret
====== Metadata ======
Key Value
--- -----
@ -352,17 +357,17 @@ See the commands below for more information:
takes a `-versions` flag to delete prior versions:
```shell-session
$ vault kv delete secret/my-secret
Success! Data deleted (if it existed) at: secret/my-secret
$ vault kv delete -mount=secret my-secret
Success! Data deleted (if it existed) at: secret/data/my-secret
```
1. Versions can be undeleted:
```shell-session
$ vault kv undelete -versions=2 secret/my-secret
$ vault kv undelete -mount=secret -versions=2 my-secret
Success! Data written to: secret/undelete/my-secret
$ vault kv get secret/my-secret
$ vault kv get -mount=secret my-secret
====== Metadata ======
Key Value
--- -----
@ -381,7 +386,7 @@ See the commands below for more information:
1. Destroying a version permanently deletes the underlying data:
```shell-session
$ vault kv destroy -versions=2 secret/my-secret
$ vault kv destroy -mount=secret -versions=2 my-secret
Success! Data written to: secret/destroy/my-secret
```
@ -396,7 +401,7 @@ See the commands below for more information:
1. All metadata and versions for a key can be viewed:
```shell-session
$ vault kv metadata get secret/my-secret
$ vault kv metadata get -mount=secret my-secret
========== Metadata ==========
Key Value
--- -----
@ -427,7 +432,7 @@ See the commands below for more information:
1. The metadata settings for a key can be configured:
```shell-session
$ vault kv metadata put -max-versions 2 -delete-version-after="3h25m19s" secret/my-secret
$ vault kv metadata put -mount=secret -max-versions 2 -delete-version-after="3h25m19s" my-secret
Success! Data written to: secret/metadata/my-secret
```
@ -435,7 +440,7 @@ See the commands below for more information:
changes will be applied on next write:
```shell-session
$ vault kv put secret/my-secret my-value=newer-s3cr3t
$ vault kv put -mount=secret my-secret my-value=newer-s3cr3t
Key Value
--- -----
created_time 2019-06-19T17:31:16.662563Z
@ -449,7 +454,7 @@ See the commands below for more information:
are cleaned up:
```shell-session
$ vault kv metadata get secret/my-secret
$ vault kv metadata get -mount=secret my-secret
========== Metadata ==========
Key Value
--- -----
@ -484,10 +489,10 @@ See the commands below for more information:
The `vault kv metadata put` command can be used to fully overwrite the value of `custom_metadata`:
```shell-session
$ vault kv metadata put -custom-metadata=foo=abc -custom-metadata=bar=123 secret/ my-secret
$ vault kv metadata put -mount=secret -custom-metadata=foo=abc -custom-metadata=bar=123 my-secret
Success! Data written to: secret/metadata/my-secret
$ vault kv get secret/my-secret
$ vault kv get -mount=secret my-secret
====== Metadata ======
Key Value
--- -----
@ -508,10 +513,10 @@ See the commands below for more information:
The following invocation will update `custom_metadata` sub-field `foo` but leave `bar` untouched:
```shell-session
$ vault kv metadata patch -custom-metadata=foo=def secret/my-secret
$ vault kv metadata patch -mount=secret -custom-metadata=foo=def my-secret
Success! Data written to: secret/metadata/my-secret
$ vault kv get secret/my-secret
$ vault kv get -mount=secret my-secret
====== Metadata ======
Key Value
--- -----
@ -531,7 +536,7 @@ See the commands below for more information:
1. Permanently delete all metadata and versions for a key:
```shell-session
$ vault kv metadata delete secret/my-secret
$ vault kv metadata delete -mount=secret my-secret
Success! Data deleted (if it existed) at: secret/metadata/my-secret
```