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:
parent
2adf4df7d7
commit
d52d69e4bb
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
secrets/kv: add patch support for KVv2 key metadata
|
||||
```
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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"])
|
||||
}
|
||||
}
|
||||
|
|
4
go.mod
4
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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
|
||||
|
||||
This endpoint permanently deletes the key metadata and all version data for the
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue