Updates and documentation

This commit is contained in:
Jeff Mitchell 2016-01-14 14:18:27 -05:00
parent d17c3f4407
commit 5341cb69cc
13 changed files with 380 additions and 119 deletions

View File

@ -27,7 +27,8 @@ func (c *Logical) Read(path string) (*Secret, error) {
}
func (c *Logical) List(path string) (*Secret, error) {
r := c.c.NewRequest("LIST", "/v1/"+path)
r := c.c.NewRequest("GET", "/v1/"+path)
r.Params.Set("list", "true")
resp, err := c.c.RawRequest(r)
if resp != nil {
defer resp.Body.Close()

View File

@ -161,7 +161,12 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
"read": func() (cli.Command, error) {
return &command.ReadCommand{
Meta: meta,
List: false,
}, nil
},
"list": func() (cli.Command, error) {
return &command.ListCommand{
Meta: meta,
}, nil
},
@ -177,13 +182,6 @@ 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,

View File

@ -28,6 +28,22 @@ func OutputSecret(ui cli.Ui, format string, secret *api.Secret) int {
}
}
func OutputList(ui cli.Ui, format string, secret *api.Secret) int {
switch format {
case "json":
return outputFormatJSONList(ui, secret)
case "yaml":
return outputFormatYAMLList(ui, secret)
case "table":
return outputFormatTableList(ui, secret, false)
case "bare":
return outputFormatTableList(ui, secret, true)
default:
ui.Error(fmt.Sprintf("Invalid output format: %s", format))
return 1
}
}
func outputFormatJSON(ui cli.Ui, s *api.Secret) int {
b, err := json.Marshal(s)
if err != nil {
@ -42,6 +58,20 @@ func outputFormatJSON(ui cli.Ui, s *api.Secret) int {
return 0
}
func outputFormatJSONList(ui cli.Ui, s *api.Secret) int {
b, err := json.Marshal(s.Data["keys"])
if err != nil {
ui.Error(fmt.Sprintf(
"Error formatting keys: %s", err))
return 1
}
var out bytes.Buffer
json.Indent(&out, b, "", "\t")
ui.Output(out.String())
return 0
}
func outputFormatYAML(ui cli.Ui, s *api.Secret) int {
b, err := yaml.Marshal(s)
if err != nil {
@ -54,6 +84,18 @@ func outputFormatYAML(ui cli.Ui, s *api.Secret) int {
return 0
}
func outputFormatYAMLList(ui cli.Ui, s *api.Secret) int {
b, err := yaml.Marshal(s.Data["keys"])
if err != nil {
ui.Error(fmt.Sprintf(
"Error formatting secret: %s", err))
return 1
}
ui.Output(strings.TrimSpace(string(b)))
return 0
}
func outputFormatTable(ui cli.Ui, s *api.Secret, whitespace bool) int {
config := columnize.DefaultConfig()
config.Delim = "♨"
@ -107,3 +149,41 @@ func outputFormatTable(ui cli.Ui, s *api.Secret, whitespace bool) int {
ui.Output(columnize.Format(input, config))
return 0
}
func outputFormatTableList(ui cli.Ui, s *api.Secret, bare bool) int {
config := columnize.DefaultConfig()
config.Delim = "♨"
config.Glue = "\t"
config.Prefix = ""
input := make([]string, 0, 5)
if !bare {
input = append(input, "Keys")
}
keys := make([]string, 0, len(s.Data["keys"].([]string)))
for _, k := range s.Data["keys"].([]string) {
keys = append(keys, k)
}
for _, k := range keys {
input = append(input, fmt.Sprintf("%s", k))
}
if !bare && len(s.Warnings) != 0 {
input = append(input, "")
for _, warning := range s.Warnings {
input = append(input, fmt.Sprintf("* %s", warning))
}
}
if bare {
for _, line := range input {
ui.Output(line)
}
} else {
ui.Output(columnize.Format(input, config))
}
return 0
}

101
command/list.go Normal file
View File

@ -0,0 +1,101 @@
package command
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/vault/api"
)
// ListCommand is a Command that lists data from the Vault.
type ListCommand struct {
Meta
}
func (c *ListCommand) Run(args []string) int {
var format string
var bare bool
var err error
var secret *api.Secret
var flags *flag.FlagSet
flags = c.Meta.FlagSet("list", FlagSetDefault)
flags.StringVar(&format, "format", "table", "")
flags.BoolVar(&bare, "bare", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 || len(args[0]) == 0 {
c.Ui.Error("read expects one argument")
flags.Usage()
return 1
}
path := args[0]
if path[0] == '/' {
path = path[1:]
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
secret, err = client.Logical().List(path)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading %s: %s", path, err))
return 1
}
if secret == nil {
c.Ui.Error(fmt.Sprintf(
"No value found at %s", path))
return 1
}
if secret.Data["keys"] == nil {
if !bare {
c.Ui.Error("No entries found")
}
return 0
}
if bare {
return OutputList(c.Ui, "bare", secret)
}
return OutputList(c.Ui, format, secret)
}
func (c *ListCommand) Synopsis() string {
return "List data or secrets in Vault"
}
func (c *ListCommand) Help() string {
helpText :=
`
Usage: vault list [options] path
List data from Vault.
Retrieve a listing of available data. The data returned, if any, is backend-
and endpoint-specific.
General Options:
` + generalOptionsUsage() + `
Read Options:
-format=table The format for output. By default it is a whitespace-
delimited table. This can also be json or yaml.
-bare Causes the key values to be output raw to stdout,
without formatting (except for newlines).
`
return strings.TrimSpace(helpText)
}

70
command/list_test.go Normal file
View File

@ -0,0 +1,70 @@
package command
import (
"reflect"
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
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,
},
}
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{}{"secret/foo", "secret/foo/"},
}
if !reflect.DeepEqual(secret.Data, exp) {
t.Fatalf("err: expected %#v, got %#v", exp, secret.Data)
}
}

View File

@ -13,7 +13,6 @@ import (
// ReadCommand is a Command that reads data from the Vault.
type ReadCommand struct {
Meta
List bool
}
func (c *ReadCommand) Run(args []string) int {
@ -22,11 +21,7 @@ func (c *ReadCommand) Run(args []string) int {
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 = c.Meta.FlagSet("read", FlagSetDefault)
flags.StringVar(&format, "format", "table", "")
flags.StringVar(&field, "field", "", "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
@ -53,11 +48,7 @@ func (c *ReadCommand) Run(args []string) int {
return 2
}
if c.List {
secret, err = client.Logical().List(path)
} else {
secret, err = client.Logical().Read(path)
}
secret, err = client.Logical().Read(path)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading %s: %s", path, err))
@ -94,9 +85,6 @@ 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"
}
@ -110,21 +98,7 @@ Usage: vault read [options] path
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() + `

View File

@ -1,7 +1,6 @@
package command
import (
"reflect"
"testing"
"github.com/hashicorp/vault/http"
@ -135,64 +134,3 @@ 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)
}
}

View File

@ -29,13 +29,16 @@ func handleLogical(core *vault.Core, dataOnly bool) http.Handler {
case "DELETE":
op = logical.DeleteOperation
case "GET":
// Need to call ParseForm to get query params loaded
err := r.ParseForm()
if err != nil {
respondError(w, http.StatusBadRequest, err)
}
if r.Form.Get("list") == "true" {
op = logical.ListOperation
} else {
op = logical.ReadOperation
}
case "LIST":
op = logical.ListOperation
case "POST", "PUT":
op = logical.UpdateOperation
case "LIST":
@ -71,7 +74,7 @@ func handleLogical(core *vault.Core, dataOnly bool) http.Handler {
if !ok {
return
}
if op == logical.ReadOperation && resp == nil {
if (op == logical.ReadOperation || op == logical.ListOperation) && resp == nil {
respondError(w, http.StatusNotFound, nil)
return
}

View File

@ -141,9 +141,11 @@ func ErrorResponse(text string) *Response {
// ListResponse is used to format a response to a list operation.
func ListResponse(keys []string) *Response {
return &Response{
Data: map[string]interface{}{
"keys": keys,
},
resp := &Response{
Data: map[string]interface{}{},
}
if keys != nil {
resp.Data["keys"] = keys
}
return resp
}

View File

@ -3,6 +3,7 @@ package vault
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/hashicorp/vault/logical"
@ -153,17 +154,22 @@ func (b *CubbyholeBackend) handleList(
if req.ClientToken == "" {
return nil, fmt.Errorf("[ERR] cubbyhole list: Client token empty")
}
// List the keys at the prefix given by the request
keys, err := req.Storage.List(req.ClientToken + "/" + req.Path)
if err != nil {
return nil, err
}
strippedKeys := []string{}
for _, key := range keys {
strippedKeys = append(strippedKeys, strings.TrimPrefix(key, req.ClientToken+"/"))
// Strip the token; also, add the path for the same reason as in
// passthrough
strippedKeys := make([]string, len(keys))
for i, key := range keys {
strippedKeys[i] = req.MountPoint + req.Path + strings.TrimPrefix(key, req.ClientToken+"/")
}
sort.Strings(strippedKeys)
// Generate the response
return logical.ListResponse(strippedKeys), nil
}

View File

@ -3,6 +3,7 @@ package vault
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
@ -218,8 +219,19 @@ func (b *PassthroughBackend) handleList(
return nil, err
}
// A list of an actual key returns "" in the list, which can cause nasty
// things downstream with JSON conversion, including in Go. So, prepend the
// path and let users do what they wish.
retKeys := make([]string, len(keys))
for i, k := range keys {
retKeys[i] = req.MountPoint + req.Path + k
}
// Sort
sort.Strings(retKeys)
// Generate the response
return logical.ListResponse(keys), nil
return logical.ListResponse(retKeys), nil
}
func (b *PassthroughBackend) GeneratesLeases() bool {

View File

@ -23,7 +23,7 @@ Also unlike the `generic` backend, because the cubbyhole's lifetime is linked
to an authentication token, there is no concept of a lease or lease TTL for
values contained in the token's cubbyhole.
Writing to a key in the `cubbyhole/` backend will replace the old value,
Writing to a key in the `cubbyhole` backend will replace the old value;
the sub-fields are not merged together.
## Quick Start
@ -52,7 +52,6 @@ As expected, the value previously set is returned to us.
## API
### /cubbyhole
#### GET
<dl class="api">
@ -90,6 +89,47 @@ As expected, the value previously set is returned to us.
</dd>
</dl>
#### LIST
<dl class="api">
<dt>Description</dt>
<dd>
Returns a list of secret entries at the specified location. Folders are
suffixed with `/`.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/cubbyhole/<path>?list=true`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
The example below shows output for a query path of `cubbyhole/` when there
are secrets at `cubbyhole/foo` and `cubbyhole/foo/bar`; note the difference
in the two entries.
```javascript
{
"auth": null,
"data": {
"keys": ["cubbyhole/foo", "cubbyhole/foo/"]
},
"lease_duration": 2592000,
"lease_id": "",
"renewable": false
}
```
</dd>
</dl>
#### POST/PUT
<dl class="api">

View File

@ -16,7 +16,7 @@ the getting started guide, you interacted with a generic secret backend
via the `secret/` prefix that Vault mounts by default. You can mount as many
of these backends at different mount points as you like.
Writing to a key in the `secret/` backend will replace the old value;
Writing to a key in the `generic` backend will replace the old value;
sub-fields are not merged together.
This backend honors the distinction between the `create` and `update`
@ -39,9 +39,6 @@ to the Vault server if it results in clients accessing the value very frequently
Also note that setting `ttl` does not actually expire the data; it is
informational only.
N.B.: Prior to version 0.3, the `ttl` parameter was called `lease`. Both will
work for 0.3, but in 0.4 `lease` will be removed.
As an example, we can write a new key "foo" to the generic backend
mounted at "secret/" by default:
@ -58,8 +55,7 @@ We can test this by doing a read:
```
$ vault read secret/foo
Key Value
ttl_seconds 3600
ttl 1h
lease_duration 3600
zip zap
```
@ -69,7 +65,6 @@ seconds (one hour) as specified.
## API
### /secret
#### GET
<dl class="api">
@ -107,6 +102,47 @@ seconds (one hour) as specified.
</dd>
</dl>
#### LIST
<dl class="api">
<dt>Description</dt>
<dd>
Returns a list of secret entries at the specified location. Folders are
suffixed with `/`.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/secret/<path>?list=true`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
The example below shows output for a query path of `secret/` when there are
secrets at `secret/foo` and `secret/foo/bar`; note the difference in the two
entries.
```javascript
{
"auth": null,
"data": {
"keys": ["secret/foo", "secret/foo/"]
},
"lease_duration": 2592000,
"lease_id": "",
"renewable": false
}
```
</dd>
</dl>
#### POST/PUT
<dl class="api">