From d52d69e4bba8ddd86543284dae3fb7ecd188d942 Mon Sep 17 00:00:00 2001 From: Chris Capurso Date: Wed, 12 Jan 2022 12:05:27 -0500 Subject: [PATCH] Add HTTP PATCH support for KV key metadata (#13215) * go get vault-plugin-secrets-kv@vault-4290-patch-metadata * add kv metadata patch command * add changelog entry * success tests for kv metadata patch flags * add more kv metadata patch flags tests * add kv metadata patch cas warning test * add kv-v2 key metadata patch API docs * add kv metadata patch to docs * prevent unintentional field overwriting in kv metadata put cmd * like create/update ops, prevent patch to paths ending in / * fix kv metadata patch cmd in docs * fix flag defaults for kv metadata put * go get vault-plugin-secrets-kv@vault-4290-patch-metadata * fix TestKvMetadataPatchCommand_Flags test * doc fixes * go get vault-plugin-secrets-kv@master; go mod tidy --- changelog/13215.txt | 3 + command/commands.go | 5 + command/kv_metadata_patch.go | 193 +++++++++++++ command/kv_metadata_patch_test.go | 273 +++++++++++++++++++ command/kv_metadata_put.go | 23 +- command/kv_metadata_put_test.go | 54 +++- go.mod | 4 +- go.sum | 4 +- vault/request_handling.go | 3 +- website/content/api-docs/secret/kv/kv-v2.mdx | 60 +++- website/content/docs/secrets/kv/kv-v2.mdx | 54 ++-- 11 files changed, 633 insertions(+), 43 deletions(-) create mode 100644 changelog/13215.txt create mode 100644 command/kv_metadata_patch.go create mode 100644 command/kv_metadata_patch_test.go diff --git a/changelog/13215.txt b/changelog/13215.txt new file mode 100644 index 000000000..20a787e1d --- /dev/null +++ b/changelog/13215.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/kv: add patch support for KVv2 key metadata +``` diff --git a/command/commands.go b/command/commands.go index a3846cd41..ce6398356 100644 --- a/command/commands.go +++ b/command/commands.go @@ -718,6 +718,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { BaseCommand: getBaseCommand(), }, nil }, + "kv metadata patch": func() (cli.Command, error) { + return &KVMetadataPatchCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, "kv metadata get": func() (cli.Command, error) { return &KVMetadataGetCommand{ BaseCommand: getBaseCommand(), diff --git a/command/kv_metadata_patch.go b/command/kv_metadata_patch.go new file mode 100644 index 000000000..83ec3d45d --- /dev/null +++ b/command/kv_metadata_patch.go @@ -0,0 +1,193 @@ +package command + +import ( + "context" + "fmt" + "io" + "strings" + "time" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*KVMetadataPutCommand)(nil) + _ cli.CommandAutocomplete = (*KVMetadataPutCommand)(nil) +) + +type KVMetadataPatchCommand struct { + *BaseCommand + + flagMaxVersions int + flagCASRequired BoolPtr + flagDeleteVersionAfter time.Duration + flagCustomMetadata map[string]string + testStdin io.Reader // for tests +} + +func (c *KVMetadataPatchCommand) Synopsis() string { + return "Patches key settings in the KV store" +} + +func (c *KVMetadataPatchCommand) Help() string { + helpText := ` +Usage: vault metadata kv patch [options] KEY + + This command can be used to create a blank key in the key-value store or to + update key configuration for a specified key. + + Create a key in the key-value store with no data: + + $ vault kv metadata patch secret/foo + + Set a max versions setting on the key: + + $ vault kv metadata patch -max-versions=5 secret/foo + + Set delete-version-after on the key: + + $ vault kv metadata patch -delete-version-after=3h25m19s secret/foo + + Require Check-and-Set for this key: + + $ vault kv metadata patch -cas-required secret/foo + + Set custom metadata on the key: + + $ vault kv metadata patch -custom-metadata=foo=abc -custom-metadata=bar=123 secret/foo + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *KVMetadataPatchCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + // Common Options + f := set.NewFlagSet("Common Options") + + f.IntVar(&IntVar{ + Name: "max-versions", + Target: &c.flagMaxVersions, + Default: -1, + Usage: `The number of versions to keep. If not set, the backend’s configured max version is used.`, + }) + + f.BoolPtrVar(&BoolPtrVar{ + Name: "cas-required", + Target: &c.flagCASRequired, + Usage: `If true the key will require the cas parameter to be set on all write requests. If false, the backend’s configuration will be used.`, + }) + + f.DurationVar(&DurationVar{ + Name: "delete-version-after", + Target: &c.flagDeleteVersionAfter, + Default: -1, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: `Specifies the length of time before a version is deleted. + If not set, the backend's configured delete-version-after is used. Cannot be + greater than the backend's delete-version-after. The delete-version-after is + specified as a numeric string with a suffix like "30s" or + "3h25m19s".`, + }) + + f.StringMapVar(&StringMapVar{ + Name: "custom-metadata", + Target: &c.flagCustomMetadata, + Default: map[string]string{}, + Usage: `Specifies arbitrary version-agnostic key=value metadata meant to describe a secret. + This can be specified multiple times to add multiple pieces of metadata.`, + }) + + return set +} + +func (c *KVMetadataPatchCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KVMetadataPatchCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVMetadataPatchCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) + return 1 + case len(args) > 1: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + path := sanitizePath(args[0]) + + mountPath, v2, err := isKVv2(path, 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 = addPrefixToVKVPath(path, mountPath, "metadata") + + data := map[string]interface{}{} + + if c.flagMaxVersions >= 0 { + data["max_versions"] = c.flagMaxVersions + } + + if c.flagCASRequired.IsSet() { + data["cas_required"] = c.flagCASRequired.Get() + } + + if c.flagDeleteVersionAfter >= 0 { + data["delete_version_after"] = c.flagDeleteVersionAfter.String() + } + + if len(c.flagCustomMetadata) > 0 { + data["custom_metadata"] = c.flagCustomMetadata + } + + secret, err := client.Logical().JSONMergePatch(context.Background(), path, data) + if err != nil { + c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) + + if secret != nil { + OutputSecret(c.UI, secret) + } + return 2 + } + + if secret == nil { + // Don't output anything unless using the "table" format + if Format(c.UI) == "table" { + c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path)) + } + return 0 + } + + return OutputSecret(c.UI, secret) +} diff --git a/command/kv_metadata_patch_test.go b/command/kv_metadata_patch_test.go new file mode 100644 index 000000000..d5ef916ba --- /dev/null +++ b/command/kv_metadata_patch_test.go @@ -0,0 +1,273 @@ +package command + +import ( + "encoding/json" + "io" + "strings" + "testing" + + "github.com/go-test/deep" + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" +) + +func testKVMetadataPatchCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPatchCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &KVMetadataPatchCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func kvMetadataPatchWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) { + t.Helper() + + return retryKVCommand(t, func() (int, string) { + ui, cmd := testKVMetadataPatchCommand(t) + cmd.client = client + + if stdin != nil { + cmd.testStdin = stdin + } + + code := cmd.Run(args) + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + + return code, combined + }) +} + +func kvMetadataPutWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) { + t.Helper() + + return retryKVCommand(t, func() (int, string) { + ui, cmd := testKVMetadataPutCommand(t) + cmd.client = client + + if stdin != nil { + cmd.testStdin = stdin + } + + code := cmd.Run(args) + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + + return code, combined + }) +} + +func TestKvMetadataPatchCommand_EmptyArgs(t *testing.T) { + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().Mount("kv/", &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatalf("kv-v2 mount error: %#v", err) + } + + args := make([]string, 0) + code, combined := kvMetadataPatchWithRetry(t, client, args, nil) + + expectedCode := 1 + expectedOutput := "Not enough arguments" + + if code != expectedCode { + t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v", expectedCode, code, args) + } + + if !strings.Contains(combined, expectedOutput) { + t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", expectedOutput, combined, args) + } +} + +func TestKvMetadataPatchCommand_Flags(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + expectedUpdates map[string]interface{} + }{ + { + "cas_required_success", + []string{"-cas-required=true"}, + "Success!", + 0, + map[string]interface{}{ + "cas_required": true, + }, + }, + { + "cas_required_invalid", + []string{"-cas-required=12345"}, + "invalid boolean value", + 1, + map[string]interface{}{}, + }, + { + "custom_metadata_success", + []string{"-custom-metadata=baz=ghi"}, + "Success!", + 0, + map[string]interface{}{ + "custom_metadata": map[string]interface{}{ + "foo": "abc", + "bar": "def", + "baz": "ghi", + }, + }, + }, + { + "delete_version_after_success", + []string{"-delete-version-after=5s"}, + "Success!", + 0, + map[string]interface{}{ + "delete_version_after": "5s", + }, + }, + { + "delete_version_after_invalid", + []string{"-delete-version-after=false"}, + "invalid duration", + 1, + map[string]interface{}{}, + }, + { + "max_versions_success", + []string{"-max-versions=10"}, + "Success!", + 0, + map[string]interface{}{ + "max_versions": json.Number("10"), + }, + }, + { + "max_versions_invalid", + []string{"-max-versions=false"}, + "invalid syntax", + 1, + map[string]interface{}{}, + }, + { + "multiple_flags_success", + []string{"-max-versions=20", "-custom-metadata=baz=123"}, + "Success!", + 0, + map[string]interface{}{ + "max_versions": json.Number("20"), + "custom_metadata": map[string]interface{}{ + "foo": "abc", + "bar": "def", + "baz": "123", + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + client, closer := testVaultServer(t) + defer closer() + + basePath := t.Name() + "/" + secretPath := basePath + "my-secret" + metadataPath := basePath + "metadata/" + "my-secret" + + if err := client.Sys().Mount(basePath, &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatalf("kv-v2 mount error: %#v", err) + } + + putArgs := []string{"-cas-required=true", "-custom-metadata=foo=abc", "-custom-metadata=bar=def", secretPath} + code, combined := kvMetadataPutWithRetry(t, client, putArgs, nil) + + if code != 0 { + t.Fatalf("initial metadata put failed, code: %d, output: %s", code, combined) + } + + initialMetadata, err := client.Logical().Read(metadataPath) + if err != nil { + t.Fatalf("metadata read failed, err: %#v", err) + } + + patchArgs := append(tc.args, secretPath) + + code, combined = kvMetadataPatchWithRetry(t, client, patchArgs, nil) + + if !strings.Contains(combined, tc.out) { + t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", tc.out, combined, patchArgs) + } + if code != tc.code { + t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v", tc.code, code, patchArgs) + } + + patchedMetadata, err := client.Logical().Read(metadataPath) + if err != nil { + t.Fatalf("metadata read failed, err: %#v", err) + } + + for k, v := range patchedMetadata.Data { + var expectedVal interface{} + + if inputVal, ok := tc.expectedUpdates[k]; ok { + expectedVal = inputVal + } else { + expectedVal = initialMetadata.Data[k] + } + + if diff := deep.Equal(expectedVal, v); len(diff) > 0 { + t.Fatalf("patched %q mismatch, diff: %#v", k, diff) + } + } + }) + } +} + +func TestKvMetadataPatchCommand_CasWarning(t *testing.T) { + client, closer := testVaultServer(t) + defer closer() + + basePath := "kv/" + if err := client.Sys().Mount(basePath, &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatalf("kv-v2 mount error: %#v", err) + } + + secretPath := basePath + "my-secret" + + args := []string{"-cas-required=true", secretPath} + code, combined := kvMetadataPutWithRetry(t, client, args, nil) + + if code != 0 { + t.Fatalf("metadata put failed, code: %d, output: %s", code, combined) + } + + casConfig := map[string]interface{}{ + "cas_required": true, + } + + _, err := client.Logical().Write(basePath + "config", casConfig) + if err != nil { + t.Fatalf("config write failed, err: #%v", err) + } + + args = []string{"-cas-required=false", secretPath} + code, combined = kvMetadataPatchWithRetry(t, client, args, nil) + + if code != 0 { + t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v", code, args) + } + + expectedOutput := "\"cas_required\" set to false, but is mandated by backend config" + if !strings.Contains(combined, expectedOutput) { + t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", expectedOutput, combined, args) + } +} \ No newline at end of file diff --git a/command/kv_metadata_put.go b/command/kv_metadata_put.go index e8a7d600c..07809d82a 100644 --- a/command/kv_metadata_put.go +++ b/command/kv_metadata_put.go @@ -19,7 +19,7 @@ type KVMetadataPutCommand struct { *BaseCommand flagMaxVersions int - flagCASRequired bool + flagCASRequired BoolPtr flagDeleteVersionAfter time.Duration flagCustomMetadata map[string]string testStdin io.Reader // for tests @@ -71,14 +71,13 @@ func (c *KVMetadataPutCommand) Flags() *FlagSets { f.IntVar(&IntVar{ Name: "max-versions", Target: &c.flagMaxVersions, - Default: 0, + Default: -1, Usage: `The number of versions to keep. If not set, the backend’s configured max version is used.`, }) - f.BoolVar(&BoolVar{ + f.BoolPtrVar(&BoolPtrVar{ Name: "cas-required", Target: &c.flagCASRequired, - Default: false, Usage: `If true the key will require the cas parameter to be set on all write requests. If false, the backend’s configuration will be used.`, }) @@ -151,16 +150,24 @@ func (c *KVMetadataPutCommand) Run(args []string) int { } path = addPrefixToVKVPath(path, mountPath, "metadata") - data := map[string]interface{}{ - "max_versions": c.flagMaxVersions, - "cas_required": c.flagCASRequired, - "custom_metadata": c.flagCustomMetadata, + data := map[string]interface{}{} + + if c.flagMaxVersions >= 0 { + data["max_versions"] = c.flagMaxVersions } if c.flagDeleteVersionAfter >= 0 { data["delete_version_after"] = c.flagDeleteVersionAfter.String() } + if c.flagCASRequired.IsSet() { + data["cas_required"] = c.flagCASRequired.Get() + } + + if len(c.flagCustomMetadata) > 0 { + data["custom_metadata"] = c.flagCustomMetadata + } + secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) diff --git a/command/kv_metadata_put_test.go b/command/kv_metadata_put_test.go index d55ea258a..680202219 100644 --- a/command/kv_metadata_put_test.go +++ b/command/kv_metadata_put_test.go @@ -1,11 +1,13 @@ package command import ( + "encoding/json" + "strings" + "testing" + "github.com/go-test/deep" "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" - "strings" - "testing" ) func testKVMetadataPutCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPutCommand) { @@ -19,7 +21,7 @@ func testKVMetadataPutCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPutCommand } } -func TestKvMetadataPutCommandDeleteVersionAfter(t *testing.T) { +func TestKvMetadataPutCommand_DeleteVersionAfter(t *testing.T) { client, closer := testVaultServer(t) defer closer() @@ -78,7 +80,7 @@ func TestKvMetadataPutCommandDeleteVersionAfter(t *testing.T) { } } -func TestKvMetadataPutCommandCustomMetadata(t *testing.T) { +func TestKvMetadataPutCommand_CustomMetadata(t *testing.T) { client, closer := testVaultServer(t) defer closer() @@ -154,3 +156,47 @@ func TestKvMetadataPutCommandCustomMetadata(t *testing.T) { t.Fatal(diff) } } + +func TestKvMetadataPutCommand_UnprovidedFlags(t *testing.T) { + client, closer := testVaultServer(t) + defer closer() + + basePath := t.Name() + "/" + secretPath := basePath + "my-secret" + + if err := client.Sys().Mount(basePath, &api.MountInput{ + Type: "kv-v2", + }); err != nil { + t.Fatalf("kv-v2 mount error: %#v", err) + } + + _, cmd := testKVMetadataPutCommand(t) + cmd.client = client + + args := []string{"-cas-required=true", "-max-versions=10", secretPath} + code, _ := kvMetadataPutWithRetry(t, client, args, nil) + + if code != 0 { + t.Fatalf("expected 0 exit status but received %d", code) + } + + args = []string{"-custom-metadata=foo=bar", secretPath} + code, _ = kvMetadataPutWithRetry(t, client, args, nil) + + if code != 0 { + t.Fatalf("expected 0 exit status but received %d", code) + } + + secret, err := client.Logical().Read(basePath + "metadata/" + "my-secret") + if err != nil { + t.Fatal(err) + } + + if secret.Data["cas_required"] != true { + t.Fatalf("expected cas_required to be true but received %#v", secret.Data["cas_required"]) + } + + if secret.Data["max_versions"] != json.Number("10") { + t.Fatalf("expected max_versions to be 10 but received %#v", secret.Data["max_versions"]) + } +} diff --git a/go.mod b/go.mod index 8aebf1577..82b030536 100644 --- a/go.mod +++ b/go.mod @@ -108,7 +108,7 @@ require ( github.com/hashicorp/vault-plugin-secrets-azure v0.11.2 github.com/hashicorp/vault-plugin-secrets-gcp v0.11.1 github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0 - github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211123171606-16933c88368a + github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6 github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1 github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0 github.com/hashicorp/vault-plugin-secrets-terraform v0.3.0 @@ -116,7 +116,7 @@ require ( github.com/hashicorp/vault/api v1.3.1 github.com/hashicorp/vault/api/auth/approle v0.1.0 github.com/hashicorp/vault/api/auth/userpass v0.1.0 - github.com/hashicorp/vault/sdk v0.3.0 + github.com/hashicorp/vault/sdk v0.3.1-0.20220112143259-b48602fdb885 github.com/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4 github.com/jcmturner/gokrb5/v8 v8.4.2 github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f diff --git a/go.sum b/go.sum index a1ba457fc..a5ae63556 100644 --- a/go.sum +++ b/go.sum @@ -965,8 +965,8 @@ github.com/hashicorp/vault-plugin-secrets-gcp v0.11.1 h1:v8XfuZVrgP4pIwaZe/GgrPC github.com/hashicorp/vault-plugin-secrets-gcp v0.11.1/go.mod h1:ndpmRkIPHW5UYqv2nn2AJNVZsucJ8lY2bp5i5Ngvhuc= github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0 h1:0Vi5WEIpZctk/ZoRClodV9WCnM/lCzw9XekMhRZdo8k= github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0/go.mod h1:6DPwGu8oGR1sZRpjwkcAnrQZWQuAJ/Ph+rQHfUo1Yf4= -github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211123171606-16933c88368a h1:GVA3sY+FRhQrMexWGMCsIfVVMgcdru36WMKvDtKed5I= -github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20211123171606-16933c88368a/go.mod h1:TNPRoB53Twd9tYvlhqqEhMsQPiVN604kZw9jr2zUzDk= +github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6 h1:Z3NnaIBragxW6iTW7OnvklRzZSZdaidxjs/vkCneGAg= +github.com/hashicorp/vault-plugin-secrets-kv v0.5.7-0.20220112155832-c2eb38b5f5b6/go.mod h1:9V2Ecim3m/qw+YAQelUeFADqZ1GVo8xwoLqfKsqh9pI= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1 h1:Maewon4nu0KL1ALBOvL6Rsj+Qyr9hdULWflyMz7+9nk= github.com/hashicorp/vault-plugin-secrets-mongodbatlas v0.5.1/go.mod h1:PLx2vxXukfsKsDRo/PlG4fxmJ1d+H2h82wT3vf4buuI= github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0 h1:d6N/aMlklMfEacyiIuu5ZnTlADhGkGZkDrOtQXBRuhI= diff --git a/vault/request_handling.go b/vault/request_handling.go index 83f654e0d..e1a4915f1 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -464,7 +464,8 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request // backends. Basically, it's all just terrible, so don't allow it. if strings.HasSuffix(req.Path, "/") && (req.Operation == logical.UpdateOperation || - req.Operation == logical.CreateOperation) { + req.Operation == logical.CreateOperation || + req.Operation == logical.PatchOperation) { return logical.ErrorResponse("cannot write to a path ending in '/'"), nil } diff --git a/website/content/api-docs/secret/kv/kv-v2.mdx b/website/content/api-docs/secret/kv/kv-v2.mdx index a3c2591a2..45c8fa702 100644 --- a/website/content/api-docs/secret/kv/kv-v2.mdx +++ b/website/content/api-docs/secret/kv/kv-v2.mdx @@ -29,7 +29,7 @@ key-value store. - `max_versions` `(int: 0)` – The number of versions to keep per key. This value applies to all keys, but a key's metadata setting can overwrite this value. - Once a key has more than the configured allowed versions the oldest version + Once a key has more than the configured allowed versions, the oldest version will be permanently deleted. When 0 is used or the value is unset, Vault will keep 10 versions. @@ -519,10 +519,10 @@ It does not create a new version. - `max_versions` `(int: 0)` – The number of versions to keep per key. If not set, the backend’s configured max version is used. Once a key has more than - the configured allowed versions the oldest version will be permanently + the configured allowed versions, the oldest version will be permanently deleted. -- `cas_required` `(bool: false)` – If true the key will require the cas +- `cas_required` `(bool: false)` – If true, the key will require the `cas` parameter to be set on all write requests. If false, the backend’s configuration will be used. @@ -561,6 +561,60 @@ $ curl \ https://127.0.0.1:8200/v1/secret/metadata/my-secret ``` +## Patch Metadata +This endpoint patches an existing metadata entry of a secret at the specified +location. The calling token must have an ACL policy granting the `patch` +capability. Currently, only JSON merge patch is supported and must be specified +using a `Content-Type` header value of `application/merge-patch+json`. It does +not create a new version. + +| Method | Path | +| :------ | :----------------------- | +| `PATCH` | `/secret/metadata/:path` | + +### Parameters + +- `max_versions` `(int: 0)` – The number of versions to keep per key. If not + set, the backend’s configured max version is used. Once a key has more than + the configured allowed versions, the oldest version will be permanently + deleted. + +- `cas_required` `(bool: false)` – If true, the key will require the `cas` + parameter to be set on all write requests. If false, the backend’s + configuration will be used. + +- `delete_version_after` `(string:"0s")` – Set the `delete_version_after` value + to a duration to specify the `deletion_time` for all new versions + written to this key. If not set, the backend's `delete_version_after` will be + used. If the value is greater than the backend's `delete_version_after`, the + backend's `delete_version_after` will be used. Accepts [Go duration + format string][duration-godoc]. + +- `custom_metadata` `(map: nil)` - A map of arbitrary string to string valued user-provided metadata meant + to describe the secret. + +### Sample Payload + +```json +{ + "max_versions": 5, + "custom_metadata": { + "bar": "123", + } +} +``` + +### Sample Request + +```shell-session +$ curl \ + --header "X-Vault-Token: ..." \ + --header "Content-Type: application/merge-patch+json" + --request PATCH \ + --data @payload.json \ + https://127.0.0.1:8200/v1/secret/metadata/my-secret +``` + ## Delete Metadata and All Versions This endpoint permanently deletes the key metadata and all version data for the diff --git a/website/content/docs/secrets/kv/kv-v2.mdx b/website/content/docs/secrets/kv/kv-v2.mdx index 31e2a8af4..d81609c48 100644 --- a/website/content/docs/secrets/kv/kv-v2.mdx +++ b/website/content/docs/secrets/kv/kv-v2.mdx @@ -169,7 +169,7 @@ allows for writing keys with arbitrary values. 1. Write arbitrary data: - ```text + ```shell-session $ vault kv put secret/my-secret foo=a bar=b Key Value --- ----- @@ -182,7 +182,7 @@ allows for writing keys with arbitrary values. 1. Read arbitrary data: - ```text + ```shell-session $ vault kv get secret/my-secret ====== Metadata ====== Key Value @@ -206,7 +206,7 @@ allows for writing keys with arbitrary values. allowed if the key’s current version matches the version specified in the cas parameter. - ```text + ```shell-session $ vault kv put -cas=1 secret/my-secret foo=aa bar=bb Key Value --- ----- @@ -219,7 +219,7 @@ allows for writing keys with arbitrary values. 1. Reading now will return the newest version of the data: - ```text + ```shell-session $ vault kv get secret/my-secret ====== Metadata ====== Key Value @@ -249,7 +249,7 @@ allows for writing keys with arbitrary values. read-then-write flow will use the `version` value from the secret returned by the read to perform a check-and-set operation in the subsequent write. - ```text + ```shell-session $ vault kv patch -cas=2 secret/my-secret bar=bbb Key Value --- ----- @@ -266,7 +266,7 @@ allows for writing keys with arbitrary values. Perform a patch using the `patch` method: - ```text + ```shell-session $ vault kv patch -method=patch -cas=2 secret/my-secret bar=bbb Key Value --- ----- @@ -278,7 +278,7 @@ allows for writing keys with arbitrary values. ``` Perform a patch using the read-then-write method: - ```text + ```shell-session $ vault kv patch -method=rw secret/my-secret bar=bbb Key Value --- ----- @@ -292,7 +292,7 @@ allows for writing keys with arbitrary values. 1. Reading after a patch will return the newest version of the data in which only the specified fields were updated: - ```text + ```shell-session $ vault kv get secret/my-secret ====== Metadata ====== Key Value @@ -312,7 +312,7 @@ allows for writing keys with arbitrary values. 1. Previous versions can be accessed with the `-version` flag: - ```text + ```shell-session $ vault kv get -version=1 secret/my-secret ====== Metadata ====== Key Value @@ -349,14 +349,14 @@ See the commands below for more information: 1. The latest version of a key can be deleted with the delete command, this also takes a `-versions` flag to delete prior versions: - ```text + ```shell-session $ vault kv delete secret/my-secret - t + Success! Data deleted (if it existed) at: secret/my-secret ``` 1. Versions can be undeleted: - ```text + ```shell-session $ vault kv undelete -versions=2 secret/my-secret Success! Data written to: secret/undelete/my-secret @@ -378,7 +378,7 @@ See the commands below for more information: 1. Destroying a version permanently deletes the underlying data: - ```text + ```shell-session $ vault kv destroy -versions=2 secret/my-secret Success! Data written to: secret/destroy/my-secret ``` @@ -393,7 +393,7 @@ See the commands below for more information: 1. All metadata and versions for a key can be viewed: - ```text + ```shell-session $ vault kv metadata get secret/my-secret ========== Metadata ========== Key Value @@ -424,7 +424,7 @@ See the commands below for more information: 1. The metadata settings for a key can be configured: - ```text + ```shell-session $ vault kv metadata put -max-versions 2 -delete-version-after="3h25m19s" secret/my-secret Success! Data written to: secret/metadata/my-secret ``` @@ -432,7 +432,7 @@ See the commands below for more information: Delete-version-after settings will apply only to new versions. Max versions changes will be applied on next write: - ```text + ```shell-session $ vault kv put secret/my-secret my-value=newer-s3cr3t Key Value --- ----- @@ -446,7 +446,7 @@ See the commands below for more information: Once a key has more versions than max versions the oldest versions are cleaned up: - ```text + ```shell-session $ vault kv metadata get secret/my-secret ========== Metadata ========== Key Value @@ -476,17 +476,25 @@ See the commands below for more information: ``` A secret's key metadata can contain custom metadata used to describe the secret. The - data will be stored as string-to-string key-value pairs. If the `-custom-metadata` flag - is set, the value of `custom_metadata` will be fully overwritten. The `-custom-metadata` - flag can be repeated to add multiple key-value pairs: + data will be stored as string-to-string key-value pairs. The `-custom-metadata` + flag can be repeated to add multiple key-value pairs. - ```text - vault kv metadata put -custom-metadata=foo=abc -custom-metadata=bar=123 secret/my-secret + 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 ``` + The `vault kv metadata patch` command can be used to partially overwrite the value of `custom_metadata`. + 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 + ``` + 1. Permanently delete all metadata and versions for a key: - ```text + ```shell-session $ vault kv metadata delete secret/my-secret Success! Data deleted (if it existed) at: secret/metadata/my-secret ```