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:
Brian Kassouf
2018-04-23 15:00:02 -07:00
committed by GitHub
parent b82bd7420e
commit a136c79147
30 changed files with 1368 additions and 475 deletions

View File

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

View File

@@ -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{

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View File

@@ -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=",