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
This commit is contained in:
Chris Capurso 2022-01-12 12:05:27 -05:00 committed by GitHub
parent 2adf4df7d7
commit d52d69e4bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 633 additions and 43 deletions

3
changelog/13215.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
secrets/kv: add patch support for KVv2 key metadata
```

View File

@ -718,6 +718,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
BaseCommand: getBaseCommand(), BaseCommand: getBaseCommand(),
}, nil }, nil
}, },
"kv metadata patch": func() (cli.Command, error) {
return &KVMetadataPatchCommand{
BaseCommand: getBaseCommand(),
}, nil
},
"kv metadata get": func() (cli.Command, error) { "kv metadata get": func() (cli.Command, error) {
return &KVMetadataGetCommand{ return &KVMetadataGetCommand{
BaseCommand: getBaseCommand(), BaseCommand: getBaseCommand(),

View File

@ -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 backends 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 backends 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)
}

View File

@ -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)
}
}

View File

@ -19,7 +19,7 @@ type KVMetadataPutCommand struct {
*BaseCommand *BaseCommand
flagMaxVersions int flagMaxVersions int
flagCASRequired bool flagCASRequired BoolPtr
flagDeleteVersionAfter time.Duration flagDeleteVersionAfter time.Duration
flagCustomMetadata map[string]string flagCustomMetadata map[string]string
testStdin io.Reader // for tests testStdin io.Reader // for tests
@ -71,14 +71,13 @@ func (c *KVMetadataPutCommand) Flags() *FlagSets {
f.IntVar(&IntVar{ f.IntVar(&IntVar{
Name: "max-versions", Name: "max-versions",
Target: &c.flagMaxVersions, Target: &c.flagMaxVersions,
Default: 0, Default: -1,
Usage: `The number of versions to keep. If not set, the backends configured max version is used.`, Usage: `The number of versions to keep. If not set, the backends configured max version is used.`,
}) })
f.BoolVar(&BoolVar{ f.BoolPtrVar(&BoolPtrVar{
Name: "cas-required", Name: "cas-required",
Target: &c.flagCASRequired, Target: &c.flagCASRequired,
Default: false,
Usage: `If true the key will require the cas parameter to be set on all write requests. If false, the backends configuration will be used.`, Usage: `If true the key will require the cas parameter to be set on all write requests. If false, the backends configuration will be used.`,
}) })
@ -151,16 +150,24 @@ func (c *KVMetadataPutCommand) Run(args []string) int {
} }
path = addPrefixToVKVPath(path, mountPath, "metadata") path = addPrefixToVKVPath(path, mountPath, "metadata")
data := map[string]interface{}{ data := map[string]interface{}{}
"max_versions": c.flagMaxVersions,
"cas_required": c.flagCASRequired, if c.flagMaxVersions >= 0 {
"custom_metadata": c.flagCustomMetadata, data["max_versions"] = c.flagMaxVersions
} }
if c.flagDeleteVersionAfter >= 0 { if c.flagDeleteVersionAfter >= 0 {
data["delete_version_after"] = c.flagDeleteVersionAfter.String() 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) secret, err := client.Logical().Write(path, data)
if err != nil { 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", path, err))

View File

@ -1,11 +1,13 @@
package command package command
import ( import (
"encoding/json"
"strings"
"testing"
"github.com/go-test/deep" "github.com/go-test/deep"
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"strings"
"testing"
) )
func testKVMetadataPutCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPutCommand) { 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) client, closer := testVaultServer(t)
defer closer() 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) client, closer := testVaultServer(t)
defer closer() defer closer()
@ -154,3 +156,47 @@ func TestKvMetadataPutCommandCustomMetadata(t *testing.T) {
t.Fatal(diff) 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"])
}
}

4
go.mod
View File

@ -108,7 +108,7 @@ require (
github.com/hashicorp/vault-plugin-secrets-azure v0.11.2 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-gcp v0.11.1
github.com/hashicorp/vault-plugin-secrets-gcpkms v0.10.0 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-mongodbatlas v0.5.1
github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0 github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0
github.com/hashicorp/vault-plugin-secrets-terraform v0.3.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 v1.3.1
github.com/hashicorp/vault/api/auth/approle v0.1.0 github.com/hashicorp/vault/api/auth/approle v0.1.0
github.com/hashicorp/vault/api/auth/userpass 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/influxdata/influxdb v0.0.0-20190411212539-d24b7ba8c4c4
github.com/jcmturner/gokrb5/v8 v8.4.2 github.com/jcmturner/gokrb5/v8 v8.4.2
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f

4
go.sum
View File

@ -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-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 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-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.20220112155832-c2eb38b5f5b6 h1:Z3NnaIBragxW6iTW7OnvklRzZSZdaidxjs/vkCneGAg=
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/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 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-mongodbatlas v0.5.1/go.mod h1:PLx2vxXukfsKsDRo/PlG4fxmJ1d+H2h82wT3vf4buuI=
github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0 h1:d6N/aMlklMfEacyiIuu5ZnTlADhGkGZkDrOtQXBRuhI= github.com/hashicorp/vault-plugin-secrets-openldap v0.6.0 h1:d6N/aMlklMfEacyiIuu5ZnTlADhGkGZkDrOtQXBRuhI=

View File

@ -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. // backends. Basically, it's all just terrible, so don't allow it.
if strings.HasSuffix(req.Path, "/") && if strings.HasSuffix(req.Path, "/") &&
(req.Operation == logical.UpdateOperation || (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 return logical.ErrorResponse("cannot write to a path ending in '/'"), nil
} }

View File

@ -29,7 +29,7 @@ key-value store.
- `max_versions` `(int: 0)` The number of versions to keep per key. This value - `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. 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 be permanently deleted. When 0 is used or the value is unset, Vault
will keep 10 versions. 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 - `max_versions` `(int: 0)` The number of versions to keep per key. If not
set, the backends configured max version is used. Once a key has more than set, the backends 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. 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 backends parameter to be set on all write requests. If false, the backends
configuration will be used. configuration will be used.
@ -561,6 +561,60 @@ $ curl \
https://127.0.0.1:8200/v1/secret/metadata/my-secret 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 backends 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 backends
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<string|string>: 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 ## Delete Metadata and All Versions
This endpoint permanently deletes the key metadata and all version data for the This endpoint permanently deletes the key metadata and all version data for the

View File

@ -169,7 +169,7 @@ allows for writing keys with arbitrary values.
1. Write arbitrary data: 1. Write arbitrary data:
```text ```shell-session
$ vault kv put secret/my-secret foo=a bar=b $ vault kv put secret/my-secret foo=a bar=b
Key Value Key Value
--- ----- --- -----
@ -182,7 +182,7 @@ allows for writing keys with arbitrary values.
1. Read arbitrary data: 1. Read arbitrary data:
```text ```shell-session
$ vault kv get secret/my-secret $ vault kv get secret/my-secret
====== Metadata ====== ====== Metadata ======
Key Value Key Value
@ -206,7 +206,7 @@ allows for writing keys with arbitrary values.
allowed if the keys current version matches the version specified in the allowed if the keys current version matches the version specified in the
cas parameter. cas parameter.
```text ```shell-session
$ vault kv put -cas=1 secret/my-secret foo=aa bar=bb $ vault kv put -cas=1 secret/my-secret foo=aa bar=bb
Key Value Key Value
--- ----- --- -----
@ -219,7 +219,7 @@ allows for writing keys with arbitrary values.
1. Reading now will return the newest version of the data: 1. Reading now will return the newest version of the data:
```text ```shell-session
$ vault kv get secret/my-secret $ vault kv get secret/my-secret
====== Metadata ====== ====== Metadata ======
Key Value 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 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. 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 $ vault kv patch -cas=2 secret/my-secret bar=bbb
Key Value Key Value
--- ----- --- -----
@ -266,7 +266,7 @@ allows for writing keys with arbitrary values.
Perform a patch using the `patch` method: Perform a patch using the `patch` method:
```text ```shell-session
$ vault kv patch -method=patch -cas=2 secret/my-secret bar=bbb $ vault kv patch -method=patch -cas=2 secret/my-secret bar=bbb
Key Value Key Value
--- ----- --- -----
@ -278,7 +278,7 @@ allows for writing keys with arbitrary values.
``` ```
Perform a patch using the read-then-write method: Perform a patch using the read-then-write method:
```text ```shell-session
$ vault kv patch -method=rw secret/my-secret bar=bbb $ vault kv patch -method=rw secret/my-secret bar=bbb
Key Value 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 1. Reading after a patch will return the newest version of the data in which
only the specified fields were updated: only the specified fields were updated:
```text ```shell-session
$ vault kv get secret/my-secret $ vault kv get secret/my-secret
====== Metadata ====== ====== Metadata ======
Key Value Key Value
@ -312,7 +312,7 @@ allows for writing keys with arbitrary values.
1. Previous versions can be accessed with the `-version` flag: 1. Previous versions can be accessed with the `-version` flag:
```text ```shell-session
$ vault kv get -version=1 secret/my-secret $ vault kv get -version=1 secret/my-secret
====== Metadata ====== ====== Metadata ======
Key Value 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 1. The latest version of a key can be deleted with the delete command, this also
takes a `-versions` flag to delete prior versions: takes a `-versions` flag to delete prior versions:
```text ```shell-session
$ vault kv delete secret/my-secret $ vault kv delete secret/my-secret
t Success! Data deleted (if it existed) at: secret/my-secret
``` ```
1. Versions can be undeleted: 1. Versions can be undeleted:
```text ```shell-session
$ vault kv undelete -versions=2 secret/my-secret $ vault kv undelete -versions=2 secret/my-secret
Success! Data written to: secret/undelete/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: 1. Destroying a version permanently deletes the underlying data:
```text ```shell-session
$ vault kv destroy -versions=2 secret/my-secret $ vault kv destroy -versions=2 secret/my-secret
Success! Data written to: secret/destroy/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: 1. All metadata and versions for a key can be viewed:
```text ```shell-session
$ vault kv metadata get secret/my-secret $ vault kv metadata get secret/my-secret
========== Metadata ========== ========== Metadata ==========
Key Value Key Value
@ -424,7 +424,7 @@ See the commands below for more information:
1. The metadata settings for a key can be configured: 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 $ vault kv metadata put -max-versions 2 -delete-version-after="3h25m19s" secret/my-secret
Success! Data written to: secret/metadata/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 Delete-version-after settings will apply only to new versions. Max versions
changes will be applied on next write: changes will be applied on next write:
```text ```shell-session
$ vault kv put secret/my-secret my-value=newer-s3cr3t $ vault kv put secret/my-secret my-value=newer-s3cr3t
Key Value 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 Once a key has more versions than max versions the oldest versions
are cleaned up: are cleaned up:
```text ```shell-session
$ vault kv metadata get secret/my-secret $ vault kv metadata get secret/my-secret
========== Metadata ========== ========== Metadata ==========
Key Value 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 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 data will be stored as string-to-string key-value pairs. The `-custom-metadata`
is set, the value of `custom_metadata` will be fully overwritten. The `-custom-metadata` flag can be repeated to add multiple key-value pairs.
flag can be repeated to add multiple key-value pairs:
```text The `vault kv metadata put` command can be used to fully overwrite the value of `custom_metadata`:
vault kv metadata put -custom-metadata=foo=abc -custom-metadata=bar=123 secret/my-secret
```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: 1. Permanently delete all metadata and versions for a key:
```text ```shell-session
$ vault kv metadata delete secret/my-secret $ vault kv metadata delete secret/my-secret
Success! Data deleted (if it existed) at: secret/metadata/my-secret Success! Data deleted (if it existed) at: secret/metadata/my-secret
``` ```