mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
Kv preflight (#4430)
* Update kv command to use a preflight check * Make the existing ui endpoint return the allowed mounts * Add kv subcommand tests * Enable `-field` in `vault kv get/put` (#4426) * Enable `-field` in `vault kv get/put` Fixes #4424 * Unify nil value handling * Use preflight helper * Update vkv plugin * Add all the mount info when authenticated * Add fix the error message on put * add metadata test * No need to sort the capabilities * Remove the kv client header * kv patch command (#4432) * Fix test * Fix tests * Use permission denied instead of entity disabled
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
kv "github.com/hashicorp/vault-plugin-secrets-kv"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/hashicorp/vault/builtin/logical/pki"
|
||||
@@ -41,6 +42,7 @@ var (
|
||||
"pki": pki.Factory,
|
||||
"ssh": ssh.Factory,
|
||||
"transit": transit.Factory,
|
||||
"kv": kv.Factory,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -695,6 +695,13 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
"kv patch": func() (cli.Command, error) {
|
||||
return &KVPatchCommand{
|
||||
BaseCommand: &BaseCommand{
|
||||
UI: ui,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
"kv get": func() (cli.Command, error) {
|
||||
return &KVGetCommand{
|
||||
BaseCommand: &BaseCommand{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
@@ -87,13 +88,25 @@ func (c *KVDeleteCommand) Run(args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
path := sanitizePath(args[0])
|
||||
var err error
|
||||
if len(c.flagVersions) > 0 {
|
||||
err = c.deleteVersions(path, kvParseVersionsFlags(c.flagVersions))
|
||||
} else {
|
||||
err = c.deleteLatest(path)
|
||||
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 {
|
||||
err = c.deleteV2(path, mountPath, client)
|
||||
} else {
|
||||
_, err = client.Logical().Delete(path)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err))
|
||||
return 2
|
||||
@@ -103,39 +116,29 @@ func (c *KVDeleteCommand) Run(args []string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *KVDeleteCommand) deleteLatest(path string) error {
|
||||
func (c *KVDeleteCommand) deleteV2(path, mountPath string, client *api.Client) error {
|
||||
var err error
|
||||
path, err = addPrefixToVKVPath(path, "data")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch {
|
||||
case len(c.flagVersions) > 0:
|
||||
path = addPrefixToVKVPath(path, mountPath, "delete")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"versions": kvParseVersionsFlags(c.flagVersions),
|
||||
}
|
||||
|
||||
_, err = kvDeleteRequest(client, path)
|
||||
_, err = client.Logical().Write(path, data)
|
||||
default:
|
||||
|
||||
path = addPrefixToVKVPath(path, mountPath, "data")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Logical().Delete(path)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *KVDeleteCommand) deleteVersions(path string, versions []string) error {
|
||||
var err error
|
||||
path, err = addPrefixToVKVPath(path, "delete")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"versions": versions,
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = kvWriteRequest(client, path, data)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -86,7 +86,23 @@ func (c *KVDestroyCommand) Run(args []string) int {
|
||||
}
|
||||
var err error
|
||||
path := sanitizePath(args[0])
|
||||
path, err = addPrefixToVKVPath(path, "destroy")
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
mountPath, v2, err := isKVv2(path, client)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
if !v2 {
|
||||
c.UI.Error("Destroy not supported on KV Version 1")
|
||||
return 1
|
||||
}
|
||||
path = addPrefixToVKVPath(path, mountPath, "destroy")
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
@@ -96,13 +112,7 @@ func (c *KVDestroyCommand) Run(args []string) int {
|
||||
"versions": kvParseVersionsFlags(c.flagVersions),
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
secret, err := kvWriteRequest(client, path, data)
|
||||
secret, err := client.Logical().Write(path, data)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
|
||||
return 2
|
||||
|
||||
@@ -43,7 +43,7 @@ Usage: vault kv get [options] KEY
|
||||
}
|
||||
|
||||
func (c *KVGetCommand) Flags() *FlagSets {
|
||||
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
|
||||
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
|
||||
|
||||
// Common Options
|
||||
f := set.NewFlagSet("Common Options")
|
||||
@@ -91,16 +91,25 @@ func (c *KVGetCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
path := sanitizePath(args[0])
|
||||
path, err = addPrefixToVKVPath(path, "data")
|
||||
mountPath, v2, err := isKVv2(path, client)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
var versionParam map[string]string
|
||||
if c.flagVersion > 0 {
|
||||
versionParam = map[string]string{
|
||||
"version": fmt.Sprintf("%d", c.flagVersion),
|
||||
|
||||
if v2 {
|
||||
path = addPrefixToVKVPath(path, mountPath, "data")
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
if c.flagVersion > 0 {
|
||||
versionParam = map[string]string{
|
||||
"version": fmt.Sprintf("%d", c.flagVersion),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +124,17 @@ func (c *KVGetCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
if c.flagField != "" {
|
||||
return PrintRawField(c.UI, secret, c.flagField)
|
||||
if v2 {
|
||||
// This is a v2, pass in the data field
|
||||
if data, ok := secret.Data["data"]; ok && data != nil {
|
||||
return PrintRawField(c.UI, data, c.flagField)
|
||||
} else {
|
||||
c.UI.Error(fmt.Sprintf("No data found at %s", path))
|
||||
return 2
|
||||
}
|
||||
} else {
|
||||
return PrintRawField(c.UI, secret, c.flagField)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have wrap info print the secret normally.
|
||||
@@ -128,8 +147,18 @@ func (c *KVGetCommand) Run(args []string) int {
|
||||
OutputData(c.UI, metadata)
|
||||
c.UI.Info("")
|
||||
}
|
||||
if data, ok := secret.Data["data"]; ok && data != nil {
|
||||
c.UI.Info(getHeaderForMap("Data", data.(map[string]interface{})))
|
||||
|
||||
data := secret.Data
|
||||
if v2 && data != nil {
|
||||
data = nil
|
||||
dataRaw := secret.Data["data"]
|
||||
if dataRaw != nil {
|
||||
data = dataRaw.(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if data != nil {
|
||||
c.UI.Info(getHeaderForMap("Data", data))
|
||||
OutputData(c.UI, data)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/helper/consts"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
)
|
||||
|
||||
func kvReadRequest(client *api.Client, path string, params map[string]string) (*api.Secret, error) {
|
||||
r := client.NewRequest("GET", "/v1/"+path)
|
||||
if r.Headers == nil {
|
||||
r.Headers = http.Header{}
|
||||
}
|
||||
r.Headers.Add(consts.VaultKVCLIClientHeader, "v2")
|
||||
|
||||
for k, v := range params {
|
||||
r.Params.Set(k, v)
|
||||
}
|
||||
@@ -48,121 +40,55 @@ func kvReadRequest(client *api.Client, path string, params map[string]string) (*
|
||||
return api.ParseSecret(resp.Body)
|
||||
}
|
||||
|
||||
func kvListRequest(client *api.Client, path string) (*api.Secret, error) {
|
||||
r := client.NewRequest("LIST", "/v1/"+path)
|
||||
if r.Headers == nil {
|
||||
r.Headers = http.Header{}
|
||||
}
|
||||
r.Headers.Add(consts.VaultKVCLIClientHeader, "v2")
|
||||
|
||||
// Set this for broader compatibility, but we use LIST above to be able to
|
||||
// handle the wrapping lookup function
|
||||
r.Method = "GET"
|
||||
r.Params.Set("list", "true")
|
||||
func kvPreflightVersionRequest(client *api.Client, path string) (string, int, error) {
|
||||
r := client.NewRequest("GET", "/v1/sys/internal/ui/mount/"+path)
|
||||
resp, err := client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
if resp != nil && resp.StatusCode == 404 {
|
||||
secret, parseErr := api.ParseSecret(resp.Body)
|
||||
switch parseErr {
|
||||
case nil:
|
||||
case io.EOF:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
if secret != nil && (len(secret.Warnings) > 0 || len(secret.Data) > 0) {
|
||||
return secret, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
return api.ParseSecret(resp.Body)
|
||||
secret, err := api.ParseSecret(resp.Body)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
var mountPath string
|
||||
if mountPathRaw, ok := secret.Data["path"]; ok {
|
||||
mountPath = mountPathRaw.(string)
|
||||
}
|
||||
options := secret.Data["options"]
|
||||
if options == nil {
|
||||
return mountPath, 1, nil
|
||||
}
|
||||
versionRaw := options.(map[string]interface{})["version"]
|
||||
if versionRaw == nil {
|
||||
return mountPath, 1, nil
|
||||
}
|
||||
version := versionRaw.(string)
|
||||
switch version {
|
||||
case "", "1":
|
||||
return mountPath, 1, nil
|
||||
case "2":
|
||||
return mountPath, 2, nil
|
||||
}
|
||||
|
||||
return mountPath, 1, nil
|
||||
}
|
||||
|
||||
func kvWriteRequest(client *api.Client, path string, data map[string]interface{}) (*api.Secret, error) {
|
||||
r := client.NewRequest("PUT", "/v1/"+path)
|
||||
if r.Headers == nil {
|
||||
r.Headers = http.Header{}
|
||||
}
|
||||
r.Headers.Add(consts.VaultKVCLIClientHeader, "v2")
|
||||
if err := r.SetJSONBody(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
if resp != nil && resp.StatusCode == 404 {
|
||||
secret, parseErr := api.ParseSecret(resp.Body)
|
||||
switch parseErr {
|
||||
case nil:
|
||||
case io.EOF:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
if secret != nil && (len(secret.Warnings) > 0 || len(secret.Data) > 0) {
|
||||
return secret, err
|
||||
}
|
||||
}
|
||||
func isKVv2(path string, client *api.Client) (string, bool, error) {
|
||||
mountPath, version, err := kvPreflightVersionRequest(client, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
return api.ParseSecret(resp.Body)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return mountPath, version == 2, nil
|
||||
}
|
||||
|
||||
func kvDeleteRequest(client *api.Client, path string) (*api.Secret, error) {
|
||||
r := client.NewRequest("DELETE", "/v1/"+path)
|
||||
if r.Headers == nil {
|
||||
r.Headers = http.Header{}
|
||||
}
|
||||
r.Headers.Add(consts.VaultKVCLIClientHeader, "v2")
|
||||
resp, err := client.RawRequest(r)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
if resp != nil && resp.StatusCode == 404 {
|
||||
secret, parseErr := api.ParseSecret(resp.Body)
|
||||
switch parseErr {
|
||||
case nil:
|
||||
case io.EOF:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
if secret != nil && (len(secret.Warnings) > 0 || len(secret.Data) > 0) {
|
||||
return secret, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
return api.ParseSecret(resp.Body)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func addPrefixToVKVPath(p, apiPrefix string) (string, error) {
|
||||
parts := strings.SplitN(p, "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", errors.New("invalid path")
|
||||
}
|
||||
|
||||
return path.Join(parts[0], apiPrefix, parts[1]), nil
|
||||
func addPrefixToVKVPath(p, mountPath, apiPrefix string) string {
|
||||
p = strings.TrimPrefix(p, mountPath)
|
||||
return path.Join(mountPath, apiPrefix, p)
|
||||
}
|
||||
|
||||
func getHeaderForMap(header string, data map[string]interface{}) string {
|
||||
|
||||
@@ -74,13 +74,21 @@ func (c *KVListCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
path := ensureTrailingSlash(sanitizePath(args[0]))
|
||||
path, err = addPrefixToVKVPath(path, "metadata")
|
||||
mountPath, v2, err := isKVv2(path, client)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
secret, err := kvListRequest(client, path)
|
||||
if v2 {
|
||||
path = addPrefixToVKVPath(path, mountPath, "metadata")
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
secret, err := client.Logical().List(path)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err))
|
||||
return 2
|
||||
|
||||
@@ -71,13 +71,18 @@ func (c *KVMetadataDeleteCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
path := sanitizePath(args[0])
|
||||
path, err = addPrefixToVKVPath(path, "metadata")
|
||||
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
|
||||
}
|
||||
|
||||
if _, err := kvDeleteRequest(client, path); err != nil {
|
||||
path = addPrefixToVKVPath(path, mountPath, "metadata")
|
||||
if _, err := client.Logical().Delete(path); err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err))
|
||||
return 2
|
||||
}
|
||||
|
||||
@@ -75,13 +75,18 @@ func (c *KVMetadataGetCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
path := sanitizePath(args[0])
|
||||
path, err = addPrefixToVKVPath(path, "metadata")
|
||||
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
|
||||
}
|
||||
|
||||
secret, err := kvReadRequest(client, path, nil)
|
||||
path = addPrefixToVKVPath(path, mountPath, "metadata")
|
||||
secret, err := client.Logical().Read(path)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err))
|
||||
return 2
|
||||
|
||||
@@ -99,26 +99,30 @@ func (c *KVMetadataPutCommand) Run(args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
var err error
|
||||
path := sanitizePath(args[0])
|
||||
path, err = addPrefixToVKVPath(path, "metadata")
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"max_versions": c.flagMaxVersions,
|
||||
"cas_required": c.flagCASRequired,
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
secret, err := kvWriteRequest(client, path, data)
|
||||
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{}{
|
||||
"max_versions": c.flagMaxVersions,
|
||||
"cas_required": c.flagCASRequired,
|
||||
}
|
||||
|
||||
secret, err := client.Logical().Write(path, data)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
|
||||
return 2
|
||||
|
||||
195
command/kv_patch.go
Normal file
195
command/kv_patch.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var _ cli.Command = (*KVPatchCommand)(nil)
|
||||
var _ cli.CommandAutocomplete = (*KVPatchCommand)(nil)
|
||||
|
||||
type KVPatchCommand struct {
|
||||
*BaseCommand
|
||||
|
||||
testStdin io.Reader // for tests
|
||||
}
|
||||
|
||||
func (c *KVPatchCommand) Synopsis() string {
|
||||
return "Sets or updates data in the KV store without overwriting."
|
||||
}
|
||||
|
||||
func (c *KVPatchCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: vault kv put [options] KEY [DATA]
|
||||
|
||||
*NOTE*: This is only supported for KV v2 engine mounts.
|
||||
|
||||
Writes the data to the given path in the key-value store. The data can be of
|
||||
any type.
|
||||
|
||||
$ vault kv patch secret/foo bar=baz
|
||||
|
||||
The data can also be consumed from a file on disk by prefixing with the "@"
|
||||
symbol. For example:
|
||||
|
||||
$ vault kv patch secret/foo @data.json
|
||||
|
||||
Or it can be read from stdin using the "-" symbol:
|
||||
|
||||
$ echo "abcd1234" | vault kv patch secret/foo bar=-
|
||||
|
||||
Additional flags and more advanced use cases are detailed below.
|
||||
|
||||
` + c.Flags().Help()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *KVPatchCommand) Flags() *FlagSets {
|
||||
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *KVPatchCommand) AutocompleteArgs() complete.Predictor {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *KVPatchCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *KVPatchCommand) Run(args []string) int {
|
||||
f := c.Flags()
|
||||
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
args = f.Args()
|
||||
// Pull our fake stdin if needed
|
||||
stdin := (io.Reader)(os.Stdin)
|
||||
if c.testStdin != nil {
|
||||
stdin = c.testStdin
|
||||
}
|
||||
|
||||
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("Must supply data")
|
||||
return 1
|
||||
}
|
||||
|
||||
var err error
|
||||
path := sanitizePath(args[0])
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
newData, err := parseArgsData(stdin, args[1:])
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
mountPath, v2, err := isKVv2(path, client)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
if !v2 {
|
||||
c.UI.Error(fmt.Sprintf("K/V engine mount must be version 2 for patch support"))
|
||||
return 2
|
||||
}
|
||||
|
||||
path = addPrefixToVKVPath(path, mountPath, "data")
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
// First, do a read
|
||||
secret, err := kvReadRequest(client, path, nil)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error doing pre-read at %s: %s", path, err))
|
||||
return 2
|
||||
}
|
||||
|
||||
// Make sure a value already exists
|
||||
if secret == nil || secret.Data == nil {
|
||||
c.UI.Error(fmt.Sprintf("No value found at %s", path))
|
||||
return 2
|
||||
}
|
||||
|
||||
// Verify metadata found
|
||||
rawMeta, ok := secret.Data["metadata"]
|
||||
if !ok || rawMeta == nil {
|
||||
c.UI.Error(fmt.Sprintf("No metadata found at %s; patch only works on existing data", path))
|
||||
return 2
|
||||
}
|
||||
meta, ok := rawMeta.(map[string]interface{})
|
||||
if !ok {
|
||||
c.UI.Error(fmt.Sprintf("Metadata found at %s is not the expected type (JSON object)", path))
|
||||
return 2
|
||||
}
|
||||
if meta == nil {
|
||||
c.UI.Error(fmt.Sprintf("No metadata found at %s; patch only works on existing data", path))
|
||||
return 2
|
||||
}
|
||||
|
||||
// Verify old data found
|
||||
rawData, ok := secret.Data["data"]
|
||||
if !ok || rawData == nil {
|
||||
c.UI.Error(fmt.Sprintf("No data found at %s; patch only works on existing data", path))
|
||||
return 2
|
||||
}
|
||||
data, ok := rawData.(map[string]interface{})
|
||||
if !ok {
|
||||
c.UI.Error(fmt.Sprintf("Data found at %s is not the expected type (JSON object)", path))
|
||||
return 2
|
||||
}
|
||||
if data == nil {
|
||||
c.UI.Error(fmt.Sprintf("No data found at %s; patch only works on existing data", path))
|
||||
return 2
|
||||
}
|
||||
|
||||
// Copy new data over
|
||||
for k, v := range newData {
|
||||
data[k] = v
|
||||
}
|
||||
|
||||
secret, err = client.Logical().Write(path, map[string]interface{}{
|
||||
"data": data,
|
||||
"options": map[string]interface{}{
|
||||
"cas": meta["version"],
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
|
||||
return 2
|
||||
}
|
||||
if secret == nil {
|
||||
// Don't output anything unless using the "table" format
|
||||
if Format(c.UI) == "table" {
|
||||
c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if c.flagField != "" {
|
||||
return PrintRawField(c.UI, secret, c.flagField)
|
||||
}
|
||||
|
||||
return OutputSecret(c.UI, secret)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ Usage: vault kv put [options] KEY [DATA]
|
||||
}
|
||||
|
||||
func (c *KVPutCommand) Flags() *FlagSets {
|
||||
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
|
||||
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
|
||||
|
||||
// Common Options
|
||||
f := set.NewFlagSet("Common Options")
|
||||
@@ -97,14 +97,19 @@ func (c *KVPutCommand) Run(args []string) int {
|
||||
stdin = c.testStdin
|
||||
}
|
||||
|
||||
if len(args) < 1 {
|
||||
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(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("Must supply data")
|
||||
return 1
|
||||
}
|
||||
|
||||
var err error
|
||||
path := sanitizePath(args[0])
|
||||
path, err = addPrefixToVKVPath(path, "data")
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
@@ -116,22 +121,25 @@ func (c *KVPutCommand) Run(args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
data = map[string]interface{}{
|
||||
"data": data,
|
||||
"options": map[string]interface{}{},
|
||||
}
|
||||
|
||||
if c.flagCAS > -1 {
|
||||
data["options"].(map[string]interface{})["cas"] = c.flagCAS
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
mountPath, v2, err := isKVv2(path, client)
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
secret, err := kvWriteRequest(client, path, data)
|
||||
if v2 {
|
||||
path = addPrefixToVKVPath(path, mountPath, "data")
|
||||
data = map[string]interface{}{
|
||||
"data": data,
|
||||
"options": map[string]interface{}{},
|
||||
}
|
||||
|
||||
if c.flagCAS > -1 {
|
||||
data["options"].(map[string]interface{})["cas"] = c.flagCAS
|
||||
}
|
||||
}
|
||||
|
||||
secret, err := client.Logical().Write(path, data)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
|
||||
return 2
|
||||
@@ -144,5 +152,9 @@ func (c *KVPutCommand) Run(args []string) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
if c.flagField != "" {
|
||||
return PrintRawField(c.UI, secret, c.flagField)
|
||||
}
|
||||
|
||||
return OutputSecret(c.UI, secret)
|
||||
}
|
||||
|
||||
529
command/kv_test.go
Normal file
529
command/kv_test.go
Normal file
@@ -0,0 +1,529 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func testKVPutCommand(tb testing.TB) (*cli.MockUi, *KVPutCommand) {
|
||||
tb.Helper()
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
return ui, &KVPutCommand{
|
||||
BaseCommand: &BaseCommand{
|
||||
UI: ui,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVPutCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
out string
|
||||
code int
|
||||
}{
|
||||
{
|
||||
"not_enough_args",
|
||||
[]string{},
|
||||
"Not enough arguments",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"empty_kvs",
|
||||
[]string{"secret/write/foo"},
|
||||
"Must supply data",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"kvs_no_value",
|
||||
[]string{"secret/write/foo", "foo"},
|
||||
"Failed to parse K=V data",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"single_value",
|
||||
[]string{"secret/write/foo", "foo=bar"},
|
||||
"Success!",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"multi_value",
|
||||
[]string{"secret/write/foo", "foo=bar", "zip=zap"},
|
||||
"Success!",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"v2_single_value",
|
||||
[]string{"kv/write/foo", "foo=bar"},
|
||||
"created_time",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"v2_multi_value",
|
||||
[]string{"kv/write/foo", "foo=bar", "zip=zap"},
|
||||
"created_time",
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
if err := client.Sys().Mount("kv/", &api.MountInput{
|
||||
Type: "kv-v2",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui, cmd := testKVPutCommand(t)
|
||||
cmd.client = client
|
||||
|
||||
code := cmd.Run(tc.args)
|
||||
if code != tc.code {
|
||||
t.Errorf("expected %d to be %d", code, tc.code)
|
||||
}
|
||||
|
||||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
if !strings.Contains(combined, tc.out) {
|
||||
t.Errorf("expected %q to contain %q", combined, tc.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("v2_cas", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
if err := client.Sys().Mount("kv/", &api.MountInput{
|
||||
Type: "kv-v2",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui, cmd := testKVPutCommand(t)
|
||||
cmd.client = client
|
||||
|
||||
code := cmd.Run([]string{
|
||||
"-cas", "0", "kv/write/cas", "bar=baz",
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 to be %d", code)
|
||||
}
|
||||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
if !strings.Contains(combined, "created_time") {
|
||||
t.Errorf("expected %q to contain %q", combined, "created_time")
|
||||
}
|
||||
|
||||
ui, cmd = testKVPutCommand(t)
|
||||
cmd.client = client
|
||||
code = cmd.Run([]string{
|
||||
"-cas", "1", "kv/write/cas", "bar=baz",
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 to be %d", code)
|
||||
}
|
||||
combined = ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
if !strings.Contains(combined, "created_time") {
|
||||
t.Errorf("expected %q to contain %q", combined, "created_time")
|
||||
}
|
||||
|
||||
ui, cmd = testKVPutCommand(t)
|
||||
cmd.client = client
|
||||
code = cmd.Run([]string{
|
||||
"-cas", "1", "kv/write/cas", "bar=baz",
|
||||
})
|
||||
if code != 2 {
|
||||
t.Fatalf("expected 2 to be %d", code)
|
||||
}
|
||||
combined = ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
if !strings.Contains(combined, "check-and-set parameter did not match the current version") {
|
||||
t.Errorf("expected %q to contain %q", combined, "check-and-set parameter did not match the current version")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
t.Run("v1_data", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
ui, cmd := testKVPutCommand(t)
|
||||
cmd.client = client
|
||||
|
||||
code := cmd.Run([]string{
|
||||
"secret/write/data", "bar=baz",
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 to be %d", code)
|
||||
}
|
||||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
if !strings.Contains(combined, "Success!") {
|
||||
t.Errorf("expected %q to contain %q", combined, "created_time")
|
||||
}
|
||||
|
||||
ui, rcmd := testReadCommand(t)
|
||||
rcmd.client = client
|
||||
code = rcmd.Run([]string{
|
||||
"secret/write/data",
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 to be %d", code)
|
||||
}
|
||||
combined = ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
if strings.Contains(combined, "data") {
|
||||
t.Errorf("expected %q not to contain %q", combined, "data")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stdin_full", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
stdinR, stdinW := io.Pipe()
|
||||
go func() {
|
||||
stdinW.Write([]byte(`{"foo":"bar"}`))
|
||||
stdinW.Close()
|
||||
}()
|
||||
|
||||
_, cmd := testKVPutCommand(t)
|
||||
cmd.client = client
|
||||
cmd.testStdin = stdinR
|
||||
|
||||
code := cmd.Run([]string{
|
||||
"secret/write/stdin_full", "-",
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 to be %d", code)
|
||||
}
|
||||
|
||||
secret, err := client.Logical().Read("secret/write/stdin_full")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if secret == nil || secret.Data == nil {
|
||||
t.Fatal("expected secret to have data")
|
||||
}
|
||||
if exp, act := "bar", secret.Data["foo"].(string); exp != act {
|
||||
t.Errorf("expected %q to be %q", act, exp)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stdin_value", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
stdinR, stdinW := io.Pipe()
|
||||
go func() {
|
||||
stdinW.Write([]byte("bar"))
|
||||
stdinW.Close()
|
||||
}()
|
||||
|
||||
_, cmd := testKVPutCommand(t)
|
||||
cmd.client = client
|
||||
cmd.testStdin = stdinR
|
||||
|
||||
code := cmd.Run([]string{
|
||||
"secret/write/stdin_value", "foo=-",
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 to be %d", code)
|
||||
}
|
||||
|
||||
secret, err := client.Logical().Read("secret/write/stdin_value")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if secret == nil || secret.Data == nil {
|
||||
t.Fatal("expected secret to have data")
|
||||
}
|
||||
if exp, act := "bar", secret.Data["foo"].(string); exp != act {
|
||||
t.Errorf("expected %q to be %q", act, exp)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("integration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
|
||||
_, cmd := testKVPutCommand(t)
|
||||
cmd.client = client
|
||||
|
||||
code := cmd.Run([]string{
|
||||
"secret/write/integration", "foo=bar", "zip=zap",
|
||||
})
|
||||
if code != 0 {
|
||||
t.Fatalf("expected 0 to be %d", code)
|
||||
}
|
||||
|
||||
secret, err := client.Logical().Read("secret/write/integration")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if secret == nil || secret.Data == nil {
|
||||
t.Fatal("expected secret to have data")
|
||||
}
|
||||
if exp, act := "bar", secret.Data["foo"].(string); exp != act {
|
||||
t.Errorf("expected %q to be %q", act, exp)
|
||||
}
|
||||
if exp, act := "zap", secret.Data["zip"].(string); exp != act {
|
||||
t.Errorf("expected %q to be %q", act, exp)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no_tabs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, cmd := testKVPutCommand(t)
|
||||
assertNoTabs(t, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
func testKVGetCommand(tb testing.TB) (*cli.MockUi, *KVGetCommand) {
|
||||
tb.Helper()
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
return ui, &KVGetCommand{
|
||||
BaseCommand: &BaseCommand{
|
||||
UI: ui,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVGetCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
out string
|
||||
code int
|
||||
}{
|
||||
{
|
||||
"not_enough_args",
|
||||
[]string{},
|
||||
"Not enough arguments",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"too_many_args",
|
||||
[]string{"foo", "bar"},
|
||||
"Too many arguments",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"not_found",
|
||||
[]string{"secret/nope/not/once/never"},
|
||||
"",
|
||||
2,
|
||||
},
|
||||
{
|
||||
"default",
|
||||
[]string{"secret/read/foo"},
|
||||
"foo",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"v1_field",
|
||||
[]string{"-field", "foo", "secret/read/foo"},
|
||||
"bar",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"v2_field",
|
||||
[]string{"-field", "foo", "kv/read/foo"},
|
||||
"bar",
|
||||
0,
|
||||
},
|
||||
|
||||
{
|
||||
"v2_not_found",
|
||||
[]string{"kv/nope/not/once/never"},
|
||||
"",
|
||||
2,
|
||||
},
|
||||
|
||||
{
|
||||
"v2_read",
|
||||
[]string{"kv/read/foo"},
|
||||
"foo",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"v2_read",
|
||||
[]string{"kv/read/foo"},
|
||||
"version",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"v2_read_version",
|
||||
[]string{"--version", "1", "kv/read/foo"},
|
||||
"foo",
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("validations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
if err := client.Sys().Mount("kv/", &api.MountInput{
|
||||
Type: "kv-v2",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := client.Logical().Write("secret/read/foo", map[string]interface{}{
|
||||
"foo": "bar",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := client.Logical().Write("kv/data/read/foo", map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui, cmd := testKVGetCommand(t)
|
||||
cmd.client = client
|
||||
|
||||
code := cmd.Run(tc.args)
|
||||
if code != tc.code {
|
||||
t.Errorf("expected %d to be %d", code, tc.code)
|
||||
}
|
||||
|
||||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
if !strings.Contains(combined, tc.out) {
|
||||
t.Errorf("expected %q to contain %q", combined, tc.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no_tabs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, cmd := testKVGetCommand(t)
|
||||
assertNoTabs(t, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand) {
|
||||
tb.Helper()
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
return ui, &KVMetadataGetCommand{
|
||||
BaseCommand: &BaseCommand{
|
||||
UI: ui,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVMetadataGetCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
out string
|
||||
code int
|
||||
}{
|
||||
{
|
||||
"v1",
|
||||
[]string{"secret/foo"},
|
||||
"Metadata not supported on KV Version 1",
|
||||
1,
|
||||
},
|
||||
{
|
||||
"metadata_exists",
|
||||
[]string{"kv/foo"},
|
||||
"current_version",
|
||||
0,
|
||||
},
|
||||
{
|
||||
"versions_exist",
|
||||
[]string{"kv/foo"},
|
||||
"deletion_time",
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("validations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, closer := testVaultServer(t)
|
||||
defer closer()
|
||||
if err := client.Sys().Mount("kv/", &api.MountInput{
|
||||
Type: "kv-v2",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := client.Logical().Write("kv/data/foo", map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ui, cmd := testKVMetadataGetCommand(t)
|
||||
cmd.client = client
|
||||
|
||||
code := cmd.Run(tc.args)
|
||||
if code != tc.code {
|
||||
t.Errorf("expected %d to be %d", code, tc.code)
|
||||
}
|
||||
|
||||
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||
if !strings.Contains(combined, tc.out) {
|
||||
t.Errorf("expected %q to contain %q", combined, tc.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no_tabs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, cmd := testKVMetadataGetCommand(t)
|
||||
assertNoTabs(t, cmd)
|
||||
})
|
||||
}
|
||||
@@ -84,17 +84,6 @@ func (c *KVUndeleteCommand) Run(args []string) int {
|
||||
c.UI.Error("No versions provided, use the \"-versions\" flag to specify the version to undelete.")
|
||||
return 1
|
||||
}
|
||||
var err error
|
||||
path := sanitizePath(args[0])
|
||||
path, err = addPrefixToVKVPath(path, "undelete")
|
||||
if err != nil {
|
||||
c.UI.Error(err.Error())
|
||||
return 2
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"versions": kvParseVersionsFlags(c.flagVersions),
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
@@ -102,7 +91,23 @@ func (c *KVUndeleteCommand) Run(args []string) int {
|
||||
return 2
|
||||
}
|
||||
|
||||
secret, err := kvWriteRequest(client, path, data)
|
||||
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("Undelete not supported on KV Version 1")
|
||||
return 1
|
||||
}
|
||||
|
||||
path = addPrefixToVKVPath(path, mountPath, "undelete")
|
||||
data := map[string]interface{}{
|
||||
"versions": kvParseVersionsFlags(c.flagVersions),
|
||||
}
|
||||
|
||||
secret, err := client.Logical().Write(path, data)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err))
|
||||
return 2
|
||||
|
||||
@@ -20,7 +20,7 @@ func DefaultTokenHelper() (token.TokenHelper, error) {
|
||||
|
||||
// RawField extracts the raw field from the given data and returns it as a
|
||||
// string for printing purposes.
|
||||
func RawField(secret *api.Secret, field string) (interface{}, bool) {
|
||||
func RawField(secret *api.Secret, field string) interface{} {
|
||||
var val interface{}
|
||||
switch {
|
||||
case secret.Auth != nil:
|
||||
@@ -72,13 +72,20 @@ func RawField(secret *api.Secret, field string) (interface{}, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
return val, val != nil
|
||||
return val
|
||||
}
|
||||
|
||||
// PrintRawField prints raw field from the secret.
|
||||
func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int {
|
||||
val, ok := RawField(secret, field)
|
||||
if !ok {
|
||||
func PrintRawField(ui cli.Ui, data interface{}, field string) int {
|
||||
var val interface{}
|
||||
switch data.(type) {
|
||||
case *api.Secret:
|
||||
val = RawField(data.(*api.Secret), field)
|
||||
case map[string]interface{}:
|
||||
val = data.(map[string]interface{})[field]
|
||||
}
|
||||
|
||||
if val == nil {
|
||||
ui.Error(fmt.Sprintf("Field %q not present in secret", field))
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -4,6 +4,4 @@ const (
|
||||
// ExpirationRestoreWorkerCount specifies the number of workers to use while
|
||||
// restoring leases into the expiration manager
|
||||
ExpirationRestoreWorkerCount = 64
|
||||
|
||||
VaultKVCLIClientHeader = "X-Vault-Kv-Client"
|
||||
)
|
||||
|
||||
@@ -65,12 +65,14 @@ func TestSysInternal_UIMounts(t *testing.T) {
|
||||
"secret/": map[string]interface{}{
|
||||
"type": "kv",
|
||||
"description": "key/value secret storage",
|
||||
"options": map[string]interface{}{"version": "1"},
|
||||
},
|
||||
},
|
||||
"auth": map[string]interface{}{
|
||||
"token/": map[string]interface{}{
|
||||
"type": "token",
|
||||
"description": "token based credentials",
|
||||
"options": interface{}(nil),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -38,11 +38,15 @@ func (c *Core) Capabilities(ctx context.Context, token, path string) ([]string,
|
||||
policies = append(policies, policy)
|
||||
}
|
||||
|
||||
_, derivedPolicies, err := c.fetchEntityAndDerivedPolicies(te.EntityID)
|
||||
entity, derivedPolicies, err := c.fetchEntityAndDerivedPolicies(te.EntityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if entity != nil && entity.Disabled {
|
||||
return nil, logical.ErrPermissionDenied
|
||||
}
|
||||
|
||||
for _, item := range derivedPolicies {
|
||||
policy, err := c.policyStore.GetPolicy(ctx, item, PolicyTypeToken)
|
||||
if err != nil {
|
||||
|
||||
@@ -2314,7 +2314,6 @@ func TestCore_HandleRequest_Headers(t *testing.T) {
|
||||
Path: "foo/test",
|
||||
ClientToken: root,
|
||||
Headers: map[string][]string{
|
||||
"X-Vault-Kv-Client": []string{"foo"},
|
||||
"Should-Passthrough": []string{"foo"},
|
||||
"Should-Passthrough-Case-Insensitive": []string{"baz"},
|
||||
"Should-Not-Passthrough": []string{"bar"},
|
||||
@@ -2328,16 +2327,6 @@ func TestCore_HandleRequest_Headers(t *testing.T) {
|
||||
// Check the headers
|
||||
headers := noop.Requests[0].Headers
|
||||
|
||||
// Test whitelisted values
|
||||
if val, ok := headers["X-Vault-Kv-Client"]; ok {
|
||||
expected := []string{"foo"}
|
||||
if !reflect.DeepEqual(val, expected) {
|
||||
t.Fatalf("expected: %v, got: %v", expected, val)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("expected 'X-Vault-Kv-Client' to be present in the headers map")
|
||||
}
|
||||
|
||||
// Test passthrough values
|
||||
if val, ok := headers["Should-Passthrough"]; ok {
|
||||
expected := []string{"foo"}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/vault/helper/consts"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
@@ -27,7 +26,6 @@ var StdAllowedHeaders = []string{
|
||||
"X-Vault-Wrap-Format",
|
||||
"X-Vault-Wrap-TTL",
|
||||
"X-Vault-Policy-Override",
|
||||
consts.VaultKVCLIClientHeader,
|
||||
}
|
||||
|
||||
// CORSConfig stores the state of the CORS configuration.
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
uuid "github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/helper/compressutil"
|
||||
"github.com/hashicorp/vault/helper/consts"
|
||||
"github.com/hashicorp/vault/helper/identity"
|
||||
"github.com/hashicorp/vault/helper/parseutil"
|
||||
"github.com/hashicorp/vault/helper/strutil"
|
||||
"github.com/hashicorp/vault/helper/wrapping"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
@@ -92,6 +94,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
||||
"wrapping/pubkey",
|
||||
"replication/status",
|
||||
"internal/ui/mounts",
|
||||
"internal/ui/mount/*",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1075,6 +1078,20 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["internal-ui-mounts"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["internal-ui-mounts"][1]),
|
||||
},
|
||||
&framework.Path{
|
||||
Pattern: "internal/ui/mount/(?P<path>.+)",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"path": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "The path of the mount.",
|
||||
},
|
||||
},
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathInternalUIMountRead,
|
||||
},
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["internal-ui-mounts"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["internal-ui-mounts"][1]),
|
||||
},
|
||||
&framework.Path{
|
||||
Pattern: "internal/ui/resultant-acl",
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
@@ -1520,6 +1537,41 @@ func (b *SystemBackend) handleRekeyDeleteRecovery(ctx context.Context, req *logi
|
||||
return b.handleRekeyDelete(ctx, req, data, true)
|
||||
}
|
||||
|
||||
func mountInfo(entry *MountEntry) map[string]interface{} {
|
||||
info := map[string]interface{}{
|
||||
"type": entry.Type,
|
||||
"description": entry.Description,
|
||||
"accessor": entry.Accessor,
|
||||
"local": entry.Local,
|
||||
"seal_wrap": entry.SealWrap,
|
||||
"options": entry.Options,
|
||||
}
|
||||
entryConfig := map[string]interface{}{
|
||||
"default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()),
|
||||
"max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()),
|
||||
"force_no_cache": entry.Config.ForceNoCache,
|
||||
"plugin_name": entry.Config.PluginName,
|
||||
}
|
||||
if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok {
|
||||
entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string)
|
||||
}
|
||||
if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok {
|
||||
entryConfig["audit_non_hmac_response_keys"] = rawVal.([]string)
|
||||
}
|
||||
// Even though empty value is valid for ListingVisibility, we can ignore
|
||||
// this case during mount since there's nothing to unset/hide.
|
||||
if len(entry.Config.ListingVisibility) > 0 {
|
||||
entryConfig["listing_visibility"] = entry.Config.ListingVisibility
|
||||
}
|
||||
if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok {
|
||||
entryConfig["passthrough_request_headers"] = rawVal.([]string)
|
||||
}
|
||||
|
||||
info["config"] = entryConfig
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// handleMountTable handles the "mounts" endpoint to provide the mount table
|
||||
func (b *SystemBackend) handleMountTable(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
b.Core.mountsLock.RLock()
|
||||
@@ -1531,36 +1583,7 @@ func (b *SystemBackend) handleMountTable(ctx context.Context, req *logical.Reque
|
||||
|
||||
for _, entry := range b.Core.mounts.Entries {
|
||||
// Populate mount info
|
||||
info := map[string]interface{}{
|
||||
"type": entry.Type,
|
||||
"description": entry.Description,
|
||||
"accessor": entry.Accessor,
|
||||
"local": entry.Local,
|
||||
"seal_wrap": entry.SealWrap,
|
||||
"options": entry.Options,
|
||||
}
|
||||
entryConfig := map[string]interface{}{
|
||||
"default_lease_ttl": int64(entry.Config.DefaultLeaseTTL.Seconds()),
|
||||
"max_lease_ttl": int64(entry.Config.MaxLeaseTTL.Seconds()),
|
||||
"force_no_cache": entry.Config.ForceNoCache,
|
||||
"plugin_name": entry.Config.PluginName,
|
||||
}
|
||||
if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_request_keys"); ok {
|
||||
entryConfig["audit_non_hmac_request_keys"] = rawVal.([]string)
|
||||
}
|
||||
if rawVal, ok := entry.synthesizedConfigCache.Load("audit_non_hmac_response_keys"); ok {
|
||||
entryConfig["audit_non_hmac_response_keys"] = rawVal.([]string)
|
||||
}
|
||||
// Even though empty value is valid for ListingVisibility, we can ignore
|
||||
// this case during mount since there's nothing to unset/hide.
|
||||
if len(entry.Config.ListingVisibility) > 0 {
|
||||
entryConfig["listing_visibility"] = entry.Config.ListingVisibility
|
||||
}
|
||||
if rawVal, ok := entry.synthesizedConfigCache.Load("passthrough_request_headers"); ok {
|
||||
entryConfig["passthrough_request_headers"] = rawVal.([]string)
|
||||
}
|
||||
|
||||
info["config"] = entryConfig
|
||||
info := mountInfo(entry)
|
||||
resp.Data[entry.Path] = info
|
||||
}
|
||||
|
||||
@@ -3402,10 +3425,49 @@ func (b *SystemBackend) pathRandomWrite(ctx context.Context, req *logical.Reques
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
b.Core.mountsLock.RLock()
|
||||
defer b.Core.mountsLock.RUnlock()
|
||||
func hasMountAccess(acl *ACL, path string) bool {
|
||||
// If an ealier policy is giving us access to the mount path then we can do
|
||||
// a fast return.
|
||||
capabilities := acl.Capabilities(path)
|
||||
if !strutil.StrListContains(capabilities, DenyCapability) {
|
||||
return true
|
||||
}
|
||||
|
||||
var aclCapabilitiesGiven bool
|
||||
walkFn := func(s string, v interface{}) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
perms := v.(*ACLPermissions)
|
||||
|
||||
switch {
|
||||
case perms.CapabilitiesBitmap&DenyCapabilityInt > 0:
|
||||
return false
|
||||
|
||||
case perms.CapabilitiesBitmap&CreateCapabilityInt > 0,
|
||||
perms.CapabilitiesBitmap&DeleteCapabilityInt > 0,
|
||||
perms.CapabilitiesBitmap&ListCapabilityInt > 0,
|
||||
perms.CapabilitiesBitmap&ReadCapabilityInt > 0,
|
||||
perms.CapabilitiesBitmap&SudoCapabilityInt > 0,
|
||||
perms.CapabilitiesBitmap&UpdateCapabilityInt > 0:
|
||||
|
||||
aclCapabilitiesGiven = true
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
acl.exactRules.WalkPrefix(path, walkFn)
|
||||
if !aclCapabilitiesGiven {
|
||||
acl.globRules.WalkPrefix(path, walkFn)
|
||||
}
|
||||
|
||||
return aclCapabilitiesGiven
|
||||
}
|
||||
|
||||
func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
resp := &logical.Response{
|
||||
Data: make(map[string]interface{}),
|
||||
}
|
||||
@@ -3415,24 +3477,103 @@ func (b *SystemBackend) pathInternalUIMountsRead(ctx context.Context, req *logic
|
||||
resp.Data["secret"] = secretMounts
|
||||
resp.Data["auth"] = authMounts
|
||||
|
||||
for _, entry := range b.Core.mounts.Entries {
|
||||
if entry.Config.ListingVisibility == ListingVisibilityUnauth {
|
||||
info := map[string]interface{}{
|
||||
"type": entry.Type,
|
||||
"description": entry.Description,
|
||||
}
|
||||
secretMounts[entry.Path] = info
|
||||
var acl *ACL
|
||||
var isAuthed bool
|
||||
var err error
|
||||
if req.ClientToken != "" {
|
||||
isAuthed = true
|
||||
|
||||
var entity *identity.Entity
|
||||
// Load the ACL policies so we can walk the prefix for this mount
|
||||
acl, _, entity, err = b.Core.fetchACLTokenEntryAndEntity(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entity != nil && entity.Disabled {
|
||||
return nil, logical.ErrPermissionDenied
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, entry := range b.Core.auth.Entries {
|
||||
if entry.Config.ListingVisibility == ListingVisibilityUnauth {
|
||||
info := map[string]interface{}{
|
||||
"type": entry.Type,
|
||||
"description": entry.Description,
|
||||
}
|
||||
authMounts[entry.Path] = info
|
||||
hasAccess := func(me *MountEntry) bool {
|
||||
if me.Config.ListingVisibility == ListingVisibilityUnauth {
|
||||
return true
|
||||
}
|
||||
|
||||
if isAuthed {
|
||||
return hasMountAccess(acl, me.Path)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
b.Core.mountsLock.RLock()
|
||||
for _, entry := range b.Core.mounts.Entries {
|
||||
if hasAccess(entry) {
|
||||
if isAuthed {
|
||||
// If this is an authed request return all the mount info
|
||||
secretMounts[entry.Path] = mountInfo(entry)
|
||||
} else {
|
||||
secretMounts[entry.Path] = map[string]interface{}{
|
||||
"type": entry.Type,
|
||||
"description": entry.Description,
|
||||
"options": entry.Options,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.Core.mountsLock.RUnlock()
|
||||
|
||||
b.Core.authLock.RLock()
|
||||
for _, entry := range b.Core.auth.Entries {
|
||||
if hasAccess(entry) {
|
||||
if isAuthed {
|
||||
// If this is an authed request return all the mount info
|
||||
authMounts[entry.Path] = mountInfo(entry)
|
||||
} else {
|
||||
authMounts[entry.Path] = map[string]interface{}{
|
||||
"type": entry.Type,
|
||||
"description": entry.Description,
|
||||
"options": entry.Options,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.Core.authLock.RUnlock()
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *SystemBackend) pathInternalUIMountRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
path := d.Get("path").(string)
|
||||
if path == "" {
|
||||
return logical.ErrorResponse("path not set"), logical.ErrInvalidRequest
|
||||
}
|
||||
path = sanitizeMountPath(path)
|
||||
|
||||
me := b.Core.router.MatchingMountEntry(path)
|
||||
if me == nil {
|
||||
// Return a permission denied error here so this path cannot be used to
|
||||
// brute force a list of mounts.
|
||||
return nil, logical.ErrPermissionDenied
|
||||
}
|
||||
|
||||
resp := &logical.Response{
|
||||
Data: mountInfo(me),
|
||||
}
|
||||
resp.Data["path"] = me.Path
|
||||
|
||||
// Load the ACL policies so we can walk the prefix for this mount
|
||||
acl, _, entity, err := b.Core.fetchACLTokenEntryAndEntity(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entity != nil && entity.Disabled {
|
||||
return nil, logical.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if !hasMountAccess(acl, me.Path) {
|
||||
return nil, logical.ErrPermissionDenied
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
|
||||
@@ -2218,7 +2218,7 @@ func TestSystemBackend_ToolsRandom(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSystemBackend_InternalUIMounts(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
_, b, rootToken := testCoreSystemBackend(t)
|
||||
|
||||
// Ensure no entries are in the endpoint as a starting point
|
||||
req := logical.TestRequest(t, logical.ReadOperation, "internal/ui/mounts")
|
||||
@@ -2235,6 +2235,95 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) {
|
||||
t.Fatalf("got: %#v expect: %#v", resp.Data, exp)
|
||||
}
|
||||
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mounts")
|
||||
req.ClientToken = rootToken
|
||||
resp, err = b.HandleRequest(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
exp = map[string]interface{}{
|
||||
"secret": map[string]interface{}{
|
||||
"secret/": map[string]interface{}{
|
||||
"type": "kv",
|
||||
"description": "key/value secret storage",
|
||||
"accessor": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["accessor"],
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["secret"].(map[string]interface{})["secret/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"plugin_name": "",
|
||||
"force_no_cache": false,
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
"options": map[string]string{
|
||||
"version": "1",
|
||||
},
|
||||
},
|
||||
"sys/": map[string]interface{}{
|
||||
"type": "system",
|
||||
"description": "system endpoints used for control, policy and debugging",
|
||||
"accessor": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["accessor"],
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["secret"].(map[string]interface{})["sys/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"plugin_name": "",
|
||||
"force_no_cache": false,
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
"options": map[string]string(nil),
|
||||
},
|
||||
"cubbyhole/": map[string]interface{}{
|
||||
"description": "per-token private secret storage",
|
||||
"type": "cubbyhole",
|
||||
"accessor": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["accessor"],
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["secret"].(map[string]interface{})["cubbyhole/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"plugin_name": "",
|
||||
"force_no_cache": false,
|
||||
},
|
||||
"local": true,
|
||||
"seal_wrap": false,
|
||||
"options": map[string]string(nil),
|
||||
},
|
||||
"identity/": map[string]interface{}{
|
||||
"description": "identity store",
|
||||
"type": "identity",
|
||||
"accessor": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["accessor"],
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["default_lease_ttl"].(int64),
|
||||
"max_lease_ttl": resp.Data["secret"].(map[string]interface{})["identity/"].(map[string]interface{})["config"].(map[string]interface{})["max_lease_ttl"].(int64),
|
||||
"plugin_name": "",
|
||||
"force_no_cache": false,
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
"options": map[string]string(nil),
|
||||
},
|
||||
},
|
||||
"auth": map[string]interface{}{
|
||||
"token/": map[string]interface{}{
|
||||
"options": map[string]string(nil),
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": int64(0),
|
||||
"max_lease_ttl": int64(0),
|
||||
"force_no_cache": false,
|
||||
"plugin_name": "",
|
||||
},
|
||||
"type": "token",
|
||||
"description": "token based credentials",
|
||||
"accessor": resp.Data["auth"].(map[string]interface{})["token/"].(map[string]interface{})["accessor"],
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(resp.Data, exp) {
|
||||
t.Fatalf("got: %#v \n\n expect: %#v", resp.Data, exp)
|
||||
}
|
||||
|
||||
// Mount-tune an auth mount
|
||||
req = logical.TestRequest(t, logical.UpdateOperation, "auth/token/tune")
|
||||
req.Data["listing_visibility"] = "unauth"
|
||||
@@ -2256,12 +2345,14 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) {
|
||||
"secret/": map[string]interface{}{
|
||||
"type": "kv",
|
||||
"description": "key/value secret storage",
|
||||
"options": map[string]string{"version": "1"},
|
||||
},
|
||||
},
|
||||
"auth": map[string]interface{}{
|
||||
"token/": map[string]interface{}{
|
||||
"type": "token",
|
||||
"description": "token based credentials",
|
||||
"options": map[string]string(nil),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -2269,3 +2360,75 @@ func TestSystemBackend_InternalUIMounts(t *testing.T) {
|
||||
t.Fatalf("got: %#v expect: %#v", resp.Data, exp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemBackend_InternalUIMount(t *testing.T) {
|
||||
core, b, rootToken := testCoreSystemBackend(t)
|
||||
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "policy/secret")
|
||||
req.ClientToken = rootToken
|
||||
req.Data = map[string]interface{}{
|
||||
"rules": `path "secret/foo/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
}`,
|
||||
}
|
||||
resp, err := b.HandleRequest(context.Background(), req)
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("Bad %#v %#v", err, resp)
|
||||
}
|
||||
|
||||
req = logical.TestRequest(t, logical.UpdateOperation, "mounts/kv")
|
||||
req.ClientToken = rootToken
|
||||
req.Data = map[string]interface{}{
|
||||
"type": "kv",
|
||||
}
|
||||
resp, err = b.HandleRequest(context.Background(), req)
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("Bad %#v %#v", err, resp)
|
||||
}
|
||||
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/kv/bar")
|
||||
req.ClientToken = rootToken
|
||||
resp, err = b.HandleRequest(context.Background(), req)
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("Bad %#v %#v", err, resp)
|
||||
}
|
||||
if resp.Data["type"] != "kv" {
|
||||
t.Fatalf("Bad Response: %#v", resp)
|
||||
}
|
||||
|
||||
testMakeToken(t, core.tokenStore, rootToken, "tokenid", "", []string{"secret"})
|
||||
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/kv")
|
||||
req.ClientToken = "tokenid"
|
||||
resp, err = b.HandleRequest(context.Background(), req)
|
||||
if err != logical.ErrPermissionDenied {
|
||||
t.Fatal("expected permission denied error")
|
||||
}
|
||||
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/secret")
|
||||
req.ClientToken = "tokenid"
|
||||
resp, err = b.HandleRequest(context.Background(), req)
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("Bad %#v %#v", err, resp)
|
||||
}
|
||||
if resp.Data["type"] != "kv" {
|
||||
t.Fatalf("Bad Response: %#v", resp)
|
||||
}
|
||||
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/sys")
|
||||
req.ClientToken = "tokenid"
|
||||
resp, err = b.HandleRequest(context.Background(), req)
|
||||
if err != nil || (resp != nil && resp.IsError()) {
|
||||
t.Fatalf("Bad %#v %#v", err, resp)
|
||||
}
|
||||
if resp.Data["type"] != "system" {
|
||||
t.Fatalf("Bad Response: %#v", resp)
|
||||
}
|
||||
|
||||
req = logical.TestRequest(t, logical.ReadOperation, "internal/ui/mount/non-existent")
|
||||
req.ClientToken = "tokenid"
|
||||
resp, err = b.HandleRequest(context.Background(), req)
|
||||
if err != logical.ErrPermissionDenied {
|
||||
t.Fatal("expected permission denied error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,10 @@ import (
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/armon/go-radix"
|
||||
"github.com/hashicorp/vault/helper/consts"
|
||||
"github.com/hashicorp/vault/helper/salt"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
var (
|
||||
whitelistedHeaders = []string{
|
||||
consts.VaultKVCLIClientHeader,
|
||||
}
|
||||
)
|
||||
|
||||
// Router is used to do prefix based routing of a request to a logical backend
|
||||
type Router struct {
|
||||
l sync.RWMutex
|
||||
@@ -639,20 +632,6 @@ func pathsToRadix(paths []string) *radix.Tree {
|
||||
func filteredPassthroughHeaders(origHeaders map[string][]string, passthroughHeaders []string) map[string][]string {
|
||||
retHeaders := make(map[string][]string)
|
||||
|
||||
// Handle whitelisted values
|
||||
for _, header := range whitelistedHeaders {
|
||||
if val, ok := origHeaders[header]; ok {
|
||||
retHeaders[header] = val
|
||||
} else {
|
||||
// Try to check if a lowercased version of the header exists in the
|
||||
// originating request. The header key that gets used is the one from the
|
||||
// whitelist.
|
||||
if val, ok := origHeaders[strings.ToLower(header)]; ok {
|
||||
retHeaders[header] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Short-circuit if there's nothing to filter
|
||||
if len(passthroughHeaders) == 0 {
|
||||
return retHeaders
|
||||
|
||||
42
vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go
generated
vendored
42
vendor/github.com/hashicorp/vault-plugin-secrets-kv/backend.go
generated
vendored
@@ -60,6 +60,10 @@ type versionedKVBackend struct {
|
||||
// upgrading is an atomic value denoting if the backend is in the process of
|
||||
// upgrading its data.
|
||||
upgrading *uint32
|
||||
|
||||
// globalConfig is a cached value for fast lookup
|
||||
globalConfig *Configuration
|
||||
globalConfigLock *sync.RWMutex
|
||||
}
|
||||
|
||||
// Factory will return a logical backend of type versionedKVBackend or
|
||||
@@ -85,7 +89,8 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
|
||||
// Factory returns a new backend as logical.Backend.
|
||||
func VersionedKVFactory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := &versionedKVBackend{
|
||||
upgrading: new(uint32),
|
||||
upgrading: new(uint32),
|
||||
globalConfigLock: new(sync.RWMutex),
|
||||
}
|
||||
if conf.BackendUUID == "" {
|
||||
return nil, errors.New("could not initialize versioned K/V Store, no UUID was provided")
|
||||
@@ -207,6 +212,10 @@ func (b *versionedKVBackend) Invalidate(ctx context.Context, key string) {
|
||||
b.l.Lock()
|
||||
b.keyEncryptedWrapper = nil
|
||||
b.l.Unlock()
|
||||
case path.Join(b.storagePrefix, configPath):
|
||||
b.globalConfigLock.Lock()
|
||||
b.globalConfig = nil
|
||||
b.globalConfigLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,19 +310,40 @@ func (b *versionedKVBackend) getKeyEncryptor(ctx context.Context, s logical.Stor
|
||||
|
||||
// config takes a storage object and returns a configuration object
|
||||
func (b *versionedKVBackend) config(ctx context.Context, s logical.Storage) (*Configuration, error) {
|
||||
b.globalConfigLock.RLock()
|
||||
if b.globalConfig != nil {
|
||||
defer b.globalConfigLock.RUnlock()
|
||||
return &Configuration{
|
||||
CasRequired: b.globalConfig.CasRequired,
|
||||
MaxVersions: b.globalConfig.MaxVersions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
b.globalConfigLock.RUnlock()
|
||||
b.globalConfigLock.Lock()
|
||||
defer b.globalConfigLock.Unlock()
|
||||
|
||||
// Verify this hasn't already changed
|
||||
if b.globalConfig != nil {
|
||||
return &Configuration{
|
||||
CasRequired: b.globalConfig.CasRequired,
|
||||
MaxVersions: b.globalConfig.MaxVersions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
raw, err := s.Get(ctx, path.Join(b.storagePrefix, configPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := &Configuration{}
|
||||
if raw == nil {
|
||||
return conf, nil
|
||||
if raw != nil {
|
||||
if err := proto.Unmarshal(raw.Value, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := proto.Unmarshal(raw.Value, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.globalConfig = conf
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
10
vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go
generated
vendored
10
vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough.go
generated
vendored
@@ -36,14 +36,10 @@ func LeasedPassthroughBackendFactory(ctx context.Context, conf *logical.BackendC
|
||||
// LeaseSwitchedPassthroughBackend returns a PassthroughBackend
|
||||
// with leases switched on or off
|
||||
func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendConfig, leases bool) (logical.Backend, error) {
|
||||
passthroughBackend := &PassthroughBackend{
|
||||
b := &PassthroughBackend{
|
||||
generateLeases: leases,
|
||||
}
|
||||
|
||||
var b Passthrough = &PassthroughDowngrader{
|
||||
next: passthroughBackend,
|
||||
}
|
||||
|
||||
backend := &framework.Backend{
|
||||
BackendType: logical.TypeLogical,
|
||||
Help: strings.TrimSpace(passthroughHelp),
|
||||
@@ -89,9 +85,9 @@ func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendC
|
||||
return nil, fmt.Errorf("Configuation passed into backend is nil")
|
||||
}
|
||||
backend.Setup(ctx, conf)
|
||||
passthroughBackend.Backend = backend
|
||||
b.Backend = backend
|
||||
|
||||
return passthroughBackend, nil
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// PassthroughBackend is used storing secrets directly into the physical
|
||||
|
||||
158
vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go
generated
vendored
158
vendor/github.com/hashicorp/vault-plugin-secrets-kv/passthrough_downgrader.go
generated
vendored
@@ -1,158 +0,0 @@
|
||||
package kv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/helper/consts"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// PassthroughDowngrader wraps a normal passthrough backend and downgrades the
|
||||
// request object from the newer Versioned API to the older Passthrough API.
|
||||
// This allows us to use the new "vault kv" subcommand with a non-versioned
|
||||
// instance of the kv store without doing a preflight API version check. The
|
||||
// CLI will always use the new API definition and this object will make it
|
||||
// compatible with the passthrough backend. The "X-Vault-Kv-Client" header is
|
||||
// used to know the request originated from the CLI and uses the newer API.
|
||||
type PassthroughDowngrader struct {
|
||||
next Passthrough
|
||||
}
|
||||
|
||||
func (b *PassthroughDowngrader) handleExistenceCheck() framework.ExistenceFunc {
|
||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
|
||||
if !b.shouldDowngrade(req) {
|
||||
return b.next.handleExistenceCheck()(ctx, req, data)
|
||||
}
|
||||
|
||||
respErr := b.invalidPath(req)
|
||||
if respErr != nil {
|
||||
return false, logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
reqDown := &logical.Request{}
|
||||
*reqDown = *req
|
||||
|
||||
reqDown.Path = strings.TrimPrefix(req.Path, "data/")
|
||||
return b.next.handleExistenceCheck()(ctx, reqDown, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PassthroughDowngrader) handleRead() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
if !b.shouldDowngrade(req) {
|
||||
return b.next.handleRead()(ctx, req, data)
|
||||
}
|
||||
|
||||
respErr := b.invalidPath(req)
|
||||
if respErr != nil {
|
||||
return respErr, logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
if _, ok := data.Raw["version"]; ok {
|
||||
return logical.ErrorResponse("retrieving a version is not supported when versioning is disabled"), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
reqDown := &logical.Request{}
|
||||
*reqDown = *req
|
||||
|
||||
reqDown.Path = strings.TrimPrefix(req.Path, "data/")
|
||||
|
||||
resp, err := b.next.handleRead()(ctx, reqDown, data)
|
||||
if resp != nil && resp.Data != nil {
|
||||
resp.Data = map[string]interface{}{
|
||||
"data": resp.Data,
|
||||
"metadata": nil,
|
||||
}
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PassthroughDowngrader) handleWrite() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
if !b.shouldDowngrade(req) {
|
||||
return b.next.handleWrite()(ctx, req, data)
|
||||
}
|
||||
|
||||
respErr := b.invalidPath(req)
|
||||
if respErr != nil {
|
||||
return respErr, logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
reqDown := &logical.Request{}
|
||||
*reqDown = *req
|
||||
reqDown.Path = strings.TrimPrefix(req.Path, "data/")
|
||||
|
||||
// Validate the data map is what we expect
|
||||
switch req.Data["data"].(type) {
|
||||
case map[string]interface{}:
|
||||
default:
|
||||
return logical.ErrorResponse("could not downgrade request, unexpected data format"), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
// Move the data object up a level and ignore the options object.
|
||||
reqDown.Data = req.Data["data"].(map[string]interface{})
|
||||
|
||||
return b.next.handleWrite()(ctx, reqDown, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PassthroughDowngrader) handleDelete() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
if !b.shouldDowngrade(req) {
|
||||
return b.next.handleDelete()(ctx, req, data)
|
||||
}
|
||||
|
||||
respErr := b.invalidPath(req)
|
||||
if respErr != nil {
|
||||
return respErr, logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
reqDown := &logical.Request{}
|
||||
*reqDown = *req
|
||||
reqDown.Path = strings.TrimPrefix(req.Path, "data/")
|
||||
|
||||
return b.next.handleDelete()(ctx, reqDown, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PassthroughDowngrader) handleList() framework.OperationFunc {
|
||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
if !b.shouldDowngrade(req) {
|
||||
return b.next.handleList()(ctx, req, data)
|
||||
}
|
||||
|
||||
reqDown := &logical.Request{}
|
||||
*reqDown = *req
|
||||
reqDown.Path = strings.TrimPrefix(req.Path, "metadata/")
|
||||
|
||||
return b.next.handleList()(ctx, reqDown, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PassthroughDowngrader) shouldDowngrade(req *logical.Request) bool {
|
||||
return http.Header(req.Headers).Get(consts.VaultKVCLIClientHeader) != ""
|
||||
}
|
||||
|
||||
// invalidPaths returns an error if we are trying to access an versioned only
|
||||
// path on a non-versioned kv store.
|
||||
func (b *PassthroughDowngrader) invalidPath(req *logical.Request) *logical.Response {
|
||||
switch {
|
||||
case req.Path == "config":
|
||||
fallthrough
|
||||
case strings.HasPrefix(req.Path, "metadata/"):
|
||||
fallthrough
|
||||
case strings.HasPrefix(req.Path, "archive/"):
|
||||
fallthrough
|
||||
case strings.HasPrefix(req.Path, "unarchive/"):
|
||||
fallthrough
|
||||
case strings.HasPrefix(req.Path, "destroy/"):
|
||||
return logical.ErrorResponse("path is not supported when versioning is disabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
8
vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go
generated
vendored
8
vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_config.go
generated
vendored
@@ -42,9 +42,6 @@ func (b *versionedKVBackend) pathConfigRead() framework.OperationFunc {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
@@ -91,6 +88,11 @@ func (b *versionedKVBackend) pathConfigWrite() framework.OperationFunc {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.globalConfigLock.Lock()
|
||||
defer b.globalConfigLock.Unlock()
|
||||
|
||||
b.globalConfig = config
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
1
vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go
generated
vendored
1
vendor/github.com/hashicorp/vault-plugin-secrets-kv/path_metadata.go
generated
vendored
@@ -105,6 +105,7 @@ func (b *versionedKVBackend) pathMetadataRead() framework.OperationFunc {
|
||||
"created_time": ptypesTimestampToString(meta.CreatedTime),
|
||||
"updated_time": ptypesTimestampToString(meta.UpdatedTime),
|
||||
"max_versions": meta.MaxVersions,
|
||||
"cas_required": meta.CasRequired,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
1
vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto
generated
vendored
1
vendor/github.com/hashicorp/vault-plugin-secrets-kv/types.proto
generated
vendored
@@ -3,6 +3,7 @@ package kv;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// If values are added to this, be sure to update the config() function
|
||||
message Configuration {
|
||||
uint32 max_versions = 1;
|
||||
bool cas_required = 2;
|
||||
|
||||
6
vendor/vendor.json
vendored
6
vendor/vendor.json
vendored
@@ -1339,10 +1339,10 @@
|
||||
"revisionTime": "2018-04-23T14:10:30Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "m3cQgQrCSuWHiPA339FaZU6LuHU=",
|
||||
"checksumSHA1": "mawUYCTqiIcegkYTCG9fZChK4kQ=",
|
||||
"path": "github.com/hashicorp/vault-plugin-secrets-kv",
|
||||
"revision": "bc6216eebacf73fab61fd5cc7535b5eda7a74c98",
|
||||
"revisionTime": "2018-04-09T21:22:48Z"
|
||||
"revision": "d5a07c3d99f7fa02dd23d6dbff98d24e0eedf06b",
|
||||
"revisionTime": "2018-04-23T19:31:27Z"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "vTfeYxi0Z1y176bjQaYh1/FpQ9s=",
|
||||
|
||||
Reference in New Issue
Block a user