From 4b67fd139f92a71b7dcc69d955a45a8458ac9ebd Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 14 Sep 2015 15:42:12 -0400 Subject: [PATCH] Add list capability, which will work with the generic and cubbyhole backends for the moment. This is pretty simple; it just adds the actual capability to make a list call into both the CLI and the HTTP handler. The real meat was already in those backends. --- api/logical.go | 14 ++++++++++ cli/commands.go | 8 ++++++ command/read.go | 40 +++++++++++++++++++++++++--- command/read_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 4 deletions(-) diff --git a/api/logical.go b/api/logical.go index 6e3f97606f..323f4058ba 100644 --- a/api/logical.go +++ b/api/logical.go @@ -26,6 +26,20 @@ func (c *Logical) Read(path string) (*Secret, error) { return ParseSecret(resp.Body) } +func (c *Logical) List(path string) (*Secret, error) { + r := c.c.NewRequest("LIST", "/v1/"+path) + resp, err := c.c.RawRequest(r) + if resp != nil && resp.StatusCode == 404 { + return nil, nil + } + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return ParseSecret(resp.Body) +} + func (c *Logical) Write(path string, data map[string]interface{}) (*Secret, error) { r := c.c.NewRequest("PUT", "/v1/"+path) if err := r.SetJSONBody(data); err != nil { diff --git a/cli/commands.go b/cli/commands.go index eae2b6f64c..0439203e38 100644 --- a/cli/commands.go +++ b/cli/commands.go @@ -161,6 +161,7 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { "read": func() (cli.Command, error) { return &command.ReadCommand{ Meta: meta, + List: false, }, nil }, @@ -176,6 +177,13 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { }, nil }, + "list": func() (cli.Command, error) { + return &command.ReadCommand{ + Meta: meta, + List: true, + }, nil + }, + "rekey": func() (cli.Command, error) { return &command.RekeyCommand{ Meta: meta, diff --git a/command/read.go b/command/read.go index 715c54f590..6bbcdc72e3 100644 --- a/command/read.go +++ b/command/read.go @@ -1,21 +1,32 @@ package command import ( + "flag" "fmt" "os" "reflect" "strings" + + "github.com/hashicorp/vault/api" ) // ReadCommand is a Command that reads data from the Vault. type ReadCommand struct { Meta + List bool } func (c *ReadCommand) Run(args []string) int { var format string var field string - flags := c.Meta.FlagSet("read", FlagSetDefault) + var err error + var secret *api.Secret + var flags *flag.FlagSet + if c.List { + flags = c.Meta.FlagSet("list", FlagSetDefault) + } else { + flags = c.Meta.FlagSet("read", FlagSetDefault) + } flags.StringVar(&format, "format", "table", "") flags.StringVar(&field, "field", "", "") flags.Usage = func() { c.Ui.Error(c.Help()) } @@ -42,7 +53,11 @@ func (c *ReadCommand) Run(args []string) int { return 2 } - secret, err := client.Logical().Read(path) + if c.List { + secret, err = client.Logical().List(path) + } else { + secret, err = client.Logical().Read(path) + } if err != nil { c.Ui.Error(fmt.Sprintf( "Error reading %s: %s", path, err)) @@ -79,6 +94,9 @@ func (c *ReadCommand) Run(args []string) int { } func (c *ReadCommand) Synopsis() string { + if c.List { + return "List data in Vault" + } return "Read data or secrets from Vault" } @@ -88,11 +106,25 @@ Usage: vault read [options] path Read data from Vault. - Read reads data at the given path from Vault. This can be used to - read secrets and configuration as well as generate dynamic values from + Reads data at the given path from Vault. This can be used to read + secrets and configuration as well as generate dynamic values from materialized backends. Please reference the documentation for the backends in use to determine key structure. +` + if c.List { + helpText = + ` +Usage: vault list [options] path + + List data from Vault. + + Retrieve a listing of available data. The data returned is + backend-specific, and not all backends implement listing capability. +` + } + + helpText += ` General Options: ` + generalOptionsUsage() + ` diff --git a/command/read_test.go b/command/read_test.go index ac46bf5ce4..79c1925eff 100644 --- a/command/read_test.go +++ b/command/read_test.go @@ -1,6 +1,7 @@ package command import ( + "reflect" "testing" "github.com/hashicorp/vault/http" @@ -134,3 +135,64 @@ func TestRead_field_notFound(t *testing.T) { t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) } } + +func TestList(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := http.TestServer(t, core) + defer ln.Close() + + ui := new(cli.MockUi) + c := &ReadCommand{ + Meta: Meta{ + ClientToken: token, + Ui: ui, + }, + List: true, + } + + args := []string{ + "-address", addr, + "-format", "json", + "secret", + } + + // Run once so the client is setup, ignore errors + c.Run(args) + + // Get the client so we can write data + client, err := c.Client() + if err != nil { + t.Fatalf("err: %s", err) + } + + data := map[string]interface{}{"value": "bar"} + if _, err := client.Logical().Write("secret/foo", data); err != nil { + t.Fatalf("err: %s", err) + } + + data = map[string]interface{}{"value": "bar"} + if _, err := client.Logical().Write("secret/foo/bar", data); err != nil { + t.Fatalf("err: %s", err) + } + + secret, err := client.Logical().List("secret/") + if err != nil { + t.Fatalf("err: %s", err) + } + + if secret == nil { + t.Fatalf("err: No value found at secret/") + } + + if secret.Data == nil { + t.Fatalf("err: Data not found") + } + + exp := map[string]interface{}{ + "keys": []interface{}{"foo", "foo/"}, + } + + if !reflect.DeepEqual(secret.Data, exp) { + t.Fatalf("err: expected %#v, got %#v", exp, secret.Data) + } +}