Add `-mount` flag to kv list command (#19378)

* add flag

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* handle kv paths

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* scaffold test

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* need metadata for list paths

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add (broken) test

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* fix test

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* update docs

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add changelog

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* format

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add godoc

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add test case for mount only

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* handle case of no unnamed arg

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add non-mount behavior

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add more detail to comment

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

* add v1 tests

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>

---------

Signed-off-by: Daniel Huckins <dhuckins@users.noreply.github.com>
This commit is contained in:
Daniel Huckins 2023-03-20 16:26:21 -04:00 committed by GitHub
parent c581f90c05
commit 058710d33d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 203 additions and 17 deletions

3
changelog/19378.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
cli/kv: add -mount flag to kv list
```

View File

@ -5,6 +5,7 @@ package command
import ( import (
"fmt" "fmt"
"path"
"strings" "strings"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
@ -18,6 +19,7 @@ var (
type KVListCommand struct { type KVListCommand struct {
*BaseCommand *BaseCommand
flagMount string
} }
func (c *KVListCommand) Synopsis() string { func (c *KVListCommand) Synopsis() string {
@ -43,7 +45,23 @@ Usage: vault kv list [options] PATH
} }
func (c *KVListCommand) Flags() *FlagSets { func (c *KVListCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP | FlagSetOutputFormat) set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.StringVar(&StringVar{
Name: "mount",
Target: &c.flagMount,
Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value
Usage: `Specifies the path where the KV backend is mounted. If specified,
the next argument will be interpreted as the secret path. If this flag is
not specified, the next argument will be interpreted as the combined mount
path and secret path, with /data/ automatically appended between KV
v2 secrets.`,
})
return set
} }
func (c *KVListCommand) AutocompleteArgs() complete.Predictor { func (c *KVListCommand) AutocompleteArgs() complete.Predictor {
@ -65,8 +83,11 @@ func (c *KVListCommand) Run(args []string) int {
args = f.Args() args = f.Args()
switch { switch {
case len(args) < 1: case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) if c.flagMount == "" {
return 1 c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
}
args = []string{""}
case len(args) > 1: case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1 return 1
@ -78,25 +99,56 @@ func (c *KVListCommand) Run(args []string) int {
return 2 return 2
} }
// Sanitize path // If true, we're working with "-mount=secret foo" syntax.
path := sanitizePath(args[0]) // If false, we're using "secret/foo" syntax.
mountPath, v2, err := isKVv2(path, client) mountFlagSyntax := c.flagMount != ""
if err != nil {
c.UI.Error(err.Error())
return 2
}
if v2 { var (
path = addPrefixToKVPath(path, mountPath, "metadata") mountPath string
partialPath string
v2 bool
)
// Parse the paths and grab the KV version
if mountFlagSyntax {
// In this case, this arg is the secret path (e.g. "foo").
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(sanitizePath(c.flagMount), client)
if err != nil {
c.UI.Error(err.Error())
return 2
}
if v2 {
partialPath = path.Join(mountPath, partialPath)
}
} else {
// In this case, this arg is a path-like combination of mountPath/secretPath.
// (e.g. "secret/foo")
partialPath = sanitizePath(args[0])
mountPath, v2, err = isKVv2(partialPath, client)
if err != nil { if err != nil {
c.UI.Error(err.Error()) c.UI.Error(err.Error())
return 2 return 2
} }
} }
secret, err := client.Logical().List(path) // Add /metadata to v2 paths only
var fullPath string
if v2 {
fullPath = addPrefixToKVPath(partialPath, mountPath, "metadata")
} else {
// v1
if mountFlagSyntax {
fullPath = path.Join(mountPath, partialPath)
} else {
fullPath = partialPath
}
}
secret, err := client.Logical().List(fullPath)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err)) c.UI.Error(fmt.Sprintf("Error listing %s: %s", fullPath, err))
return 2 return 2
} }
@ -114,12 +166,12 @@ func (c *KVListCommand) Run(args []string) int {
} }
if secret == nil || secret.Data == nil { if secret == nil || secret.Data == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path)) c.UI.Error(fmt.Sprintf("No value found at %s", fullPath))
return 2 return 2
} }
if !ok { if !ok {
c.UI.Error(fmt.Sprintf("No entries found at %s", path)) c.UI.Error(fmt.Sprintf("No entries found at %s", fullPath))
return 2 return 2
} }

View File

@ -593,6 +593,131 @@ func TestKVGetCommand(t *testing.T) {
}) })
} }
func testKVListCommand(tb testing.TB) (*cli.MockUi, *KVListCommand) {
tb.Helper()
ui := cli.NewMockUi()
cmd := &KVListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
return ui, cmd
}
// TestKVListCommand runs tests for `vault kv list`
func TestKVListCommand(t *testing.T) {
testCases := []struct {
name string
args []string
outStrings []string
code int
}{
{
name: "default",
args: []string{"kv/my-prefix"},
outStrings: []string{"secret-0", "secret-1", "secret-2"},
code: 0,
},
{
name: "not_enough_args",
args: []string{},
outStrings: []string{"Not enough arguments"},
code: 1,
},
{
name: "v2_default_with_mount",
args: []string{"-mount", "kv", "my-prefix"},
outStrings: []string{"secret-0", "secret-1", "secret-2"},
code: 0,
},
{
name: "v1_default_with_mount",
args: []string{"kv/my-prefix"},
outStrings: []string{"secret-0", "secret-1", "secret-2"},
code: 0,
},
{
name: "v2_not_found",
args: []string{"kv/nope/not/once/never"},
outStrings: []string{"No value found at kv/metadata/nope/not/once/never"},
code: 2,
},
{
name: "v1_mount_only",
args: []string{"kv"},
outStrings: []string{"my-prefix"},
code: 0,
},
{
name: "v2_mount_only",
args: []string{"-mount", "kv"},
outStrings: []string{"my-prefix"},
code: 0,
},
{
// this is behavior that should be tested
// `kv` here is an explicit mount
// `my-prefix` is not
// the current kv code will ignore `my-prefix`
name: "ignore_multi_part_mounts",
args: []string{"-mount", "kv/my-prefix"},
outStrings: []string{"my-prefix"},
code: 0,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
// test setup
client, closer := testVaultServer(t)
defer closer()
// enable kv-v2 backend
if err := client.Sys().Mount("kv/", &api.MountInput{
Type: "kv-v2",
}); err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
ctx := context.Background()
for i := 0; i < 3; i++ {
path := fmt.Sprintf("my-prefix/secret-%d", i)
_, err := client.KVv2("kv/").Put(ctx, path, map[string]interface{}{
"foo": "bar",
})
if err != nil {
t.Fatal(err)
}
}
ui, cmd := testKVListCommand(t)
cmd.client = client
code := cmd.Run(testCase.args)
if code != testCase.code {
t.Errorf("expected %d to be %d", code, testCase.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
for _, str := range testCase.outStrings {
if !strings.Contains(combined, str) {
t.Errorf("expected %q to contain %q", combined, str)
}
}
})
}
})
}
func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand) { func testKVMetadataGetCommand(tb testing.TB) (*cli.MockUi, *KVMetadataGetCommand) {
tb.Helper() tb.Helper()

View File

@ -21,7 +21,7 @@ Use this command to list all existing key names at a specific path.
List values under the key "my-app": List values under the key "my-app":
```shell-session ```shell-session
$ vault kv list secret/my-app/ $ vault kv list -mount=secret my-app/
Keys Keys
---- ----
admin_creds admin_creds
@ -38,6 +38,12 @@ included on all commands.
### Output Options ### Output Options
- `-mount` `(string: "")` - Specifies the path where the KV backend is mounted.
If specified, the next argument will be interpreted as the secret path. If
this flag is not specified, the next argument will be interpreted as the
combined mount path and secret path, with /data/ automatically inserted for
KV v2 secrets.
- `-format` `(string: "table")` - Print the output in the given format. Valid - `-format` `(string: "table")` - Print the output in the given format. Valid
formats are "table", "json", or "yaml". This can also be specified via the formats are "table", "json", or "yaml". This can also be specified via the
`VAULT_FORMAT` environment variable. `VAULT_FORMAT` environment variable.