From 74248e1b20a6f48bfc94f7d72551fc5cebdbd3ea Mon Sep 17 00:00:00 2001 From: VAL Date: Wed, 6 Apr 2022 13:58:06 -0700 Subject: [PATCH] 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 --- changelog/14807.txt | 3 + command/kv.go | 14 +- command/kv_delete.go | 81 ++++++--- command/kv_destroy.go | 62 ++++++- command/kv_get.go | 77 ++++++-- command/kv_metadata.go | 12 +- command/kv_metadata_delete.go | 68 ++++++-- command/kv_metadata_get.go | 68 ++++++-- command/kv_metadata_patch.go | 69 ++++++-- command/kv_metadata_put.go | 69 ++++++-- command/kv_patch.go | 74 ++++++-- command/kv_put.go | 78 +++++++-- command/kv_rollback.go | 90 +++++++--- command/kv_test.go | 203 ++++++++++++++-------- command/kv_undelete.go | 61 ++++++- website/content/docs/secrets/kv/kv-v2.mdx | 55 +++--- 16 files changed, 827 insertions(+), 257 deletions(-) create mode 100644 changelog/14807.txt diff --git a/changelog/14807.txt b/changelog/14807.txt new file mode 100644 index 000000000000..3c338a21155d --- /dev/null +++ b/changelog/14807.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: Alternative flag-based syntax for KV to mitigate confusion from automatically appended /data +``` \ No newline at end of file diff --git a/command/kv.go b/command/kv.go index 3fa91c8c586f..2172576dbd6f 100644 --- a/command/kv.go +++ b/command/kv.go @@ -27,19 +27,25 @@ Usage: vault kv [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. ` diff --git a/command/kv_delete.go b/command/kv_delete.go index 59e558fbf537..65e0927630ad 100644 --- a/command/kv_delete.go +++ b/command/kv_delete.go @@ -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) } diff --git a/command/kv_destroy.go b/command/kv_destroy.go index 53a1416a786e..33f5452a74e0 100644 --- a/command/kv_destroy.go +++ b/command/kv_destroy.go @@ -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 } diff --git a/command/kv_get.go b/command/kv_get.go index 14202d76449b..07033448187a 100644 --- a/command/kv_get.go +++ b/command/kv_get.go @@ -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 { diff --git a/command/kv_metadata.go b/command/kv_metadata.go index badb08a48fd1..c4ab37910555 100644 --- a/command/kv_metadata.go +++ b/command/kv_metadata.go @@ -26,16 +26,22 @@ Usage: vault kv metadata [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. ` diff --git a/command/kv_metadata_delete.go b/command/kv_metadata_delete.go index 3b9509a2df66..911f00117e76 100644 --- a/command/kv_metadata_delete.go +++ b/command/kv_metadata_delete.go @@ -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 } diff --git a/command/kv_metadata_get.go b/command/kv_metadata_get.go index 61abf3a57c9b..08e401374a70 100644 --- a/command/kv_metadata_get.go +++ b/command/kv_metadata_get.go @@ -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) diff --git a/command/kv_metadata_patch.go b/command/kv_metadata_patch.go index c2a0956e1432..cbee81a93b80 100644 --- a/command/kv_metadata_patch.go +++ b/command/kv_metadata_patch.go @@ -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]) - - 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{}{} @@ -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 } diff --git a/command/kv_metadata_put.go b/command/kv_metadata_put.go index 67a951731f5f..2681583b0ac6 100644 --- a/command/kv_metadata_put.go +++ b/command/kv_metadata_put.go @@ -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 } diff --git a/command/kv_patch.go b/command/kv_patch.go index 334ba6f463ac..5f813fb15730 100644 --- a/command/kv_patch.go +++ b/command/kv_patch.go @@ -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) diff --git a/command/kv_put.go b/command/kv_put.go index ef48d09da194..f380e2d64d99 100644 --- a/command/kv_put.go +++ b/command/kv_put.go @@ -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) diff --git a/command/kv_rollback.go b/command/kv_rollback.go index c9f3eb3621e5..e69dba6f6c80 100644 --- a/command/kv_rollback.go +++ b/command/kv_rollback.go @@ -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 } diff --git a/command/kv_test.go b/command/kv_test.go index 4909b0cdfa4e..909b3b8f75cb 100644 --- a/command/kv_test.go +++ b/command/kv_test.go @@ -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() - }() - - args := []string{"kv/patch/foo", "-"} - code, combined := kvPatchWithRetry(t, client, args, stdinR) + 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) - 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) + } + + 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\n", secret.Data) - } + if !ok { + t.Fatalf("expected secret to have nested data key, data: %#v\n", secret.Data) + } - secretData := secretDataRaw.(map[string]interface{}) + secretData := secretDataRaw.(map[string]interface{}) - if exp, act := "bar", secretData["foo"].(string); exp != act { - t.Fatalf("expected %q to be %q, data: %#v\n", act, exp, secret.Data) + 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 { diff --git a/command/kv_undelete.go b/command/kv_undelete.go index b734ff876a30..90ea608a7316 100644 --- a/command/kv_undelete.go +++ b/command/kv_undelete.go @@ -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 } diff --git a/website/content/docs/secrets/kv/kv-v2.mdx b/website/content/docs/secrets/kv/kv-v2.mdx index 46567e5efa72..789dbd24c54e 100644 --- a/website/content/docs/secrets/kv/kv-v2.mdx +++ b/website/content/docs/secrets/kv/kv-v2.mdx @@ -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 ```