From 150e81f3f0bca1f8d55ecb490f3816da873359ac Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 28 Aug 2017 16:42:19 -0400 Subject: [PATCH 001/281] Update vendor libraries for autocomplete and cli --- vendor/github.com/kr/text/License | 19 +++++++ vendor/github.com/kr/text/Readme | 3 + vendor/github.com/kr/text/doc.go | 3 + vendor/github.com/kr/text/indent.go | 74 +++++++++++++++++++++++++ vendor/github.com/kr/text/wrap.go | 86 +++++++++++++++++++++++++++++ vendor/vendor.json | 8 ++- 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/kr/text/License create mode 100644 vendor/github.com/kr/text/Readme create mode 100644 vendor/github.com/kr/text/doc.go create mode 100644 vendor/github.com/kr/text/indent.go create mode 100644 vendor/github.com/kr/text/wrap.go diff --git a/vendor/github.com/kr/text/License b/vendor/github.com/kr/text/License new file mode 100644 index 0000000000..480a328059 --- /dev/null +++ b/vendor/github.com/kr/text/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/kr/text/Readme b/vendor/github.com/kr/text/Readme new file mode 100644 index 0000000000..7e6e7c0687 --- /dev/null +++ b/vendor/github.com/kr/text/Readme @@ -0,0 +1,3 @@ +This is a Go package for manipulating paragraphs of text. + +See http://go.pkgdoc.org/github.com/kr/text for full documentation. diff --git a/vendor/github.com/kr/text/doc.go b/vendor/github.com/kr/text/doc.go new file mode 100644 index 0000000000..cf4c198f95 --- /dev/null +++ b/vendor/github.com/kr/text/doc.go @@ -0,0 +1,3 @@ +// Package text provides rudimentary functions for manipulating text in +// paragraphs. +package text diff --git a/vendor/github.com/kr/text/indent.go b/vendor/github.com/kr/text/indent.go new file mode 100644 index 0000000000..4ebac45c09 --- /dev/null +++ b/vendor/github.com/kr/text/indent.go @@ -0,0 +1,74 @@ +package text + +import ( + "io" +) + +// Indent inserts prefix at the beginning of each non-empty line of s. The +// end-of-line marker is NL. +func Indent(s, prefix string) string { + return string(IndentBytes([]byte(s), []byte(prefix))) +} + +// IndentBytes inserts prefix at the beginning of each non-empty line of b. +// The end-of-line marker is NL. +func IndentBytes(b, prefix []byte) []byte { + var res []byte + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return res +} + +// Writer indents each line of its input. +type indentWriter struct { + w io.Writer + bol bool + pre [][]byte + sel int + off int +} + +// NewIndentWriter makes a new write filter that indents the input +// lines. Each line is prefixed in order with the corresponding +// element of pre. If there are more lines than elements, the last +// element of pre is repeated for each subsequent line. +func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer { + return &indentWriter{ + w: w, + pre: pre, + bol: true, + } +} + +// The only errors returned are from the underlying indentWriter. +func (w *indentWriter) Write(p []byte) (n int, err error) { + for _, c := range p { + if w.bol { + var i int + i, err = w.w.Write(w.pre[w.sel][w.off:]) + w.off += i + if err != nil { + return n, err + } + } + _, err = w.w.Write([]byte{c}) + if err != nil { + return n, err + } + n++ + w.bol = c == '\n' + if w.bol { + w.off = 0 + if w.sel < len(w.pre)-1 { + w.sel++ + } + } + } + return n, nil +} diff --git a/vendor/github.com/kr/text/wrap.go b/vendor/github.com/kr/text/wrap.go new file mode 100644 index 0000000000..b09bb03736 --- /dev/null +++ b/vendor/github.com/kr/text/wrap.go @@ -0,0 +1,86 @@ +package text + +import ( + "bytes" + "math" +) + +var ( + nl = []byte{'\n'} + sp = []byte{' '} +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func Wrap(s string, lim int) string { + return string(WrapBytes([]byte(s), lim)) +} + +// WrapBytes wraps b into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapBytes(b []byte, lim int) []byte { + words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp) + var lines [][]byte + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, bytes.Join(line, sp)) + } + return bytes.Join(lines, nl) +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, either +// Wrap or WrapBytes will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each byte as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = len(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + len(words[j]) + } + } + + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim || i == n-1 { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + + var lines [][][]byte + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 2889bf1a53..c720dcec9a 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1273,7 +1273,13 @@ "revisionTime": "2017-06-28T15:29:38Z" }, { - "checksumSHA1": "A3ymiEhz0WaT+sfK1aJhlqhf41o=", + "checksumSHA1": "uulQHQ7IsRKqDudBC8Go9J0gtAc=", + "path": "github.com/kr/text", + "revision": "7cafcd837844e784b526369c9bce262804aebc60", + "revisionTime": "2016-05-04T02:26:26Z" + }, + { + "checksumSHA1": "ZAj/o03zG8Ui4mZ4XmzU4yyKC04=", "path": "github.com/lib/pq", "revision": "e42267488fe361b9dc034be7a6bffef5b195bceb", "revisionTime": "2017-08-10T06:12:20Z" From 7f6aa892a41c58f72290aa36d7e98c6a9c1e7f47 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 28 Aug 2017 16:44:35 -0400 Subject: [PATCH 002/281] Add start of base command, flags, prediction --- command/base.go | 434 ++++++++++++++++++++++++++++++ command/base_flags.go | 506 +++++++++++++++++++++++++++++++++++ command/base_helpers.go | 43 +++ command/base_predict.go | 173 ++++++++++++ command/base_predict_test.go | 363 +++++++++++++++++++++++++ command/base_test.go | 14 + 6 files changed, 1533 insertions(+) create mode 100644 command/base.go create mode 100644 command/base_flags.go create mode 100644 command/base_helpers.go create mode 100644 command/base_predict.go create mode 100644 command/base_predict_test.go create mode 100644 command/base_test.go diff --git a/command/base.go b/command/base.go new file mode 100644 index 0000000000..c1e64c0239 --- /dev/null +++ b/command/base.go @@ -0,0 +1,434 @@ +package command + +import ( + "bufio" + "bytes" + "flag" + "fmt" + "io" + "regexp" + "strings" + "sync" + "time" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/command/token" + "github.com/kr/text" + "github.com/mitchellh/cli" + "github.com/pkg/errors" + "github.com/posener/complete" +) + +// maxLineLength is the maximum width of any line. +const maxLineLength int = 78 + +// reRemoveWhitespace is a regular expression for stripping whitespace from +// a string. +var reRemoveWhitespace = regexp.MustCompile(`[\s]+`) + +type TokenHelperFunc func() (token.TokenHelper, error) + +type BaseCommand struct { + UI cli.Ui + + flags *FlagSets + flagsOnce sync.Once + + flagAddress string + flagCACert string + flagCAPath string + flagClientCert string + flagClientKey string + flagTLSServerName string + flagTLSSkipVerify bool + flagWrapTTL time.Duration + + flagFormat string + flagField string + + tokenHelper TokenHelperFunc + + client *api.Client + clientErr error + clientOnce sync.Once +} + +// Client returns the HTTP API client. The client is cached on the command to +// save performance on future calls. +func (c *BaseCommand) Client() (*api.Client, error) { + c.clientOnce.Do(func() { + // This should never happen in reality and is just for testing. Nothing + // should be setting the underlying client. + if c.client != nil { + return + } + + config := api.DefaultConfig() + + if err := config.ReadEnvironment(); err != nil { + c.clientErr = errors.Wrap(err, "failed to read environment") + return + } + + if c.flagAddress != "" { + config.Address = c.flagAddress + } + + // If we need custom TLS configuration, then set it + if c.flagCACert != "" || c.flagCAPath != "" || c.flagClientCert != "" || + c.flagClientKey != "" || c.flagTLSServerName != "" || c.flagTLSSkipVerify { + t := &api.TLSConfig{ + CACert: c.flagCACert, + CAPath: c.flagCAPath, + ClientCert: c.flagClientCert, + ClientKey: c.flagClientKey, + TLSServerName: c.flagTLSServerName, + Insecure: c.flagTLSSkipVerify, + } + config.ConfigureTLS(t) + } + + // Build the client + client, err := api.NewClient(config) + if err != nil { + c.clientErr = errors.Wrap(err, "failed to create client") + return + } + + // Set the wrapping function + client.SetWrappingLookupFunc(c.DefaultWrappingLookupFunc) + + // Get the token if it came in from the environment + token := client.Token() + + // If we don't have a token, check the token helper + if token == "" { + if c.tokenHelper != nil { + // If we have a token, then set that + tokenHelper, err := c.tokenHelper() + if err != nil { + c.clientErr = errors.Wrap(err, "failed to get token helper") + return + } + token, err = tokenHelper.Get() + if err != nil { + c.clientErr = errors.Wrap(err, "failed to retrieve from token helper") + return + } + } + } + + // Set the token + if token != "" { + client.SetToken(token) + } + + c.client = client + }) + + return c.client, c.clientErr +} + +// DefaultWrappingLookupFunc is the default wrapping function based on the +// CLI flag. +func (c *BaseCommand) DefaultWrappingLookupFunc(operation, path string) string { + if c.flagWrapTTL != 0 { + return c.flagWrapTTL.String() + } + + return api.DefaultWrappingLookupFunc(operation, path) +} + +type FlagSetBit uint + +const ( + FlagSetNone FlagSetBit = 1 << iota + FlagSetHTTP + FlagSetOutputField + FlagSetOutputFormat +) + +// flagSet creates the flags for this command. The result is cached on the +// command to save performance on future calls. +func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { + c.flagsOnce.Do(func() { + set := NewFlagSets(c.UI) + + if bit&FlagSetHTTP != 0 { + f := set.NewFlagSet("HTTP Options") + + f.StringVar(&StringVar{ + Name: "address", + Target: &c.flagAddress, + Default: "https://127.0.0.1:8200", + EnvVar: "VAULT_ADDR", + Completion: complete.PredictAnything, + Usage: "Address of the Vault server.", + }) + + f.StringVar(&StringVar{ + Name: "ca-cert", + Target: &c.flagCACert, + Default: "", + EnvVar: "VAULT_CACERT", + Completion: complete.PredictFiles("*"), + Usage: "Path on the local disk to a single PEM-encoded CA " + + "certificate to verify the Vault server's SSL certificate. This " + + "takes precendence over -ca-path.", + }) + + f.StringVar(&StringVar{ + Name: "ca-path", + Target: &c.flagCAPath, + Default: "", + EnvVar: "VAULT_CAPATH", + Completion: complete.PredictDirs("*"), + Usage: "Path on the local disk to a directory of PEM-encoded CA " + + "certificates to verify the Vault server's SSL certificate.", + }) + + f.StringVar(&StringVar{ + Name: "client-cert", + Target: &c.flagClientCert, + Default: "", + EnvVar: "VAULT_CLIENT_CERT", + Completion: complete.PredictFiles("*"), + Usage: "Path on the local disk to a single PEM-encoded CA " + + "certificate to use for TLS authentication to the Vault server. If " + + "this flag is specified, -client-key is also required.", + }) + + f.StringVar(&StringVar{ + Name: "client-key", + Target: &c.flagClientKey, + Default: "", + EnvVar: "VAULT_CLIENT_KEY", + Completion: complete.PredictFiles("*"), + Usage: "Path on the local disk to a single PEM-encoded private key " + + "matching the client certificate from -client-cert.", + }) + + f.StringVar(&StringVar{ + Name: "tls-server-name", + Target: &c.flagTLSServerName, + Default: "", + EnvVar: "VAULT_TLS_SERVER_NAME", + Completion: complete.PredictAnything, + Usage: "Name to use as the SNI host when connecting to the Vault " + + "server via TLS.", + }) + + f.BoolVar(&BoolVar{ + Name: "tls-skip-verify", + Target: &c.flagTLSSkipVerify, + Default: false, + EnvVar: "VAULT_SKIP_VERIFY", + Completion: complete.PredictNothing, + Usage: "Disable verification of TLS certificates. Using this option " + + "is highly discouraged and decreases the security of data " + + "transmissions to and from the Vault server.", + }) + + f.DurationVar(&DurationVar{ + Name: "wrap-ttl", + Target: &c.flagWrapTTL, + Default: 0, + EnvVar: "VAULT_WRAP_TTL", + Completion: complete.PredictAnything, + Usage: "Wraps the response in a cubbyhole token with the requested " + + "TTL. The response is available via the \"vault unwrap\" command. " + + "The TTL is specified as a numeric string with suffix like \"30s\" " + + "or \"5m\"", + }) + } + + if bit&(FlagSetOutputField|FlagSetOutputFormat) != 0 { + f := set.NewFlagSet("Output Options") + + if bit&FlagSetOutputField != 0 { + f.StringVar(&StringVar{ + Name: "field", + Target: &c.flagField, + Default: "", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Print only the field with the given name. Specifying " + + "this option will take precedence over other formatting " + + "directives. The result will not have a trailing newline " + + "making it idea for piping to other processes.", + }) + } + + if bit&FlagSetOutputFormat != 0 { + f.StringVar(&StringVar{ + Name: "format", + Target: &c.flagFormat, + Default: "table", + EnvVar: "VAULT_FORMAT", + Completion: complete.PredictSet("table", "json", "yaml"), + Usage: "Print the output in the given format. Valid formats " + + "are \"table\", \"json\", or \"yaml\".", + }) + } + } + + c.flags = set + }) + + return c.flags +} + +// printFlagTitle prints a consistently-formatted title to the given writer. +func printFlagTitle(w io.Writer, s string) { + fmt.Fprintf(w, "%s\n\n", s) +} + +// printFlagDetail prints a single flag to the given writer. +func printFlagDetail(w io.Writer, f *flag.Flag) { + example := "" + if t, ok := f.Value.(FlagExample); ok { + example = t.Example() + } + + if example != "" { + fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example) + } else { + fmt.Fprintf(w, " -%s\n", f.Name) + } + + usage := reRemoveWhitespace.ReplaceAllString(f.Usage, " ") + indented := wrapAtLength(usage, 6) + fmt.Fprintf(w, "%s\n\n", indented) +} + +// wrapAtLength wraps the given text at the maxLineLength, taking into account +// any provided left padding. +func wrapAtLength(s string, pad int) string { + wrapped := text.Wrap(s, maxLineLength-pad) + lines := strings.Split(wrapped, "\n") + for i, line := range lines { + lines[i] = strings.Repeat(" ", pad) + line + } + return strings.Join(lines, "\n") +} + +// FlagSets is a group of flag sets. +type FlagSets struct { + flagSets []*FlagSet + mainSet *flag.FlagSet + hiddens map[string]struct{} + completions complete.Flags +} + +// NewFlagSets creates a new flag sets. +func NewFlagSets(ui cli.Ui) *FlagSets { + mainSet := flag.NewFlagSet("", flag.ContinueOnError) + mainSet.Usage = func() {} + + // Pull errors from the flagset into the ui's error + errR, errW := io.Pipe() + errScanner := bufio.NewScanner(errR) + go func() { + for errScanner.Scan() { + ui.Error(errScanner.Text()) + } + }() + mainSet.SetOutput(errW) + + return &FlagSets{ + flagSets: make([]*FlagSet, 0, 6), + mainSet: mainSet, + hiddens: make(map[string]struct{}), + completions: complete.Flags{}, + } +} + +// NewFlagSet creates a new flag set from the given flag sets. +func (f *FlagSets) NewFlagSet(name string) *FlagSet { + flagSet := NewFlagSet(name) + f.AddFlagSet(flagSet) + return flagSet +} + +// AddFlagSet adds a new flag set to this flag set. +func (f *FlagSets) AddFlagSet(set *FlagSet) { + set.mainSet = f.mainSet + set.completions = f.completions + f.flagSets = append(f.flagSets, set) +} + +func (f *FlagSets) Completions() complete.Flags { + return f.completions +} + +// Parse parses the given flags, returning any errors. +func (f *FlagSets) Parse(args []string) error { + return f.mainSet.Parse(args) +} + +// Args returns the remaining args after parsing. +func (f *FlagSets) Args() []string { + return f.mainSet.Args() +} + +// HideFlag excludes the flag from the list of flags to print in help. This is +// useful when you want to include a flag in parsing for deprecations/bc, but +// you don't want to include it in help output. +func (f *FlagSets) HideFlag(n string) { + if _, ok := f.hiddens[n]; !ok { + f.hiddens[n] = struct{}{} + } +} + +// HiddenFlag returns true if the flag with the given name is hidden. +func (f *FlagSets) HiddenFlag(n string) bool { + _, ok := f.hiddens[n] + return ok +} + +// Help builds custom help for this command, grouping by flag set. +func (fs *FlagSets) Help() string { + var out bytes.Buffer + + for _, set := range fs.flagSets { + printFlagTitle(&out, set.name+":") + set.VisitAll(func(f *flag.Flag) { + // Skip any hidden flags + if fs.HiddenFlag(f.Name) { + return + } + printFlagDetail(&out, f) + }) + } + + return strings.TrimRight(out.String(), "\n") +} + +// FlagSet is a grouped wrapper around a real flag set and a grouped flag set. +type FlagSet struct { + name string + flagSet *flag.FlagSet + mainSet *flag.FlagSet + completions complete.Flags +} + +// NewFlagSet creates a new flag set. +func NewFlagSet(name string) *FlagSet { + return &FlagSet{ + name: name, + flagSet: flag.NewFlagSet(name, flag.ContinueOnError), + } +} + +// Name returns the name of this flag set. +func (f *FlagSet) Name() string { + return f.name +} + +func (f *FlagSet) Visit(fn func(*flag.Flag)) { + f.flagSet.Visit(fn) +} + +func (f *FlagSet) VisitAll(fn func(*flag.Flag)) { + f.flagSet.VisitAll(fn) +} diff --git a/command/base_flags.go b/command/base_flags.go new file mode 100644 index 0000000000..708f38f14f --- /dev/null +++ b/command/base_flags.go @@ -0,0 +1,506 @@ +package command + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/posener/complete" +) + +// FlagExample is an interface which declares an example value. +type FlagExample interface { + Example() string +} + +type BoolVar struct { + Name string + Aliases []string + Usage string + Default bool + EnvVar string + Target *bool + Completion complete.Predictor +} + +func (f *FlagSet) BoolVar(i *BoolVar) { + def := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + if b, err := strconv.ParseBool(v); err != nil { + def = b + } + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: strconv.FormatBool(i.Default), + EnvVar: i.EnvVar, + Value: newBoolValue(def, i.Target), + Completion: i.Completion, + }) +} + +type IntVar struct { + Name string + Aliases []string + Usage string + Default int + EnvVar string + Target *int + Completion complete.Predictor +} + +func (f *FlagSet) IntVar(i *IntVar) { + def := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + if i, err := strconv.ParseInt(v, 0, 64); err != nil { + def = int(i) + } + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: strconv.FormatInt(int64(i.Default), 10), + EnvVar: i.EnvVar, + Value: newIntValue(def, i.Target), + Completion: i.Completion, + }) +} + +type Int64Var struct { + Name string + Aliases []string + Usage string + Default int64 + EnvVar string + Target *int64 + Completion complete.Predictor +} + +func (f *FlagSet) Int64Var(i *Int64Var) { + def := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + if i, err := strconv.ParseInt(v, 0, 64); err != nil { + def = i + } + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: strconv.FormatInt(i.Default, 10), + EnvVar: i.EnvVar, + Value: newInt64Value(def, i.Target), + Completion: i.Completion, + }) +} + +type UintVar struct { + Name string + Aliases []string + Usage string + Default uint + EnvVar string + Target *uint + Completion complete.Predictor +} + +func (f *FlagSet) UintVar(i *UintVar) { + def := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + if i, err := strconv.ParseUint(v, 0, 64); err != nil { + def = uint(i) + } + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: strconv.FormatUint(uint64(i.Default), 10), + EnvVar: i.EnvVar, + Value: newUintValue(def, i.Target), + Completion: i.Completion, + }) +} + +type Uint64Var struct { + Name string + Aliases []string + Usage string + Default uint64 + EnvVar string + Target *uint64 + Completion complete.Predictor +} + +func (f *FlagSet) Uint64Var(i *Uint64Var) { + def := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + if i, err := strconv.ParseUint(v, 0, 64); err != nil { + def = i + } + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: strconv.FormatUint(i.Default, 10), + EnvVar: i.EnvVar, + Value: newUint64Value(def, i.Target), + Completion: i.Completion, + }) +} + +type StringVar struct { + Name string + Aliases []string + Usage string + Default string + EnvVar string + Target *string + Completion complete.Predictor +} + +func (f *FlagSet) StringVar(i *StringVar) { + def := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + def = v + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: i.Default, + EnvVar: i.EnvVar, + Value: newStringValue(def, i.Target), + Completion: i.Completion, + }) +} + +type Float64Var struct { + Name string + Aliases []string + Usage string + Default float64 + EnvVar string + Target *float64 + Completion complete.Predictor +} + +func (f *FlagSet) Float64Var(i *Float64Var) { + def := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + if i, err := strconv.ParseFloat(v, 64); err != nil { + def = i + } + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: strconv.FormatFloat(i.Default, 'e', -1, 64), + EnvVar: i.EnvVar, + Value: newFloat64Value(def, i.Target), + Completion: i.Completion, + }) +} + +type DurationVar struct { + Name string + Aliases []string + Usage string + Default time.Duration + EnvVar string + Target *time.Duration + Completion complete.Predictor +} + +func (f *FlagSet) DurationVar(i *DurationVar) { + def := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + if d, err := time.ParseDuration(v); err != nil { + def = d + } + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: i.Default.String(), + EnvVar: i.EnvVar, + Value: newDurationValue(def, i.Target), + Completion: i.Completion, + }) +} + +type VarFlag struct { + Name string + Aliases []string + Usage string + Default string + EnvVar string + Value flag.Value + Completion complete.Predictor +} + +func (f *FlagSet) VarFlag(i *VarFlag) { + // Calculate the full usage + usage := i.Usage + + if len(i.Aliases) > 0 { + sentence := make([]string, len(i.Aliases)) + for i, a := range i.Aliases { + sentence[i] = fmt.Sprintf(`"-%s"`, a) + } + + aliases := "" + switch len(sentence) { + case 0: + // impossible... + case 1: + aliases = sentence[0] + case 2: + aliases = sentence[0] + " and " + sentence[1] + default: + sentence[len(sentence)-1] = "and " + sentence[len(sentence)-1] + aliases = strings.Join(sentence, ", ") + } + + usage += fmt.Sprintf(" This is aliased as %s.", aliases) + } + + if i.Default != "" { + usage += fmt.Sprintf(" The default is %s.", i.Default) + } + + if i.EnvVar != "" { + usage += fmt.Sprintf(" This can also be specified via the %s "+ + "environment variable.", i.EnvVar) + } + + f.mainSet.Var(i.Value, i.Name, "") // No point in passing along usage here + + // Add aliases to the main set + for _, a := range i.Aliases { + f.mainSet.Var(i.Value, a, "") + } + + f.flagSet.Var(i.Value, i.Name, usage) + f.completions["-"+i.Name] = i.Completion +} + +func (f *FlagSet) Var(value flag.Value, name, usage string) { + f.mainSet.Var(value, name, usage) + f.flagSet.Var(value, name, usage) +} + +// -- bool Value +type boolValue bool + +func newBoolValue(val bool, p *bool) *boolValue { + *p = val + return (*boolValue)(p) +} + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + *b = boolValue(v) + return err +} + +func (b *boolValue) Get() interface{} { return bool(*b) } + +func (b *boolValue) String() string { return strconv.FormatBool(bool(*b)) } + +func (b *boolValue) Example() string { return "" } + +func (b *boolValue) IsBoolFlag() bool { return true } + +// optional interface to indicate boolean flags that can be +// supplied without "=value" text +type boolFlag interface { + flag.Value + IsBoolFlag() bool +} + +// -- int Value +type intValue int + +func newIntValue(val int, p *int) *intValue { + *p = val + return (*intValue)(p) +} + +func (i *intValue) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + *i = intValue(v) + return err +} + +func (i *intValue) Get() interface{} { return int(*i) } + +func (i *intValue) String() string { return strconv.Itoa(int(*i)) } + +func (i *intValue) Example() string { return "int" } + +// -- int64 Value +type int64Value int64 + +func newInt64Value(val int64, p *int64) *int64Value { + *p = val + return (*int64Value)(p) +} + +func (i *int64Value) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + *i = int64Value(v) + return err +} + +func (i *int64Value) Get() interface{} { return int64(*i) } + +func (i *int64Value) String() string { return strconv.FormatInt(int64(*i), 10) } + +func (i *int64Value) Example() string { return "int" } + +// -- uint Value +type uintValue uint + +func newUintValue(val uint, p *uint) *uintValue { + *p = val + return (*uintValue)(p) +} + +func (i *uintValue) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + *i = uintValue(v) + return err +} + +func (i *uintValue) Get() interface{} { return uint(*i) } + +func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i), 10) } + +func (i *uintValue) Example() string { return "uint" } + +// -- uint64 Value +type uint64Value uint64 + +func newUint64Value(val uint64, p *uint64) *uint64Value { + *p = val + return (*uint64Value)(p) +} + +func (i *uint64Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + *i = uint64Value(v) + return err +} + +func (i *uint64Value) Get() interface{} { return uint64(*i) } + +func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i), 10) } + +func (i *uint64Value) Example() string { return "uint" } + +// -- string Value +type stringValue string + +func newStringValue(val string, p *string) *stringValue { + *p = val + return (*stringValue)(p) +} + +func (s *stringValue) Set(val string) error { + *s = stringValue(val) + return nil +} + +func (s *stringValue) Get() interface{} { return string(*s) } + +func (s *stringValue) String() string { return string(*s) } + +func (s *stringValue) Example() string { return "string" } + +// -- float64 Value +type float64Value float64 + +func newFloat64Value(val float64, p *float64) *float64Value { + *p = val + return (*float64Value)(p) +} + +func (f *float64Value) Set(s string) error { + v, err := strconv.ParseFloat(s, 64) + *f = float64Value(v) + return err +} + +func (f *float64Value) Get() interface{} { return float64(*f) } + +func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f), 'g', -1, 64) } + +func (f *float64Value) Example() string { return "float" } + +// -- time.Duration Value +type durationValue time.Duration + +func newDurationValue(val time.Duration, p *time.Duration) *durationValue { + *p = val + return (*durationValue)(p) +} + +func (d *durationValue) Set(s string) error { + v, err := time.ParseDuration(s) + *d = durationValue(v) + return err +} + +func (d *durationValue) Get() interface{} { return time.Duration(*d) } + +func (d *durationValue) String() string { return (*time.Duration)(d).String() } + +func (d *durationValue) Example() string { return "duration" } + +// -- helpers +func envDefault(key, def string) string { + if v := os.Getenv(key); v != "" { + return v + } + return def +} + +func envBoolDefault(key string, def bool) bool { + if v := os.Getenv(key); v != "" { + b, err := strconv.ParseBool(v) + if err != nil { + panic(err) + } + return b + } + return def +} + +func envDurationDefault(key string, def time.Duration) time.Duration { + if v := os.Getenv(key); v != "" { + d, err := time.ParseDuration(v) + if err != nil { + panic(err) + } + return d + } + return def +} diff --git a/command/base_helpers.go b/command/base_helpers.go new file mode 100644 index 0000000000..ae574f6d60 --- /dev/null +++ b/command/base_helpers.go @@ -0,0 +1,43 @@ +package command + +import ( + "fmt" + "strings" +) + +var ErrMissingPath = fmt.Errorf("Missing PATH!") + +// extractPath extracts the path and list of arguments from the args. If there +// are no extra arguments, the remaining args will be nil. +func extractPath(args []string) (string, []string, error) { + if len(args) < 1 { + return "", nil, ErrMissingPath + } + + // Path is always the first argument after all flags + path := args[0] + + // Strip leading and trailing slashes + for len(path) > 0 && path[0] == '/' { + path = path[1:] + } + for len(path) > 0 && path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + + // Trim any leading/trailing whitespace + path = strings.TrimSpace(path) + + // Verify we have a path + if path == "" { + return "", nil, ErrMissingPath + } + + // Splice remaining args + var remaining []string + if len(args) > 1 { + remaining = args[1:] + } + + return path, remaining, nil +} diff --git a/command/base_predict.go b/command/base_predict.go new file mode 100644 index 0000000000..5fca56c090 --- /dev/null +++ b/command/base_predict.go @@ -0,0 +1,173 @@ +package command + +import ( + "sort" + "strings" + + "github.com/hashicorp/vault/api" + "github.com/posener/complete" +) + +// defaultPredictVaultMounts is the default list of mounts to return to the +// user. This is a best-guess, given we haven't communicated with the Vault +// server. If the user has no token or if the token does not have the default +// policy attached, it won't be able to read cubbyhole/, but it's a better UX +// that returning nothing. +var defaultPredictVaultMounts = []string{"cubbyhole/"} + +// PredictVaultPaths returns a predictor for Vault mounts and paths based on the +// configured client for the base command. Unfortunately this happens pre-flag +// parsing, so users must rely on environment variables for autocomplete if they +// are not using Vault at the default endpoints. +func (b *BaseCommand) PredictVaultPaths() complete.Predictor { + client, err := b.Client() + if err != nil { + return nil + } + return PredictVaultPaths(client) +} + +// PredictVaultPaths returns a predictor for Vault paths. This is a public API +// for consumers, but you probably want BaseCommand.PredictVaultPaths instead. +func PredictVaultPaths(client *api.Client) complete.Predictor { + return predictVaultPaths(client) +} + +// predictVaultPaths parses the CLI options and returns the "best" list of +// possible paths. If there are any errors, this function returns an empty +// result. All errors are suppressed since this is a prediction function. +func predictVaultPaths(client *api.Client) complete.PredictFunc { + return func(args complete.Args) []string { + // Do not predict more than one paths + if predictHasPathArg(args.All) { + return nil + } + + path := args.Last + + var predictions []string + if strings.Contains(path, "/") { + predictions = predictPaths(client, path) + } else { + predictions = predictMounts(client, path) + } + + // Either no results or many results, so return. + if len(predictions) != 1 { + return predictions + } + + // If this is not a "folder", do not try to recurse. + if !strings.HasSuffix(predictions[0], "/") { + return predictions + } + + // If the prediction is the same as the last guess, return it (we have no + // new information and we won't get anymore). + if predictions[0] == args.Last { + return predictions + } + + // Re-predict with the remaining path + args.Last = predictions[0] + return predictVaultPaths(client).Predict(args) + } +} + +// predictMounts predicts all mounts which start with the given prefix. These +// are predicted on mount path, not "type". +func predictMounts(client *api.Client, path string) []string { + mounts := predictListMounts(client) + + var predictions []string + for _, m := range mounts { + if strings.HasPrefix(m, path) { + predictions = append(predictions, m) + } + } + + return predictions +} + +// predictPaths predicts all paths which start with the given path. +func predictPaths(client *api.Client, path string) []string { + // Vault does not support listing based on a sub-key, so we have to back-pedal + // to the last "/" and return all paths on that "folder". Then we perform + // client-side filtering. + root := path + idx := strings.LastIndex(root, "/") + if idx > 0 && idx < len(root) { + root = root[:idx+1] + } + + paths := predictListPaths(client, root) + + var predictions []string + for _, p := range paths { + // Calculate the absolute "path" for matching. + p = root + p + + if strings.HasPrefix(p, path) { + predictions = append(predictions, p) + } + } + + // Add root to the path + if len(predictions) == 0 { + predictions = append(predictions, path) + } + + return predictions +} + +// predictListMounts returns a sorted list of the mount paths for Vault server +// for which the client is configured to communicate with. This function returns +// the default list of mounts if an error occurs. +func predictListMounts(c *api.Client) []string { + mounts, err := c.Sys().ListMounts() + if err != nil { + return defaultPredictVaultMounts + } + + list := make([]string, 0, len(mounts)) + for m := range mounts { + list = append(list, m) + } + sort.Strings(list) + return list +} + +// predictListPaths returns a list of paths (HTTP LIST) for the given path. This +// function returns an empty list of any errors occur. +func predictListPaths(c *api.Client, path string) []string { + secret, err := c.Logical().List(path) + if err != nil || secret == nil || secret.Data == nil { + return nil + } + + paths, ok := secret.Data["keys"].([]interface{}) + if !ok { + return nil + } + + list := make([]string, 0, len(paths)) + for _, p := range paths { + if str, ok := p.(string); ok { + list = append(list, str) + } + } + sort.Strings(list) + return list +} + +// predictHasPathArg determines if the args have already accepted a path. +func predictHasPathArg(args []string) bool { + var nonFlags []string + for _, a := range args { + if !strings.HasPrefix(a, "-") { + nonFlags = append(nonFlags, a) + } + } + + return len(nonFlags) > 2 +} diff --git a/command/base_predict_test.go b/command/base_predict_test.go new file mode 100644 index 0000000000..e579b9ec18 --- /dev/null +++ b/command/base_predict_test.go @@ -0,0 +1,363 @@ +package command + +import ( + "reflect" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/posener/complete" +) + +func TestPredictVaultPaths(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + data := map[string]interface{}{"a": "b"} + if _, err := client.Logical().Write("secret/bar", data); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("secret/foo", data); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("secret/zip/zap", data); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("secret/zip/zonk", data); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("secret/zip/twoot", data); err != nil { + t.Fatal(err) + } + + f := predictVaultPaths(client) + + cases := []struct { + name string + args complete.Args + exp []string + }{ + { + "has_args", + complete.Args{ + All: []string{"read", "secret/foo", "a=b"}, + Last: "a=b", + }, + nil, + }, + { + "part_mount", + complete.Args{ + All: []string{"read", "s"}, + Last: "s", + }, + []string{"secret/", "sys/"}, + }, + { + "only_mount", + complete.Args{ + All: []string{"read", "sec"}, + Last: "sec", + }, + []string{"secret/bar", "secret/foo", "secret/zip/"}, + }, + { + "full_mount", + complete.Args{ + All: []string{"read", "secret"}, + Last: "secret", + }, + []string{"secret/bar", "secret/foo", "secret/zip/"}, + }, + { + "full_mount_slash", + complete.Args{ + All: []string{"read", "secret/"}, + Last: "secret/", + }, + []string{"secret/bar", "secret/foo", "secret/zip/"}, + }, + { + "path_partial", + complete.Args{ + All: []string{"read", "secret/z"}, + Last: "secret/z", + }, + []string{"secret/zip/twoot", "secret/zip/zap", "secret/zip/zonk"}, + }, + { + "subpath_partial_z", + complete.Args{ + All: []string{"read", "secret/zip/z"}, + Last: "secret/zip/z", + }, + []string{"secret/zip/zap", "secret/zip/zonk"}, + }, + { + "subpath_partial_t", + complete.Args{ + All: []string{"read", "secret/zip/t"}, + Last: "secret/zip/t", + }, + []string{"secret/zip/twoot"}, + }, + } + + t.Run("group", func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := f(tc.args) + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + }) +} + +func TestPredictMounts(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + cases := []struct { + name string + client *api.Client + path string + exp []string + }{ + { + "no_match", + client, + "not-a-real-mount-seriously", + nil, + }, + { + "s", + client, + "s", + []string{"secret/", "sys/"}, + }, + { + "se", + client, + "se", + []string{"secret/"}, + }, + } + + t.Run("group", func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := predictMounts(tc.client, tc.path) + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + }) +} + +func TestPredictPaths(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + data := map[string]interface{}{"a": "b"} + if _, err := client.Logical().Write("secret/bar", data); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("secret/foo", data); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("secret/zip/zap", data); err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + client *api.Client + path string + exp []string + }{ + { + "bad_path", + client, + "nope/not/a/real/path/ever", + []string{"nope/not/a/real/path/ever"}, + }, + { + "good_path", + client, + "secret/", + []string{"secret/bar", "secret/foo", "secret/zip/"}, + }, + { + "partial_match", + client, + "secret/z", + []string{"secret/zip/"}, + }, + } + + t.Run("group", func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := predictPaths(tc.client, tc.path) + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + }) +} + +func TestPredictListMounts(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + cases := []struct { + name string + client *api.Client + exp []string + }{ + { + "not_connected_client", + func() *api.Client { + // Bad API client + client, _ := api.NewClient(nil) + return client + }(), + defaultPredictVaultMounts, + }, + { + "good_path", + client, + []string{"cubbyhole/", "secret/", "sys/"}, + }, + } + + t.Run("group", func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := predictListMounts(tc.client) + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + }) +} + +func TestPredictListPaths(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + data := map[string]interface{}{"a": "b"} + if _, err := client.Logical().Write("secret/bar", data); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("secret/foo", data); err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + client *api.Client + path string + exp []string + }{ + { + "bad_path", + client, + "nope/not/a/real/path/ever", + nil, + }, + { + "good_path", + client, + "secret/", + []string{"bar", "foo"}, + }, + } + + t.Run("group", func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := predictListPaths(tc.client, tc.path) + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + }) +} + +func TestPredictHasPathArg(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + exp bool + }{ + { + "nil", + nil, + false, + }, + { + "empty", + []string{}, + false, + }, + { + "empty_string", + []string{""}, + false, + }, + { + "single", + []string{"foo"}, + false, + }, + { + "multiple", + []string{"foo", "bar", "baz"}, + true, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if act := predictHasPathArg(tc.args); act != tc.exp { + t.Errorf("expected %t to be %t", act, tc.exp) + } + }) + } +} diff --git a/command/base_test.go b/command/base_test.go new file mode 100644 index 0000000000..b20be62660 --- /dev/null +++ b/command/base_test.go @@ -0,0 +1,14 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func assertNoTabs(tb testing.TB, c cli.Command) { + if strings.ContainsRune(c.Help(), '\t') { + tb.Errorf("%#v help output contains tabs", c) + } +} From ac63ed573b4a5b511fb54eeda4dd3f38c9659688 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 28 Aug 2017 16:45:04 -0400 Subject: [PATCH 003/281] Output JSON with spaces not tabs --- command/format.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/command/format.go b/command/format.go index 38f24d49e7..630056d177 100644 --- a/command/format.go +++ b/command/format.go @@ -1,7 +1,6 @@ package command import ( - "bytes" "encoding/json" "errors" "fmt" @@ -53,17 +52,15 @@ var Formatters = map[string]Formatter{ } // An output formatter for json output of an object -type JsonFormatter struct { -} +type JsonFormatter struct{} func (j JsonFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error { - b, err := json.Marshal(data) - if err == nil { - var out bytes.Buffer - json.Indent(&out, b, "", "\t") - ui.Output(out.String()) + b, err := json.MarshalIndent(data, "", " ") + if err != nil { + return err } - return err + ui.Output(string(b)) + return nil } // An output formatter for yaml output format of an object From ba685f8f86f800126b6f7730343f6159b5325fc5 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 28 Aug 2017 16:45:20 -0400 Subject: [PATCH 004/281] Add testing harness for a vault cluster --- command/command_test.go | 47 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/command/command_test.go b/command/command_test.go index 763587a05f..f41813c28d 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -4,8 +4,55 @@ import ( "testing" "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/builtin/logical/pki" + "github.com/hashicorp/vault/builtin/logical/transit" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/vault" + + vaulthttp "github.com/hashicorp/vault/http" + logxi "github.com/mgutz/logxi/v1" ) +var testVaultServerDefaultBackends = map[string]logical.Factory{ + "transit": transit.Factory, + "pki": pki.Factory, +} + +func testVaultServer(t testing.TB) (*api.Client, func()) { + return testVaultServerBackends(t, testVaultServerDefaultBackends) +} + +func testVaultServerBackends(t testing.TB, backends map[string]logical.Factory) (*api.Client, func()) { + coreConfig := &vault.CoreConfig{ + DisableMlock: true, + DisableCache: true, + Logger: logxi.NullLog, + LogicalBackends: backends, + } + + cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + + // make it easy to get access to the active + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + + client := cluster.Cores[0].Client + client.SetToken(cluster.RootToken) + + // Sanity check + secret, err := client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Data["id"].(string) != cluster.RootToken { + t.Fatalf("token mismatch: %#v vs %q", secret, cluster.RootToken) + } + return client, func() { defer cluster.Cleanup() } +} + func testClient(t *testing.T, addr string, token string) *api.Client { config := api.DefaultConfig() config.Address = addr From c9132068fae9f09c1b4a642e44267cc326479f01 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 28 Aug 2017 16:45:39 -0400 Subject: [PATCH 005/281] Remove coupling between Raw() and UI --- command/util.go | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/command/util.go b/command/util.go index 1eefc92c0f..e306559cc2 100644 --- a/command/util.go +++ b/command/util.go @@ -30,7 +30,9 @@ func DefaultTokenHelper() (token.TokenHelper, error) { return &token.ExternalTokenHelper{BinaryPath: path}, nil } -func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { +// 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) (string, bool) { var val interface{} switch { case secret.Auth != nil: @@ -74,21 +76,26 @@ func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { } } - if val != nil { - // c.Ui.Output() prints a CR character which in this case is - // not desired. Since Vault CLI currently only uses BasicUi, - // which writes to standard output, os.Stdout is used here to - // directly print the message. If mitchellh/cli exposes method - // to print without CR, this check needs to be removed. - if reflect.TypeOf(ui).String() == "*cli.BasicUi" { - fmt.Fprintf(os.Stdout, "%v", val) - } else { - ui.Output(fmt.Sprintf("%v", val)) - } - return 0 - } else { - ui.Error(fmt.Sprintf( - "Field %s not present in secret", field)) + str := fmt.Sprintf("%v", val) + return str, val != nil +} + +func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { + str, ok := RawField(secret, field) + if !ok { + ui.Error(fmt.Sprintf("Field %s not present in secret", field)) return 1 } + + // c.Ui.Output() prints a CR character which in this case is + // not desired. Since Vault CLI currently only uses BasicUi, + // which writes to standard output, os.Stdout is used here to + // directly print the message. If mitchellh/cli exposes method + // to print without CR, this check needs to be removed. + if reflect.TypeOf(ui).String() == "*cli.BasicUi" { + fmt.Fprintf(os.Stdout, str) + } else { + ui.Output(str) + } + return 0 } From fb81547a3a11488aba180abaede5f7efc8508fab Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 28 Aug 2017 17:05:09 -0400 Subject: [PATCH 006/281] Separate "files" and "folders" in predictor --- command/base_predict.go | 44 ++++++++---- command/base_predict_test.go | 130 +++++++++++++++++++++++++++++------ 2 files changed, 141 insertions(+), 33 deletions(-) diff --git a/command/base_predict.go b/command/base_predict.go index 5fca56c090..2347193523 100644 --- a/command/base_predict.go +++ b/command/base_predict.go @@ -15,28 +15,45 @@ import ( // that returning nothing. var defaultPredictVaultMounts = []string{"cubbyhole/"} -// PredictVaultPaths returns a predictor for Vault mounts and paths based on the +// PredictVaultFiles returns a predictor for Vault mounts and paths based on the // configured client for the base command. Unfortunately this happens pre-flag // parsing, so users must rely on environment variables for autocomplete if they // are not using Vault at the default endpoints. -func (b *BaseCommand) PredictVaultPaths() complete.Predictor { +func (b *BaseCommand) PredictVaultFiles() complete.Predictor { client, err := b.Client() if err != nil { return nil } - return PredictVaultPaths(client) + return PredictVaultFiles(client) } -// PredictVaultPaths returns a predictor for Vault paths. This is a public API -// for consumers, but you probably want BaseCommand.PredictVaultPaths instead. -func PredictVaultPaths(client *api.Client) complete.Predictor { - return predictVaultPaths(client) +// PredictVaultFolders returns a predictor for "folders". See PredictVaultFiles +// for more information and restrictions. +func (b *BaseCommand) PredictVaultFolders() complete.Predictor { + client, err := b.Client() + if err != nil { + return nil + } + return PredictVaultFolders(client) +} + +// PredictVaultFiles returns a predictor for Vault "files". This is a public API +// for consumers, but you probably want BaseCommand.PredictVaultFiles instead. +func PredictVaultFiles(client *api.Client) complete.Predictor { + return predictVaultPaths(client, true) +} + +// PredictVaultFolders returns a predictor for Vault "folders". This is a public +// API for consumers, but you probably want BaseCommand.PredictVaultFolders +// instead. +func PredictVaultFolders(client *api.Client) complete.Predictor { + return predictVaultPaths(client, false) } // predictVaultPaths parses the CLI options and returns the "best" list of // possible paths. If there are any errors, this function returns an empty // result. All errors are suppressed since this is a prediction function. -func predictVaultPaths(client *api.Client) complete.PredictFunc { +func predictVaultPaths(client *api.Client, includeFiles bool) complete.PredictFunc { return func(args complete.Args) []string { // Do not predict more than one paths if predictHasPathArg(args.All) { @@ -47,7 +64,7 @@ func predictVaultPaths(client *api.Client) complete.PredictFunc { var predictions []string if strings.Contains(path, "/") { - predictions = predictPaths(client, path) + predictions = predictPaths(client, path, includeFiles) } else { predictions = predictMounts(client, path) } @@ -70,7 +87,7 @@ func predictVaultPaths(client *api.Client) complete.PredictFunc { // Re-predict with the remaining path args.Last = predictions[0] - return predictVaultPaths(client).Predict(args) + return predictVaultPaths(client, includeFiles).Predict(args) } } @@ -90,7 +107,7 @@ func predictMounts(client *api.Client, path string) []string { } // predictPaths predicts all paths which start with the given path. -func predictPaths(client *api.Client, path string) []string { +func predictPaths(client *api.Client, path string, includeFiles bool) []string { // Vault does not support listing based on a sub-key, so we have to back-pedal // to the last "/" and return all paths on that "folder". Then we perform // client-side filtering. @@ -108,7 +125,10 @@ func predictPaths(client *api.Client, path string) []string { p = root + p if strings.HasPrefix(p, path) { - predictions = append(predictions, p) + // Ensure this is a directory or we've asked to include files. + if includeFiles || strings.HasSuffix(p, "/") { + predictions = append(predictions, p) + } } } diff --git a/command/base_predict_test.go b/command/base_predict_test.go index e579b9ec18..05af55da7c 100644 --- a/command/base_predict_test.go +++ b/command/base_predict_test.go @@ -31,12 +31,11 @@ func TestPredictVaultPaths(t *testing.T) { t.Fatal(err) } - f := predictVaultPaths(client) - cases := []struct { - name string - args complete.Args - exp []string + name string + args complete.Args + includeFiles bool + exp []string }{ { "has_args", @@ -44,6 +43,16 @@ func TestPredictVaultPaths(t *testing.T) { All: []string{"read", "secret/foo", "a=b"}, Last: "a=b", }, + true, + nil, + }, + { + "has_args_no_files", + complete.Args{ + All: []string{"read", "secret/foo", "a=b"}, + Last: "a=b", + }, + false, nil, }, { @@ -52,6 +61,16 @@ func TestPredictVaultPaths(t *testing.T) { All: []string{"read", "s"}, Last: "s", }, + true, + []string{"secret/", "sys/"}, + }, + { + "part_mount_no_files", + complete.Args{ + All: []string{"read", "s"}, + Last: "s", + }, + false, []string{"secret/", "sys/"}, }, { @@ -60,48 +79,108 @@ func TestPredictVaultPaths(t *testing.T) { All: []string{"read", "sec"}, Last: "sec", }, + true, []string{"secret/bar", "secret/foo", "secret/zip/"}, }, + { + "only_mount_no_files", + complete.Args{ + All: []string{"read", "sec"}, + Last: "sec", + }, + false, + []string{"secret/zip/"}, + }, { "full_mount", complete.Args{ All: []string{"read", "secret"}, Last: "secret", }, + true, []string{"secret/bar", "secret/foo", "secret/zip/"}, }, + { + "full_mount_no_files", + complete.Args{ + All: []string{"read", "secret"}, + Last: "secret", + }, + false, + []string{"secret/zip/"}, + }, { "full_mount_slash", complete.Args{ All: []string{"read", "secret/"}, Last: "secret/", }, + true, []string{"secret/bar", "secret/foo", "secret/zip/"}, }, + { + "full_mount_slash_no_files", + complete.Args{ + All: []string{"read", "secret/"}, + Last: "secret/", + }, + false, + []string{"secret/zip/"}, + }, { "path_partial", complete.Args{ All: []string{"read", "secret/z"}, Last: "secret/z", }, + true, []string{"secret/zip/twoot", "secret/zip/zap", "secret/zip/zonk"}, }, + { + "path_partial_no_files", + complete.Args{ + All: []string{"read", "secret/z"}, + Last: "secret/z", + }, + false, + []string{"secret/zip/"}, + }, { "subpath_partial_z", complete.Args{ All: []string{"read", "secret/zip/z"}, Last: "secret/zip/z", }, + true, []string{"secret/zip/zap", "secret/zip/zonk"}, }, + { + "subpath_partial_z_no_files", + complete.Args{ + All: []string{"read", "secret/zip/z"}, + Last: "secret/zip/z", + }, + false, + []string{"secret/zip/z"}, + }, { "subpath_partial_t", complete.Args{ All: []string{"read", "secret/zip/t"}, Last: "secret/zip/t", }, + true, []string{"secret/zip/twoot"}, }, + { + "subpath_partial_t_no_files", + complete.Args{ + All: []string{"read", "secret/zip/t"}, + Last: "secret/zip/t", + }, + false, + []string{"secret/zip/t"}, + }, } t.Run("group", func(t *testing.T) { @@ -110,6 +189,7 @@ func TestPredictVaultPaths(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() + f := predictVaultPaths(client, tc.includeFiles) act := f(tc.args) if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) @@ -126,26 +206,22 @@ func TestPredictMounts(t *testing.T) { defer closer() cases := []struct { - name string - client *api.Client - path string - exp []string + name string + path string + exp []string }{ { "no_match", - client, "not-a-real-mount-seriously", nil, }, { "s", - client, "s", []string{"secret/", "sys/"}, }, { "se", - client, "se", []string{"secret/"}, }, @@ -157,7 +233,7 @@ func TestPredictMounts(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - act := predictMounts(tc.client, tc.path) + act := predictMounts(client, tc.path) if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) } @@ -184,27 +260,39 @@ func TestPredictPaths(t *testing.T) { } cases := []struct { - name string - client *api.Client - path string - exp []string + name string + path string + includeFiles bool + exp []string }{ { "bad_path", - client, "nope/not/a/real/path/ever", + true, []string{"nope/not/a/real/path/ever"}, }, { "good_path", - client, "secret/", + true, []string{"secret/bar", "secret/foo", "secret/zip/"}, }, + { + "good_path_no_files", + "secret/", + false, + []string{"secret/zip/"}, + }, { "partial_match", - client, "secret/z", + true, + []string{"secret/zip/"}, + }, + { + "partial_match_no_files", + "secret/z", + false, []string{"secret/zip/"}, }, } @@ -215,7 +303,7 @@ func TestPredictPaths(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - act := predictPaths(tc.client, tc.path) + act := predictPaths(client, tc.path, tc.includeFiles) if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) } From 3a1479bc8c0191be21fd429d474462e0cc6e7303 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 29 Aug 2017 00:24:22 -0400 Subject: [PATCH 007/281] Make predict it's own struct The previous architecture would create an API client many times, slowing down the CLI exponentially for each new command added. --- command/base.go | 118 ++++++++++++++---------------- command/base_predict.go | 138 ++++++++++++++++++++++++----------- command/base_predict_test.go | 38 +++++++--- 3 files changed, 177 insertions(+), 117 deletions(-) diff --git a/command/base.go b/command/base.go index c1e64c0239..a918dc39fd 100644 --- a/command/base.go +++ b/command/base.go @@ -48,85 +48,75 @@ type BaseCommand struct { tokenHelper TokenHelperFunc - client *api.Client - clientErr error - clientOnce sync.Once + // For testing + client *api.Client } // Client returns the HTTP API client. The client is cached on the command to // save performance on future calls. func (c *BaseCommand) Client() (*api.Client, error) { - c.clientOnce.Do(func() { - // This should never happen in reality and is just for testing. Nothing - // should be setting the underlying client. - if c.client != nil { - return + // Read the test client if present + if c.client != nil { + return c.client, nil + } + + config := api.DefaultConfig() + + if err := config.ReadEnvironment(); err != nil { + return nil, errors.Wrap(err, "failed to read environment") + } + + if c.flagAddress != "" { + config.Address = c.flagAddress + } + + // If we need custom TLS configuration, then set it + if c.flagCACert != "" || c.flagCAPath != "" || c.flagClientCert != "" || + c.flagClientKey != "" || c.flagTLSServerName != "" || c.flagTLSSkipVerify { + t := &api.TLSConfig{ + CACert: c.flagCACert, + CAPath: c.flagCAPath, + ClientCert: c.flagClientCert, + ClientKey: c.flagClientKey, + TLSServerName: c.flagTLSServerName, + Insecure: c.flagTLSSkipVerify, } + config.ConfigureTLS(t) + } - config := api.DefaultConfig() + // Build the client + client, err := api.NewClient(config) + if err != nil { + return nil, errors.Wrap(err, "failed to create client") + } - if err := config.ReadEnvironment(); err != nil { - c.clientErr = errors.Wrap(err, "failed to read environment") - return - } + // Set the wrapping function + client.SetWrappingLookupFunc(c.DefaultWrappingLookupFunc) - if c.flagAddress != "" { - config.Address = c.flagAddress - } + // Get the token if it came in from the environment + token := client.Token() - // If we need custom TLS configuration, then set it - if c.flagCACert != "" || c.flagCAPath != "" || c.flagClientCert != "" || - c.flagClientKey != "" || c.flagTLSServerName != "" || c.flagTLSSkipVerify { - t := &api.TLSConfig{ - CACert: c.flagCACert, - CAPath: c.flagCAPath, - ClientCert: c.flagClientCert, - ClientKey: c.flagClientKey, - TLSServerName: c.flagTLSServerName, - Insecure: c.flagTLSSkipVerify, + // If we don't have a token, check the token helper + if token == "" { + if c.tokenHelper != nil { + // If we have a token, then set that + tokenHelper, err := c.tokenHelper() + if err != nil { + return nil, errors.Wrap(err, "failed to get token helper") } - config.ConfigureTLS(t) - } - - // Build the client - client, err := api.NewClient(config) - if err != nil { - c.clientErr = errors.Wrap(err, "failed to create client") - return - } - - // Set the wrapping function - client.SetWrappingLookupFunc(c.DefaultWrappingLookupFunc) - - // Get the token if it came in from the environment - token := client.Token() - - // If we don't have a token, check the token helper - if token == "" { - if c.tokenHelper != nil { - // If we have a token, then set that - tokenHelper, err := c.tokenHelper() - if err != nil { - c.clientErr = errors.Wrap(err, "failed to get token helper") - return - } - token, err = tokenHelper.Get() - if err != nil { - c.clientErr = errors.Wrap(err, "failed to retrieve from token helper") - return - } + token, err = tokenHelper.Get() + if err != nil { + return nil, errors.Wrap(err, "failed to retrieve from token helper") } } + } - // Set the token - if token != "" { - client.SetToken(token) - } + // Set the token + if token != "" { + client.SetToken(token) + } - c.client = client - }) - - return c.client, c.clientErr + return client, nil } // DefaultWrappingLookupFunc is the default wrapping function based on the diff --git a/command/base_predict.go b/command/base_predict.go index 2347193523..3b59147bb7 100644 --- a/command/base_predict.go +++ b/command/base_predict.go @@ -3,11 +3,30 @@ package command import ( "sort" "strings" + "sync" "github.com/hashicorp/vault/api" "github.com/posener/complete" ) +type Predict struct { + client *api.Client + clientOnce sync.Once +} + +func NewPredict() *Predict { + return &Predict{} +} + +func (p *Predict) Client() *api.Client { + p.clientOnce.Do(func() { + if p.client == nil { // For tests + p.client, _ = api.NewClient(nil) + } + }) + return p.client +} + // defaultPredictVaultMounts is the default list of mounts to return to the // user. This is a best-guess, given we haven't communicated with the Vault // server. If the user has no token or if the token does not have the default @@ -15,48 +34,63 @@ import ( // that returning nothing. var defaultPredictVaultMounts = []string{"cubbyhole/"} +// predictClient is the API client to use for prediction. We create this at the +// beginning once, because completions are generated for each command (and this +// doesn't change), and the only way to configure the predict/autocomplete +// client is via environment variables. Even if the user specifies a flag, we +// can't parse that flag until after the command is submitted. +var predictClient *api.Client +var predictClientOnce sync.Once + +// PredictClient returns the cached API client for the predictor. +func PredictClient() *api.Client { + predictClientOnce.Do(func() { + if predictClient == nil { // For tests + predictClient, _ = api.NewClient(nil) + } + }) + return predictClient +} + // PredictVaultFiles returns a predictor for Vault mounts and paths based on the // configured client for the base command. Unfortunately this happens pre-flag // parsing, so users must rely on environment variables for autocomplete if they // are not using Vault at the default endpoints. func (b *BaseCommand) PredictVaultFiles() complete.Predictor { - client, err := b.Client() - if err != nil { - return nil - } - return PredictVaultFiles(client) + return NewPredict().VaultFiles() } // PredictVaultFolders returns a predictor for "folders". See PredictVaultFiles // for more information and restrictions. func (b *BaseCommand) PredictVaultFolders() complete.Predictor { - client, err := b.Client() - if err != nil { - return nil - } - return PredictVaultFolders(client) + return NewPredict().VaultFolders() } -// PredictVaultFiles returns a predictor for Vault "files". This is a public API -// for consumers, but you probably want BaseCommand.PredictVaultFiles instead. -func PredictVaultFiles(client *api.Client) complete.Predictor { - return predictVaultPaths(client, true) +// VaultFiles returns a predictor for Vault "files". This is a public API for +// consumers, but you probably want BaseCommand.PredictVaultFiles instead. +func (p *Predict) VaultFiles() complete.Predictor { + return p.vaultPaths(true) } -// PredictVaultFolders returns a predictor for Vault "folders". This is a public +// VaultFolders returns a predictor for Vault "folders". This is a public // API for consumers, but you probably want BaseCommand.PredictVaultFolders // instead. -func PredictVaultFolders(client *api.Client) complete.Predictor { - return predictVaultPaths(client, false) +func (p *Predict) VaultFolders() complete.Predictor { + return p.vaultPaths(false) } -// predictVaultPaths parses the CLI options and returns the "best" list of -// possible paths. If there are any errors, this function returns an empty -// result. All errors are suppressed since this is a prediction function. -func predictVaultPaths(client *api.Client, includeFiles bool) complete.PredictFunc { +// vaultPaths parses the CLI options and returns the "best" list of possible +// paths. If there are any errors, this function returns an empty result. All +// errors are suppressed since this is a prediction function. +func (p *Predict) vaultPaths(includeFiles bool) complete.PredictFunc { return func(args complete.Args) []string { // Do not predict more than one paths - if predictHasPathArg(args.All) { + if p.hasPathArg(args.All) { + return nil + } + + client := p.Client() + if client == nil { return nil } @@ -64,9 +98,9 @@ func predictVaultPaths(client *api.Client, includeFiles bool) complete.PredictFu var predictions []string if strings.Contains(path, "/") { - predictions = predictPaths(client, path, includeFiles) + predictions = p.paths(path, includeFiles) } else { - predictions = predictMounts(client, path) + predictions = p.mounts(path) } // Either no results or many results, so return. @@ -87,14 +121,19 @@ func predictVaultPaths(client *api.Client, includeFiles bool) complete.PredictFu // Re-predict with the remaining path args.Last = predictions[0] - return predictVaultPaths(client, includeFiles).Predict(args) + return p.vaultPaths(includeFiles).Predict(args) } } -// predictMounts predicts all mounts which start with the given prefix. These -// are predicted on mount path, not "type". -func predictMounts(client *api.Client, path string) []string { - mounts := predictListMounts(client) +// mounts predicts all mounts which start with the given prefix. These are +// predicted on mount path, not "type". +func (p *Predict) mounts(path string) []string { + client := p.Client() + if client == nil { + return nil + } + + mounts := p.listMounts() var predictions []string for _, m := range mounts { @@ -106,8 +145,13 @@ func predictMounts(client *api.Client, path string) []string { return predictions } -// predictPaths predicts all paths which start with the given path. -func predictPaths(client *api.Client, path string, includeFiles bool) []string { +// paths predicts all paths which start with the given path. +func (p *Predict) paths(path string, includeFiles bool) []string { + client := p.Client() + if client == nil { + return nil + } + // Vault does not support listing based on a sub-key, so we have to back-pedal // to the last "/" and return all paths on that "folder". Then we perform // client-side filtering. @@ -117,7 +161,7 @@ func predictPaths(client *api.Client, path string, includeFiles bool) []string { root = root[:idx+1] } - paths := predictListPaths(client, root) + paths := p.listPaths(root) var predictions []string for _, p := range paths { @@ -140,11 +184,16 @@ func predictPaths(client *api.Client, path string, includeFiles bool) []string { return predictions } -// predictListMounts returns a sorted list of the mount paths for Vault server -// for which the client is configured to communicate with. This function returns -// the default list of mounts if an error occurs. -func predictListMounts(c *api.Client) []string { - mounts, err := c.Sys().ListMounts() +// listMounts returns a sorted list of the mount paths for Vault server for +// which the client is configured to communicate with. This function returns the +// default list of mounts if an error occurs. +func (p *Predict) listMounts() []string { + client := p.Client() + if client == nil { + return nil + } + + mounts, err := client.Sys().ListMounts() if err != nil { return defaultPredictVaultMounts } @@ -157,10 +206,15 @@ func predictListMounts(c *api.Client) []string { return list } -// predictListPaths returns a list of paths (HTTP LIST) for the given path. This +// listPaths returns a list of paths (HTTP LIST) for the given path. This // function returns an empty list of any errors occur. -func predictListPaths(c *api.Client, path string) []string { - secret, err := c.Logical().List(path) +func (p *Predict) listPaths(path string) []string { + client := p.Client() + if client == nil { + return nil + } + + secret, err := client.Logical().List(path) if err != nil || secret == nil || secret.Data == nil { return nil } @@ -180,8 +234,8 @@ func predictListPaths(c *api.Client, path string) []string { return list } -// predictHasPathArg determines if the args have already accepted a path. -func predictHasPathArg(args []string) bool { +// hasPathArg determines if the args have already accepted a path. +func (p *Predict) hasPathArg(args []string) bool { var nonFlags []string for _, a := range args { if !strings.HasPrefix(a, "-") { diff --git a/command/base_predict_test.go b/command/base_predict_test.go index 05af55da7c..3de6e5f0e7 100644 --- a/command/base_predict_test.go +++ b/command/base_predict_test.go @@ -189,7 +189,10 @@ func TestPredictVaultPaths(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - f := predictVaultPaths(client, tc.includeFiles) + p := NewPredict() + p.client = client + + f := p.vaultPaths(tc.includeFiles) act := f(tc.args) if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) @@ -199,7 +202,7 @@ func TestPredictVaultPaths(t *testing.T) { }) } -func TestPredictMounts(t *testing.T) { +func TestPredict_Mounts(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) @@ -233,7 +236,10 @@ func TestPredictMounts(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - act := predictMounts(client, tc.path) + p := NewPredict() + p.client = client + + act := p.mounts(tc.path) if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) } @@ -242,7 +248,7 @@ func TestPredictMounts(t *testing.T) { }) } -func TestPredictPaths(t *testing.T) { +func TestPredict_Paths(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) @@ -303,7 +309,10 @@ func TestPredictPaths(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - act := predictPaths(client, tc.path, tc.includeFiles) + p := NewPredict() + p.client = client + + act := p.paths(tc.path, tc.includeFiles) if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) } @@ -312,7 +321,7 @@ func TestPredictPaths(t *testing.T) { }) } -func TestPredictListMounts(t *testing.T) { +func TestPredict_ListMounts(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) @@ -345,7 +354,10 @@ func TestPredictListMounts(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - act := predictListMounts(tc.client) + p := NewPredict() + p.client = client + + act := p.listMounts() if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) } @@ -354,7 +366,7 @@ func TestPredictListMounts(t *testing.T) { }) } -func TestPredictListPaths(t *testing.T) { +func TestPredict_ListPaths(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) @@ -394,7 +406,10 @@ func TestPredictListPaths(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - act := predictListPaths(tc.client, tc.path) + p := NewPredict() + p.client = client + + act := p.listPaths(tc.path) if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) } @@ -403,7 +418,7 @@ func TestPredictListPaths(t *testing.T) { }) } -func TestPredictHasPathArg(t *testing.T) { +func TestPredict_HasPathArg(t *testing.T) { t.Parallel() cases := []struct { @@ -443,7 +458,8 @@ func TestPredictHasPathArg(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - if act := predictHasPathArg(tc.args); act != tc.exp { + p := NewPredict() + if act := p.hasPathArg(tc.args); act != tc.exp { t.Errorf("expected %t to be %t", act, tc.exp) } }) From 30dc835b2c4a800e4af4f8023e780b5c76acd4c4 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 29 Aug 2017 13:44:58 -0400 Subject: [PATCH 008/281] Update vendor complete --- vendor/vendor.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/vendor/vendor.json b/vendor/vendor.json index c720dcec9a..8447c7fa9e 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1446,20 +1446,20 @@ { "checksumSHA1": "NB7uVS0/BJDmNu68vPAlbrq4TME=", "path": "github.com/posener/complete/cmd", - "revision": "88e59760adaddb8276c9b15511302890690e2dae", - "revisionTime": "2017-09-08T12:52:45Z" + "revision": "9f41f7636a724791a3b8b1d35e84caa1124f0d3c", + "revisionTime": "2017-08-29T17:11:12Z" }, { - "checksumSHA1": "Hwojin3GxRyKwPAiz5r7UszqkPc=", + "checksumSHA1": "gSX86Xl0w9hvtntdT8h23DZtSag=", "path": "github.com/posener/complete/cmd/install", - "revision": "88e59760adaddb8276c9b15511302890690e2dae", - "revisionTime": "2017-09-08T12:52:45Z" + "revision": "9f41f7636a724791a3b8b1d35e84caa1124f0d3c", + "revisionTime": "2017-08-29T17:11:12Z" }, { "checksumSHA1": "DMo94FwJAm9ZCYCiYdJU2+bh4no=", "path": "github.com/posener/complete/match", - "revision": "88e59760adaddb8276c9b15511302890690e2dae", - "revisionTime": "2017-09-08T12:52:45Z" + "revision": "9f41f7636a724791a3b8b1d35e84caa1124f0d3c", + "revisionTime": "2017-08-29T17:11:12Z" }, { "checksumSHA1": "vCogt04lbcE8fUgvRCOaZQUo+Pk=", From eb4ab6840deb94e11ae9077bc4d7925845bc55cc Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Wed, 30 Aug 2017 12:48:39 -0400 Subject: [PATCH 009/281] Delegate usage to the UI --- command/base.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/command/base.go b/command/base.go index a918dc39fd..eda516fe45 100644 --- a/command/base.go +++ b/command/base.go @@ -313,17 +313,10 @@ type FlagSets struct { // NewFlagSets creates a new flag sets. func NewFlagSets(ui cli.Ui) *FlagSets { mainSet := flag.NewFlagSet("", flag.ContinueOnError) - mainSet.Usage = func() {} - // Pull errors from the flagset into the ui's error - errR, errW := io.Pipe() - errScanner := bufio.NewScanner(errR) - go func() { - for errScanner.Scan() { - ui.Error(errScanner.Text()) - } - }() - mainSet.SetOutput(errW) + // Errors and usage are controlled by the CLI. + mainSet.Usage = func() {} + mainSet.SetOutput(ioutil.Discard) return &FlagSets{ flagSets: make([]*FlagSet, 0, 6), From eacb3de75920cf41a92f468dbeeea2d09c6940ac Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Wed, 30 Aug 2017 12:48:54 -0400 Subject: [PATCH 010/281] More arbitrary function for wrapping at a length --- command/base.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/command/base.go b/command/base.go index eda516fe45..7c4c2e92d8 100644 --- a/command/base.go +++ b/command/base.go @@ -1,11 +1,11 @@ package command import ( - "bufio" "bytes" "flag" "fmt" "io" + "io/ioutil" "regexp" "strings" "sync" @@ -287,13 +287,13 @@ func printFlagDetail(w io.Writer, f *flag.Flag) { } usage := reRemoveWhitespace.ReplaceAllString(f.Usage, " ") - indented := wrapAtLength(usage, 6) + indented := wrapAtLengthWithPadding(usage, 6) fmt.Fprintf(w, "%s\n\n", indented) } -// wrapAtLength wraps the given text at the maxLineLength, taking into account -// any provided left padding. -func wrapAtLength(s string, pad int) string { +// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking +// into account any provided left padding. +func wrapAtLengthWithPadding(s string, pad int) string { wrapped := text.Wrap(s, maxLineLength-pad) lines := strings.Split(wrapped, "\n") for i, line := range lines { @@ -302,6 +302,11 @@ func wrapAtLength(s string, pad int) string { return strings.Join(lines, "\n") } +// wrapAtLength wraps the given text to maxLineLength. +func wrapAtLength(s string) string { + return wrapAtLengthWithPadding(s, 0) +} + // FlagSets is a group of flag sets. type FlagSets struct { flagSets []*FlagSet From b67f9404a80a6dbec28c129cebd032121bd63602 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Wed, 30 Aug 2017 12:49:06 -0400 Subject: [PATCH 011/281] Only print default values if they are non-zero --- command/base_flags.go | 91 ++++++++++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 28 deletions(-) diff --git a/command/base_flags.go b/command/base_flags.go index 708f38f14f..796709727b 100644 --- a/command/base_flags.go +++ b/command/base_flags.go @@ -56,20 +56,25 @@ type IntVar struct { } func (f *FlagSet) IntVar(i *IntVar) { - def := i.Default + initial := i.Default if v := os.Getenv(i.EnvVar); v != "" { if i, err := strconv.ParseInt(v, 0, 64); err != nil { - def = int(i) + initial = int(i) } } + def := "" + if i.Default != 0 { + def = strconv.FormatInt(int64(i.Default), 10) + } + f.VarFlag(&VarFlag{ Name: i.Name, Aliases: i.Aliases, Usage: i.Usage, - Default: strconv.FormatInt(int64(i.Default), 10), + Default: def, EnvVar: i.EnvVar, - Value: newIntValue(def, i.Target), + Value: newIntValue(initial, i.Target), Completion: i.Completion, }) } @@ -85,20 +90,25 @@ type Int64Var struct { } func (f *FlagSet) Int64Var(i *Int64Var) { - def := i.Default + initial := i.Default if v := os.Getenv(i.EnvVar); v != "" { if i, err := strconv.ParseInt(v, 0, 64); err != nil { - def = i + initial = i } } + def := "" + if i.Default != 0 { + def = strconv.FormatInt(int64(i.Default), 10) + } + f.VarFlag(&VarFlag{ Name: i.Name, Aliases: i.Aliases, Usage: i.Usage, - Default: strconv.FormatInt(i.Default, 10), + Default: def, EnvVar: i.EnvVar, - Value: newInt64Value(def, i.Target), + Value: newInt64Value(initial, i.Target), Completion: i.Completion, }) } @@ -114,20 +124,25 @@ type UintVar struct { } func (f *FlagSet) UintVar(i *UintVar) { - def := i.Default + initial := i.Default if v := os.Getenv(i.EnvVar); v != "" { if i, err := strconv.ParseUint(v, 0, 64); err != nil { - def = uint(i) + initial = uint(i) } } + def := "" + if i.Default != 0 { + def = strconv.FormatUint(uint64(i.Default), 10) + } + f.VarFlag(&VarFlag{ Name: i.Name, Aliases: i.Aliases, Usage: i.Usage, - Default: strconv.FormatUint(uint64(i.Default), 10), + Default: def, EnvVar: i.EnvVar, - Value: newUintValue(def, i.Target), + Value: newUintValue(initial, i.Target), Completion: i.Completion, }) } @@ -143,20 +158,25 @@ type Uint64Var struct { } func (f *FlagSet) Uint64Var(i *Uint64Var) { - def := i.Default + initial := i.Default if v := os.Getenv(i.EnvVar); v != "" { if i, err := strconv.ParseUint(v, 0, 64); err != nil { - def = i + initial = i } } + def := "" + if i.Default != 0 { + strconv.FormatUint(i.Default, 10) + } + f.VarFlag(&VarFlag{ Name: i.Name, Aliases: i.Aliases, Usage: i.Usage, - Default: strconv.FormatUint(i.Default, 10), + Default: def, EnvVar: i.EnvVar, - Value: newUint64Value(def, i.Target), + Value: newUint64Value(initial, i.Target), Completion: i.Completion, }) } @@ -172,18 +192,23 @@ type StringVar struct { } func (f *FlagSet) StringVar(i *StringVar) { - def := i.Default + initial := i.Default if v := os.Getenv(i.EnvVar); v != "" { - def = v + initial = v + } + + def := "" + if i.Default != "" { + def = i.Default } f.VarFlag(&VarFlag{ Name: i.Name, Aliases: i.Aliases, Usage: i.Usage, - Default: i.Default, + Default: def, EnvVar: i.EnvVar, - Value: newStringValue(def, i.Target), + Value: newStringValue(initial, i.Target), Completion: i.Completion, }) } @@ -199,20 +224,25 @@ type Float64Var struct { } func (f *FlagSet) Float64Var(i *Float64Var) { - def := i.Default + initial := i.Default if v := os.Getenv(i.EnvVar); v != "" { if i, err := strconv.ParseFloat(v, 64); err != nil { - def = i + initial = i } } + def := "" + if i.Default != 0 { + def = strconv.FormatFloat(i.Default, 'e', -1, 64) + } + f.VarFlag(&VarFlag{ Name: i.Name, Aliases: i.Aliases, Usage: i.Usage, - Default: strconv.FormatFloat(i.Default, 'e', -1, 64), + Default: def, EnvVar: i.EnvVar, - Value: newFloat64Value(def, i.Target), + Value: newFloat64Value(initial, i.Target), Completion: i.Completion, }) } @@ -228,20 +258,25 @@ type DurationVar struct { } func (f *FlagSet) DurationVar(i *DurationVar) { - def := i.Default + initial := i.Default if v := os.Getenv(i.EnvVar); v != "" { if d, err := time.ParseDuration(v); err != nil { - def = d + initial = d } } + def := "" + if i.Default != 0 { + def = i.Default.String() + } + f.VarFlag(&VarFlag{ Name: i.Name, Aliases: i.Aliases, Usage: i.Usage, - Default: i.Default.String(), + Default: def, EnvVar: i.EnvVar, - Value: newDurationValue(def, i.Target), + Value: newDurationValue(initial, i.Target), Completion: i.Completion, }) } From 08b39ecc49528d1c35381652d7b18344233bec84 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 18:48:48 -0400 Subject: [PATCH 012/281] Add API functions for token helpers --- api/api_integration_test.go | 110 +- api/renewer_integration_test.go | 10 +- api/secret.go | 197 ++++ api/secret_test.go | 1725 ++++++++++++++++++++++++++++++- 4 files changed, 2010 insertions(+), 32 deletions(-) diff --git a/api/api_integration_test.go b/api/api_integration_test.go index c4e1a1d807..a9e4409ae1 100644 --- a/api/api_integration_test.go +++ b/api/api_integration_test.go @@ -1,59 +1,131 @@ package api_test import ( + "context" "database/sql" + "encoding/base64" "fmt" + "net" + "net/http" "testing" + "time" "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/audit" + "github.com/hashicorp/vault/builtin/logical/database" "github.com/hashicorp/vault/builtin/logical/pki" "github.com/hashicorp/vault/builtin/logical/transit" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/vault" + auditFile "github.com/hashicorp/vault/builtin/audit/file" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" vaulthttp "github.com/hashicorp/vault/http" logxi "github.com/mgutz/logxi/v1" dockertest "gopkg.in/ory-am/dockertest.v3" ) -var testVaultServerDefaultBackends = map[string]logical.Factory{ - "transit": transit.Factory, - "pki": pki.Factory, -} - +// testVaultServer creates a test vault cluster and returns a configured API +// client and closer function. func testVaultServer(t testing.TB) (*api.Client, func()) { - return testVaultServerBackends(t, testVaultServerDefaultBackends) + t.Helper() + + client, _, closer := testVaultServerUnseal(t) + return client, closer } -func testVaultServerBackends(t testing.TB, backends map[string]logical.Factory) (*api.Client, func()) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: logxi.NullLog, - LogicalBackends: backends, - } +// testVaultServerUnseal creates a test vault cluster and returns a configured +// API client, list of unseal keys (as strings), and a closer function. +func testVaultServerUnseal(t testing.TB) (*api.Client, []string, func()) { + t.Helper() + + return testVaultServerCoreConfig(t, &vault.CoreConfig{ + DisableMlock: true, + DisableCache: true, + Logger: logxi.NullLog, + CredentialBackends: map[string]logical.Factory{ + "userpass": credUserpass.Factory, + }, + AuditBackends: map[string]audit.Factory{ + "file": auditFile.Factory, + }, + LogicalBackends: map[string]logical.Factory{ + "database": database.Factory, + "generic-leased": vault.LeasedPassthroughBackendFactory, + "pki": pki.Factory, + "transit": transit.Factory, + }, + }) +} + +// testVaultServerCoreConfig creates a new vault cluster with the given core +// configuration. This is a lower-level test helper. +func testVaultServerCoreConfig(t testing.TB, coreConfig *vault.CoreConfig) (*api.Client, []string, func()) { + t.Helper() cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, }) cluster.Start() - // make it easy to get access to the active + // Make it easy to get access to the active core := cluster.Cores[0].Core vault.TestWaitActive(t, core) + // Get the client already setup for us! client := cluster.Cores[0].Client client.SetToken(cluster.RootToken) - // Sanity check - secret, err := client.Auth().Token().LookupSelf() + // Convert the unseal keys to base64 encoded, since these are how the user + // will get them. + unsealKeys := make([]string, len(cluster.BarrierKeys)) + for i := range unsealKeys { + unsealKeys[i] = base64.StdEncoding.EncodeToString(cluster.BarrierKeys[i]) + } + + return client, unsealKeys, func() { defer cluster.Cleanup() } +} + +// testVaultServerBad creates an http server that returns a 500 on each request +// to simulate failures. +func testVaultServerBad(t testing.TB) (*api.Client, func()) { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } - if secret == nil || secret.Data["id"].(string) != cluster.RootToken { - t.Fatalf("token mismatch: %#v vs %q", secret, cluster.RootToken) + + server := &http.Server{ + Addr: "127.0.0.1:0", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "500 internal server error", http.StatusInternalServerError) + }), + ReadTimeout: 1 * time.Second, + ReadHeaderTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + IdleTimeout: 1 * time.Second, + } + + go func() { + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + t.Fatal(err) + } + }() + + client, err := api.NewClient(&api.Config{ + Address: "http://" + listener.Addr().String(), + }) + if err != nil { + t.Fatal(err) + } + + return client, func() { + ctx, done := context.WithTimeout(context.Background(), 5*time.Second) + defer done() + + server.Shutdown(ctx) } - return client, func() { defer cluster.Cleanup() } } // testPostgresDB creates a testing postgres database in a Docker container, diff --git a/api/renewer_integration_test.go b/api/renewer_integration_test.go index 7011c7d10a..50a775e126 100644 --- a/api/renewer_integration_test.go +++ b/api/renewer_integration_test.go @@ -5,20 +5,12 @@ import ( "time" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/logical/database" - "github.com/hashicorp/vault/builtin/logical/pki" - "github.com/hashicorp/vault/builtin/logical/transit" - "github.com/hashicorp/vault/logical" ) func TestRenewer_Renew(t *testing.T) { t.Parallel() - client, vaultDone := testVaultServerBackends(t, map[string]logical.Factory{ - "database": database.Factory, - "pki": pki.Factory, - "transit": transit.Factory, - }) + client, vaultDone := testVaultServer(t) defer vaultDone() pgURL, pgDone := testPostgresDB(t) diff --git a/api/secret.go b/api/secret.go index 7478a0c544..86117029db 100644 --- a/api/secret.go +++ b/api/secret.go @@ -1,7 +1,9 @@ package api import ( + "encoding/json" "io" + "strconv" "time" "github.com/hashicorp/vault/helper/jsonutil" @@ -35,6 +37,201 @@ type Secret struct { WrapInfo *SecretWrapInfo `json:"wrap_info,omitempty"` } +// TokenID returns the standardized token ID (token) for the given secret. +func (s *Secret) TokenID() string { + if s == nil { + return "" + } + + if s.Auth != nil && len(s.Auth.ClientToken) > 0 { + return s.Auth.ClientToken + } + + if s.Data == nil || s.Data["id"] == nil { + return "" + } + + id, ok := s.Data["id"].(string) + if !ok { + return "" + } + + return id +} + +// TokenAccessor returns the standardized token accessor for the given secret. +// If the secret is nil or does not contain an accessor, this returns the empty +// string. +func (s *Secret) TokenAccessor() string { + if s == nil { + return "" + } + + if s.Auth != nil && len(s.Auth.Accessor) > 0 { + return s.Auth.Accessor + } + + if s.Data == nil || s.Data["accessor"] == nil { + return "" + } + + accessor, ok := s.Data["accessor"].(string) + if !ok { + return "" + } + + return accessor +} + +// TokenMeta returns the standardized token metadata for the given secret. +// If the secret is nil or does not contain an accessor, this returns the empty +// string. Metadata is usually modeled as an map[string]interface{}, but token +// metdata is always a map[string]string. This function handles the coercion. +func (s *Secret) TokenMeta() map[string]string { + if s == nil { + return nil + } + + if s.Auth != nil && len(s.Auth.Metadata) > 0 { + return s.Auth.Metadata + } + + if s.Data == nil || s.Data["meta"] == nil { + return nil + } + + metaRaw, ok := s.Data["meta"].(map[string]interface{}) + if !ok { + return nil + } + + meta := make(map[string]string, len(metaRaw)) + for k, v := range metaRaw { + m, ok := v.(string) + if !ok { + return nil + } + meta[k] = m + } + + return meta +} + +// TokenRemainingUses returns the standardized remaining uses for the given +// secret. If the secret is nil or does not contain the "num_uses", this returns +// 0.. +func (s *Secret) TokenRemainingUses() int { + if s == nil || s.Data == nil || s.Data["num_uses"] == nil { + return 0 + } + + usesStr, ok := s.Data["num_uses"].(json.Number) + if !ok { + return 0 + } + + if string(usesStr) == "" { + return 0 + } + + uses, err := strconv.ParseInt(string(usesStr), 10, 64) + if err != nil { + return 0 + } + + return int(uses) +} + +// TokenPolicies returns the standardized list of policies for the given secret. +// If the secret is nil or does not contain any policies, this returns nil. +// Policies are usually returned as []interface{}, but this function ensures +// they are []string. +func (s *Secret) TokenPolicies() []string { + if s == nil { + return nil + } + + if s.Auth != nil && len(s.Auth.Policies) > 0 { + return s.Auth.Policies + } + + if s.Data == nil || s.Data["policies"] == nil { + return nil + } + + list, ok := s.Data["policies"].([]interface{}) + if !ok { + return nil + } + + policies := make([]string, len(list)) + for i := range list { + p, ok := list[i].(string) + if !ok { + return nil + } + policies[i] = p + } + + return policies +} + +// TokenIsRenewable returns the standardized token renewability for the given +// secret. If the secret is nil or does not contain the "renewable" key, this +// returns false. +func (s *Secret) TokenIsRenewable() bool { + if s == nil { + return false + } + + if s.Auth != nil && s.Auth.Renewable { + return s.Auth.Renewable + } + + if s.Data == nil || s.Data["renewable"] == nil { + return false + } + + renewable, ok := s.Data["renewable"].(bool) + if !ok { + return false + } + + return renewable +} + +// TokenTTL returns the standardized remaining token TTL for the given secret. +// If the secret is nil or does not contain a TTL, this returns the 0. +func (s *Secret) TokenTTL() time.Duration { + if s == nil { + return 0 + } + + if s.Auth != nil && s.Auth.LeaseDuration > 0 { + return time.Duration(s.Auth.LeaseDuration) * time.Second + } + + if s.Data == nil || s.Data["ttl"] == nil { + return 0 + } + + ttlStr, ok := s.Data["ttl"].(json.Number) + if !ok { + return 0 + } + + if string(ttlStr) == "" { + return 0 + } + + ttl, err := time.ParseDuration(string(ttlStr) + "s") + if err != nil { + return 0 + } + + return ttl +} + // SecretWrapInfo contains wrapping information if we have it. If what is // contained is an authentication token, the accessor for the token will be // available in WrappedAccessor. diff --git a/api/secret_test.go b/api/secret_test.go index 3b6496677a..4e4c8c3aef 100644 --- a/api/secret_test.go +++ b/api/secret_test.go @@ -1,10 +1,13 @@ -package api +package api_test import ( + "encoding/json" "reflect" "strings" "testing" "time" + + "github.com/hashicorp/vault/api" ) func TestParseSecret(t *testing.T) { @@ -29,12 +32,12 @@ func TestParseSecret(t *testing.T) { rawTime, _ := time.Parse(time.RFC3339, "2016-06-07T15:52:10-04:00") - secret, err := ParseSecret(strings.NewReader(raw)) + secret, err := api.ParseSecret(strings.NewReader(raw)) if err != nil { t.Fatalf("err: %s", err) } - expected := &Secret{ + expected := &api.Secret{ LeaseID: "foo", Renewable: true, LeaseDuration: 10, @@ -44,7 +47,7 @@ func TestParseSecret(t *testing.T) { Warnings: []string{ "a warning!", }, - WrapInfo: &SecretWrapInfo{ + WrapInfo: &api.SecretWrapInfo{ Token: "token", TTL: 60, CreationTime: rawTime, @@ -55,3 +58,1717 @@ func TestParseSecret(t *testing.T) { t.Fatalf("bad:\ngot\n%#v\nexpected\n%#v\n", secret, expected) } } + +func TestSecret_TokenID(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp string + }{ + { + "nil", + nil, + "", + }, + { + "nil_auth", + &api.Secret{ + Auth: nil, + }, + "", + }, + { + "empty_auth_client_token", + &api.Secret{ + Auth: &api.SecretAuth{ + ClientToken: "", + }, + }, + "", + }, + { + "real_auth_client_token", + &api.Secret{ + Auth: &api.SecretAuth{ + ClientToken: "my-token", + }, + }, + "my-token", + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + "", + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + "", + }, + { + "data_not_string", + &api.Secret{ + Data: map[string]interface{}{ + "id": 123, + }, + }, + "", + }, + { + "data_string", + &api.Secret{ + Data: map[string]interface{}{ + "id": "my-token", + }, + }, + "my-token", + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenID() + if act != tc.exp { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + if secret.TokenID() != token { + t.Errorf("expected %q to be %q", secret.TokenID(), token) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + if secret.TokenID() != token { + t.Errorf("expected %q to be %q", secret.TokenID(), token) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if secret.TokenID() != token { + t.Errorf("expected %q to be %q", secret.TokenID(), token) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + if secret.TokenID() != token { + t.Errorf("expected %q to be %q", secret.TokenID(), token) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenID() != token { + t.Errorf("expected %q to be %q", secret.TokenID(), token) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenID() != token { + t.Errorf("expected %q to be %q", secret.TokenID(), token) + } + }) +} + +func TestSecret_TokenAccessor(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp string + }{ + { + "nil", + nil, + "", + }, + { + "nil_auth", + &api.Secret{ + Auth: nil, + }, + "", + }, + { + "empty_auth_accessor", + &api.Secret{ + Auth: &api.SecretAuth{ + Accessor: "", + }, + }, + "", + }, + { + "real_auth_accessor", + &api.Secret{ + Auth: &api.SecretAuth{ + Accessor: "my-accessor", + }, + }, + "my-accessor", + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + "", + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + "", + }, + { + "data_not_string", + &api.Secret{ + Data: map[string]interface{}{ + "accessor": 123, + }, + }, + "", + }, + { + "data_string", + &api.Secret{ + Data: map[string]interface{}{ + "accessor": "my-accessor", + }, + }, + "my-accessor", + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenAccessor() + if act != tc.exp { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + _, accessor := secret.Auth.ClientToken, secret.Auth.Accessor + + if secret.TokenAccessor() != accessor { + t.Errorf("expected %q to be %q", secret.TokenAccessor(), accessor) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + _, accessor := secret.Auth.ClientToken, secret.Auth.Accessor + + if secret.TokenAccessor() != accessor { + t.Errorf("expected %q to be %q", secret.TokenAccessor(), accessor) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token, accessor := secret.Auth.ClientToken, secret.Auth.Accessor + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if secret.TokenAccessor() != accessor { + t.Errorf("expected %q to be %q", secret.TokenAccessor(), accessor) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token, accessor := secret.Auth.ClientToken, secret.Auth.Accessor + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + if secret.TokenAccessor() != accessor { + t.Errorf("expected %q to be %q", secret.TokenAccessor(), accessor) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token, accessor := secret.Auth.ClientToken, secret.Auth.Accessor + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenAccessor() != accessor { + t.Errorf("expected %q to be %q", secret.TokenAccessor(), accessor) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token, accessor := secret.Auth.ClientToken, secret.Auth.Accessor + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenAccessor() != accessor { + t.Errorf("expected %q to be %q", secret.TokenAccessor(), accessor) + } + }) +} + +func TestSecret_TokenMeta(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp map[string]string + }{ + { + "nil", + nil, + nil, + }, + { + "nil_auth", + &api.Secret{ + Auth: nil, + }, + nil, + }, + { + "nil_auth_metadata", + &api.Secret{ + Auth: &api.SecretAuth{ + Metadata: nil, + }, + }, + nil, + }, + { + "empty_auth_metadata", + &api.Secret{ + Auth: &api.SecretAuth{ + Metadata: map[string]string{}, + }, + }, + nil, + }, + { + "real_auth_metadata", + &api.Secret{ + Auth: &api.SecretAuth{ + Metadata: map[string]string{"foo": "bar"}, + }, + }, + map[string]string{"foo": "bar"}, + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + nil, + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + nil, + }, + { + "data_not_map", + &api.Secret{ + Data: map[string]interface{}{ + "meta": 123, + }, + }, + nil, + }, + { + "data_map", + &api.Secret{ + Data: map[string]interface{}{ + "meta": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + map[string]string{"foo": "bar"}, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenMeta() + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %#v to be %#v", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + + meta := map[string]string{"username": "test"} + if !reflect.DeepEqual(secret.TokenMeta(), meta) { + t.Errorf("expected %#v to be %#v", secret.TokenMeta(), meta) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + meta := map[string]string{"foo": "bar"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Metadata: meta, + }) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMeta(), meta) { + t.Errorf("expected %#v to be %#v", secret.TokenMeta(), meta) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + meta := map[string]string{"foo": "bar"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Metadata: meta, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMeta(), meta) { + t.Errorf("expected %#v to be %#v", secret.TokenMeta(), meta) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + meta := map[string]string{"foo": "bar"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Metadata: meta, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMeta(), meta) { + t.Errorf("expected %#v to be %#v", secret.TokenMeta(), meta) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + meta := map[string]string{"foo": "bar"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Metadata: meta, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMeta(), meta) { + t.Errorf("expected %#v to be %#v", secret.TokenMeta(), meta) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + meta := map[string]string{"foo": "bar"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Metadata: meta, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMeta(), meta) { + t.Errorf("expected %#v to be %#v", secret.TokenMeta(), meta) + } + }) +} + +func TestSecret_TokenRemainingUses(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp int + }{ + { + "nil", + nil, + 0, + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + 0, + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + 0, + }, + { + "data_not_json_number", + &api.Secret{ + Data: map[string]interface{}{ + "num_uses": 123, + }, + }, + 0, + }, + { + "data_json_number", + &api.Secret{ + Data: map[string]interface{}{ + "num_uses": json.Number("123"), + }, + }, + 123, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenRemainingUses() + if act != tc.exp { + t.Errorf("expected %d to be %d", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + uses := 5 + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + "num_uses": uses, + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + + // Remaining uses is not returned from this API + uses = 0 + if secret.TokenRemainingUses() != uses { + t.Errorf("expected %d to be %d", secret.TokenRemainingUses(), uses) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + uses := 5 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + NumUses: uses, + }) + if err != nil { + t.Fatal(err) + } + + // /auth/token/create does not return the number of uses + uses = 0 + if secret.TokenRemainingUses() != uses { + t.Errorf("expected %d to be %d", secret.TokenRemainingUses(), uses) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + uses := 5 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + NumUses: uses, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if secret.TokenRemainingUses() != uses { + t.Errorf("expected %d to be %d", secret.TokenRemainingUses(), uses) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + uses := 5 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + NumUses: uses, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + uses = uses - 1 // we just used it + if secret.TokenRemainingUses() != uses { + t.Errorf("expected %d to be %d", secret.TokenRemainingUses(), uses) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + uses := 5 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + NumUses: uses, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + // /auth/token/renew does not return the number of uses + uses = 0 + if secret.TokenRemainingUses() != uses { + t.Errorf("expected %d to be %d", secret.TokenRemainingUses(), uses) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + uses := 5 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + NumUses: uses, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + // /auth/token/renew-self does not return the number of uses + uses = 0 + if secret.TokenRemainingUses() != uses { + t.Errorf("expected %d to be %d", secret.TokenRemainingUses(), uses) + } + }) +} + +func TestSecret_TokenPolicies(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp []string + }{ + { + "nil", + nil, + nil, + }, + { + "nil_auth", + &api.Secret{ + Auth: nil, + }, + nil, + }, + { + "nil_auth_policies", + &api.Secret{ + Auth: &api.SecretAuth{ + Policies: nil, + }, + }, + nil, + }, + { + "empty_auth_policies", + &api.Secret{ + Auth: &api.SecretAuth{ + Policies: []string{}, + }, + }, + nil, + }, + { + "real_auth_policies", + &api.Secret{ + Auth: &api.SecretAuth{ + Policies: []string{"foo"}, + }, + }, + []string{"foo"}, + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + nil, + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + nil, + }, + { + "data_not_slice", + &api.Secret{ + Data: map[string]interface{}{ + "policies": 123, + }, + }, + nil, + }, + { + "data_slice", + &api.Secret{ + Data: map[string]interface{}{ + "policies": []interface{}{"foo"}, + }, + }, + []string{"foo"}, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenPolicies() + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %#v to be %#v", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policies := []string{"bar", "default", "foo"} + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": strings.Join(policies, ","), + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenPolicies(), policies) { + t.Errorf("expected %#v to be %#v", secret.TokenPolicies(), policies) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policies := []string{"bar", "default", "foo"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: policies, + }) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenPolicies(), policies) { + t.Errorf("expected %#v to be %#v", secret.TokenPolicies(), policies) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policies := []string{"bar", "default", "foo"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: policies, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenPolicies(), policies) { + t.Errorf("expected %#v to be %#v", secret.TokenPolicies(), policies) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policies := []string{"bar", "default", "foo"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: policies, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenPolicies(), policies) { + t.Errorf("expected %#v to be %#v", secret.TokenPolicies(), policies) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policies := []string{"bar", "default", "foo"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: policies, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenPolicies(), policies) { + t.Errorf("expected %#v to be %#v", secret.TokenPolicies(), policies) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policies := []string{"bar", "default", "foo"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: policies, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenPolicies(), policies) { + t.Errorf("expected %#v to be %#v", secret.TokenPolicies(), policies) + } + }) +} + +func TestSecret_TokenIsRenewable(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp bool + }{ + { + "nil", + nil, + false, + }, + { + "nil_auth", + &api.Secret{ + Auth: nil, + }, + false, + }, + { + "auth_renewable_false", + &api.Secret{ + Auth: &api.SecretAuth{ + Renewable: false, + }, + }, + false, + }, + { + "auth_renewable_true", + &api.Secret{ + Auth: &api.SecretAuth{ + Renewable: true, + }, + }, + true, + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + false, + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + false, + }, + { + "data_not_bool", + &api.Secret{ + Data: map[string]interface{}{ + "renewable": 123, + }, + }, + false, + }, + { + "data_bool_true", + &api.Secret{ + Data: map[string]interface{}{ + "renewable": true, + }, + }, + true, + }, + { + "data_bool_false", + &api.Secret{ + Data: map[string]interface{}{ + "renewable": true, + }, + }, + true, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenIsRenewable() + if act != tc.exp { + t.Errorf("expected %t to be %t", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + renewable := true + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + + if secret.TokenIsRenewable() != renewable { + t.Errorf("expected %t to be %t", secret.TokenIsRenewable(), renewable) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + renewable := true + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Renewable: &renewable, + }) + if err != nil { + t.Fatal(err) + } + + if secret.TokenIsRenewable() != renewable { + t.Errorf("expected %t to be %t", secret.TokenIsRenewable(), renewable) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + renewable := true + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Renewable: &renewable, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if secret.TokenIsRenewable() != renewable { + t.Errorf("expected %t to be %t", secret.TokenIsRenewable(), renewable) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + renewable := true + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Renewable: &renewable, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + if secret.TokenIsRenewable() != renewable { + t.Errorf("expected %t to be %t", secret.TokenIsRenewable(), renewable) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + renewable := true + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Renewable: &renewable, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenIsRenewable() != renewable { + t.Errorf("expected %t to be %t", secret.TokenIsRenewable(), renewable) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + renewable := true + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + Renewable: &renewable, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenIsRenewable() != renewable { + t.Errorf("expected %t to be %t", secret.TokenIsRenewable(), renewable) + } + }) +} + +func TestSecret_TokenTTL(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp time.Duration + }{ + { + "nil", + nil, + 0, + }, + { + "nil_auth", + &api.Secret{ + Auth: nil, + }, + 0, + }, + { + "nil_auth_lease_duration", + &api.Secret{ + Auth: &api.SecretAuth{ + LeaseDuration: 0, + }, + }, + 0, + }, + { + "real_auth_lease_duration", + &api.Secret{ + Auth: &api.SecretAuth{ + LeaseDuration: 3600, + }, + }, + 1 * time.Hour, + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + 0, + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + 0, + }, + { + "data_not_json_number", + &api.Secret{ + Data: map[string]interface{}{ + "ttl": 123, + }, + }, + 0, + }, + { + "data_json_number", + &api.Secret{ + Data: map[string]interface{}{ + "ttl": json.Number("3600"), + }, + }, + 1 * time.Hour, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenTTL() + if act != tc.exp { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 30 * time.Minute + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + "ttl": ttl.String(), + "explicit_max_ttl": ttl.String(), + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + + if secret.TokenTTL() == 0 || secret.TokenTTL() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTL(), ttl) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 30 * time.Minute + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: ttl.String(), + ExplicitMaxTTL: ttl.String(), + }) + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTL() == 0 || secret.TokenTTL() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTL(), ttl) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 30 * time.Minute + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: ttl.String(), + ExplicitMaxTTL: ttl.String(), + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTL() == 0 || secret.TokenTTL() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTL(), ttl) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 30 * time.Minute + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: ttl.String(), + ExplicitMaxTTL: ttl.String(), + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTL() == 0 || secret.TokenTTL() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTL(), ttl) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 30 * time.Minute + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: ttl.String(), + ExplicitMaxTTL: ttl.String(), + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTL() == 0 || secret.TokenTTL() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTL(), ttl) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 30 * time.Minute + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: ttl.String(), + ExplicitMaxTTL: ttl.String(), + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTL() == 0 || secret.TokenTTL() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTL(), ttl) + } + }) +} From 6984b8476b70d5ce0a27cd9c7c1a0bdbe9fafc9b Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 18:49:45 -0400 Subject: [PATCH 013/281] Update help output for aws auth --- builtin/credential/aws/cli.go | 61 ++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index 2842c24dbf..75fb08ff89 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -108,29 +108,52 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -The AWS credential provider allows you to authenticate with -AWS IAM credentials. To use it, you specify valid AWS IAM credentials -in one of a number of ways. They can be specified explicitly on the -command line (which in general you should not do), via the standard AWS -environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and -AWS_SECURITY_TOKEN), via the ~/.aws/credentials file, or via an EC2 -instance profile (in that order). +Usage: vault auth -method=aws [CONFIG K=V...] - Example: vault auth -method=aws + The AWS authentication provider allows users to authenticate with AWS IAM + credentials. The AWS IAM credentials may be specified in a number of ways, + listed in order of precedence below: -If you need to explicitly pass in credentials, you would do it like this: - Example: vault auth -method=aws aws_access_key_id= aws_secret_access_key= aws_security_token= + 1. Explicitly via the command line (not recommended) -Key/Value Pairs: + 2. Via the standard AWS environment variables (AWS_ACCESS_KEY, etc.) - mount=aws The mountpoint for the AWS credential provider. - Defaults to "aws" - aws_access_key_id= Explicitly specified AWS access key - aws_secret_access_key= Explicitly specified AWS secret key - aws_security_token= Security token for temporary credentials - header_value The Value of the X-Vault-AWS-IAM-Server-ID header. - role The name of the role you're requesting a token for - ` + 3. Via the ~/.aws/credentials file + + 4. Via EC2 instance profile + + Authenticate using locally stored credentials: + + $ vault auth -method=aws + + Authenticate by passing keys: + + $ vault auth -method=aws aws_access_key_id=... aws_secret_access_key=... + +Configuration: + + aws_access_key_id= + Explicit AWS access key ID + + aws_secret_access_key= + Explicit AWS secret access key + + aws_security_token= + Explicit AWS security token for temporary credentials + + header_value= + Value for the x-vault-aws-iam-server-id header in requests + + mount= + Path where the AWS credential provider is mounted. This is usually + provided via the -path flag in the "vault auth" command, but it can be + specified here as well. If specified here, it takes precedence over + the value for -path. The default value is "aws". + + role= + Name of the role to request a token against + +` return strings.TrimSpace(help) } From d71b7e6824cd1067d745cdf0ebd489278362dd08 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 18:49:55 -0400 Subject: [PATCH 014/281] Update help output for cert auth --- builtin/credential/cert/cli.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/builtin/credential/cert/cli.go b/builtin/credential/cert/cli.go index a1071fcd38..5316efd8c1 100644 --- a/builtin/credential/cert/cli.go +++ b/builtin/credential/cert/cli.go @@ -40,17 +40,23 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -The "cert" credential provider allows you to authenticate with a -client certificate. No other authentication materials are needed. -Optionally, you may specify the specific certificate role to -authenticate against with the "name" parameter. +Usage: vault auth -method=cert [CONFIG K=V...] - Example: vault auth -method=cert \ - -client-cert=/path/to/cert.pem \ - -client-key=/path/to/key.pem - name=cert1 + The certificate authentication provider allows uers to authenticate with a + client certificate passed with the request. The -client-cert and -client-key + flags are included with the "vault auth" command, NOT as configuration to + the authentication provider. - ` + Authenticate using a local client certificate: + + $ vault auth -method=cert -client-cert=cert.pem -client-key=key.pem + +Configuration: + + name= + Certificate role to authenticate against. + +` return strings.TrimSpace(help) } From a783af750d20e81d6d1b09133272aeb009921411 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 18:50:03 -0400 Subject: [PATCH 015/281] Update help output for github auth --- builtin/credential/github/cli.go | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/builtin/credential/github/cli.go b/builtin/credential/github/cli.go index 557939b209..21f8d56a7d 100644 --- a/builtin/credential/github/cli.go +++ b/builtin/credential/github/cli.go @@ -39,20 +39,27 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -The GitHub credential provider allows you to authenticate with GitHub. -To use it, specify the "token" parameter. The value should be a personal access -token for your GitHub account. You can generate a personal access token on your -account settings page on GitHub. +Usage: vault auth -method=github [CONFIG K=V...] - Example: vault auth -method=github token= + The GitHub authentication provider allows users to authenticate using a + GitHub personal access token. Users can generate a personal access token + from the settings page on their GitHub account. -Key/Value Pairs: + Authenticate using a GitHub token: - mount=github The mountpoint for the GitHub credential provider. - Defaults to "github" + $ vault auth -method=github token=abcd1234 - token= The GitHub personal access token for authentication. - ` +Configuration: + + mount= + Path where the GitHub credential provider is mounted. This is usually + provided via the -path flag in the "vault auth" command, but it can be + specified here as well. If specified here, it takes precedence over + the value for -path. The default value is "github". + + token= + GitHub personal access token to use for authentication. +` return strings.TrimSpace(help) } From 12ad533ea3e45a0b1f72f569b1e11df5aa7cc542 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 18:50:12 -0400 Subject: [PATCH 016/281] Update help output for ldap auth --- builtin/credential/ldap/cli.go | 41 ++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/builtin/credential/ldap/cli.go b/builtin/credential/ldap/cli.go index 262bc998e1..2559f85c47 100644 --- a/builtin/credential/ldap/cli.go +++ b/builtin/credential/ldap/cli.go @@ -62,18 +62,41 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -The LDAP credential provider allows you to authenticate with LDAP. -To use it, first configure it through the "config" endpoint, and then -login by specifying username and password. If password is not provided -on the command line, it will be read from stdin. +Usage: vault auth -method=ldap [CONFIG K=V...] -If multi-factor authentication (MFA) is enabled, a "method" and/or "passcode" -may be provided depending on the MFA backend enabled. To check -which MFA backend is in use, read "auth/[mount]/mfa_config". + The LDAP authentication provider allows users to authenticate using LDAP or + Active Directory. - Example: vault auth -method=ldap username=john + If MFA is enabled, a "method" and/or "passcode" may be required depending on + the MFA provider. To check which MFA is in use, run: - ` + $ vault read auth//mfa_config + + Authenticate as "sally": + + $ vault auth -method=ldap username=sally + Password (will be hidden): + + Authenticate as "bob": + + $ vault auth -method=ldap username=bob password=password + +Configuration: + + method= + MFA method. + + passcode= + MFA OTP/passcode. + + password= + LDAP password to use for authentication. If not provided, the CLI will + prompt for this on stdin. + + username= + LDAP username to use for authentication. + +` return strings.TrimSpace(help) } From 1c6b463267f1d3fcdd8feb1cac3065de6d9ee460 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 18:50:21 -0400 Subject: [PATCH 017/281] Update help output for okta auth --- builtin/credential/okta/cli.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/builtin/credential/okta/cli.go b/builtin/credential/okta/cli.go index f5f850209e..94f41d2f0f 100644 --- a/builtin/credential/okta/cli.go +++ b/builtin/credential/okta/cli.go @@ -53,13 +53,27 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro // Help method for okta cli func (h *CLIHandler) Help() string { help := ` -The Okta credential provider allows you to authenticate with Okta. -To use it, first configure it through the "config" endpoint, and then -login by specifying username and password. If password is not provided -on the command line, it will be read from stdin. +Usage: vault auth -method=okta [CONFIG K=V...] - Example: vault auth -method=okta username=john + The OKTA authentication provider allows users to authenticate using OKTA. + Authenticate as "sally": + + $ vault auth -method=okta username=sally + Password (will be hidden): + + Authenticate as "bob": + + $ vault auth -method=okta username=bob password=password + +Configuration: + + password= + OKTA password to use for authentication. If not provided, the CLI will + prompt for this on stdin. + + username= + OKTA username to use for authentication. ` return strings.TrimSpace(help) From 62f8416de3bad13a149a228b6c40d4a8f067e3b3 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 18:50:35 -0400 Subject: [PATCH 018/281] Update help output for userpass auth --- builtin/credential/userpass/cli.go | 43 ++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/builtin/credential/userpass/cli.go b/builtin/credential/userpass/cli.go index 4433c0e706..f4b79d1496 100644 --- a/builtin/credential/userpass/cli.go +++ b/builtin/credential/userpass/cli.go @@ -66,20 +66,41 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -The "userpass"/"radius" credential provider allows you to authenticate with -a username and password. To use it, specify the "username" and "password" -parameters. If password is not provided on the command line, it will be -read from stdin. +Usage: vault auth -method=userpass [CONFIG K=V...] -If multi-factor authentication (MFA) is enabled, a "method" and/or "passcode" -may be provided depending on the MFA backend enabled. To check -which MFA backend is in use, read "auth/[mount]/mfa_config". + The userpass authentication provider allows users to authenticate using + Vault's internal user database. - Example: vault auth -method=userpass \ - username= \ - password= + If MFA is enabled, a "method" and/or "passcode" may be required depending on + the MFA provider. To check which MFA is in use, run: - ` + $ vault read auth//mfa_config + + Authenticate as "sally": + + $ vault auth -method=userpass username=sally + Password (will be hidden): + + Authenticate as "bob": + + $ vault auth -method=userpass username=bob password=password + +Configuration: + + method= + MFA method. + + passcode= + MFA OTP/passcode. + + password= + Password to use for authentication. If not provided, the CLI will + prompt for this on stdin. + + username= + Username to use for authentication. + +` return strings.TrimSpace(help) } From 737d86a7cba5a56a1c5b8f856d523013fc91890f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 18:50:42 -0400 Subject: [PATCH 019/281] Add built-in credential provider for tokens This was previously part of the very long command/auth.go file, where it mimmicked the same API as other handlers. By making it a builtin credential, we can remove a lot of conditional logic for token-based authentication. --- builtin/credential/token/cli.go | 81 +++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 builtin/credential/token/cli.go diff --git a/builtin/credential/token/cli.go b/builtin/credential/token/cli.go new file mode 100644 index 0000000000..b8f7525b53 --- /dev/null +++ b/builtin/credential/token/cli.go @@ -0,0 +1,81 @@ +package token + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/helper/password" +) + +type CLIHandler struct { + testStdout io.Writer // for tests +} + +func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { + token, ok := m["token"] + if !ok { + // Override the output + stdout := h.testStdout + if stdout == nil { + stdout = os.Stdout + } + + // No arguments given, read the token from user input + fmt.Fprintf(stdout, "Token (will be hidden): ") + var err error + token, err = password.Read(os.Stdin) + fmt.Fprintf(stdout, "\n") + if err != nil { + return nil, fmt.Errorf( + "Error attempting to ask for token. The raw error message\n"+ + "is shown below, but the most common reason for this error is\n"+ + "that you attempted to pipe a value into auth. If you want to\n"+ + "pipe the token, please pass '-' as the token argument.\n\n"+ + "Raw error: %s", err) + } + } + + // Remove any whitespace, etc. + token = strings.TrimSpace(token) + + if token == "" { + return nil, fmt.Errorf( + "A token must be passed to auth. Please view the help\n" + + "for more information.") + } + + return &api.Secret{ + Auth: &api.SecretAuth{ + ClientToken: token, + }, + }, nil +} + +func (h *CLIHandler) Help() string { + help := ` +Usage: vault auth TOKEN [CONFIG K=V...] + + The token authentication provider allows logging in directly with a token. + This can be a token from the "token-create" command or API. There are no + configuration options for this authentication provider. + + Authenticate using a token: + + $ vault auth 96ddf4bc-d217-f3ba-f9bd-017055595017 + + This token usually comes from a different source such as the API or via the + built-in "vault token-create" command. + +Configuration: + + token= + The token to use for authentication. This is usually provided directly + via the "vault auth" command. + +` + + return strings.TrimSpace(help) +} From 792527bb830c39791cd9b5b96ef52a0dba3b3bd1 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 2 Sep 2017 21:05:13 -0400 Subject: [PATCH 020/281] Unwrap cli.Ui to get to the underlying writer This allows us to write without a newline character, since the Ui interface doesn't expose a direct Write() method. --- command/util.go | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/command/util.go b/command/util.go index e306559cc2..7b7d8e65e8 100644 --- a/command/util.go +++ b/command/util.go @@ -2,8 +2,8 @@ package command import ( "fmt" + "io" "os" - "reflect" "time" "github.com/hashicorp/vault/api" @@ -83,19 +83,31 @@ func RawField(secret *api.Secret, field string) (string, bool) { func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { str, ok := RawField(secret, field) if !ok { - ui.Error(fmt.Sprintf("Field %s not present in secret", field)) + ui.Error(fmt.Sprintf("Field %q not present in secret", field)) return 1 } - // c.Ui.Output() prints a CR character which in this case is - // not desired. Since Vault CLI currently only uses BasicUi, - // which writes to standard output, os.Stdout is used here to - // directly print the message. If mitchellh/cli exposes method - // to print without CR, this check needs to be removed. - if reflect.TypeOf(ui).String() == "*cli.BasicUi" { - fmt.Fprintf(os.Stdout, str) - } else { - ui.Output(str) - } + // The cli.Ui prints a CR, which is not wanted since the user probably wants + // just the raw value. + w := getWriterFromUI(ui) + fmt.Fprintf(w, str) return 0 } + +// getWriterFromUI accepts a cli.Ui and returns the underlying io.Writer by +// unwrapping as many wrapped Uis as necessary. If there is an unknown UI +// type, this falls back to os.Stdout. +func getWriterFromUI(ui cli.Ui) io.Writer { + switch t := ui.(type) { + case *cli.BasicUi: + return t.Writer + case *cli.ColoredUi: + return getWriterFromUI(t.Ui) + case *cli.ConcurrentUi: + return getWriterFromUI(t.Ui) + case *cli.MockUi: + return t.OutputWriter + default: + return os.Stdout + } +} From 29702fcb18d431a4592a1e2671e4bb0c84a999fe Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sun, 3 Sep 2017 00:09:18 -0400 Subject: [PATCH 021/281] Return better errors from token failures --- builtin/credential/token/cli.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/builtin/credential/token/cli.go b/builtin/credential/token/cli.go index b8f7525b53..5af0101dd8 100644 --- a/builtin/credential/token/cli.go +++ b/builtin/credential/token/cli.go @@ -11,7 +11,9 @@ import ( ) type CLIHandler struct { - testStdout io.Writer // for tests + // for tests + testStdin io.Reader + testStdout io.Writer } func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { @@ -28,13 +30,18 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro var err error token, err = password.Read(os.Stdin) fmt.Fprintf(stdout, "\n") + if err != nil { - return nil, fmt.Errorf( - "Error attempting to ask for token. The raw error message\n"+ - "is shown below, but the most common reason for this error is\n"+ - "that you attempted to pipe a value into auth. If you want to\n"+ - "pipe the token, please pass '-' as the token argument.\n\n"+ - "Raw error: %s", err) + if err == password.ErrInterrupted { + return nil, fmt.Errorf("user interrupted") + } + + return nil, fmt.Errorf("An error occurred attempting to "+ + "ask for a token. The raw error message is shown below, but usually "+ + "this is because you attempted to pipe a value into the command or "+ + "you are executing outside of a terminal (tty). If you want to pipe "+ + "the value, pass \"-\" as the argument to read from stdin. The raw "+ + "error was: %s", err) } } @@ -43,8 +50,8 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro if token == "" { return nil, fmt.Errorf( - "A token must be passed to auth. Please view the help\n" + - "for more information.") + "A token must be passed to auth. Please view the help for more " + + "information.") } return &api.Secret{ From 47a633b83eafe2768bfa6b60c7e6d4e87dec4931 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:50:13 -0400 Subject: [PATCH 022/281] Drop cli and meta packages This centralizes all command-related things in the command package --- cli/main.go | 53 -------- cli/help.go => command/main.go | 82 ++++++++---- main.go | 4 +- meta/meta.go | 233 --------------------------------- meta/meta_test.go | 41 ------ 5 files changed, 61 insertions(+), 352 deletions(-) delete mode 100644 cli/main.go rename cli/help.go => command/main.go (50%) delete mode 100644 meta/meta.go delete mode 100644 meta/meta_test.go diff --git a/cli/main.go b/cli/main.go deleted file mode 100644 index 000e1e9a4e..0000000000 --- a/cli/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package cli - -import ( - "fmt" - "os" - - "github.com/mitchellh/cli" -) - -func Run(args []string) int { - return RunCustom(args, Commands(nil)) -} - -func RunCustom(args []string, commands map[string]cli.CommandFactory) int { - // Get the command line args. We shortcut "--version" and "-v" to - // just show the version. - for _, arg := range args { - if arg == "-v" || arg == "-version" || arg == "--version" { - newArgs := make([]string, len(args)+1) - newArgs[0] = "version" - copy(newArgs[1:], args) - args = newArgs - break - } - } - - // Build the commands to include in the help now. This is pretty... - // tedious, but we don't have a better way at the moment. - commandsInclude := make([]string, 0, len(commands)) - for k, _ := range commands { - switch k { - case "token-disk": - default: - commandsInclude = append(commandsInclude, k) - } - } - - cli := &cli.CLI{ - Args: args, - Commands: commands, - Name: "vault", - Autocomplete: true, - HelpFunc: cli.FilteredHelpFunc(commandsInclude, HelpFunc), - } - - exitCode, err := cli.Run() - if err != nil { - fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error()) - return 1 - } - - return exitCode -} diff --git a/cli/help.go b/command/main.go similarity index 50% rename from cli/help.go rename to command/main.go index bd66e335a3..731deb2273 100644 --- a/cli/help.go +++ b/command/main.go @@ -1,51 +1,87 @@ -package cli +package command import ( "bytes" "fmt" + "os" "sort" "strings" "github.com/mitchellh/cli" ) -// HelpFunc is a cli.HelpFunc that can is used to output the help for Vault. -func HelpFunc(commands map[string]cli.CommandFactory) string { +func Run(args []string) int { + // Handle -v shorthand + for _, arg := range args { + if arg == "--" { + break + } + + if arg == "-v" || arg == "-version" || arg == "--version" { + args = []string{"version"} + break + } + } + + cli := &cli.CLI{ + Name: "vault", + Args: args, + Commands: Commands, + HelpFunc: helpFunc, + + Autocomplete: true, + AutocompleteNoDefaultFlags: true, + } + + exitCode, err := cli.Run() + if err != nil { + fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error()) + return 1 + } + + return exitCode +} + +// helpFunc is a cli.HelpFunc that can is used to output the help for Vault. +func helpFunc(commands map[string]cli.CommandFactory) string { commonNames := map[string]struct{}{ - "delete": struct{}{}, - "path-help": struct{}{}, - "read": struct{}{}, - "renew": struct{}{}, - "revoke": struct{}{}, - "write": struct{}{}, - "server": struct{}{}, - "status": struct{}{}, - "unwrap": struct{}{}, + "delete": struct{}{}, + "read": struct{}{}, + "renew": struct{}{}, + "revoke": struct{}{}, + "server": struct{}{}, + "status": struct{}{}, + "unwrap": struct{}{}, + "write": struct{}{}, } // Determine the maximum key length, and classify based on type commonCommands := make(map[string]cli.CommandFactory) otherCommands := make(map[string]cli.CommandFactory) - maxKeyLen := 0 - for key, f := range commands { - if len(key) > maxKeyLen { - maxKeyLen = len(key) - } + commonKeyLen, otherKeyLen := 0, 0 + for key, f := range commands { if _, ok := commonNames[key]; ok { + if len(key) > commonKeyLen { + commonKeyLen = len(key) + } commonCommands[key] = f } else { + if len(key) > otherKeyLen { + otherKeyLen = len(key) + } otherCommands[key] = f } } var buf bytes.Buffer - buf.WriteString("usage: vault [-version] [-help] [args]\n\n") - buf.WriteString("Common commands:\n") - buf.WriteString(listCommands(commonCommands, maxKeyLen)) - buf.WriteString("\nAll other commands:\n") - buf.WriteString(listCommands(otherCommands, maxKeyLen)) - return buf.String() + buf.WriteString("Usage: vault [args]\n\n") + buf.WriteString("Common commands:\n\n") + buf.WriteString(listCommands(commonCommands, commonKeyLen)) + buf.WriteString("\n") + buf.WriteString("Other commands:\n\n") + buf.WriteString(listCommands(otherCommands, otherKeyLen)) + return strings.TrimSpace(buf.String()) } // listCommands just lists the commands in the map with the diff --git a/main.go b/main.go index 6cd34fe36c..7e4b1c9d28 100644 --- a/main.go +++ b/main.go @@ -3,9 +3,9 @@ package main // import "github.com/hashicorp/vault" import ( "os" - "github.com/hashicorp/vault/cli" + "github.com/hashicorp/vault/command" ) func main() { - os.Exit(cli.Run(os.Args[1:])) + os.Exit(command.Run(os.Args[1:])) } diff --git a/meta/meta.go b/meta/meta.go deleted file mode 100644 index d75f1054c5..0000000000 --- a/meta/meta.go +++ /dev/null @@ -1,233 +0,0 @@ -package meta - -import ( - "bufio" - "flag" - "io" - "os" - - "github.com/hashicorp/errwrap" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/token" - "github.com/hashicorp/vault/helper/flag-slice" - "github.com/mitchellh/cli" -) - -// FlagSetFlags is an enum to define what flags are present in the -// default FlagSet returned by Meta.FlagSet. -type FlagSetFlags uint - -type TokenHelperFunc func() (token.TokenHelper, error) - -const ( - FlagSetNone FlagSetFlags = 0 - FlagSetServer FlagSetFlags = 1 << iota - FlagSetDefault = FlagSetServer -) - -var ( - additionalOptionsUsage = func() string { - return ` - -wrap-ttl="" Indicates that the response should be wrapped in a - cubbyhole token with the requested TTL. The response - can be fetched by calling the "sys/wrapping/unwrap" - endpoint, passing in the wrapping token's ID. This - is a numeric string with an optional suffix - "s", "m", or "h"; if no suffix is specified it will - be parsed as seconds. May also be specified via - VAULT_WRAP_TTL. - - -policy-override Indicates that any soft-mandatory Sentinel policies - be overridden. -` - } -) - -// Meta contains the meta-options and functionality that nearly every -// Vault command inherits. -type Meta struct { - ClientToken string - Ui cli.Ui - - // The things below can be set, but aren't common - ForceAddress string // Address to force for API clients - - // These are set by the command line flags. - flagAddress string - flagCACert string - flagCAPath string - flagClientCert string - flagClientKey string - flagWrapTTL string - flagInsecure bool - flagMFA []string - flagPolicyOverride bool - - // Queried if no token can be found - TokenHelper TokenHelperFunc -} - -func (m *Meta) DefaultWrappingLookupFunc(operation, path string) string { - if m.flagWrapTTL != "" { - return m.flagWrapTTL - } - - return api.DefaultWrappingLookupFunc(operation, path) -} - -// Client returns the API client to a Vault server given the configured -// flag settings for this command. -func (m *Meta) Client() (*api.Client, error) { - config := api.DefaultConfig() - - err := config.ReadEnvironment() - if err != nil { - return nil, errwrap.Wrapf("error reading environment: {{err}}", err) - } - - if m.flagAddress != "" { - config.Address = m.flagAddress - } - if m.ForceAddress != "" { - config.Address = m.ForceAddress - } - // If we need custom TLS configuration, then set it - if m.flagCACert != "" || m.flagCAPath != "" || m.flagClientCert != "" || m.flagClientKey != "" || m.flagInsecure { - t := &api.TLSConfig{ - CACert: m.flagCACert, - CAPath: m.flagCAPath, - ClientCert: m.flagClientCert, - ClientKey: m.flagClientKey, - TLSServerName: "", - Insecure: m.flagInsecure, - } - config.ConfigureTLS(t) - } - - // Build the client - client, err := api.NewClient(config) - if err != nil { - return nil, err - } - - client.SetWrappingLookupFunc(m.DefaultWrappingLookupFunc) - - var mfaCreds []string - - // Extract the MFA credentials from environment variable first - if os.Getenv(api.EnvVaultMFA) != "" { - mfaCreds = []string{os.Getenv(api.EnvVaultMFA)} - } - - // If CLI MFA flags were supplied, prefer that over environment variable - if len(m.flagMFA) != 0 { - mfaCreds = m.flagMFA - } - - client.SetMFACreds(mfaCreds) - - client.SetPolicyOverride(m.flagPolicyOverride) - - // If we have a token directly, then set that - token := m.ClientToken - - // Try to set the token to what is already stored - if token == "" { - token = client.Token() - } - - // If we don't have a token, check the token helper - if token == "" { - if m.TokenHelper != nil { - // If we have a token, then set that - tokenHelper, err := m.TokenHelper() - if err != nil { - return nil, err - } - token, err = tokenHelper.Get() - if err != nil { - return nil, err - } - } - } - - // Set the token - if token != "" { - client.SetToken(token) - } - - return client, nil -} - -// FlagSet returns a FlagSet with the common flags that every -// command implements. The exact behavior of FlagSet can be configured -// using the flags as the second parameter, for example to disable -// server settings on the commands that don't talk to a server. -func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet { - f := flag.NewFlagSet(n, flag.ContinueOnError) - - // FlagSetServer tells us to enable the settings for selecting - // the server information. - if fs&FlagSetServer != 0 { - f.StringVar(&m.flagAddress, "address", "", "") - f.StringVar(&m.flagCACert, "ca-cert", "", "") - f.StringVar(&m.flagCAPath, "ca-path", "", "") - f.StringVar(&m.flagClientCert, "client-cert", "", "") - f.StringVar(&m.flagClientKey, "client-key", "", "") - f.StringVar(&m.flagWrapTTL, "wrap-ttl", "", "") - f.BoolVar(&m.flagInsecure, "insecure", false, "") - f.BoolVar(&m.flagInsecure, "tls-skip-verify", false, "") - f.BoolVar(&m.flagPolicyOverride, "policy-override", false, "") - f.Var((*sliceflag.StringFlag)(&m.flagMFA), "mfa", "") - } - - // Create an io.Writer that writes to our Ui properly for errors. - // This is kind of a hack, but it does the job. Basically: create - // a pipe, use a scanner to break it into lines, and output each line - // to the UI. Do this forever. - errR, errW := io.Pipe() - errScanner := bufio.NewScanner(errR) - go func() { - for errScanner.Scan() { - m.Ui.Error(errScanner.Text()) - } - }() - f.SetOutput(errW) - - return f -} - -// GeneralOptionsUsage returns the usage documentation for commonly -// available options -func GeneralOptionsUsage() string { - general := ` - -address=addr The address of the Vault server. - Overrides the VAULT_ADDR environment variable if set. - - -ca-cert=path Path to a PEM encoded CA cert file to use to - verify the Vault server SSL certificate. - Overrides the VAULT_CACERT environment variable if set. - - -ca-path=path Path to a directory of PEM encoded CA cert files - to verify the Vault server SSL certificate. If both - -ca-cert and -ca-path are specified, -ca-cert is used. - Overrides the VAULT_CAPATH environment variable if set. - - -client-cert=path Path to a PEM encoded client certificate for TLS - authentication to the Vault server. Must also specify - -client-key. Overrides the VAULT_CLIENT_CERT - environment variable if set. - - -client-key=path Path to an unencrypted PEM encoded private key - matching the client certificate from -client-cert. - Overrides the VAULT_CLIENT_KEY environment variable - if set. - - -tls-skip-verify Do not verify TLS certificate. This is highly - not recommended. Verification will also be skipped - if VAULT_SKIP_VERIFY is set. -` - - general += additionalOptionsUsage() - return general -} diff --git a/meta/meta_test.go b/meta/meta_test.go deleted file mode 100644 index 99a294d249..0000000000 --- a/meta/meta_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package meta - -import ( - "flag" - "reflect" - "sort" - "testing" -) - -func TestFlagSet(t *testing.T) { - cases := []struct { - Flags FlagSetFlags - Expected []string - }{ - { - FlagSetNone, - []string{}, - }, - { - FlagSetServer, - []string{"address", "ca-cert", "ca-path", "client-cert", "client-key", "insecure", "mfa", "policy-override", "tls-skip-verify", "wrap-ttl"}, - }, - } - - for i, tc := range cases { - var m Meta - fs := m.FlagSet("foo", tc.Flags) - - actual := make([]string, 0, 0) - fs.VisitAll(func(f *flag.Flag) { - actual = append(actual, f.Name) - }) - sort.Strings(actual) - sort.Strings(tc.Expected) - - if !reflect.DeepEqual(actual, tc.Expected) { - t.Fatalf("%d: flags: %#v\n\nExpected: %#v\nGot: %#v", - i, tc.Flags, tc.Expected, actual) - } - } -} From 1552436a128c501d4af887c7df537bf95661d77a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:50:45 -0400 Subject: [PATCH 023/281] Add interface assertions for token helpers This will ensure they meet the right API --- command/token/helper_external.go | 2 ++ command/token/helper_internal.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/command/token/helper_external.go b/command/token/helper_external.go index 40de9bfde1..4483074af7 100644 --- a/command/token/helper_external.go +++ b/command/token/helper_external.go @@ -36,6 +36,8 @@ func ExternalTokenHelperPath(path string) (string, error) { return path, nil } +var _ TokenHelper = (*ExternalTokenHelper)(nil) + // ExternalTokenHelper is the struct that has all the logic for storing and retrieving // tokens from the token helper. The API for the helpers is simple: the // BinaryPath is executed within a shell with environment Env. The last argument diff --git a/command/token/helper_internal.go b/command/token/helper_internal.go index 89793cb63d..58dceaebcc 100644 --- a/command/token/helper_internal.go +++ b/command/token/helper_internal.go @@ -10,6 +10,8 @@ import ( "github.com/mitchellh/go-homedir" ) +var _ TokenHelper = (*InternalTokenHelper)(nil) + // InternalTokenHelper fulfills the TokenHelper interface when no external // token-helper is configured, and avoids shelling out type InternalTokenHelper struct { From 4d9a42aa20c2fa91e38bd09ecd53e14b99530d93 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:50:59 -0400 Subject: [PATCH 024/281] Add an in-mem token helper for testing --- command/token/helper_testing.go | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 command/token/helper_testing.go diff --git a/command/token/helper_testing.go b/command/token/helper_testing.go new file mode 100644 index 0000000000..93465931b7 --- /dev/null +++ b/command/token/helper_testing.go @@ -0,0 +1,42 @@ +package token + +import ( + "sync" +) + +var _ TokenHelper = (*TestingTokenHelper)(nil) + +// TestingTokenHelper implements token.TokenHelper which runs entirely +// in-memory. This should not be used outside of testing. +type TestingTokenHelper struct { + lock sync.RWMutex + token string +} + +func NewTestingTokenHelper() *TestingTokenHelper { + return &TestingTokenHelper{} +} + +func (t *TestingTokenHelper) Erase() error { + t.lock.Lock() + defer t.lock.Unlock() + t.token = "" + return nil +} + +func (t *TestingTokenHelper) Get() (string, error) { + t.lock.RLock() + defer t.lock.RUnlock() + return t.token, nil +} + +func (t *TestingTokenHelper) Path() string { + return "" +} + +func (t *TestingTokenHelper) Store(token string) error { + t.lock.Lock() + defer t.lock.Unlock() + t.token = token + return nil +} From aea62c18c0b62a1f239379ffc7b779a569beb6fe Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:51:29 -0400 Subject: [PATCH 025/281] Unset VAULT_DEV_* in tests Vault picks these values and then tests fail, and it's sad --- Makefile | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5701bd4e58..b06b592c0c 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,12 @@ dev-dynamic: prep # test runs the unit tests and vets the code test: prep - CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -timeout=20m -parallel=4 + @CGO_ENABLED=0 \ + VAULT_ADDR= \ + VAULT_TOKEN= \ + VAULT_DEV_ROOT_TOKEN_ID= \ + VAULT_ACC= \ + go test -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -timeout=20m -parallel=20 testcompile: prep @for pkg in $(TEST) ; do \ @@ -48,7 +53,12 @@ testacc: prep # testrace runs the race checker testrace: prep - CGO_ENABLED=1 VAULT_TOKEN= VAULT_ACC= go test -tags='$(BUILD_TAGS)' -race $(TEST) $(TESTARGS) -timeout=45m -parallel=4 + @CGO_ENABLED=1 \ + VAULT_ADDR= \ + VAULT_TOKEN= \ + VAULT_DEV_ROOT_TOKEN_ID= \ + VAULT_ACC= \ + go test -tags='$(BUILD_TAGS)' -race $(TEST) $(TESTARGS) -timeout=45m -parallel=20 cover: ./scripts/coverage.sh --html From 4b7a1967f5ff9c7aa5aaa80434f7c062ee243318 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:51:48 -0400 Subject: [PATCH 026/281] Change http testing to tb interface --- http/testing.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/http/testing.go b/http/testing.go index 543b3e6670..f47037769f 100644 --- a/http/testing.go +++ b/http/testing.go @@ -11,12 +11,12 @@ import ( "github.com/hashicorp/vault/vault" ) -func TestListener(t *testing.T) (net.Listener, string) { +func TestListener(tb testing.TB) (net.Listener, string) { fail := func(format string, args ...interface{}) { panic(fmt.Sprintf(format, args...)) } - if t != nil { - fail = t.Fatalf + if tb != nil { + fail = tb.Fatalf } ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -27,7 +27,7 @@ func TestListener(t *testing.T) (net.Listener, string) { return ln, addr } -func TestServerWithListener(t *testing.T, ln net.Listener, addr string, core *vault.Core) { +func TestServerWithListener(tb testing.TB, ln net.Listener, addr string, core *vault.Core) { // Create a muxer to handle our requests so that we can authenticate // for tests. mux := http.NewServeMux() @@ -39,20 +39,20 @@ func TestServerWithListener(t *testing.T, ln net.Listener, addr string, core *va Handler: mux, } if err := http2.ConfigureServer(server, nil); err != nil { - t.Fatal(err) + tb.Fatal(err) } go server.Serve(ln) } -func TestServer(t *testing.T, core *vault.Core) (net.Listener, string) { - ln, addr := TestListener(t) - TestServerWithListener(t, ln, addr, core) +func TestServer(tb testing.TB, core *vault.Core) (net.Listener, string) { + ln, addr := TestListener(tb) + TestServerWithListener(tb, ln, addr, core) return ln, addr } -func TestServerAuth(t *testing.T, addr string, token string) { +func TestServerAuth(tb testing.TB, addr string, token string) { if _, err := http.Get(addr + "/_test/auth?token=" + token); err != nil { - t.Fatalf("error authenticating: %s", err) + tb.Fatalf("error authenticating: %s", err) } } From 4d22edeb961717fb7cbbf7724c3c2df4df63d1a0 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:52:05 -0400 Subject: [PATCH 027/281] Expand root generation guide with a PGP example --- website/source/guides/generate-root.html.md | 138 ++++++++++++++++++-- 1 file changed, 124 insertions(+), 14 deletions(-) diff --git a/website/source/guides/generate-root.html.md b/website/source/guides/generate-root.html.md index 311524d447..574dd24d57 100644 --- a/website/source/guides/generate-root.html.md +++ b/website/source/guides/generate-root.html.md @@ -14,35 +14,145 @@ Vault's `generate-root` command only when absolutely necessary. This guide demonstrates regenerating a root token. 1. Unseal the vault using the existing quorum of unseal keys. You do not need to - be authenticated. + be authenticated to generate a new root token, but the Vault must be unsealed + and a quorum of unseal keys must be available. ```shell $ vault unseal # ... ``` -2. Generate a one-time password: +### Using OTP - ```shell - $ vault generate-root -genotp +In this method, an OTP is XORed with the generated token on final output. + +1. Generate a one-time password (OTP) to use for XORing the resulting token: + + ```text + $ vault generate-root -generate-otp + mOXx7iVimjE6LXQ2Zna6NA== ``` -3. Get the encoded root token: + Save this OTP because you will need it to get the decoded final root token. - ```shell - $ vault generate-root -otp="" +1. Initialize a root token generation, providing the OTP code from the step + above: + + ```text + $ vault generate-root -init -otp=mOXx7iVimjE6LXQ2Zna6NA== + Nonce f67f4da3-4ae4-68fb-4716-91da6b609c3e + Started true + Progress 0/5 + Complete false ``` - This will require a quorum of unseal keys. This will then output an encoded - root token. + The nonce value should be distributed to all unseal key holders. -4. Decode the encoded root token: +1. Each unseal key holder providers their unseal key: - ```shell - $ vault generate-root -otp="" -decode="" + ```text + $ vault generate-root + Root generation operation nonce: f67f4da3-4ae4-68fb-4716-91da6b609c3e + Unseal Key (will be hidden): ... ``` -Please see `vault generate-root -help` for information on the alternate -technique using a PGP key. + If there is a tty, Vault will prompt for the key and automatically + complete the nonce value. If there is no tty, or if the value is piped + from stdin, the user must specify the nonce value from the `-init` + operation. + + ```text + $ echo $UNSEAL_KEY | vault generate-root -nonce=f67f4da3... - + ``` + +1. When the quorum of unseal keys are supplied, the final user will also get + the encoded root token. + + ```text + $ vault generate-root + Root generation operation nonce: f67f4da3-4ae4-68fb-4716-91da6b609c3e + Unseal Key (will be hidden): + + Nonce f67f4da3-4ae4-68fb-4716-91da6b609c3e + Started true + Progress 5/5 + Complete true + Root Token IxJpyqxn3YafOGhqhvP6cQ== + ``` + +1. Decode the encoded token using the OTP: + + ```text + $ vault generate-root \ + -decode=IxJpyqxn3YafOGhqhvP6cQ== \ + -otp=mOXx7iVimjE6LXQ2Zna6NA== + + 24bde68f-3df3-e137-cf4d-014fe9ebc43f + ``` + +### Using PGP + +1. Initialize a root token generation, providing the path to a GPG public key + or keybase username of a user to encrypted the resulting token. + + ```text + $ vault generate-root -init -pgp-key=keybase:sethvargo + Nonce e24dec5e-f1ea-2dfe-ecce-604022006976 + Started true + Progress 0/5 + Complete false + PGP Fingerprint e2f8e2974623ba2a0e933a59c921994f9c27e0ff + ``` + + The nonce value should be distributed to all unseal key holders. + +1. Each unseal key holder providers their unseal key: + + ```text + $ vault generate-root + Root generation operation nonce: e24dec5e-f1ea-2dfe-ecce-604022006976 + Unseal Key (will be hidden): ... + ``` + + If there is a tty, Vault will prompt for the key and automatically + complete the nonce value. If there is no tty, or if the value is piped + from stdin, the user must specify the nonce value from the `-init` + operation. + + ```text + $ echo $UNSEAL_KEY | vault generate-root -nonce=f67f4da3... - + ``` + +1. When the quorum of unseal keys are supplied, the final user will also get + the encoded root token. + + ```text + $ vault generate-root + Root generation operation nonce: e24dec5e-f1ea-2dfe-ecce-604022006976 + Unseal Key (will be hidden): + + Nonce e24dec5e-f1ea-2dfe-ecce-604022006976 + Started true + Progress 1/1 + Complete true + PGP Fingerprint e2f8e2974623ba2a0e933a59c921994f9c27e0ff + Root Token wcFMA0RVkFtoqzRlARAAI3Ux8kdSpfgXdF9mg... + ``` + +1. Decrypt the encrypted token using associated private key: + + ```text + $ echo "wcFMA0RVkFtoqzRlARAAI3Ux8kdSpfgXdF9mg..." | base64 --decode | gpg --decrypt + + d0f71e9b-ebff-6d8a-50ae-b8859f2e5671 + ``` + + or via keybase: + + ```text + $ echo "wcFMA0RVkFtoqzRlARAAI3Ux8kdSpfgXdF9mg..." | base64 --decode | keybase pgp decrypt + + d0f71e9b-ebff-6d8a-50ae-b8859f2e5671 + ``` [root-tokens]: /docs/concepts/tokens.html#root-tokens From 9b18a8ab2095d165f2da50b553101e22b4903f79 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:52:14 -0400 Subject: [PATCH 028/281] Document mount types/values --- website/source/api/system/mounts.html.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/website/source/api/system/mounts.html.md b/website/source/api/system/mounts.html.md index 46e1b22c33..e1566c1b54 100644 --- a/website/source/api/system/mounts.html.md +++ b/website/source/api/system/mounts.html.md @@ -76,10 +76,16 @@ This endpoint mounts a new secret backend at the given path. - `config` `(map: nil)` – Specifies configuration options for this mount. This is an object with four possible values: - - `default_lease_ttl` - - `max_lease_ttl` - - `force_no_cache` - - `plugin_name` + - `default_lease_ttl` `(string: "")` - the default lease duration, specified + as a go string duration like "5s" or "30m". + + - `max_lease_ttl` `(string: "")` - the maximum lease duration, specified as + a go string duration like "5s" or "30m". + + - `force_no_cache` `(bool: false)` - disable caching. + + - `plugin_name` `(string: "")` - the name of the plugin in the plugin + catalog to use. These control the default and maximum lease time-to-live, force disabling backend caching, and option plugin name for plugin backends @@ -91,7 +97,7 @@ This endpoint mounts a new secret backend at the given path. use based from the name in the plugin catalog. Applies only to plugin backends. -Additionally, the following options are allowed in Vault open-source, but +Additionally, the following options are allowed in Vault open-source, but relevant functionality is only supported in Vault Enterprise: - `local` `(bool: false)` – Specifies if the secret backend is a local mount From fceddbe7248f5b67c1f7e13a699675c5f57c0919 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:53:13 -0400 Subject: [PATCH 029/281] Allow hiding flags --- command/base.go | 82 +++--- command/base_flags.go | 589 +++++++++++++++++++++++++++++------------- 2 files changed, 444 insertions(+), 227 deletions(-) diff --git a/command/base.go b/command/base.go index 7c4c2e92d8..e3eeb1f177 100644 --- a/command/base.go +++ b/command/base.go @@ -268,29 +268,6 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { return c.flags } -// printFlagTitle prints a consistently-formatted title to the given writer. -func printFlagTitle(w io.Writer, s string) { - fmt.Fprintf(w, "%s\n\n", s) -} - -// printFlagDetail prints a single flag to the given writer. -func printFlagDetail(w io.Writer, f *flag.Flag) { - example := "" - if t, ok := f.Value.(FlagExample); ok { - example = t.Example() - } - - if example != "" { - fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example) - } else { - fmt.Fprintf(w, " -%s\n", f.Name) - } - - usage := reRemoveWhitespace.ReplaceAllString(f.Usage, " ") - indented := wrapAtLengthWithPadding(usage, 6) - fmt.Fprintf(w, "%s\n\n", indented) -} - // wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking // into account any provided left padding. func wrapAtLengthWithPadding(s string, pad int) string { @@ -334,17 +311,13 @@ func NewFlagSets(ui cli.Ui) *FlagSets { // NewFlagSet creates a new flag set from the given flag sets. func (f *FlagSets) NewFlagSet(name string) *FlagSet { flagSet := NewFlagSet(name) - f.AddFlagSet(flagSet) + flagSet.mainSet = f.mainSet + flagSet.completions = f.completions + f.flagSets = append(f.flagSets, flagSet) return flagSet } -// AddFlagSet adds a new flag set to this flag set. -func (f *FlagSets) AddFlagSet(set *FlagSet) { - set.mainSet = f.mainSet - set.completions = f.completions - f.flagSets = append(f.flagSets, set) -} - +// Completions returns the completions for this flag set. func (f *FlagSets) Completions() complete.Flags { return f.completions } @@ -359,21 +332,6 @@ func (f *FlagSets) Args() []string { return f.mainSet.Args() } -// HideFlag excludes the flag from the list of flags to print in help. This is -// useful when you want to include a flag in parsing for deprecations/bc, but -// you don't want to include it in help output. -func (f *FlagSets) HideFlag(n string) { - if _, ok := f.hiddens[n]; !ok { - f.hiddens[n] = struct{}{} - } -} - -// HiddenFlag returns true if the flag with the given name is hidden. -func (f *FlagSets) HiddenFlag(n string) bool { - _, ok := f.hiddens[n] - return ok -} - // Help builds custom help for this command, grouping by flag set. func (fs *FlagSets) Help() string { var out bytes.Buffer @@ -382,7 +340,7 @@ func (fs *FlagSets) Help() string { printFlagTitle(&out, set.name+":") set.VisitAll(func(f *flag.Flag) { // Skip any hidden flags - if fs.HiddenFlag(f.Name) { + if v, ok := f.Value.(FlagVisibility); ok && v.Hidden() { return } printFlagDetail(&out, f) @@ -420,3 +378,33 @@ func (f *FlagSet) Visit(fn func(*flag.Flag)) { func (f *FlagSet) VisitAll(fn func(*flag.Flag)) { f.flagSet.VisitAll(fn) } + +// printFlagTitle prints a consistently-formatted title to the given writer. +func printFlagTitle(w io.Writer, s string) { + fmt.Fprintf(w, "%s\n\n", s) +} + +// printFlagDetail prints a single flag to the given writer. +func printFlagDetail(w io.Writer, f *flag.Flag) { + // Check if the flag is hidden - do not print any flag detail or help output + // if it is hidden. + if h, ok := f.Value.(FlagVisibility); ok && h.Hidden() { + return + } + + // Check for a detailed example + example := "" + if t, ok := f.Value.(FlagExample); ok { + example = t.Example() + } + + if example != "" { + fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example) + } else { + fmt.Fprintf(w, " -%s\n", f.Name) + } + + usage := reRemoveWhitespace.ReplaceAllString(f.Usage, " ") + indented := wrapAtLengthWithPadding(usage, 6) + fmt.Fprintf(w, "%s\n\n", indented) +} diff --git a/command/base_flags.go b/command/base_flags.go index 796709727b..339247cff7 100644 --- a/command/base_flags.go +++ b/command/base_flags.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "os" + "sort" "strconv" "strings" "time" @@ -16,11 +17,20 @@ type FlagExample interface { Example() string } +// FlagVisibility is an interface which declares whether a flag should be +// hidden from help and completions. This is usually used for deprecations +// on "internal-only" flags. +type FlagVisibility interface { + Hidden() bool +} + +// -- BoolVar and boolValue type BoolVar struct { Name string Aliases []string Usage string Default bool + Hidden bool EnvVar string Target *bool Completion complete.Predictor @@ -40,16 +50,48 @@ func (f *FlagSet) BoolVar(i *BoolVar) { Usage: i.Usage, Default: strconv.FormatBool(i.Default), EnvVar: i.EnvVar, - Value: newBoolValue(def, i.Target), + Value: newBoolValue(def, i.Target, i.Hidden), Completion: i.Completion, }) } +type boolValue struct { + hidden bool + target *bool +} + +func newBoolValue(def bool, target *bool, hidden bool) *boolValue { + *target = def + + return &boolValue{ + hidden: hidden, + target: target, + } +} + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + + *b.target = v + return nil +} + +func (b *boolValue) Get() interface{} { return *b.target } +func (b *boolValue) String() string { return strconv.FormatBool(*b.target) } +func (b *boolValue) Example() string { return "" } +func (b *boolValue) Hidden() bool { return b.hidden } +func (b *boolValue) IsBoolFlag() bool { return true } + +// -- IntVar and intValue type IntVar struct { Name string Aliases []string Usage string Default int + Hidden bool EnvVar string Target *int Completion complete.Predictor @@ -74,16 +116,46 @@ func (f *FlagSet) IntVar(i *IntVar) { Usage: i.Usage, Default: def, EnvVar: i.EnvVar, - Value: newIntValue(initial, i.Target), + Value: newIntValue(initial, i.Target, i.Hidden), Completion: i.Completion, }) } +type intValue struct { + hidden bool + target *int +} + +func newIntValue(def int, target *int, hidden bool) *intValue { + *target = def + return &intValue{ + hidden: hidden, + target: target, + } +} + +func (i *intValue) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + if err != nil { + return err + } + + *i.target = int(v) + return nil +} + +func (i *intValue) Get() interface{} { return int(*i.target) } +func (i *intValue) String() string { return strconv.Itoa(int(*i.target)) } +func (i *intValue) Example() string { return "int" } +func (i *intValue) Hidden() bool { return i.hidden } + +// -- Int64Var and int64Value type Int64Var struct { Name string Aliases []string Usage string Default int64 + Hidden bool EnvVar string Target *int64 Completion complete.Predictor @@ -108,16 +180,46 @@ func (f *FlagSet) Int64Var(i *Int64Var) { Usage: i.Usage, Default: def, EnvVar: i.EnvVar, - Value: newInt64Value(initial, i.Target), + Value: newInt64Value(initial, i.Target, i.Hidden), Completion: i.Completion, }) } +type int64Value struct { + hidden bool + target *int64 +} + +func newInt64Value(def int64, target *int64, hidden bool) *int64Value { + *target = def + return &int64Value{ + hidden: hidden, + target: target, + } +} + +func (i *int64Value) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + if err != nil { + return err + } + + *i.target = v + return nil +} + +func (i *int64Value) Get() interface{} { return int64(*i.target) } +func (i *int64Value) String() string { return strconv.FormatInt(int64(*i.target), 10) } +func (i *int64Value) Example() string { return "int" } +func (i *int64Value) Hidden() bool { return i.hidden } + +// -- UintVar && uintValue type UintVar struct { Name string Aliases []string Usage string Default uint + Hidden bool EnvVar string Target *uint Completion complete.Predictor @@ -142,16 +244,46 @@ func (f *FlagSet) UintVar(i *UintVar) { Usage: i.Usage, Default: def, EnvVar: i.EnvVar, - Value: newUintValue(initial, i.Target), + Value: newUintValue(initial, i.Target, i.Hidden), Completion: i.Completion, }) } +type uintValue struct { + hidden bool + target *uint +} + +func newUintValue(def uint, target *uint, hidden bool) *uintValue { + *target = def + return &uintValue{ + hidden: hidden, + target: target, + } +} + +func (i *uintValue) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + if err != nil { + return err + } + + *i.target = uint(v) + return nil +} + +func (i *uintValue) Get() interface{} { return uint(*i.target) } +func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i.target), 10) } +func (i *uintValue) Example() string { return "uint" } +func (i *uintValue) Hidden() bool { return i.hidden } + +// -- Uint64Var and uint64Value type Uint64Var struct { Name string Aliases []string Usage string Default uint64 + Hidden bool EnvVar string Target *uint64 Completion complete.Predictor @@ -176,16 +308,46 @@ func (f *FlagSet) Uint64Var(i *Uint64Var) { Usage: i.Usage, Default: def, EnvVar: i.EnvVar, - Value: newUint64Value(initial, i.Target), + Value: newUint64Value(initial, i.Target, i.Hidden), Completion: i.Completion, }) } +type uint64Value struct { + hidden bool + target *uint64 +} + +func newUint64Value(def uint64, target *uint64, hidden bool) *uint64Value { + *target = def + return &uint64Value{ + hidden: hidden, + target: target, + } +} + +func (i *uint64Value) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + if err != nil { + return err + } + + *i.target = v + return nil +} + +func (i *uint64Value) Get() interface{} { return uint64(*i.target) } +func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i.target), 10) } +func (i *uint64Value) Example() string { return "uint" } +func (i *uint64Value) Hidden() bool { return i.hidden } + +// -- StringVar and stringValue type StringVar struct { Name string Aliases []string Usage string Default string + Hidden bool EnvVar string Target *string Completion complete.Predictor @@ -208,16 +370,41 @@ func (f *FlagSet) StringVar(i *StringVar) { Usage: i.Usage, Default: def, EnvVar: i.EnvVar, - Value: newStringValue(initial, i.Target), + Value: newStringValue(initial, i.Target, i.Hidden), Completion: i.Completion, }) } +type stringValue struct { + hidden bool + target *string +} + +func newStringValue(def string, target *string, hidden bool) *stringValue { + *target = def + return &stringValue{ + hidden: hidden, + target: target, + } +} + +func (s *stringValue) Set(val string) error { + *s.target = val + return nil +} + +func (s *stringValue) Get() interface{} { return *s.target } +func (s *stringValue) String() string { return *s.target } +func (s *stringValue) Example() string { return "string" } +func (s *stringValue) Hidden() bool { return s.hidden } + +// -- Float64Var and float64Value type Float64Var struct { Name string Aliases []string Usage string Default float64 + Hidden bool EnvVar string Target *float64 Completion complete.Predictor @@ -242,16 +429,46 @@ func (f *FlagSet) Float64Var(i *Float64Var) { Usage: i.Usage, Default: def, EnvVar: i.EnvVar, - Value: newFloat64Value(initial, i.Target), + Value: newFloat64Value(initial, i.Target, i.Hidden), Completion: i.Completion, }) } +type float64Value struct { + hidden bool + target *float64 +} + +func newFloat64Value(def float64, target *float64, hidden bool) *float64Value { + *target = def + return &float64Value{ + hidden: hidden, + target: target, + } +} + +func (f *float64Value) Set(s string) error { + v, err := strconv.ParseFloat(s, 64) + if err != nil { + return err + } + + *f.target = v + return nil +} + +func (f *float64Value) Get() interface{} { return float64(*f.target) } +func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f.target), 'g', -1, 64) } +func (f *float64Value) Example() string { return "float" } +func (f *float64Value) Hidden() bool { return f.hidden } + +// -- DurationVar and durationValue type DurationVar struct { Name string Aliases []string Usage string Default time.Duration + Hidden bool EnvVar string Target *time.Duration Completion complete.Predictor @@ -260,7 +477,7 @@ type DurationVar struct { func (f *FlagSet) DurationVar(i *DurationVar) { initial := i.Default if v := os.Getenv(i.EnvVar); v != "" { - if d, err := time.ParseDuration(v); err != nil { + if d, err := time.ParseDuration(appendDurationSuffix(v)); err != nil { initial = d } } @@ -276,11 +493,183 @@ func (f *FlagSet) DurationVar(i *DurationVar) { Usage: i.Usage, Default: def, EnvVar: i.EnvVar, - Value: newDurationValue(initial, i.Target), + Value: newDurationValue(initial, i.Target, i.Hidden), Completion: i.Completion, }) } +type durationValue struct { + hidden bool + target *time.Duration +} + +func newDurationValue(def time.Duration, target *time.Duration, hidden bool) *durationValue { + *target = def + return &durationValue{ + hidden: hidden, + target: target, + } +} + +func (d *durationValue) Set(s string) error { + v, err := time.ParseDuration(appendDurationSuffix(s)) + if err != nil { + return err + } + *d.target = v + return nil +} + +func (d *durationValue) Get() interface{} { return time.Duration(*d.target) } +func (d *durationValue) String() string { return (*d.target).String() } +func (d *durationValue) Example() string { return "duration" } +func (d *durationValue) Hidden() bool { return d.hidden } + +// appendDurationSuffix is used as a backwards-compat tool for assuming users +// meant "seconds" when they do not provide a suffixed duration value. +func appendDurationSuffix(s string) string { + if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "m") || strings.HasSuffix(s, "h") { + return s + } + return s + "s" +} + +// -- StringSliceVar and stringSliceValue +type StringSliceVar struct { + Name string + Aliases []string + Usage string + Default []string + Hidden bool + EnvVar string + Target *[]string + Completion complete.Predictor +} + +func (f *FlagSet) StringSliceVar(i *StringSliceVar) { + initial := i.Default + if v := os.Getenv(i.EnvVar); v != "" { + parts := strings.Split(v, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + initial = parts + } + + def := "" + if i.Default != nil { + def = strings.Join(i.Default, ",") + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newStringSliceValue(initial, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type stringSliceValue struct { + hidden bool + target *[]string +} + +func newStringSliceValue(def []string, target *[]string, hidden bool) *stringSliceValue { + *target = def + return &stringSliceValue{ + hidden: hidden, + target: target, + } +} + +func (s *stringSliceValue) Set(val string) error { + *s.target = append(*s.target, strings.TrimSpace(val)) + return nil +} + +func (s *stringSliceValue) Get() interface{} { return *s.target } +func (s *stringSliceValue) String() string { return strings.Join(*s.target, ",") } +func (s *stringSliceValue) Example() string { return "string" } +func (s *stringSliceValue) Hidden() bool { return s.hidden } + +// -- StringMapVar and stringMapValue +type StringMapVar struct { + Name string + Aliases []string + Usage string + Default map[string]string + Hidden bool + Target *map[string]string + Completion complete.Predictor +} + +func (f *FlagSet) StringMapVar(i *StringMapVar) { + def := "" + if i.Default != nil { + def = mapToKV(i.Default) + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + Value: newStringMapValue(i.Default, i.Target, i.Hidden), + Completion: i.Completion, + }) +} + +type stringMapValue struct { + hidden bool + target *map[string]string +} + +func newStringMapValue(def map[string]string, target *map[string]string, hidden bool) *stringMapValue { + *target = def + return &stringMapValue{ + hidden: hidden, + target: target, + } +} + +func (s *stringMapValue) Set(val string) error { + idx := strings.Index(val, "=") + if idx == -1 { + return fmt.Errorf("Missing = in KV pair: %s", val) + } + + if *s.target == nil { + *s.target = make(map[string]string) + } + + k, v := val[0:idx], val[idx+1:] + (*s.target)[k] = v + return nil +} + +func (s *stringMapValue) Get() interface{} { return *s.target } +func (s *stringMapValue) String() string { return mapToKV(*s.target) } +func (s *stringMapValue) Example() string { return "key=value" } +func (s *stringMapValue) Hidden() bool { return s.hidden } + +func mapToKV(m map[string]string) string { + list := make([]string, 0, len(m)) + for k, _ := range m { + list = append(list, k) + } + sort.Strings(list) + + for i, k := range list { + list[i] = k + "=" + m[k] + } + + return strings.Join(list, ",") +} + +// -- VarFlag type VarFlag struct { Name string Aliases []string @@ -292,6 +681,14 @@ type VarFlag struct { } func (f *FlagSet) VarFlag(i *VarFlag) { + // If the flag is marked as hidden, just add it to the set and return to + // avoid unnecessary computations here. We do not want to add completions or + // generate help output for hidden flags. + if v, ok := i.Value.(FlagVisibility); ok && v.Hidden() { + f.Var(i.Value, i.Name, "") + return + } + // Calculate the full usage usage := i.Usage @@ -326,190 +723,22 @@ func (f *FlagSet) VarFlag(i *VarFlag) { "environment variable.", i.EnvVar) } - f.mainSet.Var(i.Value, i.Name, "") // No point in passing along usage here - // Add aliases to the main set for _, a := range i.Aliases { f.mainSet.Var(i.Value, a, "") } - f.flagSet.Var(i.Value, i.Name, usage) + f.Var(i.Value, i.Name, usage) f.completions["-"+i.Name] = i.Completion } +// Var is a lower-level API for adding something to the flags. It should be used +// wtih caution, since it bypasses all validation. Consider VarFlag instead. func (f *FlagSet) Var(value flag.Value, name, usage string) { f.mainSet.Var(value, name, usage) f.flagSet.Var(value, name, usage) } -// -- bool Value -type boolValue bool - -func newBoolValue(val bool, p *bool) *boolValue { - *p = val - return (*boolValue)(p) -} - -func (b *boolValue) Set(s string) error { - v, err := strconv.ParseBool(s) - *b = boolValue(v) - return err -} - -func (b *boolValue) Get() interface{} { return bool(*b) } - -func (b *boolValue) String() string { return strconv.FormatBool(bool(*b)) } - -func (b *boolValue) Example() string { return "" } - -func (b *boolValue) IsBoolFlag() bool { return true } - -// optional interface to indicate boolean flags that can be -// supplied without "=value" text -type boolFlag interface { - flag.Value - IsBoolFlag() bool -} - -// -- int Value -type intValue int - -func newIntValue(val int, p *int) *intValue { - *p = val - return (*intValue)(p) -} - -func (i *intValue) Set(s string) error { - v, err := strconv.ParseInt(s, 0, 64) - *i = intValue(v) - return err -} - -func (i *intValue) Get() interface{} { return int(*i) } - -func (i *intValue) String() string { return strconv.Itoa(int(*i)) } - -func (i *intValue) Example() string { return "int" } - -// -- int64 Value -type int64Value int64 - -func newInt64Value(val int64, p *int64) *int64Value { - *p = val - return (*int64Value)(p) -} - -func (i *int64Value) Set(s string) error { - v, err := strconv.ParseInt(s, 0, 64) - *i = int64Value(v) - return err -} - -func (i *int64Value) Get() interface{} { return int64(*i) } - -func (i *int64Value) String() string { return strconv.FormatInt(int64(*i), 10) } - -func (i *int64Value) Example() string { return "int" } - -// -- uint Value -type uintValue uint - -func newUintValue(val uint, p *uint) *uintValue { - *p = val - return (*uintValue)(p) -} - -func (i *uintValue) Set(s string) error { - v, err := strconv.ParseUint(s, 0, 64) - *i = uintValue(v) - return err -} - -func (i *uintValue) Get() interface{} { return uint(*i) } - -func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i), 10) } - -func (i *uintValue) Example() string { return "uint" } - -// -- uint64 Value -type uint64Value uint64 - -func newUint64Value(val uint64, p *uint64) *uint64Value { - *p = val - return (*uint64Value)(p) -} - -func (i *uint64Value) Set(s string) error { - v, err := strconv.ParseUint(s, 0, 64) - *i = uint64Value(v) - return err -} - -func (i *uint64Value) Get() interface{} { return uint64(*i) } - -func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i), 10) } - -func (i *uint64Value) Example() string { return "uint" } - -// -- string Value -type stringValue string - -func newStringValue(val string, p *string) *stringValue { - *p = val - return (*stringValue)(p) -} - -func (s *stringValue) Set(val string) error { - *s = stringValue(val) - return nil -} - -func (s *stringValue) Get() interface{} { return string(*s) } - -func (s *stringValue) String() string { return string(*s) } - -func (s *stringValue) Example() string { return "string" } - -// -- float64 Value -type float64Value float64 - -func newFloat64Value(val float64, p *float64) *float64Value { - *p = val - return (*float64Value)(p) -} - -func (f *float64Value) Set(s string) error { - v, err := strconv.ParseFloat(s, 64) - *f = float64Value(v) - return err -} - -func (f *float64Value) Get() interface{} { return float64(*f) } - -func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f), 'g', -1, 64) } - -func (f *float64Value) Example() string { return "float" } - -// -- time.Duration Value -type durationValue time.Duration - -func newDurationValue(val time.Duration, p *time.Duration) *durationValue { - *p = val - return (*durationValue)(p) -} - -func (d *durationValue) Set(s string) error { - v, err := time.ParseDuration(s) - *d = durationValue(v) - return err -} - -func (d *durationValue) Get() interface{} { return time.Duration(*d) } - -func (d *durationValue) String() string { return (*time.Duration)(d).String() } - -func (d *durationValue) Example() string { return "duration" } - // -- helpers func envDefault(key, def string) string { if v := os.Getenv(key); v != "" { From 4bd867c56a869ed15a5447153838a48af0e8e3d8 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:53:57 -0400 Subject: [PATCH 030/281] Use a TokenHelper method It's weird to have two different helper funcs that can return different errors --- command/base.go | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/command/base.go b/command/base.go index e3eeb1f177..e05e61fa08 100644 --- a/command/base.go +++ b/command/base.go @@ -26,8 +26,6 @@ const maxLineLength int = 78 // a string. var reRemoveWhitespace = regexp.MustCompile(`[\s]+`) -type TokenHelperFunc func() (token.TokenHelper, error) - type BaseCommand struct { UI cli.Ui @@ -46,7 +44,7 @@ type BaseCommand struct { flagFormat string flagField string - tokenHelper TokenHelperFunc + tokenHelper token.TokenHelper // For testing client *api.Client @@ -98,16 +96,13 @@ func (c *BaseCommand) Client() (*api.Client, error) { // If we don't have a token, check the token helper if token == "" { - if c.tokenHelper != nil { - // If we have a token, then set that - tokenHelper, err := c.tokenHelper() - if err != nil { - return nil, errors.Wrap(err, "failed to get token helper") - } - token, err = tokenHelper.Get() - if err != nil { - return nil, errors.Wrap(err, "failed to retrieve from token helper") - } + helper, err := c.TokenHelper() + if err != nil { + return nil, errors.Wrap(err, "failed to get token helper") + } + token, err = helper.Get() + if err != nil { + return nil, errors.Wrap(err, "failed to get token from token helper") } } @@ -119,6 +114,19 @@ func (c *BaseCommand) Client() (*api.Client, error) { return client, nil } +// TokenHelper returns the token helper attached to the command. +func (c *BaseCommand) TokenHelper() (token.TokenHelper, error) { + if c.tokenHelper != nil { + return c.tokenHelper, nil + } + + helper, err := DefaultTokenHelper() + if err != nil { + return nil, err + } + return helper, nil +} + // DefaultWrappingLookupFunc is the default wrapping function based on the // CLI flag. func (c *BaseCommand) DefaultWrappingLookupFunc(operation, path string) string { From 125f055903acc1870f5e02dbaab3098f52bc9801 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:54:13 -0400 Subject: [PATCH 031/281] Cleanup base flags a bit --- command/base.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/command/base.go b/command/base.go index e05e61fa08..ec0fa67c81 100644 --- a/command/base.go +++ b/command/base.go @@ -217,11 +217,10 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { }) f.BoolVar(&BoolVar{ - Name: "tls-skip-verify", - Target: &c.flagTLSSkipVerify, - Default: false, - EnvVar: "VAULT_SKIP_VERIFY", - Completion: complete.PredictNothing, + Name: "tls-skip-verify", + Target: &c.flagTLSSkipVerify, + Default: false, + EnvVar: "VAULT_SKIP_VERIFY", Usage: "Disable verification of TLS certificates. Using this option " + "is highly discouraged and decreases the security of data " + "transmissions to and from the Vault server.", @@ -236,7 +235,7 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { Usage: "Wraps the response in a cubbyhole token with the requested " + "TTL. The response is available via the \"vault unwrap\" command. " + "The TTL is specified as a numeric string with suffix like \"30s\" " + - "or \"5m\"", + "or \"5m\".", }) } @@ -248,7 +247,6 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { Name: "field", Target: &c.flagField, Default: "", - EnvVar: "", Completion: complete.PredictAnything, Usage: "Print only the field with the given name. Specifying " + "this option will take precedence over other formatting " + From 2a4404c20a33e61ce389d72a88e17a3ba7ccd376 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:54:46 -0400 Subject: [PATCH 032/281] Expand and centralize helpers --- command/base_helpers.go | 212 ++++++++++++++++++++++++++++++++--- command/base_helpers_test.go | 162 ++++++++++++++++++++++++++ 2 files changed, 359 insertions(+), 15 deletions(-) create mode 100644 command/base_helpers_test.go diff --git a/command/base_helpers.go b/command/base_helpers.go index ae574f6d60..4c5bad6b88 100644 --- a/command/base_helpers.go +++ b/command/base_helpers.go @@ -2,35 +2,72 @@ package command import ( "fmt" + "io" "strings" + "time" + + "github.com/hashicorp/vault/api" + kvbuilder "github.com/hashicorp/vault/helper/kv-builder" + homedir "github.com/mitchellh/go-homedir" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "github.com/ryanuber/columnize" ) +var ErrMissingID = fmt.Errorf("Missing ID!") var ErrMissingPath = fmt.Errorf("Missing PATH!") +var ErrMissingThing = fmt.Errorf("Missing THING!") + +// extractListData reads the secret and returns a typed list of data and a +// boolean indicating whether the extraction was successful. +func extractListData(secret *api.Secret) ([]interface{}, bool) { + if secret == nil || secret.Data == nil { + return nil, false + } + + k, ok := secret.Data["keys"] + if !ok || k == nil { + return nil, false + } + + i, ok := k.([]interface{}) + return i, ok +} // extractPath extracts the path and list of arguments from the args. If there // are no extra arguments, the remaining args will be nil. func extractPath(args []string) (string, []string, error) { + str, remaining, err := extractThings(args) + if err == ErrMissingThing { + err = ErrMissingPath + } + return str, remaining, err +} + +// extractID extracts the path and list of arguments from the args. If there +// are no extra arguments, the remaining args will be nil. +func extractID(args []string) (string, []string, error) { + str, remaining, err := extractThings(args) + if err == ErrMissingThing { + err = ErrMissingID + } + return str, remaining, err +} + +func extractThings(args []string) (string, []string, error) { if len(args) < 1 { - return "", nil, ErrMissingPath + return "", nil, ErrMissingThing } // Path is always the first argument after all flags - path := args[0] + thing := args[0] // Strip leading and trailing slashes - for len(path) > 0 && path[0] == '/' { - path = path[1:] - } - for len(path) > 0 && path[len(path)-1] == '/' { - path = path[:len(path)-1] - } + thing = sanitizePath(thing) - // Trim any leading/trailing whitespace - path = strings.TrimSpace(path) - - // Verify we have a path - if path == "" { - return "", nil, ErrMissingPath + // Verify we have a thing + if thing == "" { + return "", nil, ErrMissingThing } // Splice remaining args @@ -39,5 +76,150 @@ func extractPath(args []string) (string, []string, error) { remaining = args[1:] } - return path, remaining, nil + return thing, remaining, nil +} + +// sanitizePath removes any leading or trailing things from a "path". +func sanitizePath(s string) string { + return ensureNoTrailingSlash(ensureNoLeadingSlash(s)) +} + +// ensureTrailingSlash ensures the given string has a trailing slash. +func ensureTrailingSlash(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "" + } + + for len(s) > 0 && s[len(s)-1] != '/' { + s = s + "/" + } + return s +} + +// ensureNoTrailingSlash ensures the given string has a trailing slash. +func ensureNoTrailingSlash(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "" + } + + for len(s) > 0 && s[len(s)-1] == '/' { + s = s[:len(s)-1] + } + return s +} + +// ensureNoLeadingSlash ensures the given string has a trailing slash. +func ensureNoLeadingSlash(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "" + } + + for len(s) > 0 && s[0] == '/' { + s = s[1:] + } + return s +} + +// columnOuput prints the list of items as a table with no headers. +func columnOutput(list []string) string { + if len(list) == 0 { + return "" + } + + return columnize.Format(list, &columnize.Config{ + Glue: " ", + Empty: "n/a", + }) +} + +// tableOutput prints the list of items as columns, where the first row is +// the list of headers. +func tableOutput(list []string) string { + if len(list) == 0 { + return "" + } + + underline := "" + headers := strings.Split(list[0], "|") + for i, h := range headers { + h = strings.TrimSpace(h) + u := strings.Repeat("-", len(h)) + + underline = underline + u + if i != len(headers)-1 { + underline = underline + " | " + } + } + + list = append(list, "") + copy(list[2:], list[1:]) + list[1] = underline + + return columnOutput(list) +} + +// parseArgsData parses the given args in the format key=value into a map of +// the provided arguments. The given reader can also supply key=value pairs. +func parseArgsData(stdin io.Reader, args []string) (map[string]interface{}, error) { + builder := &kvbuilder.Builder{Stdin: stdin} + if err := builder.Add(args...); err != nil { + return nil, err + } + + return builder.Map(), nil +} + +// parseArgsDataString parses the args data and returns the values as strings. +// If the values cannot be represented as strings, an error is returned. +func parseArgsDataString(stdin io.Reader, args []string) (map[string]string, error) { + raw, err := parseArgsData(stdin, args) + if err != nil { + return nil, err + } + + var result map[string]string + if err := mapstructure.WeakDecode(raw, &result); err != nil { + return nil, errors.Wrap(err, "failed to convert values to strings") + } + return result, nil +} + +// truncateToSeconds truncates the given duaration to the number of seconds. If +// the duration is less than 1s, it is returned as 0. The integer represents +// the whole number unit of seconds for the duration. +func truncateToSeconds(d time.Duration) int { + d = d.Truncate(1 * time.Second) + + // Handle the case where someone requested a ridiculously short increment - + // incremenents must be larger than a second. + if d < 1*time.Second { + return 0 + } + + return int(d.Seconds()) +} + +// printKeyStatus prints the KeyStatus response from the API. +func printKeyStatus(ks *api.KeyStatus) string { + return columnOutput([]string{ + fmt.Sprintf("Key Term | %d", ks.Term), + fmt.Sprintf("Install Time | %s", ks.InstallTime.UTC().Format(time.RFC822)), + }) +} + +// expandPath takes a filepath and returns the full expanded path, accounting +// for user-relative things like ~/. +func expandPath(s string) string { + if s == "" { + return "" + } + + e, err := homedir.Expand(s) + if err != nil { + return s + } + return e } diff --git a/command/base_helpers_test.go b/command/base_helpers_test.go new file mode 100644 index 0000000000..87c0bff695 --- /dev/null +++ b/command/base_helpers_test.go @@ -0,0 +1,162 @@ +package command + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "testing" + "time" +) + +func TestParseArgsData(t *testing.T) { + t.Parallel() + + t.Run("stdin_full", func(t *testing.T) { + t.Parallel() + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(`{"foo":"bar"}`)) + stdinW.Close() + }() + + m, err := parseArgsData(stdinR, []string{"-"}) + if err != nil { + t.Fatal(err) + } + + if v, ok := m["foo"]; !ok || v != "bar" { + t.Errorf("expected %q to be %q", v, "bar") + } + }) + + t.Run("stdin_value", func(t *testing.T) { + t.Parallel() + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(`bar`)) + stdinW.Close() + }() + + m, err := parseArgsData(stdinR, []string{"foo=-"}) + if err != nil { + t.Fatal(err) + } + + if v, ok := m["foo"]; !ok || v != "bar" { + t.Errorf("expected %q to be %q", v, "bar") + } + }) + + t.Run("file_full", func(t *testing.T) { + t.Parallel() + + f, err := ioutil.TempFile("", "vault") + if err != nil { + t.Fatal(err) + } + f.Write([]byte(`{"foo":"bar"}`)) + f.Close() + defer os.Remove(f.Name()) + + m, err := parseArgsData(os.Stdin, []string{"@" + f.Name()}) + if err != nil { + t.Fatal(err) + } + + if v, ok := m["foo"]; !ok || v != "bar" { + t.Errorf("expected %q to be %q", v, "bar") + } + }) + + t.Run("file_value", func(t *testing.T) { + t.Parallel() + + f, err := ioutil.TempFile("", "vault") + if err != nil { + t.Fatal(err) + } + f.Write([]byte(`bar`)) + f.Close() + defer os.Remove(f.Name()) + + m, err := parseArgsData(os.Stdin, []string{"foo=@" + f.Name()}) + if err != nil { + t.Fatal(err) + } + + if v, ok := m["foo"]; !ok || v != "bar" { + t.Errorf("expected %q to be %q", v, "bar") + } + }) + + t.Run("file_value_escaped", func(t *testing.T) { + t.Parallel() + + m, err := parseArgsData(os.Stdin, []string{`foo=\@`}) + if err != nil { + t.Fatal(err) + } + + if v, ok := m["foo"]; !ok || v != "@" { + t.Errorf("expected %q to be %q", v, "@") + } + }) +} + +func TestTruncateToSeconds(t *testing.T) { + t.Parallel() + + cases := []struct { + d time.Duration + exp int + }{ + { + 10 * time.Nanosecond, + 0, + }, + { + 10 * time.Microsecond, + 0, + }, + { + 10 * time.Millisecond, + 0, + }, + { + 1 * time.Second, + 1, + }, + { + 10 * time.Second, + 10, + }, + { + 100 * time.Second, + 100, + }, + { + 3 * time.Minute, + 180, + }, + { + 3 * time.Hour, + 10800, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(fmt.Sprintf("%s", tc.d), func(t *testing.T) { + t.Parallel() + + act := truncateToSeconds(tc.d) + if act != tc.exp { + t.Errorf("expected %d to be %d", act, tc.exp) + } + }) + } +} From e3fff2a78856cb0c5e2b14399dbe7fbc45aff200 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:55:08 -0400 Subject: [PATCH 033/281] Read env config for predictions --- command/base_predict.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/command/base_predict.go b/command/base_predict.go index 3b59147bb7..9312ac163d 100644 --- a/command/base_predict.go +++ b/command/base_predict.go @@ -21,7 +21,21 @@ func NewPredict() *Predict { func (p *Predict) Client() *api.Client { p.clientOnce.Do(func() { if p.client == nil { // For tests - p.client, _ = api.NewClient(nil) + client, _ := api.NewClient(nil) + + if client.Token() == "" { + helper, err := DefaultTokenHelper() + if err != nil { + return + } + token, err := helper.Get() + if err != nil { + return + } + client.SetToken(token) + } + + p.client = client } }) return p.client From 0cfb558f0b9a05b81a9fad72cea1133512db598a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:55:19 -0400 Subject: [PATCH 034/281] Add more predictors --- command/base_predict.go | 201 +++++++++++++++++++++++++++++++---- command/base_predict_test.go | 183 ++++++++++++++++++++----------- 2 files changed, 299 insertions(+), 85 deletions(-) diff --git a/command/base_predict.go b/command/base_predict.go index 9312ac163d..7587a3544e 100644 --- a/command/base_predict.go +++ b/command/base_predict.go @@ -66,6 +66,46 @@ func PredictClient() *api.Client { return predictClient } +// PredictVaultAvailableMounts returns a predictor for the available mounts in +// Vault. For now, there is no way to programatically get this list. If, in the +// future, such a list exists, we can adapt it here. Until then, it's +// hard-coded. +func (b *BaseCommand) PredictVaultAvailableMounts() complete.Predictor { + // This list does not contain deprecated backends. At present, there is no + // API that lists all available secret backends, so this is hard-coded :(. + return complete.PredictSet( + "aws", + "consul", + "database", + "pki", + "plugin", + "rabbitmq", + "ssh", + "totp", + "transit", + ) +} + +// PredictVaultAvailableAuths returns a predictor for the available auths in +// Vault. For now, there is no way to programatically get this list. If, in the +// future, such a list exists, we can adapt it here. Until then, it's +// hard-coded. +func (b *BaseCommand) PredictVaultAvailableAuths() complete.Predictor { + return complete.PredictSet( + "app-id", + "approle", + "aws", + "cert", + "gcp", + "github", + "ldap", + "okta", + "plugin", + "radius", + "userpass", + ) +} + // PredictVaultFiles returns a predictor for Vault mounts and paths based on the // configured client for the base command. Unfortunately this happens pre-flag // parsing, so users must rely on environment variables for autocomplete if they @@ -80,6 +120,30 @@ func (b *BaseCommand) PredictVaultFolders() complete.Predictor { return NewPredict().VaultFolders() } +// PredictVaultMounts returns a predictor for "folders". See PredictVaultFiles +// for more information and restrictions. +func (b *BaseCommand) PredictVaultMounts() complete.Predictor { + return NewPredict().VaultMounts() +} + +// PredictVaultAudits returns a predictor for "folders". See PredictVaultFiles +// for more information and restrictions. +func (b *BaseCommand) PredictVaultAudits() complete.Predictor { + return NewPredict().VaultAudits() +} + +// PredictVaultAuths returns a predictor for "folders". See PredictVaultFiles +// for more information and restrictions. +func (b *BaseCommand) PredictVaultAuths() complete.Predictor { + return NewPredict().VaultAuths() +} + +// PredictVaultPolicies returns a predictor for "folders". See PredictVaultFiles +// for more information and restrictions. +func (b *BaseCommand) PredictVaultPolicies() complete.Predictor { + return NewPredict().VaultPolicies() +} + // VaultFiles returns a predictor for Vault "files". This is a public API for // consumers, but you probably want BaseCommand.PredictVaultFiles instead. func (p *Predict) VaultFiles() complete.Predictor { @@ -93,6 +157,31 @@ func (p *Predict) VaultFolders() complete.Predictor { return p.vaultPaths(false) } +// VaultMounts returns a predictor for Vault "folders". This is a public +// API for consumers, but you probably want BaseCommand.PredictVaultMounts +// instead. +func (p *Predict) VaultMounts() complete.Predictor { + return p.filterFunc(p.mounts) +} + +// VaultAudits returns a predictor for Vault "folders". This is a public API for +// consumers, but you probably want BaseCommand.PredictVaultAudits instead. +func (p *Predict) VaultAudits() complete.Predictor { + return p.filterFunc(p.audits) +} + +// VaultAuths returns a predictor for Vault "folders". This is a public API for +// consumers, but you probably want BaseCommand.PredictVaultAuths instead. +func (p *Predict) VaultAuths() complete.Predictor { + return p.filterFunc(p.auths) +} + +// VaultPolicies returns a predictor for Vault "folders". This is a public API for +// consumers, but you probably want BaseCommand.PredictVaultPolicies instead. +func (p *Predict) VaultPolicies() complete.Predictor { + return p.filterFunc(p.policies) +} + // vaultPaths parses the CLI options and returns the "best" list of possible // paths. If there are any errors, this function returns an empty result. All // errors are suppressed since this is a prediction function. @@ -114,7 +203,7 @@ func (p *Predict) vaultPaths(includeFiles bool) complete.PredictFunc { if strings.Contains(path, "/") { predictions = p.paths(path, includeFiles) } else { - predictions = p.mounts(path) + predictions = p.filter(p.mounts(), path) } // Either no results or many results, so return. @@ -139,26 +228,6 @@ func (p *Predict) vaultPaths(includeFiles bool) complete.PredictFunc { } } -// mounts predicts all mounts which start with the given prefix. These are -// predicted on mount path, not "type". -func (p *Predict) mounts(path string) []string { - client := p.Client() - if client == nil { - return nil - } - - mounts := p.listMounts() - - var predictions []string - for _, m := range mounts { - if strings.HasPrefix(m, path) { - predictions = append(predictions, m) - } - } - - return predictions -} - // paths predicts all paths which start with the given path. func (p *Predict) paths(path string, includeFiles bool) []string { client := p.Client() @@ -198,10 +267,68 @@ func (p *Predict) paths(path string, includeFiles bool) []string { return predictions } -// listMounts returns a sorted list of the mount paths for Vault server for +// audits returns a sorted list of the audit backends for Vault server for +// which the client is configured to communicate with. +func (p *Predict) audits() []string { + client := p.Client() + if client == nil { + return nil + } + + audits, err := client.Sys().ListAudit() + if err != nil { + return nil + } + + list := make([]string, 0, len(audits)) + for m := range audits { + list = append(list, m) + } + sort.Strings(list) + return list +} + +// auths returns a sorted list of the enabled auth provides for Vault server for +// which the client is configured to communicate with. +func (p *Predict) auths() []string { + client := p.Client() + if client == nil { + return nil + } + + auths, err := client.Sys().ListAuth() + if err != nil { + return nil + } + + list := make([]string, 0, len(auths)) + for m := range auths { + list = append(list, m) + } + sort.Strings(list) + return list +} + +// policies returns a sorted list of the policies stored in this Vault +// server. +func (p *Predict) policies() []string { + client := p.Client() + if client == nil { + return nil + } + + policies, err := client.Sys().ListPolicies() + if err != nil { + return nil + } + sort.Strings(policies) + return policies +} + +// mounts returns a sorted list of the mount paths for Vault server for // which the client is configured to communicate with. This function returns the // default list of mounts if an error occurs. -func (p *Predict) listMounts() []string { +func (p *Predict) mounts() []string { client := p.Client() if client == nil { return nil @@ -259,3 +386,31 @@ func (p *Predict) hasPathArg(args []string) bool { return len(nonFlags) > 2 } + +// filterFunc is used to compose a complete predictor that filters an array +// of strings as per the filter function. +func (p *Predict) filterFunc(f func() []string) complete.Predictor { + return complete.PredictFunc(func(args complete.Args) []string { + if p.hasPathArg(args.All) { + return nil + } + + client := p.Client() + if client == nil { + return nil + } + + return p.filter(f(), args.Last) + }) +} + +// filter filters the given list for items that start with the prefix. +func (p *Predict) filter(list []string, prefix string) []string { + var predictions []string + for _, item := range list { + if strings.HasPrefix(item, prefix) { + predictions = append(predictions, item) + } + } + return predictions +} diff --git a/command/base_predict_test.go b/command/base_predict_test.go index 3de6e5f0e7..c1290e6702 100644 --- a/command/base_predict_test.go +++ b/command/base_predict_test.go @@ -202,31 +202,38 @@ func TestPredictVaultPaths(t *testing.T) { }) } -func TestPredict_Mounts(t *testing.T) { +func TestPredict_Audits(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) defer closer() + badClient, badCloser := testVaultServerBad(t) + defer badCloser() + + if err := client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{ + Type: "file", + Options: map[string]string{ + "file_path": "discard", + }, + }); err != nil { + t.Fatal(err) + } + cases := []struct { - name string - path string - exp []string + name string + client *api.Client + exp []string }{ { - "no_match", - "not-a-real-mount-seriously", + "not_connected_client", + badClient, nil, }, { - "s", - "s", - []string{"secret/", "sys/"}, - }, - { - "se", - "se", - []string{"secret/"}, + "good_path", + client, + []string{"file/"}, }, } @@ -237,9 +244,97 @@ func TestPredict_Mounts(t *testing.T) { t.Parallel() p := NewPredict() - p.client = client + p.client = tc.client - act := p.mounts(tc.path) + act := p.audits() + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + }) +} + +func TestPredict_Mounts(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + badClient, badCloser := testVaultServerBad(t) + defer badCloser() + + cases := []struct { + name string + client *api.Client + exp []string + }{ + { + "not_connected_client", + badClient, + defaultPredictVaultMounts, + }, + { + "good_path", + client, + []string{"cubbyhole/", "secret/", "sys/"}, + }, + } + + t.Run("group", func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + p := NewPredict() + p.client = tc.client + + act := p.mounts() + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %q to be %q", act, tc.exp) + } + }) + } + }) +} + +func TestPredict_Policies(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + badClient, badCloser := testVaultServerBad(t) + defer badCloser() + + cases := []struct { + name string + client *api.Client + exp []string + }{ + { + "not_connected_client", + badClient, + nil, + }, + { + "good_path", + client, + []string{"default", "root"}, + }, + } + + t.Run("group", func(t *testing.T) { + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + p := NewPredict() + p.client = tc.client + + act := p.policies() if !reflect.DeepEqual(act, tc.exp) { t.Errorf("expected %q to be %q", act, tc.exp) } @@ -321,57 +416,15 @@ func TestPredict_Paths(t *testing.T) { }) } -func TestPredict_ListMounts(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - cases := []struct { - name string - client *api.Client - exp []string - }{ - { - "not_connected_client", - func() *api.Client { - // Bad API client - client, _ := api.NewClient(nil) - return client - }(), - defaultPredictVaultMounts, - }, - { - "good_path", - client, - []string{"cubbyhole/", "secret/", "sys/"}, - }, - } - - t.Run("group", func(t *testing.T) { - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - p := NewPredict() - p.client = client - - act := p.listMounts() - if !reflect.DeepEqual(act, tc.exp) { - t.Errorf("expected %q to be %q", act, tc.exp) - } - }) - } - }) -} - func TestPredict_ListPaths(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) defer closer() + badClient, badCloser := testVaultServerBad(t) + defer badCloser() + data := map[string]interface{}{"a": "b"} if _, err := client.Logical().Write("secret/bar", data); err != nil { t.Fatal(err) @@ -398,6 +451,12 @@ func TestPredict_ListPaths(t *testing.T) { "secret/", []string{"bar", "foo"}, }, + { + "not_connected_client", + badClient, + "secret/", + nil, + }, } t.Run("group", func(t *testing.T) { @@ -407,7 +466,7 @@ func TestPredict_ListPaths(t *testing.T) { t.Parallel() p := NewPredict() - p.client = client + p.client = tc.client act := p.listPaths(tc.path) if !reflect.DeepEqual(act, tc.exp) { From fc58acbd7ee281d5dd0189e590449c925eeab20c Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:55:32 -0400 Subject: [PATCH 035/281] Remove unused file for tests --- command/base_test.go | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 command/base_test.go diff --git a/command/base_test.go b/command/base_test.go deleted file mode 100644 index b20be62660..0000000000 --- a/command/base_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package command - -import ( - "strings" - "testing" - - "github.com/mitchellh/cli" -) - -func assertNoTabs(tb testing.TB, c cli.Command) { - if strings.ContainsRune(c.Help(), '\t') { - tb.Errorf("%#v help output contains tabs", c) - } -} From 48ab42c32f976fbf168f45f08baa365465a3ebbb Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:55:58 -0400 Subject: [PATCH 036/281] Add helper for decrypting via PGP in tests --- command/pgp_test.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/command/pgp_test.go b/command/pgp_test.go index c368e31133..4cfda985b7 100644 --- a/command/pgp_test.go +++ b/command/pgp_test.go @@ -62,6 +62,35 @@ func getPubKeyFiles(t *testing.T) (string, []string, error) { return tempDir, pubFiles, nil } +func testPGPDecrypt(tb testing.TB, privKey, enc string) string { + tb.Helper() + + privKeyBytes, err := base64.StdEncoding.DecodeString(privKey) + if err != nil { + tb.Fatal(err) + } + + ptBuf := bytes.NewBuffer(nil) + entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes))) + if err != nil { + tb.Fatal(err) + } + + var rootBytes []byte + rootBytes, err = base64.StdEncoding.DecodeString(enc) + if err != nil { + tb.Fatal(err) + } + + entityList := &openpgp.EntityList{entity} + md, err := openpgp.ReadMessage(bytes.NewBuffer(rootBytes), entityList, nil, nil) + if err != nil { + tb.Fatal(err) + } + ptBuf.ReadFrom(md.UnverifiedBody) + return ptBuf.String() +} + func parseDecryptAndTestUnsealKeys(t *testing.T, input, rootToken string, fingerprints bool, From 94df25dbf7cf6c56053e6449dabcc3d79ed12122 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:56:58 -0400 Subject: [PATCH 037/281] Detect terminal and use the output writer for raw fields If the value is being "piped", we don't print colors or the newline character at the end. If it's not, we still give users pretty when selecting a raw field/value. --- command/util.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/command/util.go b/command/util.go index 7b7d8e65e8..e956252631 100644 --- a/command/util.go +++ b/command/util.go @@ -6,6 +6,8 @@ import ( "os" "time" + "golang.org/x/crypto/ssh/terminal" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/command/token" "github.com/mitchellh/cli" @@ -80,6 +82,7 @@ func RawField(secret *api.Secret, field string) (string, bool) { return str, val != nil } +// PrintRawField prints raw field from the secret. func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { str, ok := RawField(secret, field) if !ok { @@ -87,10 +90,21 @@ func PrintRawField(ui cli.Ui, secret *api.Secret, field string) int { return 1 } - // The cli.Ui prints a CR, which is not wanted since the user probably wants - // just the raw value. - w := getWriterFromUI(ui) - fmt.Fprintf(w, str) + return PrintRaw(ui, str) +} + +// PrintRaw prints a raw value to the terminal. If the process is being "piped" +// to something else, the "raw" value is printed without a newline character. +// Otherwise the value is printed as normal. +func PrintRaw(ui cli.Ui, str string) int { + if terminal.IsTerminal(int(os.Stdout.Fd())) { + ui.Output(str) + } else { + // The cli.Ui prints a CR, which is not wanted since the user probably wants + // just the raw value. + w := getWriterFromUI(ui) + fmt.Fprintf(w, str) + } return 0 } From c81fc5b0138ef67da6046b63edcb12b5f611b7cf Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:57:17 -0400 Subject: [PATCH 038/281] Remove wrapping tests There are no dedicated tests for this, but ttl wrapping is littered throughout other tests --- command/wrapping_test.go | 109 --------------------------------------- 1 file changed, 109 deletions(-) delete mode 100644 command/wrapping_test.go diff --git a/command/wrapping_test.go b/command/wrapping_test.go deleted file mode 100644 index a380cfc7d9..0000000000 --- a/command/wrapping_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package command - -import ( - "os" - "testing" - - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/cli" -) - -func TestWrapping_Env(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &TokenLookupCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - } - // Run it once for client - c.Run(args) - - // Create a new token for us to use - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - prevWrapTTLEnv := os.Getenv(api.EnvVaultWrapTTL) - os.Setenv(api.EnvVaultWrapTTL, "5s") - defer func() { - os.Setenv(api.EnvVaultWrapTTL, prevWrapTTLEnv) - }() - - // Now when we do a lookup-self the response should be wrapped - args = append(args, resp.Auth.ClientToken) - - resp, err = client.Auth().Token().LookupSelf() - if err != nil { - t.Fatalf("err: %s", err) - } - if resp == nil { - t.Fatal("nil response") - } - if resp.WrapInfo == nil { - t.Fatal("nil wrap info") - } - if resp.WrapInfo.Token == "" || resp.WrapInfo.TTL != 5 { - t.Fatal("did not get token or ttl wrong") - } -} - -func TestWrapping_Flag(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &TokenLookupCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "-wrap-ttl", "5s", - } - // Run it once for client - c.Run(args) - - // Create a new token for us to use - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", - }) - if err != nil { - t.Fatalf("err: %s", err) - } - if resp == nil { - t.Fatal("nil response") - } - if resp.WrapInfo == nil { - t.Fatal("nil wrap info") - } - if resp.WrapInfo.Token == "" || resp.WrapInfo.TTL != 5 { - t.Fatal("did not get token or ttl wrong") - } -} From 0ad656e5dcfa239f9eef1d8640033728d85d8317 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:57:55 -0400 Subject: [PATCH 039/281] Make pgpkeys helper implement our flags interface --- helper/pgpkeys/flag.go | 78 ++++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/helper/pgpkeys/flag.go b/helper/pgpkeys/flag.go index ccfc64b804..a7371e50a9 100644 --- a/helper/pgpkeys/flag.go +++ b/helper/pgpkeys/flag.go @@ -11,48 +11,90 @@ import ( "github.com/keybase/go-crypto/openpgp" ) -// PGPPubKeyFiles implements the flag.Value interface and allows -// parsing and reading a list of pgp public key files +// PubKeyFileFlag implements flag.Value and command.Example to receive exactly +// one PGP or keybase key via a flag. +type PubKeyFileFlag string + +func (p *PubKeyFileFlag) String() string { return string(*p) } + +func (p *PubKeyFileFlag) Set(val string) error { + if p != nil && *p != "" { + return errors.New("can only be specified once") + } + + keys, err := ParsePGPKeys(strings.Split(val, ",")) + if err != nil { + return err + } + + if len(keys) > 1 { + return errors.New("can only specify one pgp key") + } + + *p = PubKeyFileFlag(keys[0]) + return nil +} + +func (p *PubKeyFileFlag) Example() string { return "keybase:user" } + +// PGPPubKeyFiles implements the flag.Value interface and allows parsing and +// reading a list of PGP public key files. type PubKeyFilesFlag []string func (p *PubKeyFilesFlag) String() string { return fmt.Sprint(*p) } -func (p *PubKeyFilesFlag) Set(value string) error { +func (p *PubKeyFilesFlag) Set(val string) error { if len(*p) > 0 { - return errors.New("pgp-keys can only be specified once") + return errors.New("can only be specified once") } - splitValues := strings.Split(value, ",") - - keybaseMap, err := FetchKeybasePubkeys(splitValues) + keys, err := ParsePGPKeys(strings.Split(val, ",")) if err != nil { return err } - // Now go through the actual flag, and substitute in resolved keybase - // entries where appropriate - for _, keyfile := range splitValues { + *p = PubKeyFilesFlag(keys) + return nil +} + +func (p *PubKeyFilesFlag) Example() string { return "keybase:user1, keybase:user2, ..." } + +// ParsePGPKeys takes a list of PGP keys and parses them either using keybase +// or reading them from disk and returns the "expanded" list of pgp keys in +// the same order. +func ParsePGPKeys(keyfiles []string) ([]string, error) { + keys := make([]string, len(keyfiles)) + + keybaseMap, err := FetchKeybasePubkeys(keyfiles) + if err != nil { + return nil, err + } + + for i, keyfile := range keyfiles { + keyfile = strings.TrimSpace(keyfile) + if strings.HasPrefix(keyfile, kbPrefix) { - key := keybaseMap[keyfile] - if key == "" { - return fmt.Errorf("key for keybase user %s was not found in the map", strings.TrimPrefix(keyfile, kbPrefix)) + key, ok := keybaseMap[keyfile] + if !ok || key == "" { + return nil, fmt.Errorf("keybase user %q not found", strings.TrimPrefix(keyfile, kbPrefix)) } - *p = append(*p, key) + keys[i] = key continue } pgpStr, err := ReadPGPFile(keyfile) if err != nil { - return err + return nil, err } - - *p = append(*p, pgpStr) + keys[i] = pgpStr } - return nil + + return keys, nil } +// ReadPGPFile reads the given PGP file from disk. func ReadPGPFile(path string) (string, error) { if path[0] == '@' { path = path[1:] From 738e4ea2865e80c893627d678105bba6d2d73d88 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:58:18 -0400 Subject: [PATCH 040/281] Add more testing helper functions --- command/command_test.go | 202 ++++++++++++++++++++++++++++++++++------ 1 file changed, 175 insertions(+), 27 deletions(-) diff --git a/command/command_test.go b/command/command_test.go index f41813c28d..0ff084f45b 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -1,64 +1,212 @@ package command import ( + "context" + "encoding/base64" + "net" + "net/http" + "strings" "testing" + "time" "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/builtin/logical/pki" + "github.com/hashicorp/vault/builtin/logical/ssh" "github.com/hashicorp/vault/builtin/logical/transit" "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/physical/inmem" "github.com/hashicorp/vault/vault" + "github.com/mitchellh/cli" + auditFile "github.com/hashicorp/vault/builtin/audit/file" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" vaulthttp "github.com/hashicorp/vault/http" logxi "github.com/mgutz/logxi/v1" ) -var testVaultServerDefaultBackends = map[string]logical.Factory{ - "transit": transit.Factory, - "pki": pki.Factory, -} +var ( + defaultVaultLogger = logxi.NullLog -func testVaultServer(t testing.TB) (*api.Client, func()) { - return testVaultServerBackends(t, testVaultServerDefaultBackends) -} - -func testVaultServerBackends(t testing.TB, backends map[string]logical.Factory) (*api.Client, func()) { - coreConfig := &vault.CoreConfig{ - DisableMlock: true, - DisableCache: true, - Logger: logxi.NullLog, - LogicalBackends: backends, + defaultVaultCredentialBackends = map[string]logical.Factory{ + "userpass": credUserpass.Factory, } - cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ + defaultVaultAuditBackends = map[string]audit.Factory{ + "file": auditFile.Factory, + } + + defaultVaultLogicalBackends = map[string]logical.Factory{ + "generic-leased": vault.LeasedPassthroughBackendFactory, + "pki": pki.Factory, + "ssh": ssh.Factory, + "transit": transit.Factory, + } +) + +// assertNoTabs asserts the CLI help has no tab characters. +func assertNoTabs(tb testing.TB, c cli.Command) { + tb.Helper() + + if strings.ContainsRune(c.Help(), '\t') { + tb.Errorf("%#v help output contains tabs", c) + } +} + +// testVaultServer creates a test vault cluster and returns a configured API +// client and closer function. +func testVaultServer(tb testing.TB) (*api.Client, func()) { + tb.Helper() + + client, _, closer := testVaultServerUnseal(tb) + return client, closer +} + +// testVaultServerUnseal creates a test vault cluster and returns a configured +// API client, list of unseal keys (as strings), and a closer function. +func testVaultServerUnseal(tb testing.TB) (*api.Client, []string, func()) { + tb.Helper() + + return testVaultServerCoreConfig(tb, &vault.CoreConfig{ + DisableMlock: true, + DisableCache: true, + Logger: defaultVaultLogger, + CredentialBackends: defaultVaultCredentialBackends, + AuditBackends: defaultVaultAuditBackends, + LogicalBackends: defaultVaultLogicalBackends, + }) +} + +// testVaultServerCoreConfig creates a new vault cluster with the given core +// configuration. This is a lower-level test helper. +func testVaultServerCoreConfig(tb testing.TB, coreConfig *vault.CoreConfig) (*api.Client, []string, func()) { + tb.Helper() + + cluster := vault.NewTestCluster(tb, coreConfig, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, + NumCores: 1, // Default is 3, but we don't need that many }) cluster.Start() - // make it easy to get access to the active + // Make it easy to get access to the active core := cluster.Cores[0].Core - vault.TestWaitActive(t, core) + vault.TestWaitActive(tb, core) + // Get the client already setup for us! client := cluster.Cores[0].Client client.SetToken(cluster.RootToken) - // Sanity check - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatal(err) + // Convert the unseal keys to base64 encoded, since these are how the user + // will get them. + unsealKeys := make([]string, len(cluster.BarrierKeys)) + for i := range unsealKeys { + unsealKeys[i] = base64.StdEncoding.EncodeToString(cluster.BarrierKeys[i]) } - if secret == nil || secret.Data["id"].(string) != cluster.RootToken { - t.Fatalf("token mismatch: %#v vs %q", secret, cluster.RootToken) - } - return client, func() { defer cluster.Cleanup() } + + return client, unsealKeys, func() { defer cluster.Cleanup() } } -func testClient(t *testing.T, addr string, token string) *api.Client { +// testVaultServerUninit creates an uninitialized server. +func testVaultServerUninit(tb testing.TB) (*api.Client, func()) { + tb.Helper() + + inm, err := inmem.NewInmem(nil, defaultVaultLogger) + if err != nil { + tb.Fatal(err) + } + + core, err := vault.NewCore(&vault.CoreConfig{ + DisableMlock: true, + DisableCache: true, + Logger: defaultVaultLogger, + Physical: inm, + CredentialBackends: defaultVaultCredentialBackends, + AuditBackends: defaultVaultAuditBackends, + LogicalBackends: defaultVaultLogicalBackends, + }) + if err != nil { + tb.Fatal(err) + } + + ln, addr := vaulthttp.TestServer(tb, core) + + client, err := api.NewClient(&api.Config{ + Address: addr, + }) + if err != nil { + tb.Fatal(err) + } + + return client, func() { ln.Close() } +} + +// testVaultServerBad creates an http server that returns a 500 on each request +// to simulate failures. +func testVaultServerBad(tb testing.TB) (*api.Client, func()) { + tb.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + tb.Fatal(err) + } + + server := &http.Server{ + Addr: "127.0.0.1:0", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "500 internal server error", http.StatusInternalServerError) + }), + ReadTimeout: 1 * time.Second, + ReadHeaderTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + IdleTimeout: 1 * time.Second, + } + + go func() { + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + tb.Fatal(err) + } + }() + + client, err := api.NewClient(&api.Config{ + Address: "http://" + listener.Addr().String(), + }) + if err != nil { + tb.Fatal(err) + } + + return client, func() { + ctx, done := context.WithTimeout(context.Background(), 5*time.Second) + defer done() + + server.Shutdown(ctx) + } +} + +// testTokenAndAccessor creates a new authentication token capable of being renewed with +// the default policy attached. It returns the token and it's accessor. +func testTokenAndAccessor(tb testing.TB, client *api.Client) (string, string) { + tb.Helper() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + }) + if err != nil { + tb.Fatal(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + tb.Fatalf("missing auth data: %#v", secret) + } + return secret.Auth.ClientToken, secret.Auth.Accessor +} + +func testClient(tb testing.TB, addr string, token string) *api.Client { + tb.Helper() config := api.DefaultConfig() config.Address = addr client, err := api.NewClient(config) if err != nil { - t.Fatalf("err: %s", err) + tb.Fatal(err) } client.SetToken(token) From 3186d0d5621cd86ec2cea0ea9e9ee20ca2276d67 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:58:52 -0400 Subject: [PATCH 041/281] Update audit-disable command --- command/audit_disable.go | 115 +++++++++++-------- command/audit_disable_test.go | 200 +++++++++++++++++++++++----------- 2 files changed, 204 insertions(+), 111 deletions(-) diff --git a/command/audit_disable.go b/command/audit_disable.go index 31c4457287..6bb9f68ff9 100644 --- a/command/audit_disable.go +++ b/command/audit_disable.go @@ -4,68 +4,87 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*AuditDisableCommand)(nil) +var _ cli.CommandAutocomplete = (*AuditDisableCommand)(nil) + // AuditDisableCommand is a Command that mounts a new mount. type AuditDisableCommand struct { - meta.Meta -} - -func (c *AuditDisableCommand) Run(args []string) int { - flags := c.Meta.FlagSet("mount", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - args = flags.Args() - if len(args) != 1 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\naudit-disable expects one argument: the id to disable")) - return 1 - } - - id := args[0] - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if err := client.Sys().DisableAudit(id); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error disabling audit backend: %s", err)) - return 2 - } - - c.Ui.Output(fmt.Sprintf( - "Successfully disabled audit backend '%s' if it was enabled", id)) - - return 0 + *BaseCommand } func (c *AuditDisableCommand) Synopsis() string { - return "Disable an audit backend" + return "Disables an audit backend" } func (c *AuditDisableCommand) Help() string { helpText := ` -Usage: vault audit-disable [options] id +Usage: vault audit-disable [options] PATH - Disable an audit backend. + Disables an audit backend. Once an audit backend is disabled, no future + audit logs are dispatched to it. The data associated with the audit backend + is not affected. - Once the audit backend is disabled no more audit logs will be sent to - it. The data associated with the audit backend isn't affected. + The argument corresponds to the PATH of the mount, not the TYPE! - The "id" parameter should map to the "path" used in "audit-enable". If - no path was provided to "audit-enable" you should use the backend - type (e.g. "file"). + Disable the audit backend at file/: + + $ vault audit-disable file/ + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *AuditDisableCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *AuditDisableCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultAudits() +} + +func (c *AuditDisableCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *AuditDisableCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + path, kvs, err := extractPath(args) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + path = ensureTrailingSlash(path) + + if len(kvs) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if err := client.Sys().DisableAudit(path); err != nil { + c.UI.Error(fmt.Sprintf("Error disabling audit backend: %s", err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Disabled audit backend (if it was enabled) at: %s", path)) + + return 0 +} diff --git a/command/audit_disable_test.go b/command/audit_disable_test.go index 500ee9ccb1..6980179abd 100644 --- a/command/audit_disable_test.go +++ b/command/audit_disable_test.go @@ -1,86 +1,160 @@ package command import ( + "strings" "testing" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestAuditDisable(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testAuditDisableCommand(tb testing.TB) (*cli.MockUi, *AuditDisableCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &AuditDisableCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &AuditDisableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - args := []string{ - "-address", addr, - "noop", - } - - // Run once to get the client - c.Run(args) - - // Get the client - client, err := c.Client() - if err != nil { - t.Fatalf("err: %#v", err) - } - if err := client.Sys().EnableAudit("noop", "noop", "", nil); err != nil { - t.Fatalf("err: %#v", err) - } - - // Run again - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } } -func TestAuditDisableWithOptions(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestAuditDisableCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) - c := &AuditDisableCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing PATH!", + 1, + }, + { + "slash", + []string{"/"}, + "Missing PATH!", + 1, + }, + { + "not_real", + []string{"not_real"}, + "Success! Disabled audit backend (if it was enabled) at: not_real/", + 0, + }, + { + "default", + []string{"file"}, + "Success! Disabled audit backend (if it was enabled) at: file/", + 0, }, } - args := []string{ - "-address", addr, - "noop", + 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().EnableAuditWithOptions("file", &api.EnableAuditOptions{ + Type: "file", + Options: map[string]string{ + "file_path": "discard", + }, + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuditDisableCommand(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) + } + }) } - // Run once to get the client - c.Run(args) + t.Run("integration", func(t *testing.T) { + t.Parallel() - // Get the client - client, err := c.Client() - if err != nil { - t.Fatalf("err: %#v", err) - } - if err := client.Sys().EnableAuditWithOptions("noop", &api.EnableAuditOptions{ - Type: "noop", - Description: "noop", - }); err != nil { - t.Fatalf("err: %#v", err) - } + client, closer := testVaultServer(t) + defer closer() - // Run again - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + if err := client.Sys().EnableAuditWithOptions("integration_audit_disable", &api.EnableAuditOptions{ + Type: "file", + Options: map[string]string{ + "file_path": "discard", + }, + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuditDisableCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "integration_audit_disable/", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Disabled audit backend (if it was enabled) at: integration_audit_disable/" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + mounts, err := client.Sys().ListMounts() + if err != nil { + t.Fatal(err) + } + + if _, ok := mounts["integration_audit_disable"]; ok { + t.Errorf("expected mount to not exist: %#v", mounts) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuditDisableCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "file", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error disabling audit backend: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuditDisableCommand(t) + assertNoTabs(t, cmd) + }) } From 78160740f0c4f1a77d12b26daecdfda4774a0bfe Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:59:03 -0400 Subject: [PATCH 042/281] Update audit-enable command --- command/audit_enable.go | 203 +++++++++++++++++++---------------- command/audit_enable_test.go | 178 +++++++++++++++++++++++------- 2 files changed, 249 insertions(+), 132 deletions(-) diff --git a/command/audit_enable.go b/command/audit_enable.go index 680a94ed19..61fed20486 100644 --- a/command/audit_enable.go +++ b/command/audit_enable.go @@ -7,97 +7,34 @@ import ( "strings" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/kv-builder" - "github.com/hashicorp/vault/meta" - "github.com/mitchellh/mapstructure" + "github.com/mitchellh/cli" "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*AuditEnableCommand)(nil) +var _ cli.CommandAutocomplete = (*AuditEnableCommand)(nil) + // AuditEnableCommand is a Command that mounts a new mount. type AuditEnableCommand struct { - meta.Meta + *BaseCommand - // A test stdin that can be used for tests - testStdin io.Reader -} + flagDescription string + flagPath string + flagLocal bool -func (c *AuditEnableCommand) Run(args []string) int { - var desc, path string - var local bool - flags := c.Meta.FlagSet("audit-enable", meta.FlagSetDefault) - flags.StringVar(&desc, "description", "", "") - flags.StringVar(&path, "path", "", "") - flags.BoolVar(&local, "local", 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 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\naudit-enable expects at least one argument: the type to enable")) - return 1 - } - - auditType := args[0] - if path == "" { - path = auditType - } - - // Build the options - var stdin io.Reader = os.Stdin - if c.testStdin != nil { - stdin = c.testStdin - } - builder := &kvbuilder.Builder{Stdin: stdin} - if err := builder.Add(args[1:]...); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error parsing options: %s", err)) - return 1 - } - - var opts map[string]string - if err := mapstructure.WeakDecode(builder.Map(), &opts); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error parsing options: %s", err)) - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 1 - } - - err = client.Sys().EnableAuditWithOptions(path, &api.EnableAuditOptions{ - Type: auditType, - Description: desc, - Options: opts, - Local: local, - }) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error enabling audit backend: %s", err)) - return 1 - } - - c.Ui.Output(fmt.Sprintf( - "Successfully enabled audit backend '%s' with path '%s'!", auditType, path)) - return 0 + testStdin io.Reader // For tests } func (c *AuditEnableCommand) Synopsis() string { - return "Enable an audit backend" + return "Enables an audit backend" } func (c *AuditEnableCommand) Help() string { helpText := ` -Usage: vault audit-enable [options] type [config...] +Usage: vault audit-enable [options] TYPE [CONFIG K=V...] - Enable an audit backend. + Enables an audit backend at a given path. This command enables an audit backend of type "type". Additional options for configuring the audit backend can be specified after the @@ -111,24 +48,49 @@ Usage: vault audit-enable [options] type [config...] For information on available configuration options, please see the documentation. -General Options: -` + meta.GeneralOptionsUsage() + ` -Audit Enable Options: +` + c.Flags().Help() - -description= A human-friendly description for the backend. This - shows up only when querying the enabled backends. - - -path= Specify a unique path for this audit backend. This - is purely for referencing this audit backend. By - default this will be the backend type. - - -local Mark the mount as a local mount. Local mounts - are not replicated nor (if a secondary) - removed by replication. -` return strings.TrimSpace(helpText) } +func (c *AuditEnableCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "description", + Target: &c.flagDescription, + Default: "", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Human-friendly description for the purpose of this audit " + + "backend.", + }) + + f.StringVar(&StringVar{ + Name: "path", + Target: &c.flagPath, + Default: "", // The default is complex, so we have to manually document + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Place where the audit backend will be accessible. This must be " + + "unique across all audit backends. This defaults to the \"type\" of the " + + "audit backend.", + }) + + f.BoolVar(&BoolVar{ + Name: "local", + Target: &c.flagLocal, + Default: false, + EnvVar: "", + Usage: "Mark the audit backend as a local-only backned. Local backends " + + "are not replicated nor removed by replication.", + }) + + return set +} + func (c *AuditEnableCommand) AutocompleteArgs() complete.Predictor { return complete.PredictSet( "file", @@ -138,9 +100,60 @@ func (c *AuditEnableCommand) AutocompleteArgs() complete.Predictor { } func (c *AuditEnableCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-description": complete.PredictNothing, - "-path": complete.PredictNothing, - "-local": complete.PredictNothing, - } + return c.Flags().Completions() +} + +func (c *AuditEnableCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) < 1 { + c.UI.Error("Missing TYPE!") + return 1 + } + + // Grab the type + auditType := strings.TrimSpace(args[0]) + + auditPath := c.flagPath + if auditPath == "" { + auditPath = auditType + } + auditPath = ensureTrailingSlash(auditPath) + + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + options, err := parseArgsDataString(stdin, args[1:]) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err)) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if err := client.Sys().EnableAuditWithOptions(auditPath, &api.EnableAuditOptions{ + Type: auditType, + Description: c.flagDescription, + Options: options, + Local: c.flagLocal, + }); err != nil { + c.UI.Error(fmt.Sprintf("Error enabling audit backend: %s", err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Enabled the %s audit backend at: %s", auditType, auditPath)) + return 0 } diff --git a/command/audit_enable_test.go b/command/audit_enable_test.go index 118f103d3e..6be5c5c68a 100644 --- a/command/audit_enable_test.go +++ b/command/audit_enable_test.go @@ -1,56 +1,160 @@ package command import ( - "reflect" + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestAuditEnable(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testAuditEnableCommand(tb testing.TB) (*cli.MockUi, *AuditEnableCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &AuditEnableCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &AuditEnableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestAuditEnableCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing TYPE!", + 1, + }, + { + "not_a_valid_type", + []string{"nope_definitely_not_a_valid_type_like_ever"}, + "", + 2, + }, + { + "enable", + []string{"file", "file_path=discard"}, + "Success! Enabled the file audit backend at: file/", + 0, + }, + { + "enable_path", + []string{ + "-path", "audit_path", + "file", + "file_path=discard", + }, + "Success! Enabled the file audit backend at: audit_path/", + 0, }, } - args := []string{ - "-address", addr, - "noop", - "foo=bar", + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testAuditEnableCommand(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) + } + }) } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("integration", func(t *testing.T) { + t.Parallel() - // Get the client - client, err := c.Client() - if err != nil { - t.Fatalf("err: %#v", err) - } + client, closer := testVaultServer(t) + defer closer() - audits, err := client.Sys().ListAudit() - if err != nil { - t.Fatalf("err: %#v", err) - } + ui, cmd := testAuditEnableCommand(t) + cmd.client = client - audit, ok := audits["noop/"] - if !ok { - t.Fatalf("err: %#v", audits) - } + code := cmd.Run([]string{ + "-path", "audit_enable_integration/", + "-description", "The best kind of test", + "file", + "file_path=discard", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } - expected := map[string]string{"foo": "bar"} - if !reflect.DeepEqual(audit.Options, expected) { - t.Fatalf("err: %#v", audit) - } + expected := "Success! Enabled the file audit backend at: audit_enable_integration/" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + audits, err := client.Sys().ListAudit() + if err != nil { + t.Fatal(err) + } + + auditInfo, ok := audits["audit_enable_integration/"] + if !ok { + t.Fatalf("expected audit to exist") + } + if exp := "file"; auditInfo.Type != exp { + t.Errorf("expected %q to be %q", auditInfo.Type, exp) + } + if exp := "The best kind of test"; auditInfo.Description != exp { + t.Errorf("expected %q to be %q", auditInfo.Description, exp) + } + + filePath, ok := auditInfo.Options["file_path"] + if !ok || filePath != "discard" { + t.Errorf("missing some options: %#v", auditInfo) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuditEnableCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "pki", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error enabling audit backend: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuditEnableCommand(t) + assertNoTabs(t, cmd) + }) } From ca28cde14b087ad158f7544bf1f9bff838f98b27 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:59:13 -0400 Subject: [PATCH 043/281] Update audit-list command --- command/audit_list.go | 203 ++++++++++++++++++++++++++----------- command/audit_list_test.go | 125 +++++++++++++++++------ 2 files changed, 234 insertions(+), 94 deletions(-) diff --git a/command/audit_list.go b/command/audit_list.go index b9914eb929..048e3596c8 100644 --- a/command/audit_list.go +++ b/command/audit_list.go @@ -5,83 +5,162 @@ import ( "sort" "strings" - "github.com/hashicorp/vault/meta" - "github.com/ryanuber/columnize" + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*AuditListCommand)(nil) +var _ cli.CommandAutocomplete = (*AuditListCommand)(nil) + // AuditListCommand is a Command that lists the enabled audits. type AuditListCommand struct { - meta.Meta -} + *BaseCommand -func (c *AuditListCommand) Run(args []string) int { - flags := c.Meta.FlagSet("audit-list", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - audits, err := client.Sys().ListAudit() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading audits: %s", err)) - return 2 - } - - if len(audits) == 0 { - c.Ui.Error(fmt.Sprintf( - "No audit backends are enabled. Use `vault audit-enable` to\n" + - "enable an audit backend.")) - return 1 - } - - paths := make([]string, 0, len(audits)) - for path, _ := range audits { - paths = append(paths, path) - } - sort.Strings(paths) - - columns := []string{"Path | Type | Description | Replication Behavior | Options"} - for _, path := range paths { - audit := audits[path] - opts := make([]string, 0, len(audit.Options)) - for k, v := range audit.Options { - opts = append(opts, k+"="+v) - } - replicatedBehavior := "replicated" - if audit.Local { - replicatedBehavior = "local" - } - columns = append(columns, fmt.Sprintf( - "%s | %s | %s | %s | %s", audit.Path, audit.Type, audit.Description, replicatedBehavior, strings.Join(opts, " "))) - } - - c.Ui.Output(columnize.SimpleFormat(columns)) - return 0 + flagDetailed bool } func (c *AuditListCommand) Synopsis() string { - return "Lists enabled audit backends in Vault" + return "Lists enabled audit backends" } func (c *AuditListCommand) Help() string { helpText := ` Usage: vault audit-list [options] - List the enabled audit backends. + Lists the enabled audit backends in the Vault server. The output lists + the enabled audit backends and the options for those backends. - The output lists the enabled audit backends and the options for those - backends. The options may contain sensitive information, and therefore - only a root Vault user can view this. + List all audit backends: + + $ vault audit-list + + List detailed output about the audit backends: + + $ vault audit-list -detailed + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *AuditListCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "detailed", + Target: &c.flagDetailed, + Default: false, + EnvVar: "", + Usage: "Print detailed information such as options and replication " + + "status about each mount.", + }) + + return set +} + +func (c *AuditListCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *AuditListCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *AuditListCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + audits, err := client.Sys().ListAudit() + if err != nil { + c.UI.Error(fmt.Sprintf("Error listing audits: %s", err)) + return 2 + } + + if len(audits) == 0 { + c.UI.Error(fmt.Sprintf("No audit backends are enabled.")) + return 0 + } + + if c.flagDetailed { + c.UI.Output(tableOutput(c.detailedAudits(audits))) + return 0 + } + + c.UI.Output(tableOutput(c.simpleAudits(audits))) + return 0 +} + +func (c *AuditListCommand) simpleAudits(audits map[string]*api.Audit) []string { + paths := make([]string, 0, len(audits)) + for path, _ := range audits { + paths = append(paths, path) + } + sort.Strings(paths) + + columns := []string{"Path | Type | Description"} + for _, path := range paths { + audit := audits[path] + columns = append(columns, fmt.Sprintf("%s | %s | %s", + audit.Path, + audit.Type, + audit.Description, + )) + } + + return columns +} + +func (c *AuditListCommand) detailedAudits(audits map[string]*api.Audit) []string { + paths := make([]string, 0, len(audits)) + for path, _ := range audits { + paths = append(paths, path) + } + sort.Strings(paths) + + columns := []string{"Path | Type | Description | Replication | Options"} + for _, path := range paths { + audit := audits[path] + + opts := make([]string, 0, len(audit.Options)) + for k, v := range audit.Options { + opts = append(opts, k+"="+v) + } + + replication := "replicated" + if audit.Local { + replication = "local" + } + + columns = append(columns, fmt.Sprintf("%s | %s | %s | %s | %s", + audit.Path, + audit.Type, + audit.Description, + replication, + strings.Join(opts, " "), + )) + } + + return columns +} diff --git a/command/audit_list_test.go b/command/audit_list_test.go index 01d4f83ed0..9cbb0af5ee 100644 --- a/command/audit_list_test.go +++ b/command/audit_list_test.go @@ -1,50 +1,111 @@ package command import ( + "strings" "testing" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestAuditList(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testAuditListCommand(tb testing.TB) (*cli.MockUi, *AuditListCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &AuditListCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &AuditListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestAuditListCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo"}, + "Too many arguments", + 1, + }, + { + "lists", + nil, + "Path", + 0, + }, + { + "detailed", + []string{"-detailed"}, + "Options", + 0, }, } - args := []string{ - "-address", addr, + 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().EnableAuditWithOptions("file", &api.EnableAuditOptions{ + Type: "file", + Options: map[string]string{ + "file_path": "discard", + }, + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuditListCommand(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) + } + }) } - // Run once to get the client - c.Run(args) + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() - // Get the client - client, err := c.Client() - if err != nil { - t.Fatalf("err: %#v", err) - } - if err := client.Sys().EnableAuditWithOptions("foo", &api.EnableAuditOptions{ - Type: "noop", - Description: "noop", - Options: nil, - }); err != nil { - t.Fatalf("err: %#v", err) - } + client, closer := testVaultServerBad(t) + defer closer() - // Run again - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + ui, cmd := testAuditListCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing audits: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuditListCommand(t) + assertNoTabs(t, cmd) + }) } From 9ff68fffa2298571261b44e1a3b7b97cdadff08a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:59:24 -0400 Subject: [PATCH 044/281] Update auth command --- command/auth.go | 867 +++++++++++++++++++------------------------ command/auth_test.go | 862 ++++++++++++++++++++++++------------------ 2 files changed, 881 insertions(+), 848 deletions(-) diff --git a/command/auth.go b/command/auth.go index 00b21ce414..8ebf021bd1 100644 --- a/command/auth.go +++ b/command/auth.go @@ -1,22 +1,13 @@ package command import ( - "bufio" - "encoding/json" "fmt" "io" "os" - "sort" - "strconv" "strings" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/kv-builder" - "github.com/hashicorp/vault/helper/password" - "github.com/hashicorp/vault/meta" - "github.com/mitchellh/mapstructure" "github.com/posener/complete" - "github.com/ryanuber/columnize" ) // AuthHandler is the interface that any auth handlers must implement @@ -28,39 +19,318 @@ type AuthHandler interface { // AuthCommand is a Command that handles authentication. type AuthCommand struct { - meta.Meta + *BaseCommand Handlers map[string]AuthHandler - // The fields below can be overwritten for tests - testStdin io.Reader + flagMethod string + flagPath string + flagNoVerify bool + flagNoStore bool + flagOnlyToken bool + + // Deprecations + // TODO: remove in 0.9.0 + flagTokenOnly bool + flagMethods bool + flagMethodHelp bool + + testStdin io.Reader // for tests +} + +func (c *AuthCommand) Synopsis() string { + return "Authenticates users or machines" +} + +func (c *AuthCommand) Help() string { + helpText := ` +Usage: vault auth [options] [AUTH K=V...] + + Authenticates users or machines to Vault using the provided arguments. By + default, the authentication method is "token". If not supplied via the CLI, + Vault will prompt for input. If argument is "-", the configuration options + are read from stdin. + + The -method flag allows alternative authentication providers to be used, + such as userpass, github, or cert. For these, additional "key=value" pairs + may be required. For example, to authenticate to the userpass auth backend: + + $ vault auth -method=userpass username=my-username + + Use "vault auth-help TYPE" for more information about the list of + configuration parameters and examples for a particular provider. Use the + "vault auth-list" command to see a list of enabled authentication providers. + + If an authentication provider is mounted at a different path, the -method + flag should by the canonical type, and the -path flag should be set to the + mount path. If a github authentication provider was mounted at "github-ent", + you would authenticate to this backend like this: + + $ vault auth -method=github -path=github-prod + + If the authentication is requested with response wrapping (via -wrap-ttl), + the returned token is automatically unwrapped unless: + + - The -only-token flag is used, in which case this command will output + the wrapping token + + - The -no-store flag is used, in which case this command will output + the details of the wrapping token. + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *AuthCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "method", + Target: &c.flagMethod, + Default: "token", + Completion: c.PredictVaultAvailableAuths(), + Usage: "Type of authentication to use such as \"userpass\" or " + + "\"ldap\". Note this corresponds to the TYPE, not the mount path. Use " + + "-path to specify the path where the authentication is mounted.", + }) + + f.StringVar(&StringVar{ + Name: "path", + Target: &c.flagPath, + Default: "", + Completion: c.PredictVaultAuths(), + Usage: "Mount point where the auth backend is enabled. This defaults to " + + "the TYPE of backend (e.g. userpass -> userpass/).", + }) + + f.BoolVar(&BoolVar{ + Name: "no-verify", + Target: &c.flagNoVerify, + Default: false, + Usage: "Do not verify the token after authentication. By default, Vault " + + "issue a request to get more metdata about the token. This request " + + "against the use-limit of the token. Set this to true to disable the " + + "post-authenication lookup.", + }) + + f.BoolVar(&BoolVar{ + Name: "no-store", + Target: &c.flagNoStore, + Default: false, + Usage: "Do not persist the token to the token helper (usually the " + + "local filesystem) after authentication for use in future requests. " + + "The token will only be displayed in the command output.", + }) + + f.BoolVar(&BoolVar{ + Name: "only-token", + Target: &c.flagOnlyToken, + Default: false, + Usage: "Output only the token with no verification. This flag is a " + + "shortcut for \"-field=token -no-store -no-verify\". Setting those " + + "flags to other values will have no affect.", + }) + + // Deprecations + // TODO: remove in Vault 0.9.0 + + f.BoolVar(&BoolVar{ + Name: "token-only", // Prefer only-token + Target: &c.flagTokenOnly, + Default: false, + Hidden: true, + }) + + f.BoolVar(&BoolVar{ + Name: "methods", // Prefer auth-list + Target: &c.flagMethods, + Default: false, + Hidden: true, + }) + + f.BoolVar(&BoolVar{ + Name: "method-help", // Prefer auth-help + Target: &c.flagMethodHelp, + Default: false, + Hidden: true, + }) + + return set +} + +func (c *AuthCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *AuthCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *AuthCommand) Run(args []string) int { - var method, authPath string - var methods, methodHelp, noVerify, noStore, tokenOnly bool - flags := c.Meta.FlagSet("auth", meta.FlagSetDefault) - flags.BoolVar(&methods, "methods", false, "") - flags.BoolVar(&methodHelp, "method-help", false, "") - flags.BoolVar(&noVerify, "no-verify", false, "") - flags.BoolVar(&noStore, "no-store", false, "") - flags.BoolVar(&tokenOnly, "token-only", false, "") - flags.StringVar(&method, "method", "", "method") - flags.StringVar(&authPath, "path", "", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - if methods { - return c.listMethods() + args = f.Args() + + // Deprecations - do this before any argument validations + // TODO: remove in 0.9.0 + switch { + case c.flagMethods: + c.UI.Warn(wrapAtLength( + "WARNING! The -methods flag is deprecated. Please use " + + "\"vault auth-list\". This flag will be removed in the next major " + + "release of Vault.")) + cmd := &AuthListCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + } + return cmd.Run(nil) + case c.flagMethodHelp: + c.UI.Warn(wrapAtLength( + "WARNING! The -method-help flag is deprecated. Please use " + + "\"vault auth-help\". This flag will be removed in the next major " + + "release of Vault.")) + cmd := &AuthHelpCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + Handlers: c.Handlers, + } + return cmd.Run([]string{c.flagMethod}) } - args = flags.Args() + // TODO: remove in 0.9.0 + if c.flagTokenOnly { + c.UI.Warn(wrapAtLength( + "WARNING! The -token-only flag is deprecated. Plase use -only-token " + + "instead. This flag will be removed in the next major release of " + + "Vault.")) + c.flagOnlyToken = c.flagTokenOnly + } + + // Set the right flags if the user requested only-token - this overrides + // any previously configured values, as documented. + if c.flagOnlyToken { + c.flagNoStore = true + c.flagNoVerify = true + c.flagField = "token" + } + + // Get the auth method + authMethod := sanitizePath(c.flagMethod) + if authMethod == "" { + authMethod = "token" + } + + // If no path is specified, we default the path to the backend type + // or use the plugin name if it's a plugin backend + authPath := c.flagPath + if authPath == "" { + authPath = ensureTrailingSlash(authMethod) + } + + // Get the handler function + authHandler, ok := c.Handlers[authMethod] + if !ok { + c.UI.Error(wrapAtLength(fmt.Sprintf( + "Unknown authentication method: %s. Use \"vault auth-list\" to see the "+ + "complete list of authentication providers. Additionally, some "+ + "authentication providers are only available via the HTTP API.", + authMethod))) + return 1 + } + + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + // If the user provided a token, pass it along to the auth provier. + if authMethod == "token" && len(args) == 1 { + args = []string{"token=" + args[0]} + } + + config, err := parseArgsDataString(stdin, args) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing configuration: %s", err)) + return 1 + } + + // If the user did not specify a mount path, use the provided mount path. + if config["mount"] == "" && authPath != "" { + config["mount"] = authPath + } + + // Create the client + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Authenticate delegation to the auth handler + secret, err := authHandler.Auth(client, config) + if err != nil { + c.UI.Error(wrapAtLength(fmt.Sprintf( + "Error authenticating: %s", err))) + return 2 + } + + // Unset any previous token wrapping functionality. If the original request + // was for a wrapped token, we don't want future requests to be wrapped. + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + // Recursively extract the token, handling wrapping + unwrap := !c.flagOnlyToken && !c.flagNoStore + secret, isWrapped, err := c.extractToken(client, secret, unwrap) + if err != nil { + c.UI.Error(fmt.Sprintf("Error extracting token: %s", err)) + return 2 + } + if secret == nil { + c.UI.Error("Vault returned an empty secret") + return 2 + } + + // Handle special cases if the token was wrapped + if isWrapped { + if c.flagOnlyToken { + return PrintRawField(c.UI, secret, "wrapping_token") + } + if c.flagNoStore { + return OutputSecret(c.UI, c.flagFormat, secret) + } + } + + // If we got this far, verify we have authentication data before continuing + if secret.Auth == nil { + c.UI.Error(wrapAtLength( + "Vault returned a secret, but the secret has no authentication " + + "information attached. This should never happen and is likely a " + + "bug.")) + return 2 + } + + // Pull the token itself out, since we don't need the rest of the auth + // information anymore/. + token := secret.Auth.ClientToken tokenHelper, err := c.TokenHelper() if err != nil { - c.Ui.Error(fmt.Sprintf( + c.UI.Error(fmt.Sprintf( "Error initializing token helper: %s\n\n"+ "Please verify that the token helper is available and properly\n"+ "configured for your system. Please refer to the documentation\n"+ @@ -69,489 +339,114 @@ func (c *AuthCommand) Run(args []string) int { return 1 } - // token is where the final token will go - handler := c.Handlers[method] + if !c.flagNoVerify { + // Verify the token and pull it's list of policies + client.SetToken(token) + client.SetWrappingLookupFunc(func(string, string) string { return "" }) - // Read token from stdin if first arg is exactly "-" - var stdin io.Reader = os.Stdin - if c.testStdin != nil { - stdin = c.testStdin - } - - if len(args) > 0 && args[0] == "-" { - stdinR := bufio.NewReader(stdin) - args[0], err = stdinR.ReadString('\n') - if err != nil && err != io.EOF { - c.Ui.Error(fmt.Sprintf("Error reading from stdin: %s", err)) - return 1 + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + c.UI.Error(fmt.Sprintf("Error verifying token: %s", err)) + return 2 } - args[0] = strings.TrimSpace(args[0]) - } - - if method == "" { - token := "" - if len(args) > 0 { - token = args[0] - } - - handler = &tokenAuthHandler{Token: token} - args = nil - - switch authPath { - case "", "auth/token": - default: - c.Ui.Error("Token authentication does not support custom paths") - return 1 + if secret == nil { + c.UI.Error("Empty response from lookup-self") + return 2 } } - if handler == nil { - methods := make([]string, 0, len(c.Handlers)) - for k := range c.Handlers { - methods = append(methods, k) - } - sort.Strings(methods) - - c.Ui.Error(fmt.Sprintf( - "Unknown authentication method: %s\n\n"+ - "Please use a supported authentication method. The list of supported\n"+ - "authentication methods is shown below. Note that this list may not\n"+ - "be exhaustive: Vault may support other auth methods. For auth methods\n"+ - "unsupported by the CLI, please use the HTTP API.\n\n"+ - "%s", - method, - strings.Join(methods, ", "))) - return 1 - } - - if methodHelp { - c.Ui.Output(handler.Help()) - return 0 - } - - // Warn if the VAULT_TOKEN environment variable is set, as that will take - // precedence. Don't output on token-only since we're likely piping output. - if os.Getenv("VAULT_TOKEN") != "" && !tokenOnly { - c.Ui.Output("==> WARNING: VAULT_TOKEN environment variable set!\n") - c.Ui.Output(" The environment variable takes precedence over the value") - c.Ui.Output(" set by the auth command. Either update the value of the") - c.Ui.Output(" environment variable or unset it to use the new token.\n") - } - - var vars map[string]string - if len(args) > 0 { - builder := kvbuilder.Builder{Stdin: os.Stdin} - if err := builder.Add(args...); err != nil { - c.Ui.Error(err.Error()) - return 1 + if !c.flagNoStore { + // Store the token in the local client + if err := tokenHelper.Store(token); err != nil { + c.UI.Error(fmt.Sprintf("Error storing token: %s", err)) + c.UI.Error(wrapAtLength( + "Authentication was successful, but the token was not persisted. The " + + "resulting token is shown below for your records.")) + OutputSecret(c.UI, c.flagFormat, secret) + return 2 } - if err := mapstructure.Decode(builder.Map(), &vars); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing options: %s", err)) - return 1 + // Warn if the VAULT_TOKEN environment variable is set, as that will take + // precedence. Don't output on token-only since we're likely piping output. + if c.flagField == "" && c.flagFormat == "table" { + if os.Getenv("VAULT_TOKEN") != "" { + c.UI.Warn(wrapAtLength("WARNING! The VAULT_TOKEN environment variable " + + "is set! This takes precedence over the value set by this command. To " + + "use the value set by this command, unset the VAULT_TOKEN environment " + + "variable or set it to the token displayed below.")) + } } + } + + // If the user requested a particular field, print that out now since we + // are likely piping to another process. + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + // Output the secret as json or yaml if requested. We have to maintain + // backwards compatiability + if c.flagFormat != "table" { + return OutputSecret(c.UI, c.flagFormat, secret) + } + + output := "Success! You are now authenticated. " + if c.flagNoVerify { + output += "The token was not verified for validity. " + } + if c.flagNoStore { + output += "The token was not stored in the token helper. " } else { - vars = make(map[string]string) + output += "The token information displayed below is already stored in " + + "the token helper. You do NOT need to run \"vault auth\" again." + } + c.UI.Output(wrapAtLength(output) + "\n") + + // TODO make this consistent with other printed token secrets. + c.UI.Output(fmt.Sprintf("token: %s", secret.TokenID())) + c.UI.Output(fmt.Sprintf("accessor: %s", secret.TokenAccessor())) + + if ttl := secret.TokenTTL(); ttl != 0 { + c.UI.Output(fmt.Sprintf("duration: %s", ttl)) } - // Build the client so we can auth - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client to auth: %s", err)) - return 1 + c.UI.Output(fmt.Sprintf("renewable: %t", secret.TokenIsRenewable())) + + if policies := secret.TokenPolicies(); len(policies) > 0 { + c.UI.Output(fmt.Sprintf("policies: %s", policies)) } - if authPath != "" { - vars["mount"] = authPath - } + return 0 +} - // Authenticate - secret, err := handler.Auth(client, vars) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - if secret == nil { - c.Ui.Error("Empty response from auth helper") - return 1 - } - - // If we had requested a wrapped token, we want to unset that request - // before performing further functions - client.SetWrappingLookupFunc(func(string, string) string { - return "" - }) - -CHECK_TOKEN: - var token string +// extractToken extracts the token from the given secret, automatically +// unwrapping responses and handling error conditions if unwrap is true. The +// result also returns whether it was a wrapped resonse that was not unwrapped. +func (c *AuthCommand) extractToken(client *api.Client, secret *api.Secret, unwrap bool) (*api.Secret, bool, error) { switch { case secret == nil: - c.Ui.Error("Empty response from auth helper") - return 1 + return nil, false, fmt.Errorf("empty response from auth helper") case secret.Auth != nil: - token = secret.Auth.ClientToken + return secret, false, nil case secret.WrapInfo != nil: if secret.WrapInfo.WrappedAccessor == "" { - c.Ui.Error("Got a wrapped response from Vault but wrapped reply does not seem to contain a token") - return 1 + return nil, false, fmt.Errorf("wrapped response does not contain a token") } - if tokenOnly { - c.Ui.Output(secret.WrapInfo.Token) - return 0 - } - if noStore { - return OutputSecret(c.Ui, "table", secret) + + if !unwrap { + return secret, true, nil } + client.SetToken(secret.WrapInfo.Token) - secret, err = client.Logical().Unwrap("") - goto CHECK_TOKEN + secret, err := client.Logical().Unwrap("") + if err != nil { + return nil, false, err + } + return c.extractToken(client, secret, unwrap) default: - c.Ui.Error("No auth or wrapping info in auth helper response") - return 1 - } - - // Cache the previous token so that it can be restored if authentication fails - var previousToken string - if previousToken, err = tokenHelper.Get(); err != nil { - c.Ui.Error(fmt.Sprintf("Error caching the previous token: %s\n\n", err)) - return 1 - } - - if tokenOnly { - c.Ui.Output(token) - return 0 - } - - // Store the token! - if !noStore { - if err := tokenHelper.Store(token); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error storing token: %s\n\n"+ - "Authentication was not successful and did not persist.\n"+ - "Please reauthenticate, or fix the issue above if possible.", - err)) - return 1 - } - } - - if noVerify { - c.Ui.Output(fmt.Sprintf( - "Authenticated - no token verification has been performed.", - )) - - if noStore { - if err := tokenHelper.Erase(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error removing prior token: %s\n\n"+ - "Authentication was successful, but unable to remove the\n"+ - "previous token.", - err)) - return 1 - } - } - return 0 - } - - // Build the client again so it can read the token we just wrote - client, err = c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client to verify the token: %s", err)) - if !noStore { - if err := tokenHelper.Store(previousToken); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error restoring the previous token: %s\n\n"+ - "Please reauthenticate with a valid token.", - err)) - } - } - return 1 - } - client.SetWrappingLookupFunc(func(string, string) string { - return "" - }) - - // If in no-store mode it won't have read the token from a token-helper (or - // will read an old one) so set it explicitly - if noStore { - client.SetToken(token) - } - - // Verify the token - secret, err = client.Auth().Token().LookupSelf() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error validating token: %s", err)) - if err := tokenHelper.Store(previousToken); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error restoring the previous token: %s\n\n"+ - "Please reauthenticate with a valid token.", - err)) - } - return 1 - } - if secret == nil && !noStore { - c.Ui.Error(fmt.Sprintf("Error: Invalid token")) - if err := tokenHelper.Store(previousToken); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error restoring the previous token: %s\n\n"+ - "Please reauthenticate with a valid token.", - err)) - } - return 1 - } - - if noStore { - if err := tokenHelper.Erase(); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error removing prior token: %s\n\n"+ - "Authentication was successful, but unable to remove the\n"+ - "previous token.", - err)) - return 1 - } - } - - // Get the policies we have - policiesRaw, ok := secret.Data["policies"] - if !ok || policiesRaw == nil { - policiesRaw = []interface{}{"unknown"} - } - var policies []string - for _, v := range policiesRaw.([]interface{}) { - policies = append(policies, v.(string)) - } - - output := "Successfully authenticated! You are now logged in." - if noStore { - output += "\nThe token has not been stored to the configured token helper." - } - if method != "" { - output += "\nThe token below is already saved in the session. You do not" - output += "\nneed to \"vault auth\" again with the token." - } - output += fmt.Sprintf("\ntoken: %s", secret.Data["id"]) - output += fmt.Sprintf("\ntoken_duration: %s", secret.Data["ttl"].(json.Number).String()) - if len(policies) > 0 { - output += fmt.Sprintf("\ntoken_policies: %v", policies) - } - - c.Ui.Output(output) - - return 0 - -} - -func (c *AuthCommand) getMethods() (map[string]*api.AuthMount, error) { - client, err := c.Client() - if err != nil { - return nil, err - } - client.SetWrappingLookupFunc(func(string, string) string { - return "" - }) - - auth, err := client.Sys().ListAuth() - if err != nil { - return nil, err - } - - return auth, nil -} - -func (c *AuthCommand) listMethods() int { - auth, err := c.getMethods() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading auth table: %s", err)) - return 1 - } - - paths := make([]string, 0, len(auth)) - for path := range auth { - paths = append(paths, path) - } - sort.Strings(paths) - - columns := []string{"Path | Type | Accessor | Default TTL | Max TTL | Replication Behavior | Description"} - for _, path := range paths { - auth := auth[path] - defTTL := "system" - if auth.Config.DefaultLeaseTTL != 0 { - defTTL = strconv.Itoa(auth.Config.DefaultLeaseTTL) - } - maxTTL := "system" - if auth.Config.MaxLeaseTTL != 0 { - maxTTL = strconv.Itoa(auth.Config.MaxLeaseTTL) - } - replicatedBehavior := "replicated" - if auth.Local { - replicatedBehavior = "local" - } - columns = append(columns, fmt.Sprintf( - "%s | %s | %s | %s | %s | %s | %s", path, auth.Type, auth.Accessor, defTTL, maxTTL, replicatedBehavior, auth.Description)) - } - - c.Ui.Output(columnize.SimpleFormat(columns)) - return 0 -} - -func (c *AuthCommand) Synopsis() string { - return "Prints information about how to authenticate with Vault" -} - -func (c *AuthCommand) Help() string { - helpText := ` -Usage: vault auth [options] [auth-information] - - Authenticate with Vault using the given token or via any supported - authentication backend. - - By default, the -method is assumed to be token. If not supplied via the - command-line, a prompt for input will be shown. If the authentication - information is "-", it will be read from stdin. - - The -method option allows alternative authentication methods to be used, - such as userpass, GitHub, or TLS certificates. For these, additional - values as "key=value" pairs may be required. For example, to authenticate - to the userpass auth backend: - - $ vault auth -method=userpass username=my-username - - Use "-method-help" to get help for a specific method. - - If an auth backend is enabled at a different path, the "-method" flag - should still point to the canonical name, and the "-path" flag should be - used. If a GitHub auth backend was mounted as "github-private", one would - authenticate to this backend via: - - $ vault auth -method=github -path=github-private - - The value of the "-path" flag is supplied to auth providers as the "mount" - option in the payload to specify the mount point. - - If response wrapping is used (via -wrap-ttl), the returned token will be - automatically unwrapped unless: - * -token-only is used, in which case the wrapping token will be output - * -no-store is used, in which case the details of the wrapping token - will be printed - -General Options: - - ` + meta.GeneralOptionsUsage() + ` - -Auth Options: - - -method=name Use the method given here, which is a type of backend, not - the path. If this authentication method is not available, - exit with code 1. - - -method-help If set, the help for the selected method will be shown. - - -methods List the available auth methods. - - -no-verify Do not verify the token after creation; avoids a use count - decrement. - - -no-store Do not store the token after creation; it will only be - displayed in the command output. - - -token-only Output only the token to stdout. This implies -no-verify - and -no-store. - - -path The path at which the auth backend is enabled. If an auth - backend is mounted at multiple paths, this option can be - used to authenticate against specific paths. -` - return strings.TrimSpace(helpText) -} - -// tokenAuthHandler handles retrieving the token from the command-line. -type tokenAuthHandler struct { - Token string -} - -func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (*api.Secret, error) { - token := h.Token - if token == "" { - var err error - - // No arguments given, read the token from user input - fmt.Printf("Token (will be hidden): ") - token, err = password.Read(os.Stdin) - fmt.Printf("\n") - if err != nil { - return nil, fmt.Errorf( - "Error attempting to ask for token. The raw error message\n"+ - "is shown below, but the most common reason for this error is\n"+ - "that you attempted to pipe a value into auth. If you want to\n"+ - "pipe the token, please pass '-' as the token argument.\n\n"+ - "Raw error: %s", err) - } - } - - if token == "" { - return nil, fmt.Errorf( - "A token must be passed to auth. Please view the help\n" + - "for more information.") - } - - return &api.Secret{ - Auth: &api.SecretAuth{ - ClientToken: token, - }, - }, nil -} - -func (h *tokenAuthHandler) Help() string { - help := ` -No method selected with the "-method" flag, so the "auth" command assumes -you'll be using raw token authentication. For this, specify the token to -authenticate as the parameter to "vault auth". Example: - - vault auth 123456 - -The token used to authenticate must come from some other source. A root -token is created when Vault is first initialized. After that, subsequent -tokens are created via the API or command line interface (with the -"token"-prefixed commands). -` - - return strings.TrimSpace(help) -} - -func (c *AuthCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictNothing -} - -func (c *AuthCommand) AutocompleteFlags() complete.Flags { - var predictFunc complete.PredictFunc = func(a complete.Args) []string { - auths, err := c.getMethods() - if err != nil { - return []string{} - } - - methods := make([]string, 0, len(auths)) - for _, auth := range auths { - if strings.HasPrefix(auth.Type, a.Last) { - methods = append(methods, auth.Type) - } - } - - return methods - } - - return complete.Flags{ - "-method": predictFunc, - "-methods": complete.PredictNothing, - "-method-help": complete.PredictNothing, - "-no-verify": complete.PredictNothing, - "-no-store": complete.PredictNothing, - "-token-only": complete.PredictNothing, - "-path": complete.PredictNothing, + return nil, false, fmt.Errorf("no auth or wrapping info in response") } } diff --git a/command/auth_test.go b/command/auth_test.go index 8243129083..ca1e916284 100644 --- a/command/auth_test.go +++ b/command/auth_test.go @@ -1,400 +1,538 @@ package command import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" "strings" "testing" - credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" - "github.com/hashicorp/vault/logical" + "github.com/mitchellh/cli" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/cli" + credToken "github.com/hashicorp/vault/builtin/credential/token" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/command/token" ) -func TestAuth_methods(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testAuthCommand(tb testing.TB) (*cli.MockUi, *AuthCommand) { + tb.Helper() - testAuthInit(t) + ui := cli.NewMockUi() + return ui, &AuthCommand{ + BaseCommand: &BaseCommand{ + UI: ui, - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - TokenHelper: DefaultTokenHelper, + // Override to our own token helper + tokenHelper: token.NewTestingTokenHelper(), + }, + Handlers: map[string]AuthHandler{ + "token": &credToken.CLIHandler{}, + "userpass": &credUserpass.CLIHandler{}, }, - } - - args := []string{ - "-address", addr, - "-methods", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - output := ui.OutputWriter.String() - if !strings.Contains(output, "token") { - t.Fatalf("bad: %#v", output) } } -func TestAuth_token(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestAuthCommand_Run(t *testing.T) { + t.Parallel() - testAuthInit(t) - - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, + deprecations := []struct { + name string + args []string + out string + code int + }{ + { + "methods", + []string{"-methods"}, + "token/", + 0, + }, + { + "method_help", + []string{"-method", "userpass", "-method-help"}, + "Usage: vault auth -method=userpass", + 0, }, } - args := []string{ - "-address", addr, - token, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("deprecations", func(t *testing.T) { + t.Parallel() - helper, err := c.TokenHelper() - if err != nil { - t.Fatalf("err: %s", err) - } + for _, tc := range deprecations { + tc := tc - actual, err := helper.Get() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - if actual != token { - t.Fatalf("bad: %s", actual) - } -} + client, closer := testVaultServer(t) + defer closer() -func TestAuth_wrapping(t *testing.T) { - baseConfig := &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": credUserpass.Factory, - }, - } - cluster := vault.NewTestCluster(t, baseConfig, &vault.TestClusterOptions{ - HandlerFunc: http.Handler, - BaseListenAddress: "127.0.0.1:8200", + ui, cmd := testAuthCommand(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) + } + }) + } }) - cluster.Start() - defer cluster.Cleanup() - testAuthInit(t) + t.Run("custom_path", func(t *testing.T) { + t.Parallel() - client := cluster.Cores[0].Client - err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/my-auth/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "-method", "userpass", + "-path", "my-auth", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! You are now authenticated." + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to be %q", combined, expected) + } + + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if l, exp := len(storedToken), 36; l != exp { + t.Errorf("expected token to be %d characters, was %d: %q", exp, l, storedToken) + } }) - if err != nil { - t.Fatal(err) - } - _, err = client.Logical().Write("auth/userpass/users/foo", map[string]interface{}{ - "password": "bar", - "policies": "zip,zap", + + t.Run("no_verify", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + NumUses: 1, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + _, cmd := testAuthCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-no-verify", + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + lookup, err := client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + // There was 1 use to start, make sure we didn't use it (verifying would + // use it). + uses := lookup.TokenRemainingUses() + if uses != 1 { + t.Errorf("expected %d to be %d", uses, 1) + } }) - if err != nil { - t.Fatal(err) - } - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - Handlers: map[string]AuthHandler{ - "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, - }, - } + t.Run("no_store", func(t *testing.T) { + t.Parallel() - args := []string{ - "-address", - "https://127.0.0.1:8200", - "-tls-skip-verify", - "-method", - "userpass", - "username=foo", - "password=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServer(t) + defer closer() - // Test again with wrapping - ui = new(cli.MockUi) - c = &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - Handlers: map[string]AuthHandler{ - "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, - }, - } + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken - args = []string{ - "-address", - "https://127.0.0.1:8200", - "-tls-skip-verify", - "-wrap-ttl", - "5m", - "-method", - "userpass", - "username=foo", - "password=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + _, cmd := testAuthCommand(t) + cmd.client = client - // Test again with no-store - ui = new(cli.MockUi) - c = &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - Handlers: map[string]AuthHandler{ - "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, - }, - } + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } - args = []string{ - "-address", - "https://127.0.0.1:8200", - "-tls-skip-verify", - "-wrap-ttl", - "5m", - "-no-store", - "-method", - "userpass", - "username=foo", - "password=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + // Ensure we have no token to start + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Errorf("expected token helper to be empty: %s: %q", err, storedToken) + } - // Test again with wrapping and token-only - ui = new(cli.MockUi) - c = &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - Handlers: map[string]AuthHandler{ - "userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"}, - }, - } + code := cmd.Run([]string{ + "-no-store", + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } - args = []string{ - "-address", - "https://127.0.0.1:8200", - "-tls-skip-verify", - "-wrap-ttl", - "5m", - "-token-only", - "-method", - "userpass", - "username=foo", - "password=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - token := strings.TrimSpace(ui.OutputWriter.String()) - if token == "" { - t.Fatal("expected to find token in output") - } - secret, err := client.Logical().Unwrap(token) - if err != nil { - t.Fatal(err) - } - if secret.Auth.ClientToken == "" { - t.Fatal("no client token found") - } + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if exp := ""; storedToken != exp { + t.Errorf("expected %q to be %q", storedToken, exp) + } + }) + + t.Run("stores", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + _, cmd := testAuthCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if storedToken != token { + t.Errorf("expected %q to be %q", storedToken, token) + } + }) + + t.Run("only_token", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "-only-token", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Verify only the token was printed + token := ui.OutputWriter.String() + ui.ErrorWriter.String() + if l, exp := len(token), 36; l != exp { + t.Errorf("expected token to be %d characters, was %d: %q", exp, l, token) + } + + // Verify the token was not stored + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Fatalf("expted token to not be stored: %s: %q", err, storedToken) + } + }) + + t.Run("failure_no_store", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testAuthCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "not-a-real-token", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error verifying token: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + }) + + t.Run("wrap_auto_unwrap", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + _, cmd := testAuthCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + token, err := tokenHelper.Get() + if err != nil || token == "" { + t.Fatalf("expected token from helper: %s: %q", err, token) + } + + // Ensure the resulting token is unwrapped + secret, err := client.Auth().Token().LookupSelf() + if err != nil { + t.Error(err) + } + + if secret.WrapInfo != nil { + t.Errorf("expected to be unwrapped: %#v", secret) + } + }) + + t.Run("wrap_only_token", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-only-token", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + storedToken, err := tokenHelper.Get() + if err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + + token := ui.OutputWriter.String() + if token == "" { + t.Errorf("expected %q to not be %q", token, "") + } + if strings.Contains(token, "\n") { + t.Errorf("expected %q to not contain %q", token, "\n") + } + + // Ensure the resulting token is, in fact, still wrapped. + client.SetToken(token) + secret, err := client.Logical().Unwrap("") + if err != nil { + t.Error(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("expected secret to have auth: %#v", secret) + } + }) + + t.Run("wrap_no_store", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-no-store", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + storedToken, err := tokenHelper.Get() + if err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + + expected := "wrapping_token" + output := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(output, expected) { + t.Errorf("expected %q to contain %q", output, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuthCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "token", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error verifying token: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuthCommand(t) + assertNoTabs(t, cmd) + }) } - -func TestAuth_token_nostore(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - testAuthInit(t) - - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - } - - args := []string{ - "-address", addr, - "-no-store", - token, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - helper, err := c.TokenHelper() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := helper.Get() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual != "" { - t.Fatalf("bad: %s", actual) - } -} - -func TestAuth_stdin(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - testAuthInit(t) - - stdinR, stdinW := io.Pipe() - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - testStdin: stdinR, - } - - go func() { - stdinW.Write([]byte(token)) - stdinW.Close() - }() - - args := []string{ - "-address", addr, - "-", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - -func TestAuth_badToken(t *testing.T) { - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - testAuthInit(t) - - ui := new(cli.MockUi) - c := &AuthCommand{ - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - } - - args := []string{ - "-address", addr, - "not-a-valid-token", - } - if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - -func TestAuth_method(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - testAuthInit(t) - - ui := new(cli.MockUi) - c := &AuthCommand{ - Handlers: map[string]AuthHandler{ - "test": &testAuthHandler{}, - }, - Meta: meta.Meta{ - Ui: ui, - TokenHelper: DefaultTokenHelper, - }, - } - - args := []string{ - "-address", addr, - "-method=test", - "foo=" + token, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - helper, err := c.TokenHelper() - if err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := helper.Get() - if err != nil { - t.Fatalf("err: %s", err) - } - - if actual != token { - t.Fatalf("bad: %s", actual) - } -} - -func testAuthInit(t *testing.T) { - td, err := ioutil.TempDir("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - - // Set the HOME env var so we get that right - os.Setenv("HOME", td) - - // Write a .vault config to use our custom token helper - config := fmt.Sprintf( - "token_helper = \"\"\n") - ioutil.WriteFile(filepath.Join(td, ".vault"), []byte(config), 0644) -} - -type testAuthHandler struct{} - -func (h *testAuthHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { - return &api.Secret{ - Auth: &api.SecretAuth{ - ClientToken: m["foo"], - }, - }, nil -} - -func (h *testAuthHandler) Help() string { return "" } From fb5fc77209d88ba9c992d1758eedba673a9532c3 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:59:33 -0400 Subject: [PATCH 045/281] Update auth-disable command --- command/auth_disable.go | 116 +++++++++++++--------- command/auth_disable_test.go | 187 ++++++++++++++++++++--------------- 2 files changed, 178 insertions(+), 125 deletions(-) diff --git a/command/auth_disable.go b/command/auth_disable.go index 621ce5907c..14cf092269 100644 --- a/command/auth_disable.go +++ b/command/auth_disable.go @@ -4,66 +4,88 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*AuthDisableCommand)(nil) +var _ cli.CommandAutocomplete = (*AuthDisableCommand)(nil) + // AuthDisableCommand is a Command that enables a new endpoint. type AuthDisableCommand struct { - meta.Meta -} - -func (c *AuthDisableCommand) Run(args []string) int { - flags := c.Meta.FlagSet("auth-disable", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - args = flags.Args() - if len(args) != 1 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\nauth-disable expects one argument: the path to disable.")) - return 1 - } - - path := args[0] - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if err := client.Sys().DisableAuth(path); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error: %s", err)) - return 2 - } - - c.Ui.Output(fmt.Sprintf( - "Disabled auth provider at path '%s' if it was enabled", path)) - - return 0 + *BaseCommand } func (c *AuthDisableCommand) Synopsis() string { - return "Disable an auth provider" + return "Disables an auth provider" } func (c *AuthDisableCommand) Help() string { helpText := ` -Usage: vault auth-disable [options] path +Usage: vault auth-disable [options] PATH - Disable an already-enabled auth provider. + Disables an existing authentication provider at the given PATH. The argument + corresponds to the PATH of the mount, not the TYPE!. Once the auth provider + is disabled its path can no longer be used to authenticate. All access tokens + generated via the disabled auth provider are revoked. - Once the auth provider is disabled its path can no longer be used - to authenticate. All access tokens generated via the disabled auth provider - will be revoked. This command will block until all tokens are revoked. - If the command is exited early the tokens will still be revoked. + This command will block until all tokens are revoked. + + Disable the authentication provider at userpass/: + + $ vault auth-disable userpass + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *AuthDisableCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *AuthDisableCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultAuths() +} + +func (c *AuthDisableCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *AuthDisableCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + path := ensureTrailingSlash(sanitizePath(args[0])) + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if err := client.Sys().DisableAuth(path); err != nil { + c.UI.Error(fmt.Sprintf("Error disabling auth at %s: %s", path, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Disabled the auth provider (if it existed) at: %s", path)) + return 0 +} diff --git a/command/auth_disable_test.go b/command/auth_disable_test.go index fb2b91fb23..86b1f90560 100644 --- a/command/auth_disable_test.go +++ b/command/auth_disable_test.go @@ -1,102 +1,133 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestAuthDisable(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testAuthDisableCommand(tb testing.TB) (*cli.MockUi, *AuthDisableCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &AuthDisableCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &AuthDisableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - args := []string{ - "-address", addr, - "noop", - } - - // Run the command once to setup the client, it will fail - c.Run(args) - - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - if err := client.Sys().EnableAuth("noop", "noop", ""); err != nil { - t.Fatalf("err: %s", err) - } - - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - mounts, err := client.Sys().ListAuth() - if err != nil { - t.Fatalf("err: %s", err) - } - - if _, ok := mounts["noop"]; ok { - t.Fatal("should not have noop mount") - } } -func TestAuthDisableWithOptions(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestAuthDisableCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) - c := &AuthDisableCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + nil, + "Not enough arguments", + 1, + }, + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, }, } - args := []string{ - "-address", addr, - "noop", - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Run the command once to setup the client, it will fail - c.Run(args) + for _, tc := range cases { + tc := tc - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - if err := client.Sys().EnableAuthWithOptions("noop", &api.EnableAuthOptions{ - Type: "noop", - Description: "", - }); err != nil { - t.Fatalf("err: %#v", err) - } + ui, cmd := testAuthDisableCommand(t) - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + code := cmd.Run(tc.args) + if code != tc.code { + t.Errorf("expected %d to be %d", code, tc.code) + } - mounts, err := client.Sys().ListAuth() - if err != nil { - t.Fatalf("err: %s", err) - } + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, tc.out) { + t.Errorf("expected %q to contain %q", combined, tc.out) + } + }) + } + }) - if _, ok := mounts["noop"]; ok { - t.Fatal("should not have noop mount") - } + t.Run("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthDisableCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-auth", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Disabled the auth provider" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatal(err) + } + + if auth, ok := auths["my-auth/"]; ok { + t.Errorf("expected auth to be disabled: %#v", auth) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuthDisableCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-auth", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error disabling auth at my-auth/: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuthDisableCommand(t) + assertNoTabs(t, cmd) + }) } From 5988dfc4367d24ec381a5041c7ddc7f7c94982fd Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:59:39 -0400 Subject: [PATCH 046/281] Update auth-enable command --- command/auth_enable.go | 237 ++++++++++++++++++++---------------- command/auth_enable_test.go | 160 +++++++++++++++++++----- 2 files changed, 258 insertions(+), 139 deletions(-) diff --git a/command/auth_enable.go b/command/auth_enable.go index e6b7f20f24..a5ba67f27d 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -5,137 +5,162 @@ import ( "strings" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*AuthEnableCommand)(nil) +var _ cli.CommandAutocomplete = (*AuthEnableCommand)(nil) + // AuthEnableCommand is a Command that enables a new endpoint. type AuthEnableCommand struct { - meta.Meta -} + *BaseCommand -func (c *AuthEnableCommand) Run(args []string) int { - var description, path, pluginName string - var local bool - flags := c.Meta.FlagSet("auth-enable", meta.FlagSetDefault) - flags.StringVar(&description, "description", "", "") - flags.StringVar(&path, "path", "", "") - flags.StringVar(&pluginName, "plugin-name", "", "") - flags.BoolVar(&local, "local", 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 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\nauth-enable expects one argument: the type to enable.")) - return 1 - } - - authType := args[0] - - // If no path is specified, we default the path to the backend type - // or use the plugin name if it's a plugin backend - if path == "" { - if authType == "plugin" { - path = pluginName - } else { - path = authType - } - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if err := client.Sys().EnableAuthWithOptions(path, &api.EnableAuthOptions{ - Type: authType, - Description: description, - Config: api.AuthConfigInput{ - PluginName: pluginName, - }, - Local: local, - }); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error: %s", err)) - return 2 - } - - authTypeOutput := fmt.Sprintf("'%s'", authType) - if authType == "plugin" { - authTypeOutput = fmt.Sprintf("plugin '%s'", pluginName) - } - - c.Ui.Output(fmt.Sprintf( - "Successfully enabled %s at '%s'!", - authTypeOutput, path)) - - return 0 + flagDescription string + flagPath string + flagPluginName string + flagLocal bool } func (c *AuthEnableCommand) Synopsis() string { - return "Enable a new auth provider" + return "Enables a new auth provider" } func (c *AuthEnableCommand) Help() string { helpText := ` -Usage: vault auth-enable [options] type +Usage: vault auth-enable [options] TYPE - Enable a new auth provider. + Enables a new authentication provider. An authentication provider is + responsible for authenticating users or machiens and assigning them + policies with which they can access Vault. - This command enables a new auth provider. An auth provider is responsible - for authenticating a user and assigning them policies with which they can - access Vault. + Enable the userpass auth provider at userpass/: -General Options: -` + meta.GeneralOptionsUsage() + ` -Auth Enable Options: + $ vault auth-enable userpass - -description= Human-friendly description of the purpose of the - auth provider. This shows up in the auth -methods command. + Enable the LDAP auth provider at auth-prod/: - -path= Mount point for the auth provider. This defaults - to the type of the mount. This will make the auth - provider available at "/auth/" + $ vault auth-enable -path=auth-prod ldap - -plugin-name Name of the auth plugin to use based from the name - in the plugin catalog. + Enable a custom auth plugin (after it is registered in the plugin registry): + + $ vault auth-enable -path=my-auth -plugin-name=my-auth-plugin plugin + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() - -local Mark the mount as a local mount. Local mounts - are not replicated nor (if a secondary) - removed by replication. -` return strings.TrimSpace(helpText) } -func (c *AuthEnableCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictSet( - "approle", - "cert", - "aws", - "app-id", - "gcp", - "github", - "userpass", - "ldap", - "okta", - "radius", - "plugin", - ) +func (c *AuthEnableCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "description", + Target: &c.flagDescription, + Completion: complete.PredictAnything, + Usage: "Human-friendly description for the purpose of this " + + "authentication provider.", + }) + + f.StringVar(&StringVar{ + Name: "path", + Target: &c.flagPath, + Default: "", // The default is complex, so we have to manually document + Completion: complete.PredictAnything, + Usage: "Place where the auth provider will be accessible. This must be " + + "unique across all auth providers. This defaults to the \"type\" of " + + "the mount. The auth provider will be accessible at \"/auth/\".", + }) + + f.StringVar(&StringVar{ + Name: "plugin-name", + Target: &c.flagPluginName, + Completion: complete.PredictAnything, + Usage: "Name of the auth provider plugin. This plugin name must already " + + "exist in the Vault server's plugin catalog.", + }) + + f.BoolVar(&BoolVar{ + Name: "local", + Target: &c.flagLocal, + Default: false, + Usage: "Mark the auth provider as local-only. Local auth providers are " + + "not replicated nor removed by replication.", + }) + + return set +} + +func (c *AuthEnableCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultAvailableAuths() } func (c *AuthEnableCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-description": complete.PredictNothing, - "-path": complete.PredictNothing, - "-plugin-name": complete.PredictNothing, - "-local": complete.PredictNothing, - } + return c.Flags().Completions() +} + +func (c *AuthEnableCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + authType := strings.TrimSpace(args[0]) + + // If no path is specified, we default the path to the backend type + // or use the plugin name if it's a plugin backend + authPath := c.flagPath + if authPath == "" { + if authType == "plugin" { + authPath = c.flagPluginName + } else { + authPath = authType + } + } + + // Append a trailing slash to indicate it's a path in output + authPath = ensureTrailingSlash(authPath) + + if err := client.Sys().EnableAuthWithOptions(authPath, &api.EnableAuthOptions{ + Type: authType, + Description: c.flagDescription, + Config: api.AuthConfigInput{ + PluginName: c.flagPluginName, + }, + Local: c.flagLocal, + }); err != nil { + c.UI.Error(fmt.Sprintf("Error enabling %s auth: %s", authType, err)) + return 2 + } + + authThing := authType + " auth provider" + if authType == "plugin" { + authThing = c.flagPluginName + " plugin" + } + + c.UI.Output(fmt.Sprintf("Success! Enabled %s at: %s", authThing, authPath)) + return 0 } diff --git a/command/auth_enable_test.go b/command/auth_enable_test.go index 0f8348700f..00016d0138 100644 --- a/command/auth_enable_test.go +++ b/command/auth_enable_test.go @@ -1,50 +1,144 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestAuthEnable(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testAuthEnableCommand(tb testing.TB) (*cli.MockUi, *AuthEnableCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &AuthEnableCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &AuthEnableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestAuthEnableCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + nil, + "Not enough arguments", + 1, + }, + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "not_a_valid_auth", + []string{"nope_definitely_not_a_valid_mount_like_ever"}, + "", + 2, }, } - args := []string{ - "-address", addr, - "noop", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testAuthEnableCommand(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) + } + }) } - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run("integration", func(t *testing.T) { + t.Parallel() - mounts, err := client.Sys().ListAuth() - if err != nil { - t.Fatalf("err: %s", err) - } + client, closer := testVaultServer(t) + defer closer() - mount, ok := mounts["noop/"] - if !ok { - t.Fatal("should have noop mount") - } - if mount.Type != "noop" { - t.Fatal("should be noop type") - } + ui, cmd := testAuthEnableCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-path", "auth_integration/", + "-description", "The best kind of test", + "userpass", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Enabled userpass auth provider at:" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatal(err) + } + + authInfo, ok := auths["auth_integration/"] + if !ok { + t.Fatalf("expected mount to exist") + } + if exp := "userpass"; authInfo.Type != exp { + t.Errorf("expected %q to be %q", authInfo.Type, exp) + } + if exp := "The best kind of test"; authInfo.Description != exp { + t.Errorf("expected %q to be %q", authInfo.Description, exp) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuthEnableCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "userpass", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error enabling userpass auth: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuthEnableCommand(t) + assertNoTabs(t, cmd) + }) } From 4e55d014f5ccb870e6592a302ad7bf29fd599f42 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:59:45 -0400 Subject: [PATCH 047/281] Add new auth-help command --- command/auth_help.go | 126 +++++++++++++++++++++++++++++++ command/auth_help_test.go | 152 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 command/auth_help.go create mode 100644 command/auth_help_test.go diff --git a/command/auth_help.go b/command/auth_help.go new file mode 100644 index 0000000000..7fbd5e1443 --- /dev/null +++ b/command/auth_help.go @@ -0,0 +1,126 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*AuthHelpCommand)(nil) +var _ cli.CommandAutocomplete = (*AuthHelpCommand)(nil) + +// AuthHelpCommand is a Command that prints help output for a given auth +// provider +type AuthHelpCommand struct { + *BaseCommand + + Handlers map[string]AuthHandler +} + +func (c *AuthHelpCommand) Synopsis() string { + return "Prints usage for an auth provider" +} + +func (c *AuthHelpCommand) Help() string { + helpText := ` +Usage: vault path-help [options] TYPE | PATH + + Prints usage and help for an authentication provider. If provided a TYPE, + this command retrieves the default help for the given authentication + provider of that type. If given a PATH, this command returns the help + output for the authentication provider mounted at that path. If given a + PATH argument, the path must exist and be mounted. + + Get usage instructions for the userpass authentication provider: + + $ vault auth-help userpass + + Print usage for the authentication provider mounted at my-provider/ + + $ vault auth-help my-provider/: + + Each authentication provider produces its own help output. For additional + information, please view the online documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *AuthHelpCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *AuthHelpCommand) AutocompleteArgs() complete.Predictor { + handlers := make([]string, 0, len(c.Handlers)) + for k := range c.Handlers { + handlers = append(handlers, k) + } + return complete.PredictSet(handlers...) +} + +func (c *AuthHelpCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *AuthHelpCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Start with the assumption that we have an auth type, not a path. + authType := strings.TrimSpace(args[0]) + + authHandler, ok := c.Handlers[authType] + if !ok { + // There was no auth type by that name, see if it's a mount + auths, err := client.Sys().ListAuth() + if err != nil { + c.UI.Error(fmt.Sprintf("Error listing authentication providers: %s", err)) + return 2 + } + + authPath := ensureTrailingSlash(sanitizePath(args[0])) + auth, ok := auths[authPath] + if !ok { + c.UI.Error(fmt.Sprintf( + "Error retrieving help: unknown authentication provider: %s", args[0])) + return 1 + } + + authHandler, ok = c.Handlers[auth.Type] + if !ok { + c.UI.Error(wrapAtLength(fmt.Sprintf( + "INTERNAL ERROR! Found an authentication provider mounted at %s, but "+ + "its type %q is not registered in Vault. This is a bug and should "+ + "be reported. Please open an issue at github.com/hashicorp/vault.", + authPath, authType))) + return 2 + } + } + + c.UI.Output(authHandler.Help()) + return 0 +} diff --git a/command/auth_help_test.go b/command/auth_help_test.go new file mode 100644 index 0000000000..b157552019 --- /dev/null +++ b/command/auth_help_test.go @@ -0,0 +1,152 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" + + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" +) + +func testAuthHelpCommand(tb testing.TB) (*cli.MockUi, *AuthHelpCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &AuthHelpCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + Handlers: map[string]AuthHandler{ + "userpass": &credUserpass.CLIHandler{ + DefaultMount: "userpass", + }, + }, + } +} + +func TestAuthHelpCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "not_enough_args", + nil, + "Not enough arguments", + 1, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ui, cmd := testAuthHelpCommand(t) + + 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("path", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("foo", "userpass", ""); err != nil { + t.Fatal(err) + } + + ui, cmd := testAuthHelpCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "foo/", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Usage: vault auth -method=userpass" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("type", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + // No mounted auth backends + + ui, cmd := testAuthHelpCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "userpass", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Usage: vault auth -method=userpass" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuthHelpCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "sys/mounts", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing authentication providers: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuthHelpCommand(t) + assertNoTabs(t, cmd) + }) +} From ae4bf4eec73ef0a136276654e12f9b87e4d578de Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 4 Sep 2017 23:59:52 -0400 Subject: [PATCH 048/281] Add new auth-list command --- command/auth_list.go | 170 ++++++++++++++++++++++++++++++++++++++ command/auth_list_test.go | 105 +++++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 command/auth_list.go create mode 100644 command/auth_list_test.go diff --git a/command/auth_list.go b/command/auth_list.go new file mode 100644 index 0000000000..7adda391e6 --- /dev/null +++ b/command/auth_list.go @@ -0,0 +1,170 @@ +package command + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*AuthListCommand)(nil) +var _ cli.CommandAutocomplete = (*AuthListCommand)(nil) + +// AuthListCommand is a Command that lists the enabled authentication methods +// and data about them. +type AuthListCommand struct { + *BaseCommand + + flagDetailed bool +} + +func (c *AuthListCommand) Synopsis() string { + return "Lists enabled auth providers" +} + +func (c *AuthListCommand) Help() string { + helpText := ` +Usage: vault auth-methods [options] + + Lists the enabled authentication providers on the Vault server. This command + also outputs information about the provider including configuration and + human-friendly descriptions. A TTL of "system" indicates that the system + default is in use. + + List all enabled authentication providers: + + $ vault auth-list + + List all enabled authentication providers with detailed output: + + $ vault auth-list -detailed + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *AuthListCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "detailed", + Target: &c.flagDetailed, + Default: false, + Usage: "Print detailed information such as configuration and replication " + + "status about each authentication provider.", + }) + + return set +} + +func (c *AuthListCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *AuthListCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *AuthListCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + auths, err := client.Sys().ListAuth() + if err != nil { + c.UI.Error(fmt.Sprintf("Error listing enabled authentications: %s", err)) + return 2 + } + + if c.flagDetailed { + c.UI.Output(tableOutput(c.detailedMounts(auths))) + return 0 + } + + c.UI.Output(tableOutput(c.simpleMounts(auths))) + return 0 +} + +func (c *AuthListCommand) simpleMounts(auths map[string]*api.AuthMount) []string { + paths := make([]string, 0, len(auths)) + for path := range auths { + paths = append(paths, path) + } + sort.Strings(paths) + + out := []string{"Path | Type | Description"} + for _, path := range paths { + mount := auths[path] + out = append(out, fmt.Sprintf("%s | %s | %s", path, mount.Type, mount.Description)) + } + + return out +} + +func (c *AuthListCommand) detailedMounts(auths map[string]*api.AuthMount) []string { + paths := make([]string, 0, len(auths)) + for path := range auths { + paths = append(paths, path) + } + sort.Strings(paths) + + calcTTL := func(typ string, ttl int) string { + switch { + case typ == "system", typ == "cubbyhole": + return "" + case ttl != 0: + return strconv.Itoa(ttl) + default: + return "system" + } + } + + out := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Replication | Description"} + for _, path := range paths { + mount := auths[path] + + defaultTTL := calcTTL(mount.Type, mount.Config.DefaultLeaseTTL) + maxTTL := calcTTL(mount.Type, mount.Config.MaxLeaseTTL) + + replication := "replicated" + if mount.Local { + replication = "local" + } + + out = append(out, fmt.Sprintf("%s | %s | %s | %s | %s | %s | %v | %s", + path, + mount.Type, + mount.Accessor, + mount.Config.PluginName, + defaultTTL, + maxTTL, + replication, + mount.Description, + )) + } + + return out +} diff --git a/command/auth_list_test.go b/command/auth_list_test.go new file mode 100644 index 0000000000..decf6e9b06 --- /dev/null +++ b/command/auth_list_test.go @@ -0,0 +1,105 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func testAuthListCommand(tb testing.TB) (*cli.MockUi, *AuthListCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &AuthListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestAuthListCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo"}, + "Too many arguments", + 1, + }, + { + "lists", + nil, + "Path", + 0, + }, + { + "detailed", + []string{"-detailed"}, + "Default TTL", + 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() + + ui, cmd := testAuthListCommand(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("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuthListCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing enabled authentications: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuthListCommand(t) + assertNoTabs(t, cmd) + }) +} From a7589f761307ebdc7b01bc9a0006a0cc37008d1a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:00:00 -0400 Subject: [PATCH 049/281] Update capabilities command --- command/capabilities.go | 109 ++++++++++++--------- command/capabilities_test.go | 181 +++++++++++++++++++++++++++++------ 2 files changed, 215 insertions(+), 75 deletions(-) diff --git a/command/capabilities.go b/command/capabilities.go index bb60bd4ea8..6a49ebe395 100644 --- a/command/capabilities.go +++ b/command/capabilities.go @@ -2,49 +2,86 @@ package command import ( "fmt" + "sort" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*CapabilitiesCommand)(nil) +var _ cli.CommandAutocomplete = (*CapabilitiesCommand)(nil) + // CapabilitiesCommand is a Command that enables a new endpoint. type CapabilitiesCommand struct { - meta.Meta + *BaseCommand +} + +func (c *CapabilitiesCommand) Synopsis() string { + return "Fetchs the capabilities of a token" +} + +func (c *CapabilitiesCommand) Help() string { + helpText := ` +Usage: vault capabilities [options] [TOKEN] PATH + + Fetches the capabilities of a token for a given path. If a TOKEN is provided + as an argument, the "/sys/capabilities" endpoint and permission is used. If + no TOKEN is provided, the "/sys/capabilities-self" endpoint and permission + is used with the locally authenticated token. + + List capabilities for the local token on the "secret/foo" path: + + $ vault capabilities secret/foo + + List capabilities for a token on the "cubbyhole/foo" path: + + $ vault capabilities 96ddf4bc-d217-f3ba-f9bd-017055595017 cubbyhole/foo + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *CapabilitiesCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *CapabilitiesCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *CapabilitiesCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *CapabilitiesCommand) Run(args []string) int { - flags := c.Meta.FlagSet("capabilities", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) > 2 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\ncapabilities expects at most two arguments")) - return 1 - } - - var token string - var path string + token := "" + path := "" + args = f.Args() switch { case len(args) == 1: path = args[0] case len(args) == 2: - token = args[0] - path = args[1] + token, path = args[0], args[1] default: - flags.Usage() - c.Ui.Error(fmt.Sprintf("\ncapabilities expects at least one argument")) + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1-2, got %d)", len(args))) return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } @@ -55,33 +92,11 @@ func (c *CapabilitiesCommand) Run(args []string) int { capabilities, err = client.Sys().Capabilities(token, path) } if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error retrieving capabilities: %s", err)) - return 1 + c.UI.Error(fmt.Sprintf("Error listing capabilities: %s", err)) + return 2 } - c.Ui.Output(fmt.Sprintf("Capabilities: %s", capabilities)) + sort.Strings(capabilities) + c.UI.Output(strings.Join(capabilities, ", ")) return 0 } - -func (c *CapabilitiesCommand) Synopsis() string { - return "Fetch the capabilities of a token on a given path" -} - -func (c *CapabilitiesCommand) Help() string { - helpText := ` -Usage: vault capabilities [options] [token] path - - Fetch the capabilities of a token on a given path. - If a token is provided as an argument, the '/sys/capabilities' endpoint will be invoked - with the given token; otherwise the '/sys/capabilities-self' endpoint will be invoked - with the client token. - - If a token does not have any capability on a given path, or if any of the policies - belonging to the token explicitly have ["deny"] capability, or if the argument path - is invalid, this command will respond with a ["deny"]. - -General Options: -` + meta.GeneralOptionsUsage() - return strings.TrimSpace(helpText) -} diff --git a/command/capabilities_test.go b/command/capabilities_test.go index 5d106a14e9..4ca2b1d285 100644 --- a/command/capabilities_test.go +++ b/command/capabilities_test.go @@ -1,45 +1,170 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" ) -func TestCapabilities_Basic(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - ui := new(cli.MockUi) - c := &CapabilitiesCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, +func testCapabilitiesCommand(tb testing.TB) (*cli.MockUi, *CapabilitiesCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &CapabilitiesCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestCapabilitiesCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar", "zip"}, + "Too many arguments", + 1, }, } - var args []string + for _, tc := range cases { + tc := tc - args = []string{"-address", addr} - if code := c.Run(args); code == 0 { - t.Fatalf("expected failure due to no args") + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ui, cmd := testCapabilitiesCommand(t) + + 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) + } + }) } - args = []string{"-address", addr, "testpath"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("token", func(t *testing.T) { + t.Parallel() - args = []string{"-address", addr, token, "test"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServer(t) + defer closer() - args = []string{"-address", addr, "invalidtoken", "test"} - if code := c.Run(args); code == 0 { - t.Fatalf("expected failure due to invalid token") - } + policy := `path "secret/foo" { capabilities = ["read"] }` + if err := client.Sys().PutPolicy("policy", policy); err != nil { + t.Error(err) + } + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"policy"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("missing auth data: %#v", secret) + } + token := secret.Auth.ClientToken + + ui, cmd := testCapabilitiesCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + token, "secret/foo", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "read" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("local", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policy := `path "secret/foo" { capabilities = ["read"] }` + if err := client.Sys().PutPolicy("policy", policy); err != nil { + t.Error(err) + } + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"policy"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("missing auth data: %#v", secret) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + + ui, cmd := testCapabilitiesCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/foo", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "read" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testCapabilitiesCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "foo", "bar", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing capabilities: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testCapabilitiesCommand(t) + assertNoTabs(t, cmd) + }) } From d38abb665bc24c9dc98bffee69b0b2c73eed482b Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:00:06 -0400 Subject: [PATCH 050/281] Update delete command --- command/delete.go | 119 ++++++++++++++++++++------------ command/delete_test.go | 151 ++++++++++++++++++++++++++++++----------- 2 files changed, 187 insertions(+), 83 deletions(-) diff --git a/command/delete.go b/command/delete.go index d9a8ee8a66..fbac3b0b32 100644 --- a/command/delete.go +++ b/command/delete.go @@ -4,64 +4,93 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*DeleteCommand)(nil) +var _ cli.CommandAutocomplete = (*DeleteCommand)(nil) + // DeleteCommand is a Command that puts data into the Vault. type DeleteCommand struct { - meta.Meta -} - -func (c *DeleteCommand) Run(args []string) int { - flags := c.Meta.FlagSet("delete", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - args = flags.Args() - if len(args) != 1 { - c.Ui.Error("delete expects one argument") - flags.Usage() - return 1 - } - - path := args[0] - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if _, err := client.Logical().Delete(path); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error deleting '%s': %s", path, err)) - return 1 - } - - c.Ui.Output(fmt.Sprintf("Success! Deleted '%s' if it existed.", path)) - return 0 + *BaseCommand } func (c *DeleteCommand) Synopsis() string { - return "Delete operation on secrets in Vault" + return "Deletes secrets and configuration" } func (c *DeleteCommand) Help() string { helpText := ` -Usage: vault delete [options] path +Usage: vault delete [options] PATH - Delete data (secrets or configuration) from Vault. + Deletes secrets and configuration from Vault at the given path. The behavior + of "delete" is delegated to the backend corresponding to the given path. - Delete sends a delete operation request to the given path. The - behavior of the delete is determined by the backend at the given - path. For example, deleting "aws/policy/ops" will delete the "ops" - policy for the AWS backend. Use "vault help" for more details on - whether delete is supported for a path and what the behavior is. + Remove data in the status secret backend: + + $ vault delete secret/my-secret + + Uninstall an encryption key in the transit backend: + + $ vault delete transit/keys/my-key + + Delete an IAM role: + + $ vault delete aws/roles/ops + + For a full list of examples and paths, please see the documentation that + corresponds to the secret backend in use. + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *DeleteCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *DeleteCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *DeleteCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *DeleteCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + path, kvs, err := extractPath(args) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if len(kvs) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if _, err := client.Logical().Delete(path); err != nil { + c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) + return 2 + } + + c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", path)) + return 0 +} diff --git a/command/delete_test.go b/command/delete_test.go index c5efc415b8..6d06c7e972 100644 --- a/command/delete_test.go +++ b/command/delete_test.go @@ -1,56 +1,131 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestDelete(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testDeleteCommand(tb testing.TB) (*cli.MockUi, *DeleteCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &DeleteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &DeleteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestDeleteCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing PATH!", + 1, + }, + { + "slash", + []string{"/"}, + "Missing PATH!", + 1, }, } - args := []string{ - "-address", addr, - "secret/foo", - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Run once so the client is setup, ignore errors - c.Run(args) + for _, tc := range cases { + tc := tc - // Get the client so we can write data - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - data := map[string]interface{}{"value": "bar"} - if _, err := client.Logical().Write("secret/foo", data); err != nil { - t.Fatalf("err: %s", err) - } + ui, cmd := testDeleteCommand(t) - // Run the delete - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + code := cmd.Run(tc.args) + if code != tc.code { + t.Errorf("expected %d to be %d", code, tc.code) + } - resp, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } - if resp != nil { - t.Fatalf("bad: %#v", resp) - } + 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("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if _, err := client.Logical().Write("secret/delete/foo", map[string]interface{}{ + "foo": "bar", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testDeleteCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/delete/foo", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Data deleted (if it existed) at: secret/delete/foo" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + secret, _ := client.Logical().Read("secret/delete/foo") + if secret != nil { + t.Errorf("expected deletion: %#v", secret) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testDeleteCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/delete/foo", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error deleting secret/delete/foo: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testDeleteCommand(t) + assertNoTabs(t, cmd) + }) } From 9d4e8c3529b09aecd21f68786f58ab07cebbdeac Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:01:44 -0400 Subject: [PATCH 051/281] Update format to not use colored UI for json/yaml --- command/format.go | 7 +++++++ command/format_test.go | 16 ++++------------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/command/format.go b/command/format.go index 630056d177..5db41c7e11 100644 --- a/command/format.go +++ b/command/format.go @@ -28,6 +28,13 @@ func OutputList(ui cli.Ui, format string, secret *api.Secret) int { } func outputWithFormat(ui cli.Ui, format string, secret *api.Secret, data interface{}) int { + // If we had a colored UI, pull out the nested ui so we don't add escape + // sequences for outputting json, etc. + colorUI, ok := ui.(*cli.ColoredUi) + if ok { + ui = colorUI.Ui + } + formatter, ok := Formatters[strings.ToLower(format)] if !ok { ui.Error(fmt.Sprintf("Invalid output format: %s", format)) diff --git a/command/format_test.go b/command/format_test.go index 8e32d2419c..44020a1aa6 100644 --- a/command/format_test.go +++ b/command/format_test.go @@ -24,18 +24,10 @@ func (m mockUi) AskSecret(_ string) (string, error) { m.t.FailNow() return "", nil } -func (m mockUi) Output(s string) { - output = s -} -func (m mockUi) Info(s string) { - m.t.Log(s) -} -func (m mockUi) Error(s string) { - m.t.Log(s) -} -func (m mockUi) Warn(s string) { - m.t.Log(s) -} +func (m mockUi) Output(s string) { output = s } +func (m mockUi) Info(s string) { m.t.Log(s) } +func (m mockUi) Error(s string) { m.t.Log(s) } +func (m mockUi) Warn(s string) { m.t.Log(s) } func TestJsonFormatter(t *testing.T) { ui := mockUi{t: t, SampleData: "something"} From 6028c84a02eb8c7ee5f4c77c6abf35122426e577 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:01:55 -0400 Subject: [PATCH 052/281] Update generate-root command --- command/generate-root.go | 372 ---------------------------- command/generate-root_test.go | 294 ---------------------- command/generate_root.go | 452 ++++++++++++++++++++++++++++++++++ command/generate_root_test.go | 448 +++++++++++++++++++++++++++++++++ 4 files changed, 900 insertions(+), 666 deletions(-) delete mode 100644 command/generate-root.go delete mode 100644 command/generate-root_test.go create mode 100644 command/generate_root.go create mode 100644 command/generate_root_test.go diff --git a/command/generate-root.go b/command/generate-root.go deleted file mode 100644 index 2d9521b7df..0000000000 --- a/command/generate-root.go +++ /dev/null @@ -1,372 +0,0 @@ -package command - -import ( - "crypto/rand" - "encoding/base64" - "fmt" - "os" - "strings" - - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/password" - "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/helper/xor" - "github.com/hashicorp/vault/meta" - "github.com/posener/complete" -) - -// GenerateRootCommand is a Command that generates a new root token. -type GenerateRootCommand struct { - meta.Meta - - // Key can be used to pre-seed the key. If it is set, it will not - // be asked with the `password` helper. - Key string - - // The nonce for the rekey request to send along - Nonce string -} - -func (c *GenerateRootCommand) Run(args []string) int { - var init, cancel, status, genotp bool - var nonce, decode, otp, pgpKey string - var pgpKeyArr pgpkeys.PubKeyFilesFlag - flags := c.Meta.FlagSet("generate-root", meta.FlagSetDefault) - flags.BoolVar(&init, "init", false, "") - flags.BoolVar(&cancel, "cancel", false, "") - flags.BoolVar(&status, "status", false, "") - flags.BoolVar(&genotp, "genotp", false, "") - flags.StringVar(&decode, "decode", "", "") - flags.StringVar(&otp, "otp", "", "") - flags.StringVar(&nonce, "nonce", "", "") - flags.Var(&pgpKeyArr, "pgp-key", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - if genotp { - buf := make([]byte, 16) - readLen, err := rand.Read(buf) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading random bytes: %s", err)) - return 1 - } - if readLen != 16 { - c.Ui.Error(fmt.Sprintf("Read %d bytes when we should have read 16", readLen)) - return 1 - } - c.Ui.Output(fmt.Sprintf("OTP: %s", base64.StdEncoding.EncodeToString(buf))) - return 0 - } - - if len(decode) > 0 { - if len(otp) == 0 { - c.Ui.Error("Both the value to decode and the OTP must be passed in") - return 1 - } - return c.decode(decode, otp) - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - // Check if the root generation is started - rootGenerationStatus, err := client.Sys().GenerateRootStatus() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading root generation status: %s", err)) - return 1 - } - - // If we are initing, or if we are not started but are not running a - // special function, check otp and pgpkey - checkOtpPgp := false - switch { - case init: - checkOtpPgp = true - case cancel: - case status: - case genotp: - case len(decode) != 0: - case rootGenerationStatus.Started: - default: - checkOtpPgp = true - } - if checkOtpPgp { - switch { - case len(otp) == 0 && (pgpKeyArr == nil || len(pgpKeyArr) == 0): - c.Ui.Error(c.Help()) - return 1 - case len(otp) != 0 && pgpKeyArr != nil && len(pgpKeyArr) != 0: - c.Ui.Error(c.Help()) - return 1 - case len(otp) != 0: - err := c.verifyOTP(otp) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error verifying the provided OTP: %s", err)) - return 1 - } - case pgpKeyArr != nil: - if len(pgpKeyArr) != 1 { - c.Ui.Error("Could not parse PGP key") - return 1 - } - if len(pgpKeyArr[0]) == 0 { - c.Ui.Error("Got an empty PGP key") - return 1 - } - pgpKey = pgpKeyArr[0] - default: - panic("unreachable case") - } - } - - if nonce != "" { - c.Nonce = nonce - } - - // Check if we are running doing any restricted variants - switch { - case init: - return c.initGenerateRoot(client, otp, pgpKey) - case cancel: - return c.cancelGenerateRoot(client) - case status: - return c.rootGenerationStatus(client) - } - - // Start the root generation process if not started - if !rootGenerationStatus.Started { - rootGenerationStatus, err = client.Sys().GenerateRootInit(otp, pgpKey) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing root generation: %s", err)) - return 1 - } - c.Nonce = rootGenerationStatus.Nonce - } - - serverNonce := rootGenerationStatus.Nonce - - // Get the unseal key - args = flags.Args() - key := c.Key - if len(args) > 0 { - key = args[0] - } - if key == "" { - c.Nonce = serverNonce - fmt.Printf("Root generation operation nonce: %s\n", serverNonce) - fmt.Printf("Key (will be hidden): ") - key, err = password.Read(os.Stdin) - fmt.Printf("\n") - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error attempting to ask for password. The raw error message\n"+ - "is shown below, but the most common reason for this error is\n"+ - "that you attempted to pipe a value into unseal or you're\n"+ - "executing `vault generate-root` from outside of a terminal.\n\n"+ - "You should use `vault generate-root` from a terminal for maximum\n"+ - "security. If this isn't an option, the unseal key can be passed\n"+ - "in using the first parameter.\n\n"+ - "Raw error: %s", err)) - return 1 - } - } - - // Provide the key, this may potentially complete the update - statusResp, err := client.Sys().GenerateRootUpdate(strings.TrimSpace(key), c.Nonce) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error attempting generate-root update: %s", err)) - return 1 - } - - c.dumpStatus(statusResp) - - return 0 -} - -func (c *GenerateRootCommand) verifyOTP(otp string) error { - if len(otp) == 0 { - return fmt.Errorf("No OTP passed in") - } - otpBytes, err := base64.StdEncoding.DecodeString(otp) - if err != nil { - return fmt.Errorf("Error decoding base64 OTP value: %s", err) - } - if otpBytes == nil || len(otpBytes) != 16 { - return fmt.Errorf("Decoded OTP value is invalid or wrong length") - } - - return nil -} - -func (c *GenerateRootCommand) decode(encodedVal, otp string) int { - tokenBytes, err := xor.XORBase64(encodedVal, otp) - if err != nil { - c.Ui.Error(err.Error()) - return 1 - } - - token, err := uuid.FormatUUID(tokenBytes) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error formatting base64 token value: %v", err)) - return 1 - } - - c.Ui.Output(fmt.Sprintf("Root token: %s", token)) - - return 0 -} - -// initGenerateRoot is used to start the generation process -func (c *GenerateRootCommand) initGenerateRoot(client *api.Client, otp string, pgpKey string) int { - // Start the rekey - status, err := client.Sys().GenerateRootInit(otp, pgpKey) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing root generation: %s", err)) - return 1 - } - - c.dumpStatus(status) - - return 0 -} - -// cancelGenerateRoot is used to abort the generation process -func (c *GenerateRootCommand) cancelGenerateRoot(client *api.Client) int { - err := client.Sys().GenerateRootCancel() - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to cancel root generation: %s", err)) - return 1 - } - c.Ui.Output("Root generation canceled.") - return 0 -} - -// rootGenerationStatus is used just to fetch and dump the status -func (c *GenerateRootCommand) rootGenerationStatus(client *api.Client) int { - // Check the status - status, err := client.Sys().GenerateRootStatus() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading root generation status: %s", err)) - return 1 - } - - c.dumpStatus(status) - - return 0 -} - -// dumpStatus dumps the status to output -func (c *GenerateRootCommand) dumpStatus(status *api.GenerateRootStatusResponse) { - // Dump the status - statString := fmt.Sprintf( - "Nonce: %s\n"+ - "Started: %v\n"+ - "Generate Root Progress: %d\n"+ - "Required Keys: %d\n"+ - "Complete: %t", - status.Nonce, - status.Started, - status.Progress, - status.Required, - status.Complete, - ) - if len(status.PGPFingerprint) > 0 { - statString = fmt.Sprintf("%s\nPGP Fingerprint: %s", statString, status.PGPFingerprint) - } - if len(status.EncodedRootToken) > 0 { - statString = fmt.Sprintf("%s\n\nEncoded root token: %s", statString, status.EncodedRootToken) - } - c.Ui.Output(statString) -} - -func (c *GenerateRootCommand) Synopsis() string { - return "Generates a new root token" -} - -func (c *GenerateRootCommand) Help() string { - helpText := ` -Usage: vault generate-root [options] [key] - - 'generate-root' is used to create a new root token. - - Root generation can only be done when the vault is already unsealed. The - operation is done online, but requires that a threshold of the current unseal - keys be provided. - - One (and only one) of the following must be provided when initializing the - root generation attempt: - - 1) A 16-byte, base64-encoded One Time Password (OTP) provided in the '-otp' - flag; the token is XOR'd with this value before it is returned once the final - unseal key has been provided. The '-decode' operation can be used with this - value and the OTP to output the final token value. The '-genotp' flag can be - used to generate a suitable value. - - or - - 2) A file containing a PGP key (binary or base64-encoded) or a Keybase.io - username in the format of "keybase:" in the '-pgp-key' flag. The - final token value will be encrypted with this public key and base64-encoded. - -General Options: -` + meta.GeneralOptionsUsage() + ` -Generate Root Options: - - -init Initialize the root generation attempt. This can only - be done if no generation is already initiated. - - -cancel Reset the root generation process by throwing away - prior unseal keys and the configuration. - - -status Prints the status of the current attempt. This can be - used to see the status without attempting to provide - an unseal key. - - -decode=abcd Decodes and outputs the generated root token. The OTP - used at '-init' time must be provided in the '-otp' - parameter. - - -genotp Returns a high-quality OTP suitable for passing into - the '-init' method. - - -otp=abcd The base64-encoded 16-byte OTP for use with the - '-init' or '-decode' methods. - - -pgp-key A file on disk containing a binary- or base64-format - public PGP key, or a Keybase username specified as - "keybase:". The output root token will be - encrypted and base64-encoded, in order, with the given - public key. - - -nonce=abcd The nonce provided at initialization time. This same - nonce value must be provided with each unseal key. If - the unseal key is not being passed in via the command - line the nonce parameter is not required, and will - instead be displayed with the key prompt. -` - return strings.TrimSpace(helpText) -} - -func (c *GenerateRootCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictNothing -} - -func (c *GenerateRootCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-init": complete.PredictNothing, - "-cancel": complete.PredictNothing, - "-status": complete.PredictNothing, - "-decode": complete.PredictNothing, - "-genotp": complete.PredictNothing, - "-otp": complete.PredictNothing, - "-pgp-key": complete.PredictNothing, - "-nonce": complete.PredictNothing, - } -} diff --git a/command/generate-root_test.go b/command/generate-root_test.go deleted file mode 100644 index 31d956d73a..0000000000 --- a/command/generate-root_test.go +++ /dev/null @@ -1,294 +0,0 @@ -package command - -import ( - "encoding/base64" - "encoding/hex" - "os" - "strings" - "testing" - - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/helper/xor" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" - "github.com/mitchellh/cli" -) - -func TestGenerateRoot_Cancel(t *testing.T) { - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &GenerateRootCommand{ - Meta: meta.Meta{ - Ui: ui, - }, - } - - otpBytes, err := vault.GenerateRandBytes(16) - if err != nil { - t.Fatal(err) - } - otp := base64.StdEncoding.EncodeToString(otpBytes) - - args := []string{"-address", addr, "-init", "-otp", otp} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - args = []string{"-address", addr, "-cancel"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - config, err := core.GenerateRootConfiguration() - if err != nil { - t.Fatalf("err: %s", err) - } - if config != nil { - t.Fatal("should not have a config for root generation") - } -} - -func TestGenerateRoot_status(t *testing.T) { - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &GenerateRootCommand{ - Meta: meta.Meta{ - Ui: ui, - }, - } - - otpBytes, err := vault.GenerateRandBytes(16) - if err != nil { - t.Fatal(err) - } - otp := base64.StdEncoding.EncodeToString(otpBytes) - - args := []string{"-address", addr, "-init", "-otp", otp} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - args = []string{"-address", addr, "-status"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - if !strings.Contains(ui.OutputWriter.String(), "Started: true") { - t.Fatalf("bad: %s", ui.OutputWriter.String()) - } -} - -func TestGenerateRoot_OTP(t *testing.T) { - core, ts, keys, _ := vault.TestCoreWithTokenStore(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &GenerateRootCommand{ - Meta: meta.Meta{ - Ui: ui, - }, - } - - // Generate an OTP - otpBytes, err := vault.GenerateRandBytes(16) - if err != nil { - t.Fatal(err) - } - otp := base64.StdEncoding.EncodeToString(otpBytes) - - // Init the attempt - args := []string{ - "-address", addr, - "-init", - "-otp", otp, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - config, err := core.GenerateRootConfiguration() - if err != nil { - t.Fatalf("err: %v", err) - } - - for _, key := range keys { - ui = new(cli.MockUi) - c = &GenerateRootCommand{ - Key: hex.EncodeToString(key), - Meta: meta.Meta{ - Ui: ui, - }, - } - - c.Nonce = config.Nonce - - // Provide the key - args = []string{ - "-address", addr, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - } - - beforeNAfter := strings.Split(ui.OutputWriter.String(), "Encoded root token: ") - if len(beforeNAfter) != 2 { - t.Fatalf("did not find encoded root token in %s", ui.OutputWriter.String()) - } - encodedToken := strings.TrimSpace(beforeNAfter[1]) - - decodedToken, err := xor.XORBase64(encodedToken, otp) - if err != nil { - t.Fatal(err) - } - - token, err := uuid.FormatUUID(decodedToken) - if err != nil { - t.Fatal(err) - } - - req := logical.TestRequest(t, logical.ReadOperation, "lookup-self") - req.ClientToken = token - - resp, err := ts.HandleRequest(req) - if err != nil { - t.Fatalf("error running token lookup-self: %v", err) - } - if resp == nil { - t.Fatalf("got nil resp with token lookup-self") - } - if resp.Data == nil { - t.Fatalf("got nil resp.Data with token lookup-self") - } - - if resp.Data["orphan"].(bool) != true || - resp.Data["ttl"].(int64) != 0 || - resp.Data["num_uses"].(int) != 0 || - resp.Data["meta"].(map[string]string) != nil || - len(resp.Data["policies"].([]string)) != 1 || - resp.Data["policies"].([]string)[0] != "root" { - t.Fatalf("bad: %#v", resp.Data) - } - - // Clear the output and run a decode to verify we get the same result - ui.OutputWriter.Reset() - args = []string{ - "-address", addr, - "-decode", encodedToken, - "-otp", otp, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - beforeNAfter = strings.Split(ui.OutputWriter.String(), "Root token: ") - if len(beforeNAfter) != 2 { - t.Fatalf("did not find decoded root token in %s", ui.OutputWriter.String()) - } - - outToken := strings.TrimSpace(beforeNAfter[1]) - if outToken != token { - t.Fatalf("tokens do not match:\n%s\n%s", token, outToken) - } -} - -func TestGenerateRoot_PGP(t *testing.T) { - core, ts, keys, _ := vault.TestCoreWithTokenStore(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &GenerateRootCommand{ - Meta: meta.Meta{ - Ui: ui, - }, - } - - tempDir, pubFiles, err := getPubKeyFiles(t) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - // Init the attempt - args := []string{ - "-address", addr, - "-init", - "-pgp-key", pubFiles[0], - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - config, err := core.GenerateRootConfiguration() - if err != nil { - t.Fatalf("err: %v", err) - } - - for _, key := range keys { - c = &GenerateRootCommand{ - Key: hex.EncodeToString(key), - Meta: meta.Meta{ - Ui: ui, - }, - } - - c.Nonce = config.Nonce - - // Provide the key - args = []string{ - "-address", addr, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - } - - beforeNAfter := strings.Split(ui.OutputWriter.String(), "Encoded root token: ") - if len(beforeNAfter) != 2 { - t.Fatalf("did not find encoded root token in %s", ui.OutputWriter.String()) - } - encodedToken := strings.TrimSpace(beforeNAfter[1]) - - ptBuf, err := pgpkeys.DecryptBytes(encodedToken, pgpkeys.TestPrivKey1) - if err != nil { - t.Fatal(err) - } - if ptBuf == nil { - t.Fatal("returned plain text buffer is nil") - } - - token := ptBuf.String() - - req := logical.TestRequest(t, logical.ReadOperation, "lookup-self") - req.ClientToken = token - - resp, err := ts.HandleRequest(req) - if err != nil { - t.Fatalf("error running token lookup-self: %v", err) - } - if resp == nil { - t.Fatalf("got nil resp with token lookup-self") - } - if resp.Data == nil { - t.Fatalf("got nil resp.Data with token lookup-self") - } - - if resp.Data["orphan"].(bool) != true || - resp.Data["ttl"].(int64) != 0 || - resp.Data["num_uses"].(int) != 0 || - resp.Data["meta"].(map[string]string) != nil || - len(resp.Data["policies"].([]string)) != 1 || - resp.Data["policies"].([]string)[0] != "root" { - t.Fatalf("bad: %#v", resp.Data) - } -} diff --git a/command/generate_root.go b/command/generate_root.go new file mode 100644 index 0000000000..58f7442564 --- /dev/null +++ b/command/generate_root.go @@ -0,0 +1,452 @@ +package command + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "os" + "strings" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/helper/password" + "github.com/hashicorp/vault/helper/pgpkeys" + "github.com/hashicorp/vault/helper/xor" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*GenerateRootCommand)(nil) +var _ cli.CommandAutocomplete = (*GenerateRootCommand)(nil) + +// GenerateRootCommand is a Command that generates a new root token. +type GenerateRootCommand struct { + *BaseCommand + + flagInit bool + flagCancel bool + flagStatus bool + flagDecode string + flagOTP string + flagPGPKey string + flagNonce string + flagGenerateOTP bool + + // Deprecation + // TODO: remove in 0.9.9 + flagGenOTP bool + + testStdin io.Reader // for tests +} + +func (c *GenerateRootCommand) Synopsis() string { + return "Generates a new root token" +} + +func (c *GenerateRootCommand) Help() string { + helpText := ` +Usage: vault generate-root [options] [KEY] + + Generates a new root token by combining a quorum of share holders. One of + the following must be provided to start the root token generation: + + - A base64-encoded one-time-password (OTP) provided via the "-otp" flag. + Use the "-generate-otp" flag to generate a usable value. The resulting + token is XORed with this value when it is returend. Use the "-decode" + flag to output the final value. + + - A file containing a PGP key or a keybase username in the "-pgp-key" + flag. The resulting token is encrypted with this public key. + + An unseal key may be provided directly on the command line as an argument to + the command. If key is specified as "-", the command will read from stdin. If + a TTY is available, the command will prompt for text. + + Generate an OTP code for the final token: + + $ vault generate-root -generate-otp + + Start a root token generation: + + $ vault generate-root -init -otp="..." + $ vault generate-root -init -pgp-key="..." + + Enter an unseal key to progress root token generation: + + $ vault generate-root -otp="..." + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *GenerateRootCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "init", + Target: &c.flagInit, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Start a root token generation. This can only be done if " + + "there is not currently one in progress.", + }) + + f.BoolVar(&BoolVar{ + Name: "cancel", + Target: &c.flagCancel, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Reset the root token generation progress. This will discard any " + + "submitted unseal keys or configuration.", + }) + + f.BoolVar(&BoolVar{ + Name: "status", + Target: &c.flagStatus, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Print the status of the current attempt without provding an " + + "unseal key.", + }) + + f.StringVar(&StringVar{ + Name: "decode", + Target: &c.flagDecode, + Default: "", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Decode and output the generated root token. This option requires " + + "the \"-otp\" flag be set to the OTP used during initialization.", + }) + + f.BoolVar(&BoolVar{ + Name: "generate-otp", + Target: &c.flagGenerateOTP, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Generate and print a high-entropy one-time-password (OTP) " + + "suitable for use with the \"-init\" flag.", + }) + + f.StringVar(&StringVar{ + Name: "otp", + Target: &c.flagOTP, + Default: "", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "OTP code to use with \"-decode\" or \"-init\".", + }) + + f.VarFlag(&VarFlag{ + Name: "pgp-key", + Value: (*pgpkeys.PubKeyFileFlag)(&c.flagPGPKey), + Default: "", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Path to a file on disk containing a binary or base64-encoded " + + "public GPG key. This can also be specified as a Keybase username " + + "using the format \"keybase:\". When supplied, the generated " + + "root token will be encrypted and base64-encoded with the given public " + + "key.", + }) + + f.StringVar(&StringVar{ + Name: "nonce", + Target: &c.flagNonce, + Default: "", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Nonce value provided at initialization. The same nonce value " + + "must be provided with each unseal key.", + }) + + // Deprecations: prefer longer-form, descriptive flags + // TODO: remove in 0.9.0 + f.BoolVar(&BoolVar{ + Name: "genotp", // -generate-otp + Target: &c.flagGenOTP, + Default: false, + Hidden: true, + }) + + return set +} + +func (c *GenerateRootCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *GenerateRootCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *GenerateRootCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 1 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-1, got %d)", len(args))) + return 1 + } + + // Deprecations + // TODO: remove in 0.9.0 + switch { + case c.flagGenOTP: + c.UI.Warn(wrapAtLength( + "The -gen-otp flag is deprecated. Please use the -generate-otp flag " + + "instead.")) + c.flagGenerateOTP = c.flagGenOTP + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + switch { + case c.flagGenerateOTP: + return c.generateOTP() + case c.flagDecode != "": + return c.decode(c.flagDecode, c.flagOTP) + case c.flagCancel: + return c.cancel(client) + case c.flagInit: + return c.init(client, c.flagOTP, c.flagPGPKey) + case c.flagStatus: + return c.status(client) + default: + // If there are no other flags, prompt for an unseal key. + key := "" + if len(args) > 0 { + key = strings.TrimSpace(args[0]) + } + return c.provide(client, key) + } +} + +// verifyOTP verifies the given OTP code is exactly 16 bytes. +func (c *GenerateRootCommand) verifyOTP(otp string) error { + if len(otp) == 0 { + return fmt.Errorf("No OTP passed in") + } + otpBytes, err := base64.StdEncoding.DecodeString(otp) + if err != nil { + return fmt.Errorf("Error decoding base64 OTP value: %s", err) + } + if otpBytes == nil || len(otpBytes) != 16 { + return fmt.Errorf("Decoded OTP value is invalid or wrong length") + } + + return nil +} + +// generateOTP generates a suitable OTP code for generating a root token. +func (c *GenerateRootCommand) generateOTP() int { + buf := make([]byte, 16) + readLen, err := rand.Read(buf) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading random bytes: %s", err)) + return 2 + } + + if readLen != 16 { + c.UI.Error(fmt.Sprintf("Read %d bytes when we should have read 16", readLen)) + return 2 + } + + return PrintRaw(c.UI, base64.StdEncoding.EncodeToString(buf)) +} + +// decode decodes the given value using the otp. +func (c *GenerateRootCommand) decode(encoded, otp string) int { + if encoded == "" { + c.UI.Error("Missing encoded value: use -decode= to supply it") + return 1 + } + if otp == "" { + c.UI.Error("Missing otp: use -otp to supply it") + return 1 + } + + tokenBytes, err := xor.XORBase64(encoded, otp) + if err != nil { + c.UI.Error(fmt.Sprintf("Error xoring token: %s", err)) + return 1 + } + + token, err := uuid.FormatUUID(tokenBytes) + if err != nil { + c.UI.Error(fmt.Sprintf("Error formatting base64 token value: %s", err)) + return 1 + } + + return PrintRaw(c.UI, strings.TrimSpace(token)) +} + +// init is used to start the generation process +func (c *GenerateRootCommand) init(client *api.Client, otp string, pgpKey string) int { + // Validate incoming fields. Either OTP OR PGP keys must be supplied. + switch { + case otp == "" && pgpKey == "": + c.UI.Error("Error initializing: must specify either -otp or -pgp-key") + return 1 + case otp != "" && pgpKey != "": + c.UI.Error("Error initializing: cannot specify both -otp and -pgp-key") + return 1 + case otp != "": + if err := c.verifyOTP(otp); err != nil { + c.UI.Error(fmt.Sprintf("Error initializing: invalid OTP: %s", err)) + return 1 + } + case pgpKey != "": + // OK + } + + // Start the root generation + status, err := client.Sys().GenerateRootInit(otp, pgpKey) + if err != nil { + c.UI.Error(fmt.Sprintf("Error initializing root generation: %s", err)) + return 2 + } + return c.printStatus(status) +} + +// provide prompts the user for the seal key and posts it to the update root +// endpoint. If this is the last unseal, this function outputs it. +func (c *GenerateRootCommand) provide(client *api.Client, key string) int { + status, err := client.Sys().GenerateRootStatus() + if err != nil { + c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err)) + return 2 + } + + // Verify a root token generation is in progress. If there is not one in + // progress, return an error instructing the user to start one. + if !status.Started { + c.UI.Error(wrapAtLength( + "No root generation is in progress. Start a root generation by " + + "running \"vault generate-root -init\".")) + return 1 + } + + var nonce string + + switch key { + case "-": // Read from stdin + nonce = c.flagNonce + + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + var buf bytes.Buffer + if _, err := io.Copy(&buf, stdin); err != nil { + c.UI.Error(fmt.Sprintf("Failed to read from stdin: %s", err)) + return 1 + } + + key = buf.String() + case "": // Prompt using the tty + // Nonce value is not required if we are prompting via the terminal + nonce = status.Nonce + + w := getWriterFromUI(c.UI) + fmt.Fprintf(w, "Root generation operation nonce: %s\n", nonce) + fmt.Fprintf(w, "Unseal Key (will be hidden): ") + key, err = password.Read(os.Stdin) + fmt.Fprintf(w, "\n") + if err != nil { + if err == password.ErrInterrupted { + c.UI.Error("user canceled") + return 1 + } + + c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+ + "ask for the unseal key. The raw error message is shown below, but "+ + "usually this is because you attempted to pipe a value into the "+ + "command or you are executing outside of a terminal (tty). If you "+ + "want to pipe the value, pass \"-\" as the argument to read from "+ + "stdin. The raw error was: %s", err))) + return 1 + } + default: // Supplied directly as an arg + nonce = c.flagNonce + } + + // Trim any whitespace from they key, especially since we might have prompted + // the user for it. + key = strings.TrimSpace(key) + + // Verify we have a nonce value + if nonce == "" { + c.UI.Error("Missing nonce value: specify it via the -nonce flag") + return 1 + } + + // Provide the key, this may potentially complete the update + status, err = client.Sys().GenerateRootUpdate(key, nonce) + if err != nil { + c.UI.Error(fmt.Sprintf("Error posting unseal key: %s", err)) + return 2 + } + return c.printStatus(status) +} + +// cancel cancels the root token generation +func (c *GenerateRootCommand) cancel(client *api.Client) int { + if err := client.Sys().GenerateRootCancel(); err != nil { + c.UI.Error(fmt.Sprintf("Error canceling root token generation: %s", err)) + return 2 + } + c.UI.Output("Success! Root token generation canceled (if it was started)") + return 0 +} + +// status is used just to fetch and dump the status +func (c *GenerateRootCommand) status(client *api.Client) int { + status, err := client.Sys().GenerateRootStatus() + if err != nil { + c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err)) + return 2 + } + return c.printStatus(status) +} + +// printStatus dumps the status to output +func (c *GenerateRootCommand) printStatus(status *api.GenerateRootStatusResponse) int { + out := []string{} + out = append(out, fmt.Sprintf("Nonce | %s", status.Nonce)) + out = append(out, fmt.Sprintf("Started | %t", status.Started)) + out = append(out, fmt.Sprintf("Progress | %d/%d", status.Progress, status.Required)) + out = append(out, fmt.Sprintf("Complete | %t", status.Complete)) + if status.PGPFingerprint != "" { + out = append(out, fmt.Sprintf("PGP Fingerprint | %s", status.PGPFingerprint)) + } + if status.EncodedRootToken != "" { + out = append(out, fmt.Sprintf("Root Token | %s", status.EncodedRootToken)) + } + + output := columnOutput(out) + c.UI.Output(output) + return 0 +} diff --git a/command/generate_root_test.go b/command/generate_root_test.go new file mode 100644 index 0000000000..ba13f6b47a --- /dev/null +++ b/command/generate_root_test.go @@ -0,0 +1,448 @@ +package command + +import ( + "io" + "regexp" + "strings" + "testing" + + uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/xor" + "github.com/mitchellh/cli" +) + +func testGenerateRootCommand(tb testing.TB) (*cli.MockUi, *GenerateRootCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &GenerateRootCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestGenerateRootCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "init_no_args", + []string{ + "-init", + }, + "must specify either -otp or -pgp-key", + 1, + }, + { + "init_invalid_otp", + []string{ + "-init", + "-otp", "not-a-valid-otp", + }, + "Error initializing: invalid OTP:", + 1, + }, + { + "init_pgp_multi", + []string{ + "-init", + "-pgp-key", "keybase:hashicorp", + "-pgp-key", "keybase:jefferai", + }, + "can only be specified once", + 1, + }, + { + "init_pgp_multi_inline", + []string{ + "-init", + "-pgp-key", "keybase:hashicorp,keybase:jefferai", + }, + "can only specify one pgp key", + 1, + }, + { + "init_pgp_otp", + []string{ + "-init", + "-pgp-key", "keybase:hashicorp", + "-otp", "abcd1234", + }, + "cannot specify both -otp and -pgp-key", + 1, + }, + } + + 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() + + ui, cmd := testGenerateRootCommand(t) + + 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("generate_otp", func(t *testing.T) { + t.Parallel() + + ui, cmd := testGenerateRootCommand(t) + + code := cmd.Run([]string{ + "-generate-otp", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + output := ui.OutputWriter.String() + ui.ErrorWriter.String() + if err := cmd.verifyOTP(output); err != nil { + t.Fatal(err) + } + }) + + t.Run("decode", func(t *testing.T) { + t.Parallel() + + encoded := "L9MaZ/4mQanpOV6QeWd84g==" + otp := "dIeeezkjpDUv3fy7MYPOLQ==" + + ui, cmd := testGenerateRootCommand(t) + + code := cmd.Run([]string{ + "-decode", encoded, + "-otp", otp, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "5b54841c-c705-e59c-c6e4-a22b48e4b2cf" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if combined != expected { + t.Errorf("expected %q to be %q", combined, expected) + } + }) + + t.Run("cancel", func(t *testing.T) { + t.Parallel() + + otp := "dIeeezkjpDUv3fy7MYPOLQ==" + + client, closer := testVaultServer(t) + defer closer() + + // Initialize a generation + if _, err := client.Sys().GenerateRootInit(otp, ""); err != nil { + t.Fatal(err) + } + + ui, cmd := testGenerateRootCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-cancel", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Root token generation canceled" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + status, err := client.Sys().GenerateRootStatus() + if err != nil { + t.Fatal(err) + } + + if status.Started { + t.Errorf("expected status to be canceled: %#v", status) + } + }) + + t.Run("init_otp", func(t *testing.T) { + t.Parallel() + + otp := "dIeeezkjpDUv3fy7MYPOLQ==" + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testGenerateRootCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-init", + "-otp", otp, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Nonce" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + status, err := client.Sys().GenerateRootStatus() + if err != nil { + t.Fatal(err) + } + + if !status.Started { + t.Errorf("expected status to be started: %#v", status) + } + }) + + t.Run("init_pgp", func(t *testing.T) { + t.Parallel() + + pgpKey := "keybase:hashicorp" + pgpFingerprint := "91a6e7f85d05c65630bef18951852d87348ffc4c" + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testGenerateRootCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-init", + "-pgp-key", pgpKey, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Nonce" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + status, err := client.Sys().GenerateRootStatus() + if err != nil { + t.Fatal(err) + } + + if !status.Started { + t.Errorf("expected status to be started: %#v", status) + } + if status.PGPFingerprint != pgpFingerprint { + t.Errorf("expected %q to be %q", status.PGPFingerprint, pgpFingerprint) + } + }) + + t.Run("status", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testGenerateRootCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-status", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Nonce" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("provide_arg", func(t *testing.T) { + t.Parallel() + + otp := "dIeeezkjpDUv3fy7MYPOLQ==" + + client, keys, closer := testVaultServerUnseal(t) + defer closer() + + // Initialize a generation + status, err := client.Sys().GenerateRootInit(otp, "") + if err != nil { + t.Fatal(err) + } + nonce := status.Nonce + + // Supply the first n-1 unseal keys + for _, key := range keys[:len(keys)-1] { + _, cmd := testGenerateRootCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-nonce", nonce, + key, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + } + + ui, cmd := testGenerateRootCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-nonce", nonce, + keys[len(keys)-1], // the last unseal key + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + reToken := regexp.MustCompile(`Root Token\s+(.+)`) + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + match := reToken.FindAllStringSubmatch(combined, -1) + if len(match) < 1 || len(match[0]) < 2 { + t.Fatalf("no match: %#v", match) + } + + tokenBytes, err := xor.XORBase64(match[0][1], otp) + if err != nil { + t.Fatal(err) + } + token, err := uuid.FormatUUID(tokenBytes) + if err != nil { + t.Fatal(err) + } + + if l, exp := len(token), 36; l != exp { + t.Errorf("expected %d to be %d: %s", l, exp, token) + } + }) + + t.Run("provide_stdin", func(t *testing.T) { + t.Parallel() + + otp := "dIeeezkjpDUv3fy7MYPOLQ==" + + client, keys, closer := testVaultServerUnseal(t) + defer closer() + + // Initialize a generation + status, err := client.Sys().GenerateRootInit(otp, "") + if err != nil { + t.Fatal(err) + } + nonce := status.Nonce + + // Supply the first n-1 unseal keys + for _, key := range keys[:len(keys)-1] { + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(key)) + stdinW.Close() + }() + + _, cmd := testGenerateRootCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + code := cmd.Run([]string{ + "-nonce", nonce, + "-", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + } + + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(keys[len(keys)-1])) // the last unseal key + stdinW.Close() + }() + + ui, cmd := testGenerateRootCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + code := cmd.Run([]string{ + "-nonce", nonce, + "-", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + reToken := regexp.MustCompile(`Root Token\s+(.+)`) + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + match := reToken.FindAllStringSubmatch(combined, -1) + if len(match) < 1 || len(match[0]) < 2 { + t.Fatalf("no match: %#v", match) + } + + tokenBytes, err := xor.XORBase64(match[0][1], otp) + if err != nil { + t.Fatal(err) + } + token, err := uuid.FormatUUID(tokenBytes) + if err != nil { + t.Fatal(err) + } + + if l, exp := len(token), 36; l != exp { + t.Errorf("expected %d to be %d: %s", l, exp, token) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testGenerateRootCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/foo", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error getting root generation status: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testGenerateRootCommand(t) + assertNoTabs(t, cmd) + }) +} From a3c4e35848dcdf69f5822904f75e69806984ce5f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:02:02 -0400 Subject: [PATCH 053/281] Update init command --- command/init.go | 906 ++++++++++++++++++++++++++----------------- command/init_test.go | 628 +++++++++++++++--------------- 2 files changed, 870 insertions(+), 664 deletions(-) diff --git a/command/init.go b/command/init.go index 470c325107..c2d1120816 100644 --- a/command/init.go +++ b/command/init.go @@ -1,406 +1,594 @@ package command import ( + "encoding/json" "fmt" "net/url" - "os" "runtime" "strings" - consulapi "github.com/hashicorp/consul/api" + "github.com/ghodss/yaml" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/physical/consul" + "github.com/mitchellh/cli" "github.com/posener/complete" + + consulapi "github.com/hashicorp/consul/api" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*InitCommand)(nil) +var _ cli.CommandAutocomplete = (*InitCommand)(nil) + // InitCommand is a Command that initializes a new Vault server. type InitCommand struct { - meta.Meta -} + *BaseCommand -func (c *InitCommand) Run(args []string) int { - var threshold, shares, storedShares, recoveryThreshold, recoveryShares int - var pgpKeys, recoveryPgpKeys, rootTokenPgpKey pgpkeys.PubKeyFilesFlag - var auto, check bool - var consulServiceName string - flags := c.Meta.FlagSet("init", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - flags.IntVar(&shares, "key-shares", 5, "") - flags.IntVar(&threshold, "key-threshold", 3, "") - flags.IntVar(&storedShares, "stored-shares", 0, "") - flags.Var(&pgpKeys, "pgp-keys", "") - flags.Var(&rootTokenPgpKey, "root-token-pgp-key", "") - flags.IntVar(&recoveryShares, "recovery-shares", 5, "") - flags.IntVar(&recoveryThreshold, "recovery-threshold", 3, "") - flags.Var(&recoveryPgpKeys, "recovery-pgp-keys", "") - flags.BoolVar(&check, "check", false, "") - flags.BoolVar(&auto, "auto", false, "") - flags.StringVar(&consulServiceName, "consul-service", consul.DefaultServiceName, "") - if err := flags.Parse(args); err != nil { - return 1 - } + flagStatus bool + flagKeyShares int + flagKeyThreshold int + flagPGPKeys []string + flagRootTokenPGPKey string - initRequest := &api.InitRequest{ - SecretShares: shares, - SecretThreshold: threshold, - StoredShares: storedShares, - PGPKeys: pgpKeys, - RecoveryShares: recoveryShares, - RecoveryThreshold: recoveryThreshold, - RecoveryPGPKeys: recoveryPgpKeys, - } + // HSM + flagStoredShares int + flagRecoveryShares int + flagRecoveryThreshold int + flagRecoveryPGPKeys []string - switch len(rootTokenPgpKey) { - case 0: - case 1: - initRequest.RootTokenPGPKey = rootTokenPgpKey[0] - default: - c.Ui.Error("Only one PGP key can be specified for encrypting the root token") - return 1 - } + // Consul + flagConsulAuto bool + flagConsulService string - // If running in 'auto' mode, run service discovery based on environment - // variables of Consul. - if auto { - - // Create configuration for Consul - consulConfig := consulapi.DefaultConfig() - - // Create a client to communicate with Consul - consulClient, err := consulapi.NewClient(consulConfig) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to create Consul client:%v", err)) - return 1 - } - - // Fetch Vault's protocol scheme from the client - vaultclient, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to fetch Vault client: %v", err)) - return 1 - } - - if vaultclient.Address() == "" { - c.Ui.Error("Failed to fetch Vault client address") - return 1 - } - - clientURL, err := url.Parse(vaultclient.Address()) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %v", err)) - return 1 - } - - if clientURL == nil { - c.Ui.Error("Failed to parse Vault client address") - return 1 - } - - var uninitializedVaults []string - var initializedVault string - - // Query the nodes belonging to the cluster - if services, _, err := consulClient.Catalog().Service(consulServiceName, "", &consulapi.QueryOptions{AllowStale: true}); err == nil { - Loop: - for _, service := range services { - vaultAddress := &url.URL{ - Scheme: clientURL.Scheme, - Host: fmt.Sprintf("%s:%d", service.ServiceAddress, service.ServicePort), - } - - // Set VAULT_ADDR to the discovered node - os.Setenv(api.EnvVaultAddress, vaultAddress.String()) - - // Create a client to communicate with the discovered node - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing client: %v", err)) - return 1 - } - - // Check the initialization status of the discovered node - inited, err := client.Sys().InitStatus() - switch { - case err != nil: - c.Ui.Error(fmt.Sprintf("Error checking initialization status of discovered node: %+q. Err: %v", vaultAddress.String(), err)) - return 1 - case inited: - // One of the nodes in the cluster is initialized. Break out. - initializedVault = vaultAddress.String() - break Loop - default: - // Vault is uninitialized. - uninitializedVaults = append(uninitializedVaults, vaultAddress.String()) - } - } - } - - export := "export" - quote := "'" - if runtime.GOOS == "windows" { - export = "set" - quote = "" - } - - if initializedVault != "" { - vaultURL, err := url.Parse(initializedVault) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", initializedVault, err)) - } - c.Ui.Output(fmt.Sprintf("Discovered an initialized Vault node at %+q, using Consul service name %+q", vaultURL.String(), consulServiceName)) - c.Ui.Output("\nSet the following environment variable to operate on the discovered Vault:\n") - c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote)) - return 0 - } - - switch len(uninitializedVaults) { - case 0: - c.Ui.Error(fmt.Sprintf("Failed to discover Vault nodes using Consul service name %+q", consulServiceName)) - return 1 - case 1: - // There was only one node found in the Vault cluster and it - // was uninitialized. - - vaultURL, err := url.Parse(uninitializedVaults[0]) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", uninitializedVaults[0], err)) - } - - // Set the VAULT_ADDR to the discovered node. This will ensure - // that the client created will operate on the discovered node. - os.Setenv(api.EnvVaultAddress, vaultURL.String()) - - // Let the client know that initialization is perfomed on the - // discovered node. - c.Ui.Output(fmt.Sprintf("Discovered Vault at %+q using Consul service name %+q\n", vaultURL.String(), consulServiceName)) - - // Attempt initializing it - ret := c.runInit(check, initRequest) - - // Regardless of success or failure, instruct client to update VAULT_ADDR - c.Ui.Output("\nSet the following environment variable to operate on the discovered Vault:\n") - c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote)) - - return ret - default: - // If more than one Vault node were discovered, print out all of them, - // requiring the client to update VAULT_ADDR and to run init again. - c.Ui.Output(fmt.Sprintf("Discovered more than one uninitialized Vaults using Consul service name %+q\n", consulServiceName)) - c.Ui.Output("To initialize these Vaults, set any *one* of the following environment variables and run 'vault init':") - - // Print valid commands to make setting the variables easier - for _, vaultNode := range uninitializedVaults { - vaultURL, err := url.Parse(vaultNode) - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", vaultNode, err)) - } - c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote)) - - } - return 0 - } - } - - return c.runInit(check, initRequest) -} - -func (c *InitCommand) runInit(check bool, initRequest *api.InitRequest) int { - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 1 - } - - if check { - return c.checkStatus(client) - } - - resp, err := client.Sys().Init(initRequest) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing Vault: %s", err)) - return 1 - } - - for i, key := range resp.Keys { - if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) { - c.Ui.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, resp.KeysB64[i])) - } else { - c.Ui.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, key)) - } - } - for i, key := range resp.RecoveryKeys { - if resp.RecoveryKeysB64 != nil && len(resp.RecoveryKeysB64) == len(resp.RecoveryKeys) { - c.Ui.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, resp.RecoveryKeysB64[i])) - } else { - c.Ui.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, key)) - } - } - - c.Ui.Output(fmt.Sprintf("Initial Root Token: %s", resp.RootToken)) - - if initRequest.StoredShares < 1 { - c.Ui.Output(fmt.Sprintf( - "\n"+ - "Vault initialized with %d keys and a key threshold of %d. Please\n"+ - "securely distribute the above keys. When the vault is re-sealed,\n"+ - "restarted, or stopped, you must provide at least %d of these keys\n"+ - "to unseal it again.\n\n"+ - "Vault does not store the master key. Without at least %d keys,\n"+ - "your vault will remain permanently sealed.", - initRequest.SecretShares, - initRequest.SecretThreshold, - initRequest.SecretThreshold, - initRequest.SecretThreshold, - )) - } else { - c.Ui.Output( - "\n" + - "Vault initialized successfully.", - ) - } - if len(resp.RecoveryKeys) > 0 { - c.Ui.Output(fmt.Sprintf( - "\n"+ - "Recovery key initialized with %d keys and a key threshold of %d. Please\n"+ - "securely distribute the above keys.", - initRequest.RecoveryShares, - initRequest.RecoveryThreshold, - )) - } - - return 0 -} - -func (c *InitCommand) checkStatus(client *api.Client) int { - inited, err := client.Sys().InitStatus() - switch { - case err != nil: - c.Ui.Error(fmt.Sprintf( - "Error checking initialization status: %s", err)) - return 1 - case inited: - c.Ui.Output("Vault has been initialized") - return 0 - default: - c.Ui.Output("Vault is not initialized") - return 2 - } + // Deprecations + // TODO: remove in 0.9.0 + flagAuto bool + flagCheck bool } func (c *InitCommand) Synopsis() string { - return "Initialize a new Vault server" + return "Initializes a server" } func (c *InitCommand) Help() string { helpText := ` Usage: vault init [options] - Initialize a new Vault server. + Initializes a Vault server. Initialization is the process by which Vault's + storage backend is prepared to receive data. Since Vault server's share the + same storage backend in HA mode, you only need to initialize one Vault to + initialize the storage backend. - This command connects to a Vault server and initializes it for the - first time. This sets up the initial set of master keys and the - backend data store structure. + During initialization, Vault generates an in-memory master key and applies + Shamir's secret sharing algorithm to disassemble that master key into a + configuration number of key shares such that a configurable subset of those + key shares must come together to regenerate the master key. These keys are + often called "unseal keys" in Vault's documentation. - This command can't be called on an already-initialized Vault server. + This command cannot be run against already-initialized Vault cluster. -General Options: -` + meta.GeneralOptionsUsage() + ` -Init Options: + Start initialization with the default options: - -check Don't actually initialize, just check if Vault is - already initialized. A return code of 0 means Vault - is initialized; a return code of 2 means Vault is not - initialized; a return code of 1 means an error was - encountered. + $ vault init - -key-shares=5 The number of key shares to split the master key - into. + Initialize, but encrypt the unseal keys with pgp keys: - -key-threshold=3 The number of key shares required to reconstruct - the master key. + $ vault init \ + -key-shares=3 \ + -key-threshold=2 \ + -pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo" - -stored-shares=0 The number of unseal keys to store. Only used with - Vault HSM. Must currently be equivalent to the - number of shares. + Encrypt the initial root token using a pgp key: - -pgp-keys If provided, must be a comma-separated list of - files on disk containing binary- or base64-format - public PGP keys, or Keybase usernames specified as - "keybase:". The output unseal keys will - be encrypted and base64-encoded, in order, with the - given public keys. If you want to use them with the - 'vault unseal' command, you will need to base64- - decode and decrypt; this will be the plaintext - unseal key. When 'stored-shares' are not used, the - number of entries in this field must match 'key-shares'. - When 'stored-shares' are used, the number of entries - should match the difference between 'key-shares' - and 'stored-shares'. + $ vault init -root-token-pgp-key="keybase:hashicorp" - -root-token-pgp-key If provided, a file on disk with a binary- or - base64-format public PGP key, or a Keybase username - specified as "keybase:". The output root - token will be encrypted and base64-encoded, in - order, with the given public key. You will need - to base64-decode and decrypt the result. + For a complete list of examples, please see the documentation. - -recovery-shares=5 The number of key shares to split the recovery key - into. Only used with Vault HSM. - - -recovery-threshold=3 The number of key shares required to reconstruct - the recovery key. Only used with Vault HSM. - - -recovery-pgp-keys If provided, behaves like "pgp-keys" but for the - recovery key shares. Only used with Vault HSM. - - -auto If set, performs service discovery using Consul. - When all the nodes of a Vault cluster are - registered with Consul, setting this flag will - trigger service discovery using the service name - with which Vault nodes are registered. This option - works well when each Vault cluster is registered - under a unique service name. Note that, when Consul - is serving as Vault's HA backend, Vault nodes are - registered with Consul by default. The service name - can be changed using 'consul-service' flag. Ensure - that environment variables required to communicate - with Consul, like (CONSUL_HTTP_ADDR, - CONSUL_HTTP_TOKEN, CONSUL_HTTP_SSL, et al) are - properly set. When only one Vault node is - discovered, it will be initialized and when more - than one Vault node is discovered, they will be - output for easy selection. - - -consul-service Service name under which all the nodes of a Vault - cluster are registered with Consul. Note that, when - Vault uses Consul as its HA backend, by default, - Vault will register itself as a service with Consul - with the service name "vault". This name can be - modified in Vault's configuration file, using the - "service" option for the Consul backend. -` +` + c.Flags().Help() return strings.TrimSpace(helpText) } +func (c *InitCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + // Common Options + f := set.NewFlagSet("Common Options") + + f.BoolVar(&BoolVar{ + Name: "status", + Target: &c.flagStatus, + Default: false, + Usage: "Print the current initialization status. An exit code of 0 means " + + "the Vault is already initialized. An exit code of 1 means an error " + + "occurred. An exit code of 2 means the mean is not initialized.", + }) + + f.IntVar(&IntVar{ + Name: "key-shares", + Aliases: []string{"n"}, + Target: &c.flagKeyShares, + Default: 5, + Completion: complete.PredictAnything, + Usage: "Number of key shares to split the generated master key into. " + + "This is the number of \"unseal keys\" to generate.", + }) + + f.IntVar(&IntVar{ + Name: "key-threshold", + Aliases: []string{"t"}, + Target: &c.flagKeyThreshold, + Default: 3, + Completion: complete.PredictAnything, + Usage: "Number of key shares required to reconstruct the master key. " + + "This must be less than or equal to -key-shares.", + }) + + f.VarFlag(&VarFlag{ + Name: "pgp-keys", + Value: (*pgpkeys.PubKeyFilesFlag)(&c.flagPGPKeys), + Completion: complete.PredictAnything, + Usage: "Comma-separated list of paths to files on disk containing " + + "public GPG keys OR a comma-separated list of Keybase usernames using " + + "the format \"keybase:\". When supplied, the generated " + + "unseal keys will be encrypted and base64-encoded in the order " + + "specified in this list. The number of entires must match -key-shares, " + + "unless -store-shares are used.", + }) + + f.VarFlag(&VarFlag{ + Name: "root-token-pgp-key", + Value: (*pgpkeys.PubKeyFileFlag)(&c.flagRootTokenPGPKey), + Completion: complete.PredictAnything, + Usage: "Path to a file on disk containing a binary or base64-encoded " + + "public GPG key. This can also be specified as a Keybase username " + + "using the format \"keybase:\". When supplied, the generated " + + "root token will be encrypted and base64-encoded with the given public " + + "key.", + }) + + // Consul Options + f = set.NewFlagSet("Consul Options") + + f.BoolVar(&BoolVar{ + Name: "consul-auto", + Target: &c.flagConsulAuto, + Default: false, + Usage: "Perform automatic service discovery using Consul in HA mode. " + + "When all nodes in a Vault HA cluster are registered with Consul, " + + "enabling this option will trigger automatic service discovery based " + + "on the provided -consul-service value. When Consul is Vault's HA " + + "backend, this functionality is automatically enabled. Ensure the " + + "proper Consul environment variables are set (CONSUL_HTTP_ADDR, etc). " + + "When only one Vault server is discovered, it will be initialized " + + "automatically. When more than one Vault server is discovered, they " + + "will each be output for selection.", + }) + + f.StringVar(&StringVar{ + Name: "consul-service", + Target: &c.flagConsulService, + Default: "vault", + Completion: complete.PredictAnything, + Usage: "Name of the service in Consul under which the Vault servers are " + + "registered.", + }) + + // HSM Options + f = set.NewFlagSet("HSM Options") + + f.IntVar(&IntVar{ + Name: "recovery-shares", + Target: &c.flagRecoveryShares, + Default: 5, + Completion: complete.PredictAnything, + Usage: "Number of key shares to split the recovery key into. " + + "This is only used in HSM mode.", + }) + + f.IntVar(&IntVar{ + Name: "recovery-threshold", + Target: &c.flagRecoveryThreshold, + Default: 3, + Completion: complete.PredictAnything, + Usage: "Number of key shares required to reconstruct the recovery key. " + + "This is only used in HSM mode.", + }) + + f.VarFlag(&VarFlag{ + Name: "recovery-pgp-keys", + Value: (*pgpkeys.PubKeyFilesFlag)(&c.flagRecoveryPGPKeys), + Completion: complete.PredictAnything, + Usage: "Behaves like -pgp-keys, but for the recovery key shares. This " + + "is only used in HSM mode.", + }) + + f.IntVar(&IntVar{ + Name: "stored-shares", + Target: &c.flagStoredShares, + Default: 0, // No default, because we need to check if was supplied + Completion: complete.PredictAnything, + Usage: "Number of unseal keys to store on an HSM. This must be equal to " + + "-key-shares. This is only used in HSM mode.", + }) + + // Deprecations + // TODO: remove in 0.9.0 + f.BoolVar(&BoolVar{ + Name: "check", // prefer -status + Target: &c.flagCheck, + Default: false, + Hidden: true, + Usage: "", + }) + f.BoolVar(&BoolVar{ + Name: "auto", // prefer -consul-auto + Target: &c.flagAuto, + Default: false, + Hidden: true, + Usage: "", + }) + + return set +} + func (c *InitCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictNothing + return nil } func (c *InitCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-check": complete.PredictNothing, - "-key-shares": complete.PredictNothing, - "-key-threshold": complete.PredictNothing, - "-pgp-keys": complete.PredictNothing, - "-root-token-pgp-key": complete.PredictNothing, - "-recovery-shares": complete.PredictNothing, - "-recovery-threshold": complete.PredictNothing, - "-recovery-pgp-keys": complete.PredictNothing, - "-auto": complete.PredictNothing, - "-consul-service": complete.PredictNothing, + return c.Flags().Completions() +} + +func (c *InitCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + // Deprecations + // TODO: remove in 0.9.0 + if c.flagAuto { + c.UI.Warn(wrapAtLength("WARNING! -auto is deprecated. Please use " + + "-consul-auto instead. This will be removed the next major release " + + "of Vault.")) + c.flagConsulAuto = true + } + if c.flagCheck { + c.UI.Warn(wrapAtLength("WARNING! -check is deprecated. Please use " + + "-status instead. This will be removed in the next major release " + + "of Vault.")) + c.flagStatus = true + } + + // Build the initial init request + initReq := &api.InitRequest{ + SecretShares: c.flagKeyShares, + SecretThreshold: c.flagKeyThreshold, + PGPKeys: c.flagPGPKeys, + RootTokenPGPKey: c.flagRootTokenPGPKey, + + StoredShares: c.flagStoredShares, + RecoveryShares: c.flagRecoveryShares, + RecoveryThreshold: c.flagRecoveryThreshold, + RecoveryPGPKeys: c.flagRecoveryPGPKeys, + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Check auto mode + switch { + case c.flagStatus: + return c.status(client) + case c.flagConsulAuto: + return c.consulAuto(client, initReq) + default: + return c.init(client, initReq) } } + +// consulAuto enables auto-joining via Consul. +func (c *InitCommand) consulAuto(client *api.Client, req *api.InitRequest) int { + // Capture the client original address and reset it + originalAddr := client.Address() + defer client.SetAddress(originalAddr) + + // Create a client to communicate with Consul + consulClient, err := consulapi.NewClient(consulapi.DefaultConfig()) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to create Consul client:%v", err)) + return 1 + } + + // Pull the scheme from the Vault client to determine if the Consul agent + // should talk via HTTP or HTTPS. + addr := client.Address() + clientURL, err := url.Parse(addr) + if err != nil || clientURL == nil { + c.UI.Error(fmt.Sprintf("Failed to parse Vault address %s: %s", addr, err)) + return 1 + } + + var uninitedVaults []string + var initedVault string + + // Query the nodes belonging to the cluster + services, _, err := consulClient.Catalog().Service(c.flagConsulService, "", &consulapi.QueryOptions{ + AllowStale: true, + }) + if err == nil { + for _, service := range services { + // Set the address on the client temporarily + vaultAddr := (&url.URL{ + Scheme: clientURL.Scheme, + Host: fmt.Sprintf("%s:%d", service.ServiceAddress, service.ServicePort), + }).String() + client.SetAddress(vaultAddr) + + // Check the initialization status of the discovered node + inited, err := client.Sys().InitStatus() + if err != nil { + c.UI.Error(fmt.Sprintf("Error checking init status of %q: %s", vaultAddr, err)) + } + if inited { + initedVault = vaultAddr + break + } + + // If we got this far, we communicated successfully with Vault, but it + // was not initialized. + uninitedVaults = append(uninitedVaults, vaultAddr) + } + } + + // Get the correct export keywords and quotes for *nix vs Windows + export := "export" + quote := "\"" + if runtime.GOOS == "windows" { + export = "set" + quote = "" + } + + if initedVault != "" { + vaultURL, err := url.Parse(initedVault) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err)) + return 2 + } + vaultAddr := vaultURL.String() + + c.UI.Output(wrapAtLength(fmt.Sprintf( + "Discovered an initialized Vault node at %q with Consul service name "+ + "%q. Set the following environment variable to target the discovered "+ + "Vault server:", + vaultURL.String(), c.flagConsulService))) + c.UI.Output("") + c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote)) + c.UI.Output("") + return 0 + } + + switch len(uninitedVaults) { + case 0: + c.UI.Error(fmt.Sprintf("No Vault nodes registered as %q in Consul", c.flagConsulService)) + return 2 + case 1: + // There was only one node found in the Vault cluster and it was + // uninitialized. + vaultURL, err := url.Parse(uninitedVaults[0]) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err)) + return 2 + } + vaultAddr := vaultURL.String() + + // Update the client to connect to this Vault server + client.SetAddress(vaultAddr) + + // Let the client know that initialization is perfomed on the + // discovered node. + c.UI.Output(wrapAtLength(fmt.Sprintf( + "Discovered an initialized Vault node at %q with Consul service name "+ + "%q. Set the following environment variable to target the discovered "+ + "Vault server:", + vaultURL.String(), c.flagConsulService))) + c.UI.Output("") + c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote)) + c.UI.Output("") + c.UI.Output("Attempting to initialize it...") + c.UI.Output("") + + // Attempt to initialize it + return c.init(client, req) + default: + // If more than one Vault node were discovered, print out all of them, + // requiring the client to update VAULT_ADDR and to run init again. + c.UI.Output(wrapAtLength(fmt.Sprintf( + "Discovered %d uninitialized Vault servers with Consul service name "+ + "%q. To initialize these Vatuls, set any one of the following "+ + "environment variables and run \"vault init\":", + len(uninitedVaults), c.flagConsulService))) + c.UI.Output("") + + // Print valid commands to make setting the variables easier + for _, node := range uninitedVaults { + vaultURL, err := url.Parse(node) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err)) + return 2 + } + vaultAddr := vaultURL.String() + + c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote)) + } + + c.UI.Output("") + return 0 + } +} + +func (c *InitCommand) init(client *api.Client, req *api.InitRequest) int { + resp, err := client.Sys().Init(req) + if err != nil { + c.UI.Error(fmt.Sprintf("Error initializing: %s", err)) + return 2 + } + + switch c.flagFormat { + case "yaml", "yml": + return c.initOutputYAML(req, resp) + case "json": + return c.initOutputJSON(req, resp) + case "table": + default: + c.UI.Error(fmt.Sprintf("Unknown format: %s", c.flagFormat)) + return 1 + } + + for i, key := range resp.Keys { + if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) { + c.UI.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, resp.KeysB64[i])) + } else { + c.UI.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, key)) + } + } + for i, key := range resp.RecoveryKeys { + if resp.RecoveryKeysB64 != nil && len(resp.RecoveryKeysB64) == len(resp.RecoveryKeys) { + c.UI.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, resp.RecoveryKeysB64[i])) + } else { + c.UI.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, key)) + } + } + + c.UI.Output("") + c.UI.Output(fmt.Sprintf("Initial Root Token: %s", resp.RootToken)) + + if req.StoredShares < 1 { + c.UI.Output("") + c.UI.Output(wrapAtLength(fmt.Sprintf( + "Vault initialized with %d key shares an a key threshold of %d. Please "+ + "securely distributed the key shares printed above. When the Vault is "+ + "re-sealed, restarted, or stopped, you must supply at least %d of "+ + "these keys to unseal it before it can start servicing requests.", + req.SecretShares, + req.SecretThreshold, + req.SecretThreshold))) + + c.UI.Output("") + c.UI.Output(wrapAtLength(fmt.Sprintf( + "Vault does not store the generated master key. Without at least %d "+ + "key to reconstruct the master key, Vault will remain permanently "+ + "sealed!", + req.SecretThreshold))) + + c.UI.Output("") + c.UI.Output(wrapAtLength( + "It is possible to generate new unseal keys, provided you have a quorum " + + "of existing unseal keys shares. See \"vault rekey\" for more " + + "information.")) + } else { + c.UI.Output("") + c.UI.Output("Success! Vault is initialized") + } + + if len(resp.RecoveryKeys) > 0 { + c.UI.Output("") + c.UI.Output(wrapAtLength(fmt.Sprintf( + "Recovery key initialized with %d key shares and a key threshold of %d. "+ + "Please securely distribute the key shares printed above.", + req.RecoveryShares, + req.RecoveryThreshold))) + } + + return 0 +} + +// initOutputYAML outputs the init output as YAML. +func (c *InitCommand) initOutputYAML(req *api.InitRequest, resp *api.InitResponse) int { + b, err := yaml.Marshal(newMachineInit(req, resp)) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshaling YAML: %s", err)) + return 2 + } + return PrintRaw(c.UI, strings.TrimSpace(string(b))) +} + +// initOutputJSON outputs the init output as JSON. +func (c *InitCommand) initOutputJSON(req *api.InitRequest, resp *api.InitResponse) int { + b, err := json.Marshal(newMachineInit(req, resp)) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshaling JSON: %s", err)) + return 2 + } + return PrintRaw(c.UI, strings.TrimSpace(string(b))) +} + +// status inspects the init status of vault and returns an appropriate error +// code and message. +func (c *InitCommand) status(client *api.Client) int { + inited, err := client.Sys().InitStatus() + if err != nil { + c.UI.Error(fmt.Sprintf("Error checking init status: %s", err)) + return 1 // Normally we'd return 2, but 2 means something special here + } + + if inited { + c.UI.Output("Vault is initialized") + return 0 + } + + c.UI.Output("Vault is not initialized") + return 2 +} + +// machineInit is used to output information about the init command. +type machineInit struct { + UnsealKeysB64 []string `json:"unseal_keys_b64"` + UnsealKeysHex []string `json:"unseal_keys_hex"` + UnsealShares int `json:"unseal_shares"` + UnsealThreshold int `json:"unseal_threshold"` + RecoveryKeysB64 []string `json:"recovery_keys_b64"` + RecoveryKeysHex []string `json:"recovery_keys_hex"` + RecoveryShares int `json:"recovery_keys_shares"` + RecoveryThreshold int `json:"recovery_keys_threshold"` + RootToken string `json:"root_token"` +} + +func newMachineInit(req *api.InitRequest, resp *api.InitResponse) *machineInit { + init := &machineInit{} + + init.UnsealKeysHex = make([]string, len(resp.Keys)) + for i, v := range resp.Keys { + init.UnsealKeysHex[i] = v + } + + init.UnsealKeysB64 = make([]string, len(resp.KeysB64)) + for i, v := range resp.KeysB64 { + init.UnsealKeysB64[i] = v + } + + init.UnsealShares = req.SecretShares + init.UnsealThreshold = req.SecretThreshold + + init.RecoveryKeysHex = make([]string, len(resp.RecoveryKeys)) + for i, v := range resp.RecoveryKeys { + init.RecoveryKeysHex[i] = v + } + + init.RecoveryKeysB64 = make([]string, len(resp.RecoveryKeysB64)) + for i, v := range resp.RecoveryKeysB64 { + init.RecoveryKeysB64[i] = v + } + + init.RecoveryShares = req.RecoveryShares + init.RecoveryThreshold = req.RecoveryThreshold + + init.RootToken = resp.RootToken + + return init +} diff --git a/command/init_test.go b/command/init_test.go index e09ba80dc0..c0f88e58d9 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -1,343 +1,361 @@ package command import ( - "bytes" - "encoding/base64" + "fmt" "os" - "reflect" "regexp" + "strconv" "strings" "testing" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" - "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/packet" "github.com/mitchellh/cli" ) -func TestInit(t *testing.T) { - ui := new(cli.MockUi) - c := &InitCommand{ - Meta: meta.Meta{ - Ui: ui, +func testInitCommand(tb testing.TB) (*cli.MockUi, *InitCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &InitCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - core := vault.TestCore(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - init, err := core.Initialized() - if err != nil { - t.Fatalf("err: %s", err) - } - if init { - t.Fatal("should not be initialized") - } - - args := []string{"-address", addr} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - init, err = core.Initialized() - if err != nil { - t.Fatalf("err: %s", err) - } - if !init { - t.Fatal("should be initialized") - } - - sealConf, err := core.SealAccess().BarrierConfig() - if err != nil { - t.Fatalf("err: %s", err) - } - expected := &vault.SealConfig{ - Type: "shamir", - SecretShares: 5, - SecretThreshold: 3, - } - if !reflect.DeepEqual(expected, sealConf) { - t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, sealConf) - } } -func TestInit_Check(t *testing.T) { - ui := new(cli.MockUi) - c := &InitCommand{ - Meta: meta.Meta{ - Ui: ui, +func TestInitCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "pgp_keys_multi", + []string{ + "-pgp-keys", "keybase:hashicorp", + "-pgp-keys", "keybase:jefferai", + }, + "can only be specified once", + 1, + }, + { + "root_token_pgp_key_multi", + []string{ + "-root-token-pgp-key", "keybase:hashicorp", + "-root-token-pgp-key", "keybase:jefferai", + }, + "can only be specified once", + 1, + }, + { + "root_token_pgp_key_multi_inline", + []string{ + "-root-token-pgp-key", "keybase:hashicorp,keybase:jefferai", + }, + "can only specify one pgp key", + 1, + }, + { + "recovery_pgp_keys_multi", + []string{ + "-recovery-pgp-keys", "keybase:hashicorp", + "-recovery-pgp-keys", "keybase:jefferai", + }, + "can only be specified once", + 1, + }, + { + "key_shares_pgp_less", + []string{ + "-key-shares", "10", + "-pgp-keys", "keybase:jefferai,keybase:sethvargo", + }, + "incorrect number", + 2, + }, + { + "key_shares_pgp_more", + []string{ + "-key-shares", "1", + "-pgp-keys", "keybase:jefferai,keybase:sethvargo", + }, + "incorrect number", + 2, }, } - core := vault.TestCore(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Should return 2, not initialized - args := []string{"-address", addr, "-check"} - if code := c.Run(args); code != 2 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + for _, tc := range cases { + tc := tc - // Now initialize it - args = []string{"-address", addr} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - // Should return 0, initialized - args = []string{"-address", addr, "-check"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServer(t) + defer closer() - init, err := core.Initialized() - if err != nil { - t.Fatalf("err: %s", err) - } - if !init { - t.Fatal("should be initialized") - } -} + ui, cmd := testInitCommand(t) + cmd.client = client -func TestInit_custom(t *testing.T) { - ui := new(cli.MockUi) - c := &InitCommand{ - Meta: meta.Meta{ - Ui: ui, - }, - } + code := cmd.Run(tc.args) + if code != tc.code { + t.Errorf("expected %d to be %d", code, tc.code) + } - core := vault.TestCore(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - init, err := core.Initialized() - if err != nil { - t.Fatalf("err: %s", err) - } - if init { - t.Fatal("should not be initialized") - } - - args := []string{ - "-address", addr, - "-key-shares", "7", - "-key-threshold", "3", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - init, err = core.Initialized() - if err != nil { - t.Fatalf("err: %s", err) - } - if !init { - t.Fatal("should be initialized") - } - - sealConf, err := core.SealAccess().BarrierConfig() - if err != nil { - t.Fatalf("err: %s", err) - } - expected := &vault.SealConfig{ - Type: "shamir", - SecretShares: 7, - SecretThreshold: 3, - } - if !reflect.DeepEqual(expected, sealConf) { - t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, sealConf) - } - - re, err := regexp.Compile("\\s+Initial Root Token:\\s+(.*)") - if err != nil { - t.Fatalf("Error compiling regex: %s", err) - } - matches := re.FindAllStringSubmatch(ui.OutputWriter.String(), -1) - if len(matches) != 1 { - t.Fatalf("Unexpected number of tokens found, got %d", len(matches)) - } - - rootToken := matches[0][1] - - client, err := c.Client() - if err != nil { - t.Fatalf("Error fetching client: %v", err) - } - - client.SetToken(rootToken) - - re, err = regexp.Compile("\\s*Unseal Key \\d+: (.*)") - if err != nil { - t.Fatalf("Error compiling regex: %s", err) - } - matches = re.FindAllStringSubmatch(ui.OutputWriter.String(), -1) - if len(matches) != 7 { - t.Fatalf("Unexpected number of keys returned, got %d, matches was \n\n%#v\n\n, input was \n\n%s\n\n", len(matches), matches, ui.OutputWriter.String()) - } - - var unsealed bool - for i := 0; i < 3; i++ { - decodedKey, err := base64.StdEncoding.DecodeString(strings.TrimSpace(matches[i][1])) - if err != nil { - t.Fatalf("err decoding key %v: %v", matches[i][1], err) + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, tc.out) { + t.Errorf("expected %q to contain %q", combined, tc.out) + } + }) } - unsealed, err = core.Unseal(decodedKey) - if err != nil { - t.Fatalf("err during unseal: %v; key was %v", err, matches[i][1]) + }) + + t.Run("status", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerUninit(t) + defer closer() + + ui, cmd := testInitCommand(t) + cmd.client = client + + // Verify the non-init response code + code := cmd.Run([]string{ + "-status", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) } - } - if !unsealed { - t.Fatal("expected to be unsealed") - } - tokenInfo, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatalf("Error looking up root token info: %v", err) - } - - if tokenInfo.Data["policies"].([]interface{})[0].(string) != "root" { - t.Fatalf("expected root policy") - } -} - -func TestInit_PGP(t *testing.T) { - ui := new(cli.MockUi) - c := &InitCommand{ - Meta: meta.Meta{ - Ui: ui, - }, - } - - core := vault.TestCore(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - init, err := core.Initialized() - if err != nil { - t.Fatalf("err: %s", err) - } - if init { - t.Fatal("should not be initialized") - } - - tempDir, pubFiles, err := getPubKeyFiles(t) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - args := []string{ - "-address", addr, - "-key-shares", "2", - "-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2], - "-key-threshold", "2", - "-root-token-pgp-key", pubFiles[0], - } - - // This should fail, as key-shares does not match pgp-keys size - if code := c.Run(args); code == 0 { - t.Fatalf("bad (command should have failed): %d\n\n%s", code, ui.ErrorWriter.String()) - } - - args = []string{ - "-address", addr, - "-key-shares", "4", - "-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2] + "," + pubFiles[3], - "-key-threshold", "2", - "-root-token-pgp-key", pubFiles[0], - } - - ui.OutputWriter.Reset() - - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - init, err = core.Initialized() - if err != nil { - t.Fatalf("err: %s", err) - } - if !init { - t.Fatal("should be initialized") - } - - sealConf, err := core.SealAccess().BarrierConfig() - if err != nil { - t.Fatalf("err: %s", err) - } - - pgpKeys := []string{} - for _, pubFile := range pubFiles { - pub, err := pgpkeys.ReadPGPFile(pubFile) - if err != nil { - t.Fatalf("bad: %v", err) + // Now init to verify the init response code + if _, err := client.Sys().Init(&api.InitRequest{ + SecretShares: 1, + SecretThreshold: 1, + }); err != nil { + t.Fatal(err) } - pgpKeys = append(pgpKeys, pub) - } - expected := &vault.SealConfig{ - Type: "shamir", - SecretShares: 4, - SecretThreshold: 2, - PGPKeys: pgpKeys, - } - if !reflect.DeepEqual(expected, sealConf) { - t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, sealConf) - } + // Verify the init response code + ui, cmd = testInitCommand(t) + cmd.client = client + code = cmd.Run([]string{ + "-status", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + }) - re, err := regexp.Compile("\\s+Initial Root Token:\\s+(.*)") - if err != nil { - t.Fatalf("Error compiling regex: %s", err) - } - matches := re.FindAllStringSubmatch(ui.OutputWriter.String(), -1) - if len(matches) != 1 { - t.Fatalf("Unexpected number of tokens found, got %d", len(matches)) - } + t.Run("default", func(t *testing.T) { + t.Parallel() - encRootToken := matches[0][1] - privKeyBytes, err := base64.StdEncoding.DecodeString(pgpkeys.TestPrivKey1) - if err != nil { - t.Fatalf("error decoding private key: %v", err) - } - ptBuf := bytes.NewBuffer(nil) - entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes))) - if err != nil { - t.Fatalf("Error parsing private key: %s", err) - } - var rootBytes []byte - rootBytes, err = base64.StdEncoding.DecodeString(encRootToken) - if err != nil { - t.Fatalf("Error decoding root token: %s", err) - } - entityList := &openpgp.EntityList{entity} - md, err := openpgp.ReadMessage(bytes.NewBuffer(rootBytes), entityList, nil, nil) - if err != nil { - t.Fatalf("Error decrypting root token: %s", err) - } - ptBuf.ReadFrom(md.UnverifiedBody) - rootToken := ptBuf.String() + client, closer := testVaultServerUninit(t) + defer closer() - parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, false, nil, nil, core) + ui, cmd := testInitCommand(t) + cmd.client = client - client, err := c.Client() - if err != nil { - t.Fatalf("Error fetching client: %v", err) - } + code := cmd.Run([]string{}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } - client.SetToken(rootToken) + init, err := client.Sys().InitStatus() + if err != nil { + t.Fatal(err) + } + if !init { + t.Error("expected initialized") + } - tokenInfo, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Fatalf("Error looking up root token info: %v", err) - } + re := regexp.MustCompile(`Unseal Key \d+: (.+)`) + output := ui.OutputWriter.String() + match := re.FindAllStringSubmatch(output, -1) + if len(match) < 5 || len(match[0]) < 2 { + t.Fatalf("no match: %#v", match) + } - if tokenInfo.Data["policies"].([]interface{})[0].(string) != "root" { - t.Fatalf("expected root policy") - } + keys := make([]string, len(match)) + for i := range match { + keys[i] = match[i][1] + } + + // Try unsealing with those keys - only use 3, which is the default + // threshold. + for i, key := range keys[:3] { + resp, err := client.Sys().Unseal(key) + if err != nil { + t.Fatal(err) + } + + exp := (i + 1) % 3 // 1, 2, 0 + if resp.Progress != exp { + t.Errorf("expected %d to be %d", resp.Progress, exp) + } + } + + status, err := client.Sys().SealStatus() + if err != nil { + t.Fatal(err) + } + if status.Sealed { + t.Errorf("expected vault to be unsealed: %#v", status) + } + }) + + t.Run("custom_shares_threshold", func(t *testing.T) { + t.Parallel() + + keyShares, keyThreshold := 20, 15 + + client, closer := testVaultServerUninit(t) + defer closer() + + ui, cmd := testInitCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-key-shares", strconv.Itoa(keyShares), + "-key-threshold", strconv.Itoa(keyThreshold), + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + + init, err := client.Sys().InitStatus() + if err != nil { + t.Fatal(err) + } + if !init { + t.Error("expected initialized") + } + + re := regexp.MustCompile(`Unseal Key \d+: (.+)`) + output := ui.OutputWriter.String() + match := re.FindAllStringSubmatch(output, -1) + if len(match) < keyShares || len(match[0]) < 2 { + t.Fatalf("no match: %#v", match) + } + + keys := make([]string, len(match)) + for i := range match { + keys[i] = match[i][1] + } + + // Try unsealing with those keys - only use 3, which is the default + // threshold. + for i, key := range keys[:keyThreshold] { + resp, err := client.Sys().Unseal(key) + if err != nil { + t.Fatal(err) + } + + exp := (i + 1) % keyThreshold + if resp.Progress != exp { + t.Errorf("expected %d to be %d", resp.Progress, exp) + } + } + + status, err := client.Sys().SealStatus() + if err != nil { + t.Fatal(err) + } + if status.Sealed { + t.Errorf("expected vault to be unsealed: %#v", status) + } + }) + + t.Run("pgp", func(t *testing.T) { + t.Parallel() + + tempDir, pubFiles, err := getPubKeyFiles(t) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + client, closer := testVaultServerUninit(t) + defer closer() + + ui, cmd := testInitCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-key-shares", "4", + "-key-threshold", "2", + "-pgp-keys", fmt.Sprintf("%s,@%s, %s, %s ", + pubFiles[0], pubFiles[1], pubFiles[2], pubFiles[3]), + "-root-token-pgp-key", pubFiles[0], + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + + re := regexp.MustCompile(`Unseal Key \d+: (.+)`) + output := ui.OutputWriter.String() + match := re.FindAllStringSubmatch(output, -1) + if len(match) < 4 || len(match[0]) < 2 { + t.Fatalf("no match: %#v", match) + } + + keys := make([]string, len(match)) + for i := range match { + keys[i] = match[i][1] + } + + // Try unsealing with one key + decryptedKey := testPGPDecrypt(t, pgpkeys.TestPrivKey1, keys[0]) + if _, err := client.Sys().Unseal(decryptedKey); err != nil { + t.Fatal(err) + } + + // Decrypt the root token + reToken := regexp.MustCompile(`Root Token: (.+)`) + match = reToken.FindAllStringSubmatch(output, -1) + if len(match) < 1 || len(match[0]) < 2 { + t.Fatalf("no match") + } + root := match[0][1] + decryptedRoot := testPGPDecrypt(t, pgpkeys.TestPrivKey1, root) + + if l, exp := len(decryptedRoot), 36; l != exp { + t.Errorf("expected %d to be %d", l, exp) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testInitCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/foo", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error initializing: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testInitCommand(t) + assertNoTabs(t, cmd) + }) } From f93e3e3e705fc82c64bc04c1119ea7a5f37efb5a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:02:09 -0400 Subject: [PATCH 054/281] Update key-status command --- command/key_status.go | 81 ++++++++++++++++---------- command/key_status_test.go | 115 +++++++++++++++++++++++++++++++------ 2 files changed, 148 insertions(+), 48 deletions(-) diff --git a/command/key_status.go b/command/key_status.go index ff1b0860c6..b870035b75 100644 --- a/command/key_status.go +++ b/command/key_status.go @@ -4,38 +4,17 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*KeyStatusCommand)(nil) +var _ cli.CommandAutocomplete = (*KeyStatusCommand)(nil) + // KeyStatusCommand is a Command that provides information about the key status type KeyStatusCommand struct { - meta.Meta -} - -func (c *KeyStatusCommand) Run(args []string) int { - flags := c.Meta.FlagSet("key-status", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - status, err := client.Sys().KeyStatus() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading audits: %s", err)) - return 2 - } - - c.Ui.Output(fmt.Sprintf("Key Term: %d", status.Term)) - c.Ui.Output(fmt.Sprintf("Installation Time: %v", status.InstallTime)) - return 0 + *BaseCommand } func (c *KeyStatusCommand) Synopsis() string { @@ -49,7 +28,49 @@ Usage: vault key-status [options] Provides information about the active encryption key. Specifically, the current key term and the key installation time. -General Options: -` + meta.GeneralOptionsUsage() +` + c.Flags().Help() + return strings.TrimSpace(helpText) } + +func (c *KeyStatusCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *KeyStatusCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *KeyStatusCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KeyStatusCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + status, err := client.Sys().KeyStatus() + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading key status: %s", err)) + return 2 + } + + c.UI.Output(printKeyStatus(status)) + return 0 +} diff --git a/command/key_status_test.go b/command/key_status_test.go index 0adcefa7f4..640cef7bb9 100644 --- a/command/key_status_test.go +++ b/command/key_status_test.go @@ -1,31 +1,110 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestKeyStatus(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testKeyStatusCommand(tb testing.TB) (*cli.MockUi, *KeyStatusCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &KeyStatusCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &KeyStatusCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestKeyStatusCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, }, } - args := []string{ - "-address", addr, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + 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() + + ui, cmd := testKeyStatusCommand(t) + + 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("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testKeyStatusCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Key Term" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testKeyStatusCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error reading key status: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testKeyStatusCommand(t) + assertNoTabs(t, cmd) + }) } From 1047792f2d7c772f3bbe5a4ec00b8712a5a07053 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:02:15 -0400 Subject: [PATCH 055/281] Update list command --- command/list.go | 130 ++++++++++++++++--------------- command/list_test.go | 177 +++++++++++++++++++++++++++++++------------ 2 files changed, 196 insertions(+), 111 deletions(-) diff --git a/command/list.go b/command/list.go index 71bf388c90..bdf5953cf9 100644 --- a/command/list.go +++ b/command/list.go @@ -1,97 +1,103 @@ package command import ( - "flag" "fmt" "strings" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*ListCommand)(nil) +var _ cli.CommandAutocomplete = (*ListCommand)(nil) + // ListCommand is a Command that lists data from the Vault. type ListCommand struct { - meta.Meta + *BaseCommand +} + +func (c *ListCommand) Synopsis() string { + return "Lists data or secrets" +} + +func (c *ListCommand) Help() string { + helpText := ` + +Usage: vault list [options] PATH + + Lists data from Vault at the given path. This can be used to list keys in a, + given backend. + + List values under the "my-app" folder: + + $ vault list secret/my-app/ + + For a full list of examples and paths, please see the documentation that + corresponds to the secret backend in use. Not all backends support listing. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *ListCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP | FlagSetOutputFormat) +} + +func (c *ListCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFolders() +} + +func (c *ListCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *ListCommand) Run(args []string) int { - var format string - var err error - var secret *api.Secret - var flags *flag.FlagSet - flags = c.Meta.FlagSet("list", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) != 1 || len(args[0]) == 0 { - c.Ui.Error("list expects one argument") - flags.Usage() + args = f.Args() + path, kvs, err := extractPath(args) + if err != nil { + c.UI.Error(err.Error()) return 1 } - path := args[0] - if path[0] == '/' { - path = path[1:] - } - - if !strings.HasSuffix(path, "/") { - path = path + "/" + if len(kvs) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } - secret, err = client.Logical().List(path) + secret, err := client.Logical().List(path) if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading %s: %s", path, err)) - return 1 + c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err)) + return 2 } - if secret == nil { - c.Ui.Error(fmt.Sprintf( - "No value found at %s", path)) - return 1 + if secret == nil || secret.Data == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 } + + // If the secret is wrapped, return the wrapped response. if secret.WrapInfo != nil && secret.WrapInfo.TTL != 0 { - return OutputSecret(c.Ui, format, secret) + return OutputSecret(c.UI, c.flagFormat, secret) } - if secret.Data["keys"] == nil { - c.Ui.Error("No entries found") - return 0 + if _, ok := extractListData(secret); !ok { + c.UI.Error(fmt.Sprintf("No entries found at %s", path)) + return 2 } - 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: -` + meta.GeneralOptionsUsage() + ` -Read Options: - - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. -` - return strings.TrimSpace(helpText) + return OutputList(c.UI, c.flagFormat, secret) } diff --git a/command/list_test.go b/command/list_test.go index 1f75c0b25b..712f1cae35 100644 --- a/command/list_test.go +++ b/command/list_test.go @@ -1,71 +1,150 @@ package command import ( - "reflect" + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "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() +func testListCommand(tb testing.TB) (*cli.MockUi, *ListCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &ReadCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &ListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestListCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing PATH!", + 1, + }, + { + "slash", + []string{"/"}, + "Missing PATH!", + 1, + }, + { + "not_found", + []string{"nope/not/once/never"}, + "", + 2, + }, + { + "default", + []string{"secret/list"}, + "bar\nbaz\nfoo", + 0, + }, + { + "default_slash", + []string{"secret/list/"}, + "bar\nbaz\nfoo", + 0, + }, + { + "format", + []string{ + "-format", "json", + "secret/list/", + }, + "[", + 0, + }, + { + "format_bad", + []string{ + "-format", "nope-not-real", + "secret/list/", + }, + "Invalid output format", + 1, }, } - args := []string{ - "-address", addr, - "-format", "json", - "secret", - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Run once so the client is setup, ignore errors - c.Run(args) + for _, tc := range cases { + tc := tc - // Get the client so we can write data - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - data := map[string]interface{}{"value": "bar"} - if _, err := client.Logical().Write("secret/foo", data); err != nil { - t.Fatalf("err: %s", err) - } + client, closer := testVaultServer(t) + defer closer() - data = map[string]interface{}{"value": "bar"} - if _, err := client.Logical().Write("secret/foo/bar", data); err != nil { - t.Fatalf("err: %s", err) - } + keys := []string{ + "secret/list/foo", + "secret/list/bar", + "secret/list/baz", + } + for _, k := range keys { + if _, err := client.Logical().Write(k, map[string]interface{}{ + "foo": "bar", + }); err != nil { + t.Fatal(err) + } + } - secret, err := client.Logical().List("secret/") - if err != nil { - t.Fatalf("err: %s", err) - } + ui, cmd := testListCommand(t) + cmd.client = client - if secret == nil { - t.Fatalf("err: No value found at secret/") - } + code := cmd.Run(tc.args) + if code != tc.code { + t.Errorf("expected %d to be %d", code, tc.code) + } - if secret.Data == nil { - t.Fatalf("err: Data not found") - } + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, tc.out) { + t.Errorf("expected %q to contain %q", combined, tc.out) + } + }) + } + }) - exp := map[string]interface{}{ - "keys": []interface{}{"foo", "foo/"}, - } + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() - if !reflect.DeepEqual(secret.Data, exp) { - t.Fatalf("err: expected %#v, got %#v", exp, secret.Data) - } + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testListCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/list", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing secret/list: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testListCommand(t) + assertNoTabs(t, cmd) + }) } From 5cc5b6c6a65951d6e6f68ce5ca8bdc8259478360 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:02:24 -0400 Subject: [PATCH 056/281] Update mount command --- command/mount.go | 297 ++++++++++++++++++++++++------------------ command/mount_test.go | 226 +++++++++++++++++++++----------- 2 files changed, 324 insertions(+), 199 deletions(-) diff --git a/command/mount.go b/command/mount.go index 895e7b8bc0..c89115d782 100644 --- a/command/mount.go +++ b/command/mount.go @@ -3,162 +3,207 @@ package command import ( "fmt" "strings" + "time" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*MountCommand)(nil) +var _ cli.CommandAutocomplete = (*MountCommand)(nil) + // MountCommand is a Command that mounts a new mount. type MountCommand struct { - meta.Meta -} + *BaseCommand -func (c *MountCommand) Run(args []string) int { - var description, path, defaultLeaseTTL, maxLeaseTTL, pluginName string - var local, forceNoCache bool - flags := c.Meta.FlagSet("mount", meta.FlagSetDefault) - flags.StringVar(&description, "description", "", "") - flags.StringVar(&path, "path", "", "") - flags.StringVar(&defaultLeaseTTL, "default-lease-ttl", "", "") - flags.StringVar(&maxLeaseTTL, "max-lease-ttl", "", "") - flags.StringVar(&pluginName, "plugin-name", "", "") - flags.BoolVar(&forceNoCache, "force-no-cache", false, "") - flags.BoolVar(&local, "local", 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 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\nmount expects one argument: the type to mount.")) - return 1 - } - - mountType := args[0] - - // If no path is specified, we default the path to the backend type - // or use the plugin name if it's a plugin backend - if path == "" { - if mountType == "plugin" { - path = pluginName - } else { - path = mountType - } - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - mountInfo := &api.MountInput{ - Type: mountType, - Description: description, - Config: api.MountConfigInput{ - DefaultLeaseTTL: defaultLeaseTTL, - MaxLeaseTTL: maxLeaseTTL, - ForceNoCache: forceNoCache, - PluginName: pluginName, - }, - Local: local, - } - - if err := client.Sys().Mount(path, mountInfo); err != nil { - c.Ui.Error(fmt.Sprintf( - "Mount error: %s", err)) - return 2 - } - - mountTypeOutput := fmt.Sprintf("'%s'", mountType) - if mountType == "plugin" { - mountTypeOutput = fmt.Sprintf("plugin '%s'", pluginName) - } - - c.Ui.Output(fmt.Sprintf( - "Successfully mounted %s at '%s'!", - mountTypeOutput, path)) - - return 0 + flagDescription string + flagPath string + flagDefaultLeaseTTL time.Duration + flagMaxLeaseTTL time.Duration + flagForceNoCache bool + flagPluginName string + flagLocal bool } func (c *MountCommand) Synopsis() string { - return "Mount a logical backend" + return "Mounts a secret backend at a path" } func (c *MountCommand) Help() string { helpText := ` -Usage: vault mount [options] type +Usage: vault mount [options] TYPE - Mount a logical backend. + Mount a secret backend at a particular path. By default, secret backends are + mounted at the path corresponding to their "type", but users can customize + the mount point using the -path option. - This command mounts a logical backend for storing and/or generating - secrets. + Once mounted at a path, Vault will route all requests which begin with the + path to the secret backend. -General Options: -` + meta.GeneralOptionsUsage() + ` -Mount Options: + Mount the AWS backend at aws/: - -description= Human-friendly description of the purpose for - the mount. This shows up in the mounts command. + $ vault mount aws - -path= Mount point for the logical backend. This - defaults to the type of the mount. + Mount the SSH backend at ssh-prod/: - -default-lease-ttl= Default lease time-to-live for this backend. - If not specified, uses the global default, or - the previously set value. Set to '0' to - explicitly set it to use the global default. + $ vault mount -path=ssh-prod ssh - -max-lease-ttl= Max lease time-to-live for this backend. - If not specified, uses the global default, or - the previously set value. Set to '0' to - explicitly set it to use the global default. + Mount the database backend with an explicit maximum TTL of 30m: - -force-no-cache Forces the backend to disable caching. If not - specified, uses the global default. This does - not affect caching of the underlying encrypted - data storage. + $ vault mount -max-lease-ttl=30m database - -plugin-name Name of the plugin to mount based from the name - in the plugin catalog. + Mount a custom plugin (after it is registered in the plugin registry): + + $ vault mount -path=my-secrets -plugin-name=my-custom-plugin plugin + + For a full list of secret backends and examples, please see the documentation. + +` + c.Flags().Help() - -local Mark the mount as a local mount. Local mounts - are not replicated nor (if a secondary) - removed by replication. -` return strings.TrimSpace(helpText) } -func (c *MountCommand) AutocompleteArgs() complete.Predictor { - // This list does not contain deprecated backends - return complete.PredictSet( - "aws", - "consul", - "pki", - "transit", - "ssh", - "rabbitmq", - "database", - "totp", - "plugin", - ) +func (c *MountCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "description", + Target: &c.flagDescription, + Completion: complete.PredictAnything, + Usage: "Human-friendly description for the purpose of this mount.", + }) + + f.StringVar(&StringVar{ + Name: "path", + Target: &c.flagPath, + Default: "", // The default is complex, so we have to manually document + Completion: complete.PredictAnything, + Usage: "Place where the mount will be accessible. This must be " + + "unique across all mounts. This defaults to the \"type\" of the mount.", + }) + + f.DurationVar(&DurationVar{ + Name: "default-lease-ttl", + Target: &c.flagDefaultLeaseTTL, + Completion: complete.PredictAnything, + Usage: "The default lease TTL for this backend. If unspecified, this " + + "defaults to the Vault server's globally configured default lease TTL.", + }) + + f.DurationVar(&DurationVar{ + Name: "max-lease-ttl", + Target: &c.flagMaxLeaseTTL, + Completion: complete.PredictAnything, + Usage: "The maximum lease TTL for this backend. If unspecified, this " + + "defaults to the Vault server's globally configured maximum lease TTL.", + }) + + f.BoolVar(&BoolVar{ + Name: "force-no-cache", + Target: &c.flagForceNoCache, + Default: false, + Usage: "Force the backend to disable caching. If unspecified, this " + + "defaults to the Vault server's globally configured cache settings. " + + "This does not affect caching of the underlying encrypted data storage.", + }) + + f.StringVar(&StringVar{ + Name: "plugin-name", + Target: &c.flagPluginName, + Completion: complete.PredictAnything, + Usage: "Name of the plugin to mount. This plugin name must already exist " + + "in the Vault server's plugin catalog.", + }) + + f.BoolVar(&BoolVar{ + Name: "local", + Target: &c.flagLocal, + Default: false, + Usage: "Mark the mount as a local-only mount. Local mounts are not " + + "replicated nor removed by replication.", + }) + + return set +} + +func (c *MountCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultAvailableMounts() } func (c *MountCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-description": complete.PredictNothing, - "-path": complete.PredictNothing, - "-default-lease-ttl": complete.PredictNothing, - "-max-lease-ttl": complete.PredictNothing, - "-force-no-cache": complete.PredictNothing, - "-plugin-name": complete.PredictNothing, - "-local": complete.PredictNothing, - } + return c.Flags().Completions() +} + +func (c *MountCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + switch len(args) { + case 0: + c.UI.Error("Missing TYPE!") + return 1 + case 1: + // OK + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Get the mount type (first arg) + mountType := strings.TrimSpace(args[0]) + + // If no path is specified, we default the path to the backend type + // or use the plugin name if it's a plugin backend + mountPath := c.flagPath + if mountPath == "" { + if mountType == "plugin" { + mountPath = c.flagPluginName + } else { + mountPath = mountType + } + } + + // Append a trailing slash to indicate it's a path in output + mountPath = ensureTrailingSlash(mountPath) + + // Build mount input + mountInput := &api.MountInput{ + Type: mountType, + Description: c.flagDescription, + Local: c.flagLocal, + Config: api.MountConfigInput{ + DefaultLeaseTTL: c.flagDefaultLeaseTTL.String(), + MaxLeaseTTL: c.flagMaxLeaseTTL.String(), + ForceNoCache: c.flagForceNoCache, + PluginName: c.flagPluginName, + }, + } + + if err := client.Sys().Mount(mountPath, mountInput); err != nil { + c.UI.Error(fmt.Sprintf("Error mounting: %s", err)) + return 2 + } + + mountThing := mountType + " secret backend" + if mountType == "plugin" { + mountThing = c.flagPluginName + " plugin" + } + + c.UI.Output(fmt.Sprintf("Success! Mounted the %s at: %s", mountThing, mountPath)) + return 0 } diff --git a/command/mount_test.go b/command/mount_test.go index ea9108cb71..dd7659127d 100644 --- a/command/mount_test.go +++ b/command/mount_test.go @@ -1,90 +1,170 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestMount(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testMountCommand(tb testing.TB) (*cli.MockUi, *MountCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &MountCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &MountCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} +func TestMountCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing TYPE!", + 1, + }, + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "not_a_valid_mount", + []string{"nope_definitely_not_a_valid_mount_like_ever"}, + "", + 2, + }, + { + "mount", + []string{"transit"}, + "Success! Mounted the transit secret backend at: transit/", + 0, + }, + { + "mount_path", + []string{ + "-path", "transit_mount_point", + "transit", + }, + "Success! Mounted the transit secret backend at: transit_mount_point/", + 0, }, } - args := []string{ - "-address", addr, - "kv", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testMountCommand(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) + } + }) } - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run("integration", func(t *testing.T) { + t.Parallel() - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatalf("err: %s", err) - } + client, closer := testVaultServer(t) + defer closer() - mount, ok := mounts["kv/"] - if !ok { - t.Fatal("should have kv mount") - } - if mount.Type != "kv" { - t.Fatal("should be kv type") - } -} - -func TestMount_Generic(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &MountCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "generic", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatalf("err: %s", err) - } - - mount, ok := mounts["generic/"] - if !ok { - t.Fatal("should have generic mount path") - } - if mount.Type != "generic" { - t.Fatal("should be generic type") - } + ui, cmd := testMountCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-path", "mount_integration/", + "-description", "The best kind of test", + "-default-lease-ttl", "30m", + "-max-lease-ttl", "1h", + "-force-no-cache", + "pki", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Mounted the pki secret backend at: mount_integration/" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + mounts, err := client.Sys().ListMounts() + if err != nil { + t.Fatal(err) + } + + mountInfo, ok := mounts["mount_integration/"] + if !ok { + t.Fatalf("expected mount to exist") + } + if exp := "pki"; mountInfo.Type != exp { + t.Errorf("expected %q to be %q", mountInfo.Type, exp) + } + if exp := "The best kind of test"; mountInfo.Description != exp { + t.Errorf("expected %q to be %q", mountInfo.Description, exp) + } + if exp := 1800; mountInfo.Config.DefaultLeaseTTL != exp { + t.Errorf("expected %d to be %d", mountInfo.Config.DefaultLeaseTTL, exp) + } + if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp { + t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp) + } + if exp := true; mountInfo.Config.ForceNoCache != exp { + t.Errorf("expected %t to be %t", mountInfo.Config.ForceNoCache, exp) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testMountCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "pki", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error mounting: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testMountCommand(t) + assertNoTabs(t, cmd) + }) } From 8f6a5c4a4554195e32cf31ec7cc1d0d89cd88d07 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:02:33 -0400 Subject: [PATCH 057/281] Update mount-tune command --- command/mount_tune.go | 161 +++++++++++++++++++++++-------------- command/mount_tune_test.go | 155 +++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 61 deletions(-) create mode 100644 command/mount_tune_test.go diff --git a/command/mount_tune.go b/command/mount_tune.go index e1efdd241d..962ebb5f5f 100644 --- a/command/mount_tune.go +++ b/command/mount_tune.go @@ -3,87 +3,126 @@ package command import ( "fmt" "strings" + "time" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*MountTuneCommand)(nil) +var _ cli.CommandAutocomplete = (*MountTuneCommand)(nil) + // MountTuneCommand is a Command that remounts a mounted secret backend // to a new endpoint. type MountTuneCommand struct { - meta.Meta -} + *BaseCommand -func (c *MountTuneCommand) Run(args []string) int { - var defaultLeaseTTL, maxLeaseTTL string - flags := c.Meta.FlagSet("mount-tune", meta.FlagSetDefault) - flags.StringVar(&defaultLeaseTTL, "default-lease-ttl", "", "") - flags.StringVar(&maxLeaseTTL, "max-lease-ttl", "", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - args = flags.Args() - if len(args) != 1 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\nmount-tune expects one arguments: the mount path")) - return 1 - } - - path := args[0] - - mountConfig := api.MountConfigInput{ - DefaultLeaseTTL: defaultLeaseTTL, - MaxLeaseTTL: maxLeaseTTL, - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if err := client.Sys().TuneMount(path, mountConfig); err != nil { - c.Ui.Error(fmt.Sprintf( - "Mount tune error: %s", err)) - return 2 - } - - c.Ui.Output(fmt.Sprintf( - "Successfully tuned mount '%s'!", path)) - - return 0 + flagDefaultLeaseTTL time.Duration + flagMaxLeaseTTL time.Duration } func (c *MountTuneCommand) Synopsis() string { - return "Tune mount configuration parameters" + return "Tunes an existing mount's configuration" } func (c *MountTuneCommand) Help() string { helpText := ` - Usage: vault mount-tune [options] path +Usage: vault mount-tune [options] PATH - Tune configuration options for a mounted secret backend. + Tune the configuration options for a mounted secret backend at the given + path. The argument corresponds to the PATH of the mount, not the TYPE! - Example: vault mount-tune -default-lease-ttl="24h" secret + Tune the default lease for the PKI secret backend: -General Options: -` + meta.GeneralOptionsUsage() + ` -Mount Options: + $ vault mount-tune -default-lease-ttl=72h pki/ - -default-lease-ttl= Default lease time-to-live for this backend. - If not specified, uses the system default, or - the previously set value. Set to 'system' to - explicitly set it to use the system default. + For a full list of examples and paths, please see the documentation that + corresponds to the secret backend in use. - -max-lease-ttl= Max lease time-to-live for this backend. - If not specified, uses the system default, or - the previously set value. Set to 'system' to - explicitly set it to use the system default. +` + c.Flags().Help() -` return strings.TrimSpace(helpText) } + +func (c *MountTuneCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.DurationVar(&DurationVar{ + Name: "default-lease-ttl", + Target: &c.flagDefaultLeaseTTL, + Default: 0, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "The default lease TTL for this backend. If unspecified, this " + + "defaults to the Vault server's globally configured default lease TTL, " + + "or a previously configured value for the backend.", + }) + + f.DurationVar(&DurationVar{ + Name: "max-lease-ttl", + Target: &c.flagMaxLeaseTTL, + Default: 0, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "The maximum lease TTL for this backend. If unspecified, this " + + "defaults to the Vault server's globally configured maximum lease TTL, " + + "or a previously configured value for the backend.", + }) + + return set +} + +func (c *MountTuneCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultMounts() +} + +func (c *MountTuneCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *MountTuneCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + mountPath, remaining, err := extractPath(args) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if len(remaining) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Append a trailing slash to indicate it's a path in output + mountPath = ensureTrailingSlash(mountPath) + + mountConfig := api.MountConfigInput{ + DefaultLeaseTTL: c.flagDefaultLeaseTTL.String(), + MaxLeaseTTL: c.flagMaxLeaseTTL.String(), + } + + if err := client.Sys().TuneMount(mountPath, mountConfig); err != nil { + c.UI.Error(fmt.Sprintf("Error tuning mount %s: %s", mountPath, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Tuned the mount at: %s", mountPath)) + return 0 +} diff --git a/command/mount_tune_test.go b/command/mount_tune_test.go new file mode 100644 index 0000000000..b60c532f72 --- /dev/null +++ b/command/mount_tune_test.go @@ -0,0 +1,155 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" +) + +func testMountTuneCommand(tb testing.TB) (*cli.MockUi, *MountTuneCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &MountTuneCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestMountTuneCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing PATH!", + 1, + }, + { + "slash", + []string{"/"}, + "Missing PATH!", + 1, + }, + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + } + + 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() + + ui, cmd := testMountTuneCommand(t) + + 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("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testMountTuneCommand(t) + cmd.client = client + + // Mount + if err := client.Sys().Mount("mount_tune_integration", &api.MountInput{ + Type: "pki", + }); err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "-default-lease-ttl", "30m", + "-max-lease-ttl", "1h", + "mount_tune_integration/", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Tuned the mount at: mount_tune_integration/" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + mounts, err := client.Sys().ListMounts() + if err != nil { + t.Fatal(err) + } + + mountInfo, ok := mounts["mount_tune_integration/"] + if !ok { + t.Fatalf("expected mount to exist") + } + if exp := "pki"; mountInfo.Type != exp { + t.Errorf("expected %q to be %q", mountInfo.Type, exp) + } + if exp := 1800; mountInfo.Config.DefaultLeaseTTL != exp { + t.Errorf("expected %d to be %d", mountInfo.Config.DefaultLeaseTTL, exp) + } + if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp { + t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testMountTuneCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "pki/", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error tuning mount pki/: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testMountTuneCommand(t) + assertNoTabs(t, cmd) + }) +} From 3a0af6b8ebc19714901b1e472fdb2b4ae770992f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:02:45 -0400 Subject: [PATCH 058/281] Update mounts command --- command/mounts.go | 216 +++++++++++++++++++++++++++-------------- command/mounts_test.go | 110 +++++++++++++++++---- 2 files changed, 236 insertions(+), 90 deletions(-) diff --git a/command/mounts.go b/command/mounts.go index 403d9d4d91..b25bfe4dcb 100644 --- a/command/mounts.go +++ b/command/mounts.go @@ -6,93 +6,165 @@ import ( "strconv" "strings" - "github.com/hashicorp/vault/meta" - "github.com/ryanuber/columnize" + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*MountsCommand)(nil) +var _ cli.CommandAutocomplete = (*MountsCommand)(nil) + // MountsCommand is a Command that lists the mounts. type MountsCommand struct { - meta.Meta -} + *BaseCommand -func (c *MountsCommand) Run(args []string) int { - flags := c.Meta.FlagSet("mounts", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading mounts: %s", err)) - return 2 - } - - paths := make([]string, 0, len(mounts)) - for path := range mounts { - paths = append(paths, path) - } - sort.Strings(paths) - - columns := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Force No Cache | Replication Behavior | Description"} - for _, path := range paths { - mount := mounts[path] - pluginName := "n/a" - if mount.Config.PluginName != "" { - pluginName = mount.Config.PluginName - } - defTTL := "system" - switch { - case mount.Type == "system", mount.Type == "cubbyhole", mount.Type == "identity": - defTTL = "n/a" - case mount.Config.DefaultLeaseTTL != 0: - defTTL = strconv.Itoa(mount.Config.DefaultLeaseTTL) - } - - maxTTL := "system" - switch { - case mount.Type == "system", mount.Type == "cubbyhole", mount.Type == "identity": - maxTTL = "n/a" - case mount.Config.MaxLeaseTTL != 0: - maxTTL = strconv.Itoa(mount.Config.MaxLeaseTTL) - } - - replicatedBehavior := "replicated" - if mount.Local { - replicatedBehavior = "local" - } - columns = append(columns, fmt.Sprintf( - "%s | %s | %s | %s | %s | %s | %v | %s | %s", path, mount.Type, mount.Accessor, pluginName, defTTL, maxTTL, - mount.Config.ForceNoCache, replicatedBehavior, mount.Description)) - } - - c.Ui.Output(columnize.SimpleFormat(columns)) - return 0 + flagDetailed bool } func (c *MountsCommand) Synopsis() string { - return "Lists mounted backends in Vault" + return "Lists mounted secret backends" } func (c *MountsCommand) Help() string { helpText := ` Usage: vault mounts [options] - Outputs information about the mounted backends. + Lists the mounted secret backends on the Vault server. This command also + outputs information about the mount point including configured TTLs and + human-friendly descriptions. A TTL of "system" indicates that the system + default is in use. - This command lists the mounted backends, their mount points, the - configured TTLs, and a human-friendly description of the mount point. - A TTL of 'system' indicates that the system default is being used. + List all mounts: + + $ vault mounts + + List all mounts with detailed output: + + $ vault mounts -detailed + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *MountsCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "detailed", + Target: &c.flagDetailed, + Default: false, + Usage: "Print detailed information such as TTLs and replication status " + + "about each mount.", + }) + + return set +} + +func (c *MountsCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *MountsCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *MountsCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + mounts, err := client.Sys().ListMounts() + if err != nil { + c.UI.Error(fmt.Sprintf("Error listing mounts: %s", err)) + return 2 + } + + if c.flagDetailed { + c.UI.Output(tableOutput(c.detailedMounts(mounts))) + return 0 + } + + c.UI.Output(tableOutput(c.simpleMounts(mounts))) + return 0 +} + +func (c *MountsCommand) simpleMounts(mounts map[string]*api.MountOutput) []string { + paths := make([]string, 0, len(mounts)) + for path := range mounts { + paths = append(paths, path) + } + sort.Strings(paths) + + out := []string{"Path | Type | Description"} + for _, path := range paths { + mount := mounts[path] + out = append(out, fmt.Sprintf("%s | %s | %s", path, mount.Type, mount.Description)) + } + + return out +} + +func (c *MountsCommand) detailedMounts(mounts map[string]*api.MountOutput) []string { + paths := make([]string, 0, len(mounts)) + for path := range mounts { + paths = append(paths, path) + } + sort.Strings(paths) + + calcTTL := func(typ string, ttl int) string { + switch { + case typ == "system", typ == "cubbyhole": + return "" + case ttl != 0: + return strconv.Itoa(ttl) + default: + return "system" + } + } + + out := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Force No Cache | Replication | Description"} + for _, path := range paths { + mount := mounts[path] + + defaultTTL := calcTTL(mount.Type, mount.Config.DefaultLeaseTTL) + maxTTL := calcTTL(mount.Type, mount.Config.MaxLeaseTTL) + + replication := "replicated" + if mount.Local { + replication = "local" + } + + out = append(out, fmt.Sprintf("%s | %s | %s | %s | %s | %s | %v | %s | %s", + path, + mount.Type, + mount.Accessor, + mount.Config.PluginName, + defaultTTL, + maxTTL, + mount.Config.ForceNoCache, + replication, + mount.Description, + )) + } + + return out +} diff --git a/command/mounts_test.go b/command/mounts_test.go index 55e5f679f6..8f1c8b9c85 100644 --- a/command/mounts_test.go +++ b/command/mounts_test.go @@ -1,31 +1,105 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestMounts(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testMountsCommand(tb testing.TB) (*cli.MockUi, *MountsCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &MountsCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &MountsCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestMountsCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo"}, + "Too many arguments", + 1, + }, + { + "lists", + nil, + "Path", + 0, + }, + { + "detailed", + []string{"-detailed"}, + "Default TTL", + 0, }, } - args := []string{ - "-address", addr, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + 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() + + ui, cmd := testMountsCommand(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("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testMountsCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing mounts: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testMountsCommand(t) + assertNoTabs(t, cmd) + }) } From f244e03fda08d1c5af61e182aac080bc79a6132a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:02:55 -0400 Subject: [PATCH 059/281] Update path-help command --- command/path_help.go | 134 +++++++++++++++++++++++--------------- command/path_help_test.go | 119 ++++++++++++++++++++++++++++----- 2 files changed, 183 insertions(+), 70 deletions(-) diff --git a/command/path_help.go b/command/path_help.go index 6eed9607d8..fb7b52c30f 100644 --- a/command/path_help.go +++ b/command/path_help.go @@ -4,73 +4,103 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*PathHelpCommand)(nil) +var _ cli.CommandAutocomplete = (*PathHelpCommand)(nil) + +var pathHelpVaultSealedMessage = strings.TrimSpace(` +Error: Vault is sealed. + +The "path-help" command requires the Vault to be unsealed so that the mount +points of the secret backends are known. +`) + // PathHelpCommand is a Command that lists the mounts. type PathHelpCommand struct { - meta.Meta -} - -func (c *PathHelpCommand) Run(args []string) int { - flags := c.Meta.FlagSet("help", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - args = flags.Args() - if len(args) != 1 { - flags.Usage() - c.Ui.Error("\nhelp expects a single argument") - return 1 - } - - path := args[0] - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - help, err := client.Help(path) - if err != nil { - if strings.Contains(err.Error(), "Vault is sealed") { - c.Ui.Error(`Error: Vault is sealed. - -The path-help command requires the vault to be unsealed so that -mount points of secret backends are known.`) - } else { - c.Ui.Error(fmt.Sprintf( - "Error reading help: %s", err)) - } - return 1 - } - - c.Ui.Output(help.Help) - return 0 + *BaseCommand } func (c *PathHelpCommand) Synopsis() string { - return "Look up the help for a path" + return "Retrieves API help for paths" } func (c *PathHelpCommand) Help() string { helpText := ` Usage: vault path-help [options] path - Look up the help for a path. + Retrieves API help for paths. All endpoints in Vault provide built-in help + in markdown format. This includes system paths, secret paths, and credential + providers. - All endpoints in Vault from system paths, secret paths, and credential - providers provide built-in help. This command looks up and outputs that - help. + A backend must be mounted before help is available: - The command requires that the vault be unsealed, because otherwise - the mount points of the backends are unknown. + $ vault mount database + $ vault path-help database/ + + The response object will return additional paths to retrieve help: + + $ vault path-help database/roles/ + + Each backend produces different help output. For additional information, + please view the online documentation. + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *PathHelpCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *PathHelpCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything // TODO: programatic way to invoke help +} + +func (c *PathHelpCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *PathHelpCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + path, kvs, err := extractPath(args) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if len(kvs) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + help, err := client.Help(path) + if err != nil { + if strings.Contains(err.Error(), "Vault is sealed") { + c.UI.Error(pathHelpVaultSealedMessage) + } else { + c.UI.Error(fmt.Sprintf("Error retrieving help: %s", err)) + } + return 2 + } + + c.UI.Output(help.Help) + return 0 +} diff --git a/command/path_help_test.go b/command/path_help_test.go index 46219ba5dc..ff38c02250 100644 --- a/command/path_help_test.go +++ b/command/path_help_test.go @@ -1,32 +1,115 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestHelp(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testPathHelpCommand(tb testing.TB) (*cli.MockUi, *PathHelpCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &PathHelpCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &PathHelpCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestPathHelpCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing PATH!", + 1, + }, + { + "slash", + []string{"/"}, + "Missing PATH!", + 1, + }, + { + "not_found", + []string{"nope/not/once/never"}, + "", + 2, + }, + { + "generic", + []string{"secret/"}, + "The generic backend", + 0, + }, + { + "sys", + []string{"sys/mounts"}, + "currently mounted backends", + 0, }, } - args := []string{ - "-address", addr, - "sys/mounts", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testPathHelpCommand(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("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testPathHelpCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "sys/mounts", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error retrieving help: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testPathHelpCommand(t) + assertNoTabs(t, cmd) + }) } From eece6eea4aae30b46db19b7552cdf5a9e0a406c2 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:03:04 -0400 Subject: [PATCH 060/281] Update policy-delete command --- command/policy_delete.go | 94 ++++++++++++-------- command/policy_delete_test.go | 163 +++++++++++++++++++++++++--------- 2 files changed, 177 insertions(+), 80 deletions(-) diff --git a/command/policy_delete.go b/command/policy_delete.go index ff8342a625..a2bbc25bda 100644 --- a/command/policy_delete.go +++ b/command/policy_delete.go @@ -4,62 +4,84 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*PolicyDeleteCommand)(nil) +var _ cli.CommandAutocomplete = (*PolicyDeleteCommand)(nil) + // PolicyDeleteCommand is a Command that enables a new endpoint. type PolicyDeleteCommand struct { - meta.Meta + *BaseCommand +} + +func (c *PolicyDeleteCommand) Synopsis() string { + return "Deletes a policy by name" +} + +func (c *PolicyDeleteCommand) Help() string { + helpText := ` +Usage: vault policy-delete [options] NAME + + Deletes a policy in the Vault server with the given name. Once the policy + is deleted, all tokens associated with the policy will be affected + immediately. + + Delete the policy named "my-policy": + + $ vault policy-delete my-policy + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *PolicyDeleteCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *PolicyDeleteCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultPolicies() +} + +func (c *PolicyDeleteCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *PolicyDeleteCommand) Run(args []string) int { - flags := c.Meta.FlagSet("policy-delete", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) != 1 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\npolicy-delete expects exactly one argument")) + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } - name := args[0] + name := strings.TrimSpace(strings.ToLower(args[0])) if err := client.Sys().DeletePolicy(name); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error: %s", err)) - return 1 + c.UI.Error(fmt.Sprintf("Error deleting %s: %s", name, err)) + return 2 } - c.Ui.Output(fmt.Sprintf("Policy '%s' deleted.", name)) + c.UI.Output(fmt.Sprintf("Success! Deleted policy: %s", name)) return 0 } - -func (c *PolicyDeleteCommand) Synopsis() string { - return "Delete a policy from the server" -} - -func (c *PolicyDeleteCommand) Help() string { - helpText := ` -Usage: vault policy-delete [options] name - - Delete a policy with the given name. - - Once the policy is deleted, all users associated with the policy will - be affected immediately. When a user is associated with a policy that - doesn't exist, it is identical to not being associated with that policy. - -General Options: -` + meta.GeneralOptionsUsage() - return strings.TrimSpace(helpText) -} diff --git a/command/policy_delete_test.go b/command/policy_delete_test.go index 4f62a1028d..1c30c739d7 100644 --- a/command/policy_delete_test.go +++ b/command/policy_delete_test.go @@ -1,61 +1,136 @@ package command import ( + "reflect" + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestPolicyDelete(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testPolicyDeleteCommand(tb testing.TB) (*cli.MockUi, *PolicyDeleteCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &PolicyDeleteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &PolicyDeleteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestPolicyDeleteCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + nil, + "Not enough arguments", + 1, + }, + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, }, } - args := []string{ - "-address", addr, - "foo", - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Run once so the client is setup, ignore errors - c.Run(args) + for _, tc := range cases { + tc := tc - // Get the client so we can write data - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - if err := client.Sys().PutPolicy("foo", testPolicyDeleteRules); err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - // Test that the delete works - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + ui, cmd := testPolicyDeleteCommand(t) - // Test the policy is gone - rules, err := client.Sys().GetPolicy("foo") - if err != nil { - t.Fatalf("err: %s", err) - } - if rules != "" { - t.Fatalf("bad: %#v", rules) - } + 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("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policy := `path "secret/" {}` + if err := client.Sys().PutPolicy("my-policy", policy); err != nil { + t.Fatal(err) + } + + ui, cmd := testPolicyDeleteCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-policy", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Deleted policy: my-policy" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + policies, err := client.Sys().ListPolicies() + if err != nil { + t.Fatal(err) + } + + list := []string{"default", "root"} + if !reflect.DeepEqual(policies, list) { + t.Errorf("expected %q to be %q", policies, list) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testPolicyDeleteCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-policy", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error deleting my-policy: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testPolicyDeleteCommand(t) + assertNoTabs(t, cmd) + }) } - -const testPolicyDeleteRules = ` -path "sys" { - policy = "deny" -} -` From cfd378187a75831b6558363580c88f5d5b583641 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:03:21 -0400 Subject: [PATCH 061/281] Update policy-list command --- command/policy_list.go | 153 ++++++++++++++++++--------------- command/policy_list_test.go | 164 ++++++++++++++++++++++++++++-------- 2 files changed, 212 insertions(+), 105 deletions(-) diff --git a/command/policy_list.go b/command/policy_list.go index 73cb9c5b4e..1169752854 100644 --- a/command/policy_list.go +++ b/command/policy_list.go @@ -4,89 +4,102 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*PolicyListCommand)(nil) +var _ cli.CommandAutocomplete = (*PolicyListCommand)(nil) + // PolicyListCommand is a Command that enables a new endpoint. type PolicyListCommand struct { - meta.Meta -} - -func (c *PolicyListCommand) Run(args []string) int { - flags := c.Meta.FlagSet("policy-list", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - args = flags.Args() - if len(args) == 1 { - return c.read(args[0]) - } else if len(args) == 0 { - return c.list() - } else { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\npolicies expects zero or one arguments")) - return 1 - } -} - -func (c *PolicyListCommand) list() int { - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - policies, err := client.Sys().ListPolicies() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error: %s", err)) - return 1 - } - - for _, p := range policies { - c.Ui.Output(p) - } - - return 0 -} - -func (c *PolicyListCommand) read(n string) int { - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - rules, err := client.Sys().GetPolicy(n) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error: %s", err)) - return 1 - } - - c.Ui.Output(rules) - return 0 + *BaseCommand } func (c *PolicyListCommand) Synopsis() string { - return "List the policies on the server" + return "Lists the installed policies" } func (c *PolicyListCommand) Help() string { helpText := ` -Usage: vault policies [options] [name] +Usage: vault policies [options] [NAME] - List the policies that are available or read a single policy. + Lists the policies that are installed on the Vault server. If the optional + argument is given, this command returns the policy's contents. - This command lists the policies that are written to the Vault server. - If a name of a policy is specified, that policy is outputted. + List all policies stored in Vault: + + $ vault policies + + Read the contents of the policy named "my-policy": + + $ vault policies my-policy + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *PolicyListCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *PolicyListCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultPolicies() +} + +func (c *PolicyListCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *PolicyListCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + switch len(args) { + case 0, 1: + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-2, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + switch len(args) { + case 0: + policies, err := client.Sys().ListPolicies() + if err != nil { + c.UI.Error(fmt.Sprintf("Error listing policies: %s", err)) + return 2 + } + for _, p := range policies { + c.UI.Output(p) + } + case 1: + name := strings.ToLower(strings.TrimSpace(args[0])) + rules, err := client.Sys().GetPolicy(name) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading policy %s: %s", name, err)) + return 2 + } + if rules == "" { + c.UI.Error(fmt.Sprintf("Error reading policy: no policy named: %s", name)) + return 2 + } + c.UI.Output(strings.TrimSpace(rules)) + } + + return 0 +} diff --git a/command/policy_list_test.go b/command/policy_list_test.go index b2afe293c1..1c140edf7a 100644 --- a/command/policy_list_test.go +++ b/command/policy_list_test.go @@ -1,53 +1,147 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestPolicyList(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testPolicyListCommand(tb testing.TB) (*cli.MockUi, *PolicyListCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &PolicyListCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &PolicyListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - args := []string{ - "-address", addr, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } } -func TestPolicyRead(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestPolicyListCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) - c := &PolicyListCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "no_policy_exists", + []string{"not-a-real-policy"}, + "no policy named", + 2, }, } - args := []string{ - "-address", addr, - "root", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + 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() + + ui, cmd := testPolicyListCommand(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("list", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testPolicyListCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "default\nroot" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("read", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policy := `path "secret/" {}` + if err := client.Sys().PutPolicy("my-policy", policy); err != nil { + t.Fatal(err) + } + + ui, cmd := testPolicyListCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-policy", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, policy) { + t.Errorf("expected %q to contain %q", combined, policy) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testPolicyListCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error listing policies: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testPolicyListCommand(t) + assertNoTabs(t, cmd) + }) } From 0d598a7f1ecda9591983e09b24297ee6ef0c49da Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:03:29 -0400 Subject: [PATCH 062/281] Update policy-write command --- command/policy_write.go | 137 ++++++++++++++-------- command/policy_write_test.go | 214 +++++++++++++++++++++++++++++++---- 2 files changed, 283 insertions(+), 68 deletions(-) diff --git a/command/policy_write.go b/command/policy_write.go index 59b26fb472..73979458c6 100644 --- a/command/policy_write.go +++ b/command/policy_write.go @@ -7,84 +7,125 @@ import ( "os" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) -// PolicyWriteCommand is a Command that enables a new endpoint. +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*PolicyWriteCommand)(nil) +var _ cli.CommandAutocomplete = (*PolicyWriteCommand)(nil) + +// PolicyWriteCommand is a Command uploads a policy type PolicyWriteCommand struct { - meta.Meta + *BaseCommand + + testStdin io.Reader // for tests +} + +func (c *PolicyWriteCommand) Synopsis() string { + return "Uploads a policy file" +} + +func (c *PolicyWriteCommand) Help() string { + helpText := ` +Usage: vault policy-write [options] NAME PATH + + Uploads a policy with the given name from the contents of a local file or + stdin. If the path is "-", the policy is read from stdin. Otherwise, it is + loaded from the file at the given path. + + Upload a policy named "my-policy" from /tmp/policy.hcl on the local disk: + + $ vault policy-write my-policy /tmp/policy.hcl + + Upload a policy from stdin: + + $ cat my-policy.hcl | vault policy-write my-policy - + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *PolicyWriteCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *PolicyWriteCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictFunc(func(args complete.Args) []string { + // Predict the LAST argument hcl files - we don't want to predict the + // name argument as a filepath. + if len(args.All) == 3 { + return complete.PredictFiles("*.hcl").Predict(args) + } + return nil + }) +} + +func (c *PolicyWriteCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *PolicyWriteCommand) Run(args []string) int { - flags := c.Meta.FlagSet("policy-write", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) != 2 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\npolicy-write expects exactly two arguments")) + args = f.Args() + switch { + case len(args) < 2: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 2, got %d)", len(args))) + return 1 + case len(args) > 2: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 2, got %d)", len(args))) return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } // Policies are normalized to lowercase - name := strings.ToLower(args[0]) - path := args[1] + name := strings.TrimSpace(strings.ToLower(args[0])) + path := strings.TrimSpace(args[1]) - // Read the policy - var f io.Reader = os.Stdin - if path != "-" { + // Get the policy contents, either from stdin of a file + var reader io.Reader + if path == "-" { + reader = os.Stdin + if c.testStdin != nil { + reader = c.testStdin + } + } else { file, err := os.Open(path) if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error opening file: %s", err)) - return 1 + c.UI.Error(fmt.Sprintf("Error opening policy file: %s", err)) + return 2 } defer file.Close() - f = file + reader = file } + + // Read the policy var buf bytes.Buffer - if _, err := io.Copy(&buf, f); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading file: %s", err)) - return 1 + if _, err := io.Copy(&buf, reader); err != nil { + c.UI.Error(fmt.Sprintf("Error reading policy: %s", err)) + return 2 } rules := buf.String() if err := client.Sys().PutPolicy(name, rules); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error: %s", err)) - return 1 + c.UI.Error(fmt.Sprintf("Error uploading policy: %s", err)) + return 2 } - c.Ui.Output(fmt.Sprintf("Policy '%s' written.", name)) + c.UI.Output(fmt.Sprintf("Success! Uploaded policy: %s", name)) return 0 } - -func (c *PolicyWriteCommand) Synopsis() string { - return "Write a policy to the server" -} - -func (c *PolicyWriteCommand) Help() string { - helpText := ` -Usage: vault policy-write [options] name path - - Write a policy with the given name from the contents of a file or stdin. - - If the path is "-", the policy is read from stdin. Otherwise, it is - loaded from the file at the given path. - -General Options: -` + meta.GeneralOptionsUsage() - return strings.TrimSpace(helpText) -} diff --git a/command/policy_write_test.go b/command/policy_write_test.go index d0deeaac69..c8db7dc9dd 100644 --- a/command/policy_write_test.go +++ b/command/policy_write_test.go @@ -1,33 +1,207 @@ package command import ( + "bytes" + "io" + "io/ioutil" + "os" + "reflect" + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestPolicyWrite(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testPolicyWriteCommand(tb testing.TB) (*cli.MockUi, *PolicyWriteCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &PolicyWriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &PolicyWriteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func testPolicyWritePolicyContents(tb testing.TB) []byte { + return bytes.TrimSpace([]byte(` +path "secret/" { + capabilities = ["read"] +} + `)) +} + +func TestPolicyWriteCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar", "baz"}, + "Too many arguments", + 1, + }, + { + "not_enough_args", + []string{"foo"}, + "Not enough arguments", + 1, + }, + { + "bad_file", + []string{"my-policy", "/not/a/real/path.hcl"}, + "Error opening policy file", + 2, }, } - args := []string{ - "-address", addr, - "foo", - "./test-fixtures/policy.hcl", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + 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() + + ui, cmd := testPolicyWriteCommand(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("file", func(t *testing.T) { + t.Parallel() + + policy := testPolicyWritePolicyContents(t) + f, err := ioutil.TempFile("", "vault-policy-write") + if err != nil { + t.Fatal(err) + } + if _, err := f.Write(policy); err != nil { + t.Fatal(err) + } + if err := f.Close(); err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testPolicyWriteCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-policy", f.Name(), + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Uploaded policy: my-policy" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + policies, err := client.Sys().ListPolicies() + if err != nil { + t.Fatal(err) + } + + list := []string{"default", "my-policy", "root"} + if !reflect.DeepEqual(policies, list) { + t.Errorf("expected %q to be %q", policies, list) + } + }) + + t.Run("stdin", func(t *testing.T) { + t.Parallel() + + stdinR, stdinW := io.Pipe() + go func() { + policy := testPolicyWritePolicyContents(t) + stdinW.Write(policy) + stdinW.Close() + }() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testPolicyWriteCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + code := cmd.Run([]string{ + "my-policy", "-", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Uploaded policy: my-policy" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + policies, err := client.Sys().ListPolicies() + if err != nil { + t.Fatal(err) + } + + list := []string{"default", "my-policy", "root"} + if !reflect.DeepEqual(policies, list) { + t.Errorf("expected %q to be %q", policies, list) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testPolicyWriteCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-policy", "-", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error uploading policy: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testPolicyWriteCommand(t) + assertNoTabs(t, cmd) + }) } From ad1482e123cf6330f4ec0f76698e889c1e2bcbd5 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:03:36 -0400 Subject: [PATCH 063/281] Update read command --- command/read.go | 143 +++++++++++------------- command/read_test.go | 256 +++++++++++++++++++++++-------------------- 2 files changed, 202 insertions(+), 197 deletions(-) diff --git a/command/read.go b/command/read.go index d989178229..99bfdf5b99 100644 --- a/command/read.go +++ b/command/read.go @@ -1,109 +1,96 @@ package command import ( - "flag" "fmt" "strings" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" "github.com/posener/complete" ) -// ReadCommand is a Command that reads data from the Vault. +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*ReadCommand)(nil) +var _ cli.CommandAutocomplete = (*ReadCommand)(nil) + +// ReadCommand is a command that reads data from the Vault. type ReadCommand struct { - meta.Meta -} - -func (c *ReadCommand) Run(args []string) int { - var format string - var field string - var err error - var secret *api.Secret - var flags *flag.FlagSet - flags = c.Meta.FlagSet("read", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - flags.StringVar(&field, "field", "", "") - 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().Read(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 - } - - // Handle single field output - if field != "" { - return PrintRawField(c.Ui, secret, field) - } - - return OutputSecret(c.Ui, format, secret) + *BaseCommand } func (c *ReadCommand) Synopsis() string { - return "Read data or secrets from Vault" + return "Reads data and retrieves secrets" } func (c *ReadCommand) Help() string { helpText := ` -Usage: vault read [options] path +Usage: vault read [options] PATH - Read data from Vault. + Reads data from Vault at the given path. This can be used to read secrets, + generate dynamic credentials, get configuration details, and more. - 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. + Read a secret from the static secret backend: -General Options: -` + meta.GeneralOptionsUsage() + ` -Read Options: + $ vault read secret/my-secret - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. + For a full list of examples and paths, please see the documentation that + corresponds to the secret backend in use. - -field=field If included, the raw value of the specified field - will be output raw to stdout. +` + c.Flags().Help() -` return strings.TrimSpace(helpText) } +func (c *ReadCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) +} + func (c *ReadCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictNothing + return c.PredictVaultFiles() } func (c *ReadCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-format": predictFormat, - "-field": complete.PredictNothing, - } + return c.Flags().Completions() +} + +func (c *ReadCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + path, kvs, err := extractPath(args) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if len(kvs) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + secret, err := client.Logical().Read(path) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err)) + return 2 + } + if secret == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 + } + + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + return OutputSecret(c.UI, c.flagFormat, secret) } diff --git a/command/read_test.go b/command/read_test.go index 5cf0f08c3e..b880d96ba6 100644 --- a/command/read_test.go +++ b/command/read_test.go @@ -1,137 +1,155 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestRead(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testReadCommand(tb testing.TB) (*cli.MockUi, *ReadCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &ReadCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &ReadCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - args := []string{ - "-address", addr, - "sys/mounts", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } } -func TestRead_notFound(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestReadCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) - c := &ReadCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing PATH!", + 1, + }, + { + "slash", + []string{"/"}, + "Missing PATH!", + 1, + }, + { + "not_found", + []string{"nope/not/once/never"}, + "", + 2, + }, + { + "default", + []string{"secret/read/foo"}, + "foo", + 0, + }, + { + "field", + []string{ + "-field", "foo", + "secret/read/foo", + }, + "bar", + 0, + }, + { + "field_not_found", + []string{ + "-field", "not-a-real-field", + "secret/read/foo", + }, + "not present in secret", + 1, + }, + { + "format", + []string{ + "-format", "json", + "secret/read/foo", + }, + "{", + 0, + }, + { + "format_bad", + []string{ + "-format", "nope-not-real", + "secret/read/foo", + }, + "Invalid output format", + 1, }, } - args := []string{ - "-address", addr, - "secret/nope", - } - if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - -func TestRead_field(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.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "-field", "value", - "secret/foo", - } - - // 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) - } - - // Run the read - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - output := ui.OutputWriter.String() - if output != "bar\n" { - t.Fatalf("unexpectd output:\n%s", output) - } -} - -func TestRead_field_notFound(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.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "-field", "nope", - "secret/foo", - } - - // 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) - } - - // Run the read - if code := c.Run(args); code != 1 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + 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.Logical().Write("secret/read/foo", map[string]interface{}{ + "foo": "bar", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testReadCommand(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("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testReadCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/foo", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error reading secret/foo: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testReadCommand(t) + assertNoTabs(t, cmd) + }) } From ec1677f3e70202d927d5d7c39377a1d1a0866dc3 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:03:44 -0400 Subject: [PATCH 064/281] Update rekey command --- command/rekey.go | 942 +++++++++++++++++++++++++----------------- command/rekey_test.go | 755 ++++++++++++++++++++------------- 2 files changed, 1052 insertions(+), 645 deletions(-) diff --git a/command/rekey.go b/command/rekey.go index 1c6b85412f..b4b5195ece 100644 --- a/command/rekey.go +++ b/command/rekey.go @@ -1,7 +1,9 @@ package command import ( + "bytes" "fmt" + "io" "os" "strings" @@ -9,13 +11,17 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/password" "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*RekeyCommand)(nil) +var _ cli.CommandAutocomplete = (*RekeyCommand)(nil) + // RekeyCommand is a Command that rekeys the vault. type RekeyCommand struct { - meta.Meta + *BaseCommand // Key can be used to pre-seed the key. If it is set, it will not // be asked with the `password` helper. @@ -26,424 +32,622 @@ type RekeyCommand struct { // Whether to use the recovery key instead of barrier key, if available RecoveryKey bool + + flagCancel bool + flagInit bool + flagKeyShares int + flagKeyThreshold int + flagNonce string + flagPGPKeys []string + flagStatus bool + flagTarget string + + // Backup options + flagBackup bool + flagBackupDelete bool + flagBackupRetrieve bool + + // Deprecations + // TODO: remove in 0.9.0 + flagDelete bool + flagRecoveryKey bool + flagRetrieve bool + + testStdin io.Reader // for tests +} + +func (c *RekeyCommand) Synopsis() string { + return "Generates new unseal keys" +} + +func (c *RekeyCommand) Help() string { + helpText := ` +Usage: vault rekey [options] [KEY] + + Generates a new set of unseal keys. This can optionally change the total + number of key shares or the required threshold of those key shares to + reconstruct the master key. This operation is zero downtime, but it requires + the Vault is unsealed and a quorum of existing unseal keys are provided. + + An unseal key may be provided directly on the command line as an argument to + the command. If key is specified as "-", the command will read from stdin. If + a TTY is available, the command will prompt for text. + + Initialize a rekey: + + $ vault rekey \ + -init \ + -key-shares=15 \ + -key-threshold=9 + + Rekey and encrypt the resulting unseal keys with PGP: + + $ vault rekey \ + -init \ + -key-shares=3 \ + -key-threshold=2 \ + -pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo" + + Store encrypted PGP keys in Vault's core: + + $ vault rekey \ + -init \ + -pgp-keys="..." \ + -backup + + Retrieve backed-up unseal keys: + + $ vault rekey -backup-retrieve + + Delete backed-up unseal keys: + + $ vault rekey -backup-delete + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *RekeyCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "init", + Target: &c.flagInit, + Default: false, + Usage: "Initialize the rekeying operation. This can only be done if no " + + "rekeying operation is in progress. Customize the new number of key " + + "shares and key threshold using the -key-shares and -key-threshold " + + "flags.", + }) + + f.BoolVar(&BoolVar{ + Name: "cancel", + Target: &c.flagCancel, + Default: false, + Usage: "Reset the rekeying progress. This will discard any submitted " + + "unseal keys or configuration.", + }) + + f.BoolVar(&BoolVar{ + Name: "status", + Target: &c.flagStatus, + Default: false, + Usage: "Print the status of the current attempt without provding an " + + "unseal key.", + }) + + f.IntVar(&IntVar{ + Name: "key-shares", + Aliases: []string{"n"}, + Target: &c.flagKeyShares, + Default: 5, + Completion: complete.PredictAnything, + Usage: "Number of key shares to split the generated master key into. " + + "This is the number of \"unseal keys\" to generate.", + }) + + f.IntVar(&IntVar{ + Name: "key-threshold", + Aliases: []string{"t"}, + Target: &c.flagKeyThreshold, + Default: 3, + Completion: complete.PredictAnything, + Usage: "Number of key shares required to reconstruct the master key. " + + "This must be less than or equal to -key-shares.", + }) + + f.StringVar(&StringVar{ + Name: "nonce", + Target: &c.flagNonce, + Default: "", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Nonce value provided at initialization. The same nonce value " + + "must be provided with each unseal key.", + }) + + f.StringVar(&StringVar{ + Name: "target", + Target: &c.flagTarget, + Default: "barrier", + EnvVar: "", + Completion: complete.PredictSet("barrier", "recovery"), + Usage: "Target for rekeying. \"recovery\" only applies when HSM support " + + "is enabled.", + }) + + f.VarFlag(&VarFlag{ + Name: "pgp-keys", + Value: (*pgpkeys.PubKeyFilesFlag)(&c.flagPGPKeys), + Completion: complete.PredictAnything, + Usage: "Comma-separated list of paths to files on disk containing " + + "public GPG keys OR a comma-separated list of Keybase usernames using " + + "the format \"keybase:\". When supplied, the generated " + + "unseal keys will be encrypted and base64-encoded in the order " + + "specified in this list. The number of entires must match -key-shares, " + + "unless -store-shares are used.", + }) + + f = set.NewFlagSet("Backup Options") + + f.BoolVar(&BoolVar{ + Name: "backup", + Target: &c.flagBackup, + Default: false, + Usage: "Store a backup of the current PGP encrypted unseal keys in " + + "Vault's core. The encrypted values can be recovered in the event of " + + "failure or discarded after success. See the -backup-delete and " + + "-backup-retrieve options for more information. This option only " + + "applies when the existing unseal keys were PGP encrypted.", + }) + + f.BoolVar(&BoolVar{ + Name: "backup-delete", + Target: &c.flagBackupDelete, + Default: false, + Usage: "Delete any stored backup unseal keys.", + }) + + f.BoolVar(&BoolVar{ + Name: "backup-retrieve", + Target: &c.flagBackupRetrieve, + Default: false, + Usage: "Retrieve the backed-up unseal keys. This option is only avaiable " + + "if the PGP keys were provided and the backup has not been deleted.", + }) + + // Deprecations + // TODO: remove in 0.9.0 + f.BoolVar(&BoolVar{ + Name: "delete", // prefer -backup-delete + Target: &c.flagDelete, + Default: false, + Hidden: true, + Usage: "", + }) + + f.BoolVar(&BoolVar{ + Name: "retrieve", // prefer -backup-retrieve + Target: &c.flagRetrieve, + Default: false, + Hidden: true, + Usage: "", + }) + + f.BoolVar(&BoolVar{ + Name: "recovery-key", // prefer -target=recovery + Target: &c.flagRecoveryKey, + Default: false, + Hidden: true, + Usage: "", + }) + + return set +} + +func (c *RekeyCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *RekeyCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *RekeyCommand) Run(args []string) int { - var init, cancel, status, delete, retrieve, backup, recoveryKey bool - var shares, threshold, storedShares int - var nonce string - var pgpKeys pgpkeys.PubKeyFilesFlag - flags := c.Meta.FlagSet("rekey", meta.FlagSetDefault) - flags.BoolVar(&init, "init", false, "") - flags.BoolVar(&cancel, "cancel", false, "") - flags.BoolVar(&status, "status", false, "") - flags.BoolVar(&delete, "delete", false, "") - flags.BoolVar(&retrieve, "retrieve", false, "") - flags.BoolVar(&backup, "backup", false, "") - flags.BoolVar(&recoveryKey, "recovery-key", c.RecoveryKey, "") - flags.IntVar(&shares, "key-shares", 5, "") - flags.IntVar(&threshold, "key-threshold", 3, "") - flags.IntVar(&storedShares, "stored-shares", 0, "") - flags.StringVar(&nonce, "nonce", "", "") - flags.Var(&pgpKeys, "pgp-keys", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - if nonce != "" { - c.Nonce = nonce + args = f.Args() + if len(args) > 1 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-1, got %d)", len(args))) + return 1 } + // Deprecations + // TODO: remove in 0.9.0 + if c.flagDelete { + c.UI.Warn(wrapAtLength( + "WARNING! The -delete flag is deprecated. Please use -backup-delete " + + "instead. This flag will be removed in the next major release of " + + "Vault.")) + c.flagBackupDelete = true + } + if c.flagRetrieve { + c.UI.Warn(wrapAtLength( + "WARNING! The -retrieve flag is deprecated. Please use -backup-retrieve " + + "instead. This flag will be removed in the next major release of " + + "Vault.")) + c.flagBackupRetrieve = true + } + if c.flagRecoveryKey { + c.UI.Warn(wrapAtLength( + "WARNING! The -recovery-key flag is deprecated. Please use -target=recovery " + + "instead. This flag will be removed in the next major release of " + + "Vault.")) + c.flagTarget = "recovery" + } + + // Create the client client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } - // Check if we are running doing any restricted variants switch { - case init: - return c.initRekey(client, shares, threshold, storedShares, pgpKeys, backup, recoveryKey) - case cancel: - return c.cancelRekey(client, recoveryKey) - case status: - return c.rekeyStatus(client, recoveryKey) - case retrieve: - return c.rekeyRetrieveStored(client, recoveryKey) - case delete: - return c.rekeyDeleteStored(client, recoveryKey) - } - - // Check if the rekey is started - var rekeyStatus *api.RekeyStatusResponse - if recoveryKey { - rekeyStatus, err = client.Sys().RekeyRecoveryKeyStatus() - } else { - rekeyStatus, err = client.Sys().RekeyStatus() - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading rekey status: %s", err)) - return 1 - } - - // Start the rekey process if not started - if !rekeyStatus.Started { - if recoveryKey { - rekeyStatus, err = client.Sys().RekeyRecoveryKeyInit(&api.RekeyInitRequest{ - SecretShares: shares, - SecretThreshold: threshold, - PGPKeys: pgpKeys, - Backup: backup, - }) - } else { - rekeyStatus, err = client.Sys().RekeyInit(&api.RekeyInitRequest{ - SecretShares: shares, - SecretThreshold: threshold, - PGPKeys: pgpKeys, - Backup: backup, - }) + case c.flagBackupDelete: + return c.backupDelete(client) + case c.flagBackupRetrieve: + return c.backupRetrieve(client) + case c.flagCancel: + return c.cancel(client) + case c.flagInit: + return c.init(client) + case c.flagStatus: + return c.status(client) + default: + // If there are no other flags, prompt for an unseal key. + key := "" + if len(args) > 0 { + key = strings.TrimSpace(args[0]) } - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing rekey: %s", err)) - return 1 - } - c.Nonce = rekeyStatus.Nonce + return c.provide(client, key) } - - shares = rekeyStatus.N - threshold = rekeyStatus.T - serverNonce := rekeyStatus.Nonce - - // Get the unseal key - args = flags.Args() - key := c.Key - if len(args) > 0 { - key = args[0] - } - if key == "" { - c.Nonce = serverNonce - fmt.Printf("Rekey operation nonce: %s\n", serverNonce) - fmt.Printf("Key (will be hidden): ") - key, err = password.Read(os.Stdin) - fmt.Printf("\n") - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error attempting to ask for password. The raw error message\n"+ - "is shown below, but the most common reason for this error is\n"+ - "that you attempted to pipe a value into unseal or you're\n"+ - "executing `vault rekey` from outside of a terminal.\n\n"+ - "You should use `vault rekey` from a terminal for maximum\n"+ - "security. If this isn't an option, the unseal key can be passed\n"+ - "in using the first parameter.\n\n"+ - "Raw error: %s", err)) - return 1 - } - } - - // Provide the key, this may potentially complete the update - var result *api.RekeyUpdateResponse - if recoveryKey { - result, err = client.Sys().RekeyRecoveryKeyUpdate(strings.TrimSpace(key), c.Nonce) - } else { - result, err = client.Sys().RekeyUpdate(strings.TrimSpace(key), c.Nonce) - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Error attempting rekey update: %s", err)) - return 1 - } - - // If we are not complete, then dump the status - if !result.Complete { - return c.rekeyStatus(client, recoveryKey) - } - - // Space between the key prompt, if any, and the output - c.Ui.Output("\n") - // Provide the keys - var haveB64 bool - if result.KeysB64 != nil && len(result.KeysB64) == len(result.Keys) { - haveB64 = true - } - for i, key := range result.Keys { - if len(result.PGPFingerprints) > 0 { - if haveB64 { - c.Ui.Output(fmt.Sprintf("Key %d fingerprint: %s; value: %s", i+1, result.PGPFingerprints[i], result.KeysB64[i])) - } else { - c.Ui.Output(fmt.Sprintf("Key %d fingerprint: %s; value: %s", i+1, result.PGPFingerprints[i], key)) - } - } else { - if haveB64 { - c.Ui.Output(fmt.Sprintf("Key %d: %s", i+1, result.KeysB64[i])) - } else { - c.Ui.Output(fmt.Sprintf("Key %d: %s", i+1, key)) - } - } - } - - c.Ui.Output(fmt.Sprintf("\nOperation nonce: %s", result.Nonce)) - - if len(result.PGPFingerprints) > 0 && result.Backup { - c.Ui.Output(fmt.Sprintf( - "\n" + - "The encrypted unseal keys have been backed up to \"core/unseal-keys-backup\"\n" + - "in your physical backend. It is your responsibility to remove these if and\n" + - "when desired.", - )) - } - - c.Ui.Output(fmt.Sprintf( - "\n"+ - "Vault rekeyed with %d keys and a key threshold of %d.\n", - shares, - threshold, - )) - - // Print this message if keys are returned - if len(result.Keys) > 0 { - c.Ui.Output(fmt.Sprintf( - "\n"+ - "Please securely distribute the above keys. When the vault is re-sealed,\n"+ - "restarted, or stopped, you must provide at least %d of these keys\n"+ - "to unseal it again.\n\n"+ - "Vault does not store the master key. Without at least %[1]d keys,\n"+ - "your vault will remain permanently sealed.", - threshold, - )) - } - - return 0 } -// initRekey is used to start the rekey process -func (c *RekeyCommand) initRekey(client *api.Client, - shares, threshold, storedShares int, - pgpKeys pgpkeys.PubKeyFilesFlag, - backup, recoveryKey bool) int { - // Start the rekey - request := &api.RekeyInitRequest{ - SecretShares: shares, - SecretThreshold: threshold, - StoredShares: storedShares, - PGPKeys: pgpKeys, - Backup: backup, - } - var status *api.RekeyStatusResponse - var err error - if recoveryKey { - status, err = client.Sys().RekeyRecoveryKeyInit(request) - } else { - status, err = client.Sys().RekeyInit(request) - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing rekey: %s", err)) +// init starts the rekey process. +func (c *RekeyCommand) init(client *api.Client) int { + // Handle the different API requests + var fn func(*api.RekeyInitRequest) (*api.RekeyStatusResponse, error) + switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { + case "barrier": + fn = client.Sys().RekeyInit + case "recovery", "hsm": + fn = client.Sys().RekeyRecoveryKeyInit + default: + c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget)) return 1 } - if pgpKeys == nil || len(pgpKeys) == 0 { - c.Ui.Output(` -WARNING: If you lose the keys after they are returned to you, there is no -recovery. Consider using the '-pgp-keys' option to protect the returned unseal -keys along with '-backup=true' to allow recovery of the encrypted keys in case -of emergency. They can easily be deleted at a later time with -'vault rekey -delete'. -`) + // Make the request + status, err := fn(&api.RekeyInitRequest{ + SecretShares: c.flagKeyShares, + SecretThreshold: c.flagKeyThreshold, + PGPKeys: c.flagPGPKeys, + Backup: c.flagBackup, + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error initializing rekey: %s", err)) + return 2 } - if pgpKeys != nil && len(pgpKeys) > 0 && !backup { - c.Ui.Output(` -WARNING: You are using PGP keys for encryption, but have not set the option to -back up the new unseal keys to physical storage. If you lose the keys after -they are returned to you, there is no recovery. Consider setting '-backup=true' -to allow recovery of the encrypted keys in case of emergency. They can easily -be deleted at a later time with 'vault rekey -delete'. -`) + // Print warnings about recovery, etc. + if len(c.flagPGPKeys) == 0 { + c.UI.Warn(wrapAtLength( + "WARNING! If you lose the keys after they are returned, there is no " + + "recovery. Consider canceling this operation and re-initializing " + + "with the -pgp-keys flag to protect the returned unseal keys along " + + "with -backup to allow recovery of the encrypted keys in case of " + + "emergency. You can delete the stored keys later using the -delete " + + "flag.")) + c.UI.Output("") + } + if len(c.flagPGPKeys) > 0 && !c.flagBackup { + c.UI.Warn(wrapAtLength( + "WARNING! You are using PGP keys for encrypted the resulting unseal " + + "keys, but you did not enable the option to backup the keys to " + + "Vault's core. If you lose the encrypted keys after they are " + + "returned, you will not be able to recover them. Consider canceling " + + "this operation and re-running with -backup to allow recovery of the " + + "encrypted unseal keys in case of emergency. You can delete the " + + "stored keys later using the -delete flag.")) + c.UI.Output("") } // Provide the current status - return c.dumpRekeyStatus(status) + return c.printStatus(status) } -// cancelRekey is used to abort the rekey process -func (c *RekeyCommand) cancelRekey(client *api.Client, recovery bool) int { - var err error - if recovery { - err = client.Sys().RekeyRecoveryKeyCancel() - } else { - err = client.Sys().RekeyCancel() - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to cancel rekey: %s", err)) +// cancel is used to abort the rekey process. +func (c *RekeyCommand) cancel(client *api.Client) int { + // Handle the different API requests + var fn func() error + switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { + case "barrier": + fn = client.Sys().RekeyCancel + case "recovery", "hsm": + fn = client.Sys().RekeyRecoveryKeyCancel + default: + c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget)) return 1 } - c.Ui.Output("Rekey canceled.") + + // Make the request + if err := fn(); err != nil { + c.UI.Error(fmt.Sprintf("Error canceling rekey: %s", err)) + return 2 + } + + c.UI.Output("Success! Canceled rekeying (if it was started)") return 0 } -// rekeyStatus is used just to fetch and dump the status -func (c *RekeyCommand) rekeyStatus(client *api.Client, recovery bool) int { - // Check the status - var status *api.RekeyStatusResponse - var err error - if recovery { - status, err = client.Sys().RekeyRecoveryKeyStatus() - } else { - status, err = client.Sys().RekeyStatus() - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Error reading rekey status: %s", err)) +// provide prompts the user for the seal key and posts it to the update root +// endpoint. If this is the last unseal, this function outputs it. +func (c *RekeyCommand) provide(client *api.Client, key string) int { + var statusFn func() (*api.RekeyStatusResponse, error) + var updateFn func(string, string) (*api.RekeyUpdateResponse, error) + + switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { + case "barrier": + statusFn = client.Sys().RekeyStatus + updateFn = client.Sys().RekeyUpdate + case "recovery", "hsm": + statusFn = client.Sys().RekeyRecoveryKeyStatus + updateFn = client.Sys().RekeyRecoveryKeyUpdate + default: + c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget)) return 1 } - return c.dumpRekeyStatus(status) -} - -func (c *RekeyCommand) dumpRekeyStatus(status *api.RekeyStatusResponse) int { - // Dump the status - statString := fmt.Sprintf( - "Nonce: %s\n"+ - "Started: %t\n"+ - "Key Shares: %d\n"+ - "Key Threshold: %d\n"+ - "Rekey Progress: %d\n"+ - "Required Keys: %d", - status.Nonce, - status.Started, - status.N, - status.T, - status.Progress, - status.Required, - ) - if len(status.PGPFingerprints) != 0 { - statString = fmt.Sprintf("%s\nPGP Key Fingerprints: %s", statString, status.PGPFingerprints) - statString = fmt.Sprintf("%s\nBackup Storage: %t", statString, status.Backup) - } - c.Ui.Output(statString) - return 0 -} - -func (c *RekeyCommand) rekeyRetrieveStored(client *api.Client, recovery bool) int { - var storedKeys *api.RekeyRetrieveResponse - var err error - if recovery { - storedKeys, err = client.Sys().RekeyRetrieveRecoveryBackup() - } else { - storedKeys, err = client.Sys().RekeyRetrieveBackup() - } + status, err := statusFn() if err != nil { - c.Ui.Error(fmt.Sprintf("Error retrieving stored keys: %s", err)) + c.UI.Error(fmt.Sprintf("Error getting rekey status: %s", err)) + return 2 + } + + // Verify a root token generation is in progress. If there is not one in + // progress, return an error instructing the user to start one. + if !status.Started { + c.UI.Error(wrapAtLength( + "No rekey is in progress. Start a rekey process by running " + + "\"vault rekey -init\".")) return 1 } + var nonce string + + switch key { + case "-": // Read from stdin + nonce = c.flagNonce + + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + var buf bytes.Buffer + if _, err := io.Copy(&buf, stdin); err != nil { + c.UI.Error(fmt.Sprintf("Failed to read from stdin: %s", err)) + return 1 + } + + key = buf.String() + case "": // Prompt using the tty + // Nonce value is not required if we are prompting via the terminal + nonce = status.Nonce + + w := getWriterFromUI(c.UI) + fmt.Fprintf(w, "Rekey operation nonce: %s\n", nonce) + fmt.Fprintf(w, "Unseal Key (will be hidden): ") + key, err = password.Read(os.Stdin) + fmt.Fprintf(w, "\n") + if err != nil { + if err == password.ErrInterrupted { + c.UI.Error("user canceled") + return 1 + } + + c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+ + "ask for the unseal key. The raw error message is shown below, but "+ + "usually this is because you attempted to pipe a value into the "+ + "command or you are executing outside of a terminal (tty). If you "+ + "want to pipe the value, pass \"-\" as the argument to read from "+ + "stdin. The raw error was: %s", err))) + return 1 + } + default: // Supplied directly as an arg + nonce = c.flagNonce + } + + // Trim any whitespace from they key, especially since we might have + // prompted the user for it. + key = strings.TrimSpace(key) + + // Verify we have a nonce value + if nonce == "" { + c.UI.Error("Missing nonce value: specify it via the -nonce flag") + return 1 + } + + // Provide the key, this may potentially complete the update + resp, err := updateFn(key, nonce) + if err != nil { + c.UI.Error(fmt.Sprintf("Error posting unseal key: %s", err)) + return 2 + } + + if !resp.Complete { + return c.status(client) + } + + return c.printUnsealKeys(status, resp) +} + +// status is used just to fetch and dump the status. +func (c *RekeyCommand) status(client *api.Client) int { + // Handle the different API requests + var fn func() (*api.RekeyStatusResponse, error) + switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { + case "barrier": + fn = client.Sys().RekeyStatus + case "recovery", "hsm": + fn = client.Sys().RekeyRecoveryKeyStatus + default: + c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget)) + return 1 + } + + // Make the request + status, err := fn() + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading rekey status: %s", err)) + return 2 + } + + return c.printStatus(status) +} + +// backupRetrieve retrieves the stored backup keys. +func (c *RekeyCommand) backupRetrieve(client *api.Client) int { + // Handle the different API requests + var fn func() (*api.RekeyRetrieveResponse, error) + switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { + case "barrier": + fn = client.Sys().RekeyRetrieveBackup + case "recovery", "hsm": + fn = client.Sys().RekeyRetrieveRecoveryBackup + default: + c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget)) + return 1 + } + + // Make the request + storedKeys, err := fn() + if err != nil { + c.UI.Error(fmt.Sprintf("Error retrieving rekey stored keys: %s", err)) + return 2 + } + secret := &api.Secret{ Data: structs.New(storedKeys).Map(), } - return OutputSecret(c.Ui, "table", secret) + return OutputSecret(c.UI, "table", secret) } -func (c *RekeyCommand) rekeyDeleteStored(client *api.Client, recovery bool) int { - var err error - if recovery { - err = client.Sys().RekeyDeleteRecoveryBackup() - } else { - err = client.Sys().RekeyDeleteBackup() - } - if err != nil { - c.Ui.Error(fmt.Sprintf("Failed to delete stored keys: %s", err)) +// backupDelete deletes the stored backup keys. +func (c *RekeyCommand) backupDelete(client *api.Client) int { + // Handle the different API requests + var fn func() error + switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { + case "barrier": + fn = client.Sys().RekeyDeleteBackup + case "recovery", "hsm": + fn = client.Sys().RekeyDeleteRecoveryBackup + default: + c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget)) return 1 } - c.Ui.Output("Stored keys deleted.") + + // Make the request + if err := fn(); err != nil { + c.UI.Error(fmt.Sprintf("Error deleting rekey stored keys: %s", err)) + return 2 + } + + c.UI.Output("Success! Delete stored keys (if they existed)") return 0 } -func (c *RekeyCommand) Synopsis() string { - return "Rekeys Vault to generate new unseal keys" -} +// printStatus dumps the status to output +func (c *RekeyCommand) printStatus(status *api.RekeyStatusResponse) int { + out := []string{} + out = append(out, fmt.Sprintf("Nonce | %s", status.Nonce)) + out = append(out, fmt.Sprintf("Started | %t", status.Started)) -func (c *RekeyCommand) Help() string { - helpText := ` -Usage: vault rekey [options] [key] - - Rekey is used to change the unseal keys. This can be done to generate - a new set of unseal keys or to change the number of shares and the - required threshold. - - Rekey can only be done when the vault is already unsealed. The operation - is done online, but requires that a threshold of the current unseal - keys be provided. - -General Options: -` + meta.GeneralOptionsUsage() + ` -Rekey Options: - - -init Initialize the rekey operation by setting the desired - number of shares and the key threshold. This can only be - done if no rekey is already initiated. - - -cancel Reset the rekey process by throwing away - prior keys and the rekey configuration. - - -status Prints the status of the current rekey operation. - This can be used to see the status without attempting - to provide an unseal key. - - -retrieve Retrieve backed-up keys. Only available if the PGP keys - were provided and the backup has not been deleted. - - -delete Delete any backed-up keys. - - -key-shares=5 The number of key shares to split the master key - into. - - -key-threshold=3 The number of key shares required to reconstruct - the master key. - - -nonce=abcd The nonce provided at rekey initialization time. This - same nonce value must be provided with each unseal - key. If the unseal key is not being passed in via the - the command line the nonce parameter is not required, - and will instead be displayed with the key prompt. - - -pgp-keys If provided, must be a comma-separated list of - files on disk containing binary- or base64-format - public PGP keys, or Keybase usernames specified as - "keybase:". The number of given entries - must match 'key-shares'. The output unseal keys will - be encrypted and base64-encoded, in order, with the - given public keys. If you want to use them with the - 'vault unseal' command, you will need to base64-decode - and decrypt; this will be the plaintext unseal key. - - -backup=false If true, and if the key shares are PGP-encrypted, a - plaintext backup of the PGP-encrypted keys will be - stored at "core/unseal-keys-backup" in your physical - storage. You can retrieve or delete them via the - 'sys/rekey/backup' endpoint. - - -recovery-key=false Whether to rekey the recovery key instead of the - barrier key. Only used with Vault HSM. -` - return strings.TrimSpace(helpText) -} - -func (c *RekeyCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictNothing -} - -func (c *RekeyCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-init": complete.PredictNothing, - "-cancel": complete.PredictNothing, - "-status": complete.PredictNothing, - "-retrieve": complete.PredictNothing, - "-delete": complete.PredictNothing, - "-key-shares": complete.PredictNothing, - "-key-threshold": complete.PredictNothing, - "-nonce": complete.PredictNothing, - "-pgp-keys": complete.PredictNothing, - "-backup": complete.PredictNothing, - "-recovery-key": complete.PredictNothing, + if status.Started { + out = append(out, fmt.Sprintf("Progress | %d/%d", status.Progress, status.Required)) + out = append(out, fmt.Sprintf("New Shares | %d", status.N)) + out = append(out, fmt.Sprintf("New Threshold | %d", status.T)) } + + if len(status.PGPFingerprints) > 0 { + out = append(out, fmt.Sprintf("PGP Fingerprints | %s", status.PGPFingerprints)) + out = append(out, fmt.Sprintf("Backup | %t", status.Backup)) + } + + output := columnOutput(out) + c.UI.Output(output) + return 0 +} + +func (c *RekeyCommand) printUnsealKeys(status *api.RekeyStatusResponse, resp *api.RekeyUpdateResponse) int { + // Space between the key prompt, if any, and the output + c.UI.Output("") + + // Provide the keys + var haveB64 bool + if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) { + haveB64 = true + } + for i, key := range resp.Keys { + if len(resp.PGPFingerprints) > 0 { + if haveB64 { + c.UI.Output(fmt.Sprintf("Key %d fingerprint: %s; value: %s", i+1, resp.PGPFingerprints[i], resp.KeysB64[i])) + } else { + c.UI.Output(fmt.Sprintf("Key %d fingerprint: %s; value: %s", i+1, resp.PGPFingerprints[i], key)) + } + } else { + if haveB64 { + c.UI.Output(fmt.Sprintf("Key %d: %s", i+1, resp.KeysB64[i])) + } else { + c.UI.Output(fmt.Sprintf("Key %d: %s", i+1, key)) + } + } + } + + c.UI.Output("") + c.UI.Output(fmt.Sprintf("Operation nonce: %s", resp.Nonce)) + + if len(resp.PGPFingerprints) > 0 && resp.Backup { + c.UI.Output("") + c.UI.Output(wrapAtLength(fmt.Sprintf( + "The encrypted unseal keys are backed up to \"core/unseal-keys-backup\"" + + "in the physical backend. Remove these keys at any time using " + + "\"vault rekey -delete-backup\". Vault does not automatically remove " + + "these keys.", + ))) + } + + c.UI.Output("") + c.UI.Output(wrapAtLength(fmt.Sprintf( + "Vault rekeyed with %d key shares an a key threshold of %d. Please "+ + "securely distributed the key shares printed above. When the Vault is "+ + "re-sealed, restarted, or stopped, you must supply at least %d of "+ + "these keys to unseal it before it can start servicing requests.", + status.N, + status.T, + status.T))) + + return 0 } diff --git a/command/rekey_test.go b/command/rekey_test.go index 6f12d7834b..7c08287d05 100644 --- a/command/rekey_test.go +++ b/command/rekey_test.go @@ -1,312 +1,515 @@ package command import ( - "encoding/hex" - "os" - "sort" + "io" + "reflect" + "regexp" "strings" "testing" - "time" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" ) -func TestRekey(t *testing.T) { - core, keys, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testRekeyCommand(tb testing.TB) (*cli.MockUi, *RekeyCommand) { + tb.Helper() - ui := new(cli.MockUi) + ui := cli.NewMockUi() + return ui, &RekeyCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} - for i, key := range keys { - c := &RekeyCommand{ - Key: hex.EncodeToString(key), - RecoveryKey: false, - Meta: meta.Meta{ - Ui: ui, +func TestRekeyCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "pgp_keys_multi", + []string{ + "-init", + "-pgp-keys", "keybase:hashicorp", + "-pgp-keys", "keybase:jefferai", }, + "can only be specified once", + 1, + }, + { + "key_shares_pgp_less", + []string{ + "-init", + "-key-shares", "10", + "-pgp-keys", "keybase:jefferai,keybase:sethvargo", + }, + "incorrect number", + 2, + }, + { + "key_shares_pgp_more", + []string{ + "-init", + "-key-shares", "1", + "-pgp-keys", "keybase:jefferai,keybase:sethvargo", + }, + "incorrect number", + 2, + }, + } + + 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() + + ui, cmd := testRekeyCommand(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("status", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testRekeyCommand(t) + cmd.client = client + + // Verify the non-init response + code := cmd.Run([]string{ + "-status", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) } - if i > 0 { - conf, err := core.RekeyConfig(false) - if err != nil { - t.Fatal(err) + expected := "Nonce" + combined := ui.OutputWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + // Now init to verify the init response + if _, err := client.Sys().RekeyInit(&api.RekeyInitRequest{ + SecretShares: 1, + SecretThreshold: 1, + }); err != nil { + t.Fatal(err) + } + + // Verify the init response + ui, cmd = testRekeyCommand(t) + cmd.client = client + code = cmd.Run([]string{ + "-status", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + + expected = "Progress" + combined = ui.OutputWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("cancel", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + // Initialize a rekey + if _, err := client.Sys().RekeyInit(&api.RekeyInitRequest{ + SecretShares: 1, + SecretThreshold: 1, + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testRekeyCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-cancel", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Canceled rekeying" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + status, err := client.Sys().GenerateRootStatus() + if err != nil { + t.Fatal(err) + } + + if status.Started { + t.Errorf("expected status to be canceled: %#v", status) + } + }) + + t.Run("init", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testRekeyCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-init", + "-key-shares", "1", + "-key-threshold", "1", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + + expected := "Nonce" + combined := ui.OutputWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + status, err := client.Sys().RekeyStatus() + if err != nil { + t.Fatal(err) + } + if !status.Started { + t.Errorf("expected status to be started: %#v", status) + } + }) + + t.Run("init_pgp", func(t *testing.T) { + t.Parallel() + + pgpKey := "keybase:hashicorp" + pgpFingerprints := []string{"91a6e7f85d05c65630bef18951852d87348ffc4c"} + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testRekeyCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-init", + "-key-shares", "1", + "-key-threshold", "1", + "-pgp-keys", pgpKey, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + + expected := "Nonce" + combined := ui.OutputWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + status, err := client.Sys().RekeyStatus() + if err != nil { + t.Fatal(err) + } + if !status.Started { + t.Errorf("expected status to be started: %#v", status) + } + if !reflect.DeepEqual(status.PGPFingerprints, pgpFingerprints) { + t.Errorf("expected %#v to be %#v", status.PGPFingerprints, pgpFingerprints) + } + }) + + t.Run("provide_arg", func(t *testing.T) { + t.Parallel() + + client, keys, closer := testVaultServerUnseal(t) + defer closer() + + // Initialize a rekey + status, err := client.Sys().RekeyInit(&api.RekeyInitRequest{ + SecretShares: 1, + SecretThreshold: 1, + }) + if err != nil { + t.Fatal(err) + } + nonce := status.Nonce + + // Supply the first n-1 unseal keys + for _, key := range keys[:len(keys)-1] { + ui, cmd := testRekeyCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-nonce", nonce, + key, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) } - c.Nonce = conf.Nonce } - args := []string{"-address", addr} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - } + ui, cmd := testRekeyCommand(t) + cmd.client = client - config, err := core.SealAccess().BarrierConfig() - if err != nil { - t.Fatalf("err: %s", err) - } - if config.SecretShares != 5 { - t.Fatal("should rekey") - } -} - -func TestRekey_arg(t *testing.T) { - core, keys, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - - for i, key := range keys { - c := &RekeyCommand{ - RecoveryKey: false, - Meta: meta.Meta{ - Ui: ui, - }, + code := cmd.Run([]string{ + "-nonce", nonce, + keys[len(keys)-1], // the last unseal key + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) } - if i > 0 { - conf, err := core.RekeyConfig(false) - if err != nil { - t.Fatal(err) + re := regexp.MustCompile(`Key 1: (.+)`) + output := ui.OutputWriter.String() + match := re.FindAllStringSubmatch(output, -1) + if len(match) < 1 || len(match[0]) < 2 { + t.Fatalf("bad match: %#v", match) + } + + // Grab the unseal key and try to unseal + unsealKey := match[0][1] + if err := client.Sys().Seal(); err != nil { + t.Fatal(err) + } + sealStatus, err := client.Sys().Unseal(unsealKey) + if err != nil { + t.Fatal(err) + } + if sealStatus.Sealed { + t.Errorf("expected vault to be unsealed: %#v", sealStatus) + } + }) + + t.Run("provide_stdin", func(t *testing.T) { + t.Parallel() + + client, keys, closer := testVaultServerUnseal(t) + defer closer() + + // Initialize a rekey + status, err := client.Sys().RekeyInit(&api.RekeyInitRequest{ + SecretShares: 1, + SecretThreshold: 1, + }) + if err != nil { + t.Fatal(err) + } + nonce := status.Nonce + + // Supply the first n-1 unseal keys + for _, key := range keys[:len(keys)-1] { + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(key)) + stdinW.Close() + }() + + ui, cmd := testRekeyCommand(t) + cmd.client = client + cmd.testStdin = stdinR + + code := cmd.Run([]string{ + "-nonce", nonce, + "-", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) } - c.Nonce = conf.Nonce } - args := []string{"-address", addr, hex.EncodeToString(key)} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - } + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(keys[len(keys)-1])) // the last unseal key + stdinW.Close() + }() - config, err := core.SealAccess().BarrierConfig() - if err != nil { - t.Fatalf("err: %s", err) - } - if config.SecretShares != 5 { - t.Fatal("should rekey") - } -} + ui, cmd := testRekeyCommand(t) + cmd.client = client + cmd.testStdin = stdinR -func TestRekey_init(t *testing.T) { - core, _, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - - c := &RekeyCommand{ - Meta: meta.Meta{ - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "-init", - "-key-threshold", "10", - "-key-shares", "10", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - config, err := core.RekeyConfig(false) - if err != nil { - t.Fatalf("err: %s", err) - } - if config.SecretShares != 10 { - t.Fatal("should rekey") - } - if config.SecretThreshold != 10 { - t.Fatal("should rekey") - } -} - -func TestRekey_cancel(t *testing.T) { - core, keys, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &RekeyCommand{ - Key: hex.EncodeToString(keys[0]), - Meta: meta.Meta{ - Ui: ui, - }, - } - - args := []string{"-address", addr, "-init"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - args = []string{"-address", addr, "-cancel"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - config, err := core.RekeyConfig(false) - if err != nil { - t.Fatalf("err: %s", err) - } - if config != nil { - t.Fatal("should not rekey") - } -} - -func TestRekey_status(t *testing.T) { - core, keys, _ := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &RekeyCommand{ - Key: hex.EncodeToString(keys[0]), - Meta: meta.Meta{ - Ui: ui, - }, - } - - args := []string{"-address", addr, "-init"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - args = []string{"-address", addr, "-status"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - if !strings.Contains(ui.OutputWriter.String(), "Started: true") { - t.Fatalf("bad: %s", ui.OutputWriter.String()) - } -} - -func TestRekey_init_pgp(t *testing.T) { - core, keys, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - bc := &logical.BackendConfig{ - Logger: nil, - System: logical.StaticSystemView{ - DefaultLeaseTTLVal: time.Hour * 24, - MaxLeaseTTLVal: time.Hour * 24 * 32, - }, - } - sysBackend := vault.NewSystemBackend(core) - err := sysBackend.Backend.Setup(bc) - if err != nil { - t.Fatal(err) - } - - ui := new(cli.MockUi) - c := &RekeyCommand{ - Meta: meta.Meta{ - Ui: ui, - }, - } - - tempDir, pubFiles, err := getPubKeyFiles(t) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - args := []string{ - "-address", addr, - "-init", - "-key-shares", "4", - "-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2] + "," + pubFiles[3], - "-key-threshold", "2", - "-backup", "true", - } - - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - config, err := core.RekeyConfig(false) - if err != nil { - t.Fatalf("err: %s", err) - } - if config.SecretShares != 4 { - t.Fatal("should rekey") - } - if config.SecretThreshold != 2 { - t.Fatal("should rekey") - } - - for _, key := range keys { - c = &RekeyCommand{ - Key: hex.EncodeToString(key), - Meta: meta.Meta{ - Ui: ui, - }, + code := cmd.Run([]string{ + "-nonce", nonce, + "-", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) } - c.Nonce = config.Nonce - - args = []string{ - "-address", addr, + re := regexp.MustCompile(`Key 1: (.+)`) + output := ui.OutputWriter.String() + match := re.FindAllStringSubmatch(output, -1) + if len(match) < 1 || len(match[0]) < 2 { + t.Fatalf("bad match: %#v", match) } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + + // Grab the unseal key and try to unseal + unsealKey := match[0][1] + if err := client.Sys().Seal(); err != nil { + t.Fatal(err) } - } + sealStatus, err := client.Sys().Unseal(unsealKey) + if err != nil { + t.Fatal(err) + } + if sealStatus.Sealed { + t.Errorf("expected vault to be unsealed: %#v", sealStatus) + } + }) - type backupStruct struct { - Keys map[string][]string - KeysB64 map[string][]string - } - backupVals := &backupStruct{} + t.Run("backup", func(t *testing.T) { + t.Parallel() - req := logical.TestRequest(t, logical.ReadOperation, "rekey/backup") - resp, err := sysBackend.HandleRequest(req) - if err != nil { - t.Fatalf("error running backed-up unseal key fetch: %v", err) - } - if resp == nil { - t.Fatalf("got nil resp with unseal key fetch") - } - if resp.Data["keys"] == nil { - t.Fatalf("could not retrieve unseal keys from token") - } - if resp.Data["nonce"] != config.Nonce { - t.Fatalf("nonce mismatch between rekey and backed-up keys") - } + pgpKey := "keybase:hashicorp" + // pgpFingerprints := []string{"91a6e7f85d05c65630bef18951852d87348ffc4c"} - backupVals.Keys = resp.Data["keys"].(map[string][]string) - backupVals.KeysB64 = resp.Data["keys_base64"].(map[string][]string) + client, keys, closer := testVaultServerUnseal(t) + defer closer() - // Now delete and try again; the values should be inaccessible - req = logical.TestRequest(t, logical.DeleteOperation, "rekey/backup") - resp, err = sysBackend.HandleRequest(req) - if err != nil { - t.Fatalf("error running backed-up unseal key delete: %v", err) - } - req = logical.TestRequest(t, logical.ReadOperation, "rekey/backup") - resp, err = sysBackend.HandleRequest(req) - if err != nil { - t.Fatalf("error running backed-up unseal key fetch: %v", err) - } - if resp == nil { - t.Fatalf("got nil resp with unseal key fetch") - } - if resp.Data["keys"] != nil { - t.Fatalf("keys found when they should have been deleted") - } + ui, cmd := testRekeyCommand(t) + cmd.client = client - // Sort, because it'll be tested with DeepEqual later - for k, _ := range backupVals.Keys { - sort.Strings(backupVals.Keys[k]) - sort.Strings(backupVals.KeysB64[k]) - } + code := cmd.Run([]string{ + "-init", + "-key-shares", "1", + "-key-threshold", "1", + "-pgp-keys", pgpKey, + "-backup", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } - parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), token, true, backupVals.Keys, backupVals.KeysB64, core) + // Get the status for the nonce + status, err := client.Sys().RekeyStatus() + if err != nil { + t.Fatal(err) + } + nonce := status.Nonce + + var combined string + // Supply the unseal keys + for _, key := range keys { + ui, cmd := testRekeyCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-nonce", nonce, + key, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + + // Append to our output string + combined += ui.OutputWriter.String() + } + + re := regexp.MustCompile(`Key 1 fingerprint: (.+); value: (.+)`) + match := re.FindAllStringSubmatch(combined, -1) + if len(match) < 1 || len(match[0]) < 3 { + t.Fatalf("bad match: %#v", match) + } + + // Grab the output fingerprint and encrypted key + fingerprint, encryptedKey := match[0][1], match[0][2] + + // Get the backup + ui, cmd = testRekeyCommand(t) + cmd.client = client + + code = cmd.Run([]string{ + "-backup-retrieve", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + if !strings.Contains(output, fingerprint) { + t.Errorf("expected %q to contain %q", output, fingerprint) + } + if !strings.Contains(output, encryptedKey) { + t.Errorf("expected %q to contain %q", output, encryptedKey) + } + + // Delete the backup + ui, cmd = testRekeyCommand(t) + cmd.client = client + + code = cmd.Run([]string{ + "-backup-delete", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + + secret, err := client.Sys().RekeyRetrieveBackup() + if err == nil { + t.Errorf("expected error: %#v", secret) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testRekeyCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/foo", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error getting rekey status: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testRekeyCommand(t) + assertNoTabs(t, cmd) + }) } From 02dd8b975e0bfdb48669e371a82974bad44d0396 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:03:50 -0400 Subject: [PATCH 065/281] Update remount command --- command/remount.go | 82 +++++++++++++++------ command/remount_test.go | 153 +++++++++++++++++++++++++++++++--------- 2 files changed, 180 insertions(+), 55 deletions(-) diff --git a/command/remount.go b/command/remount.go index a36f1410ad..999e5ac3a3 100644 --- a/command/remount.go +++ b/command/remount.go @@ -4,49 +4,91 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/hashicorp/vault-enterprise/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*RemountCommand)(nil) +var _ cli.CommandAutocomplete = (*RemountCommand)(nil) + // RemountCommand is a Command that remounts a mounted secret backend // to a new endpoint. type RemountCommand struct { - meta.Meta + *BaseCommand +} + +func (c *RemountCommand) Synopsis() string { + return "Remounts a secret backend to a new path" +} + +func (c *RemountCommand) Help() string { + helpText := ` +Usage: vault remount [options] SOURCE DESTINATION + + Remounts an existing secret backend to a new path. Any leases from the old + backend are revoked, but the data associated with the backend (such as + configuration), is preserved. + + Move the existing mount at secret/ to generic/: + + $ vault remount secret/ generic/ + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *RemountCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *RemountCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultMounts() +} + +func (c *RemountCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *RemountCommand) Run(args []string) int { - flags := c.Meta.FlagSet("remount", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) != 2 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\nremount expects two arguments: the from and to path")) + args = f.Args() + switch len(args) { + case 0, 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 2, got %d)", len(args))) + return 1 + case 2: + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 2, got %d)", len(args))) return 1 } - from := args[0] - to := args[1] + // Grab the source and destination + source := ensureTrailingSlash(args[0]) + destination := ensureTrailingSlash(args[1]) client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } - if err := client.Sys().Remount(from, to); err != nil { - c.Ui.Error(fmt.Sprintf( - "Unmount error: %s", err)) + if err := client.Sys().Remount(source, destination); err != nil { + c.UI.Error(fmt.Sprintf("Error remounting %s to %s: %s", source, destination, err)) return 2 } - c.Ui.Output(fmt.Sprintf( - "Successfully remounted from '%s' to '%s'!", from, to)) - + c.UI.Output(fmt.Sprintf("Success! Remounted %s to: %s", source, destination)) return 0 } diff --git a/command/remount_test.go b/command/remount_test.go index 7ec1321432..6a6815e520 100644 --- a/command/remount_test.go +++ b/command/remount_test.go @@ -1,52 +1,135 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestRemount(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testRemountCommand(tb testing.TB) (*cli.MockUi, *RemountCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &RemountCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &RemountCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestRemountCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + nil, + "Not enough arguments", + 1, + }, + { + "too_many_args", + []string{"foo", "bar", "baz"}, + "Too many arguments", + 1, + }, + { + "non_existent", + []string{"not_real", "over_here"}, + "Error remounting not_real/ to over_here/", + 2, }, } - args := []string{ - "-address", addr, - "secret/", "kv", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + for _, tc := range cases { + tc := tc - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - _, ok := mounts["secret/"] - if ok { - t.Fatal("should not have mount") - } + ui, cmd := testRemountCommand(t) - _, ok = mounts["kv/"] - if !ok { - t.Fatal("should have kv") - } + 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("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testRemountCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/", "generic/", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Remounted secret/ to: generic/" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + mounts, err := client.Sys().ListMounts() + if err != nil { + t.Fatal(err) + } + + if _, ok := mounts["generic/"]; !ok { + t.Errorf("expected mount at generic/: %#v", mounts) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testRemountCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/", "generic/", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error remounting secret/ to generic/: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testRemountCommand(t) + assertNoTabs(t, cmd) + }) } From c6380da6cea223aa4437c9cc157a93db586ce9b9 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:03:58 -0400 Subject: [PATCH 066/281] Update renew command --- command/renew.go | 169 ++++++++++++++++----------- command/renew_test.go | 263 +++++++++++++++++++++++------------------- 2 files changed, 249 insertions(+), 183 deletions(-) diff --git a/command/renew.go b/command/renew.go index 6a3eafe52a..c2130d122a 100644 --- a/command/renew.go +++ b/command/renew.go @@ -2,89 +2,128 @@ package command import ( "fmt" - "strconv" "strings" + "time" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*RenewCommand)(nil) +var _ cli.CommandAutocomplete = (*RenewCommand)(nil) + // RenewCommand is a Command that mounts a new mount. type RenewCommand struct { - meta.Meta -} + *BaseCommand -func (c *RenewCommand) Run(args []string) int { - var format string - flags := c.Meta.FlagSet("renew", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - 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) >= 3 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\nrenew expects at least one argument: the lease ID to renew")) - return 1 - } - - var increment int - leaseId := args[0] - if len(args) > 1 { - parsed, err := strconv.ParseInt(args[1], 10, 0) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Invalid increment, must be an int: %s", err)) - return 1 - } - - increment = int(parsed) - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - secret, err := client.Sys().Renew(leaseId, increment) - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Renew error: %s", err)) - return 1 - } - - return OutputSecret(c.Ui, format, secret) + flagIncrement time.Duration } func (c *RenewCommand) Synopsis() string { - return "Renew the lease of a secret" + return "Renews the lease of a secret" } func (c *RenewCommand) Help() string { helpText := ` -Usage: vault renew [options] id [increment] +Usage: vault renew [options] ID - Renew the lease on a secret, extending the time that it can be used - before it is revoked by Vault. + Renews the lease on a secret, extending the time that it can be used before + it is revoked by Vault. - Every secret in Vault has a lease associated with it. If the user of - the secret wants to use it longer than the lease, then it must be - renewed. Renewing the lease will not change the contents of the secret. + Every secret in Vault has a lease associated with it. If the owner of the + secret wants to use it longer than the lease, then it must be renewed. + Renewing the lease does not change the contents of the secret. The ID is the + full path lease ID. - To renew a secret, run this command with the lease ID returned when it - was read. Optionally, request a specific increment in seconds. Vault - is not required to honor this request. + Renew a secret: -General Options: -` + meta.GeneralOptionsUsage() + ` -Renew Options: + $ vault renew database/creds/readonly/2f6a614c-4aa2-7b19-24b9-ad944a8d4de6 + + Lease renewal will fail if the secret is not renewable, the secret has already + been revoked, or if the secret has already reached its maximum TTL. + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. -` return strings.TrimSpace(helpText) } + +func (c *RenewCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.DurationVar(&DurationVar{ + Name: "increment", + Target: &c.flagIncrement, + Default: 0, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Request a specific increment in seconds. Vault is not required " + + "to honor this request.", + }) + + return set +} + +func (c *RenewCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *RenewCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *RenewCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + leaseID := "" + increment := c.flagIncrement + + args = f.Args() + switch len(args) { + case 0: + c.UI.Error("Missing ID!") + return 1 + case 1: + leaseID = strings.TrimSpace(args[0]) + case 2: + // Deprecation + // TODO: remove in 0.9.0 + c.UI.Warn(wrapAtLength( + "WARNING! Specifying INCREMENT as a second argument is deprecated. " + + "Please use -increment instead. This will be removed in the next " + + "major release of Vault.")) + + leaseID = strings.TrimSpace(args[0]) + parsed, err := time.ParseDuration(appendDurationSuffix(args[1])) + if err != nil { + c.UI.Error(fmt.Sprintf("Invalid increment: %s", err)) + return 1 + } + increment = parsed + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1-2, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + secret, err := client.Sys().Renew(leaseID, truncateToSeconds(increment)) + if err != nil { + c.UI.Error(fmt.Sprintf("Error renewing %s: %s", leaseID, err)) + return 2 + } + + return OutputSecret(c.UI, c.flagFormat, secret) +} diff --git a/command/renew_test.go b/command/renew_test.go index 2191662220..5861cc16f0 100644 --- a/command/renew_test.go +++ b/command/renew_test.go @@ -1,143 +1,170 @@ package command import ( + "strings" "testing" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestRenew(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testRenewCommand(tb testing.TB) (*cli.MockUi, *RenewCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &RenewCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &RenewCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +// testRenewCommandMountAndLease mounts a leased secret backend and returns +// the leaseID of an item. +func testRenewCommandMountAndLease(tb testing.TB, client *api.Client) string { + if err := client.Sys().Mount("testing", &api.MountInput{ + Type: "generic-leased", + }); err != nil { + tb.Fatal(err) + } + + if _, err := client.Logical().Write("testing/foo", map[string]interface{}{ + "key": "value", + "lease": "5m", + }); err != nil { + tb.Fatal(err) + } + + // Read the secret back to get the leaseID + secret, err := client.Logical().Read("testing/foo") + if err != nil { + tb.Fatal(err) + } + if secret == nil || secret.LeaseID == "" { + tb.Fatalf("missing secret or lease: %#v", secret) + } + + return secret.LeaseID +} + +func TestRenewCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing ID!", + 1, + }, + { + "increment", + []string{"-increment", "60s"}, + "foo", + 0, + }, + { + "increment_no_suffix", + []string{"-increment", "60"}, + "foo", + 0, + }, + { + "format", + []string{"-format", "json"}, + "{", + 0, + }, + { + "format_bad", + []string{"-format", "nope-not-real"}, + "Invalid output format", + 1, }, } - // write a secret with a lease - client := testClient(t, addr, token) - _, err := client.Logical().Write("secret/foo", map[string]interface{}{ - "key": "value", - "lease": "1m", + t.Run("group", 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() + + leaseID := testRenewCommandMountAndLease(t, client) + + ui, cmd := testRenewCommand(t) + cmd.client = client + + if tc.args != nil { + tc.args = append(tc.args, leaseID) + } + 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) + } + }) + } }) - if err != nil { - t.Fatalf("err: %s", err) - } - // read the secret to get its lease ID - secret, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run("integration", func(t *testing.T) { + t.Parallel() - args := []string{ - "-address", addr, - secret.LeaseID, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} + client, closer := testVaultServer(t) + defer closer() -func TestRenewBothWays(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() + leaseID := testRenewCommandMountAndLease(t, client) - // write a secret with a lease - client := testClient(t, addr, token) - _, err := client.Logical().Write("secret/foo", map[string]interface{}{ - "key": "value", - "ttl": "1m", + _, cmd := testRenewCommand(t) + cmd.client = client + + code := cmd.Run([]string{leaseID}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } }) - if err != nil { - t.Fatalf("err: %s", err) - } - // read the secret to get its lease ID - secret, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() - // Test one renew path - r := client.NewRequest("PUT", "/v1/sys/renew") - body := map[string]interface{}{ - "lease_id": secret.LeaseID, - } - if err := r.SetJSONBody(body); err != nil { - t.Fatal(err) - } - resp, err := client.RawRequest(r) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - secret, err = api.ParseSecret(resp.Body) - if err != nil { - t.Fatal(err) - } - if secret.LeaseDuration != 60 { - t.Fatal("bad lease duration") - } + client, closer := testVaultServerBad(t) + defer closer() - // Test another - r = client.NewRequest("PUT", "/v1/sys/leases/renew") - body = map[string]interface{}{ - "lease_id": secret.LeaseID, - } - if err := r.SetJSONBody(body); err != nil { - t.Fatal(err) - } - resp, err = client.RawRequest(r) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - secret, err = api.ParseSecret(resp.Body) - if err != nil { - t.Fatal(err) - } - if secret.LeaseDuration != 60 { - t.Fatal("bad lease duration") - } + ui, cmd := testRenewCommand(t) + cmd.client = client - // Test the other - r = client.NewRequest("PUT", "/v1/sys/renew/"+secret.LeaseID) - resp, err = client.RawRequest(r) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - secret, err = api.ParseSecret(resp.Body) - if err != nil { - t.Fatal(err) - } - if secret.LeaseDuration != 60 { - t.Fatalf("bad lease duration; secret is %#v\n", *secret) - } + code := cmd.Run([]string{ + "foo/bar", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } - // Test another - r = client.NewRequest("PUT", "/v1/sys/leases/renew/"+secret.LeaseID) - resp, err = client.RawRequest(r) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - secret, err = api.ParseSecret(resp.Body) - if err != nil { - t.Fatal(err) - } - if secret.LeaseDuration != 60 { - t.Fatalf("bad lease duration; secret is %#v\n", *secret) - } + expected := "Error renewing foo/bar: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testRenewCommand(t) + assertNoTabs(t, cmd) + }) } From 8df5905c342ccdbdc518f59922458a385295b8d7 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:04:05 -0400 Subject: [PATCH 067/281] Update revoke command --- command/revoke.go | 185 ++++++++++++++++++++++++++--------------- command/revoke_test.go | 148 ++++++++++++++++++++++++++------- 2 files changed, 235 insertions(+), 98 deletions(-) diff --git a/command/revoke.go b/command/revoke.go index 50933ada42..8b4ba6118c 100644 --- a/command/revoke.go +++ b/command/revoke.go @@ -4,92 +4,141 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*ReadCommand)(nil) +var _ cli.CommandAutocomplete = (*ReadCommand)(nil) + // RevokeCommand is a Command that mounts a new mount. type RevokeCommand struct { - meta.Meta -} + *BaseCommand -func (c *RevokeCommand) Run(args []string) int { - var prefix, force bool - flags := c.Meta.FlagSet("revoke", meta.FlagSetDefault) - flags.BoolVar(&prefix, "prefix", false, "") - flags.BoolVar(&force, "force", 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 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\nrevoke expects one argument: the ID to revoke")) - return 1 - } - leaseId := args[0] - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - switch { - case force && !prefix: - c.Ui.Error(fmt.Sprintf( - "-force requires -prefix")) - return 1 - case force && prefix: - err = client.Sys().RevokeForce(leaseId) - case prefix: - err = client.Sys().RevokePrefix(leaseId) - default: - err = client.Sys().Revoke(leaseId) - } - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Revoke error: %s", err)) - return 1 - } - - c.Ui.Output(fmt.Sprintf("Success! Revoked the secret with ID '%s', if it existed.", leaseId)) - return 0 + flagForce bool + flagPrefix bool } func (c *RevokeCommand) Synopsis() string { - return "Revoke a secret." + return "Revokes leases and secrets" } func (c *RevokeCommand) Help() string { helpText := ` -Usage: vault revoke [options] id +Usage: vault revoke [options] ID - Revoke a secret by its lease ID. + Revokes secrets by their lease ID. This command can revoke a single secret + or multiple secrets based on a path-matched prefix. - This command revokes a secret by its lease ID that was returned with it. Once - the key is revoked, it is no longer valid. + Revoke a single lease: - With the -prefix flag, the revoke is done by prefix: any secret prefixed with - the given partial ID is revoked. Lease IDs are structured in such a way to - make revocation of prefixes useful. + $ vault revoke database/creds/readonly/2f6a614c... - With the -force flag, the lease is removed from Vault even if the revocation - fails. This is meant for certain recovery scenarios and should not be used - lightly. This option requires -prefix. + Revoke all leases for a role: -General Options: -` + meta.GeneralOptionsUsage() + ` -Revoke Options: + $ vault revoke -prefix aws/creds/deploy - -prefix=true Revoke all secrets with the matching prefix. This - defaults to false: an exact revocation. + Force delete leases from Vault even if backend revocation fails: + + $ vault revoke -force -prefix consul/creds + + For a full list of examples and paths, please see the documentation that + corresponds to the secret backend in use. + +` + c.Flags().Help() - -force=true Delete the lease even if the actual revocation - operation fails. -` return strings.TrimSpace(helpText) } + +func (c *RevokeCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "force", + Aliases: []string{"f"}, + Target: &c.flagForce, + Default: false, + Usage: "Delete the lease from Vault even if the backend revocation " + + "fails. This is meant for recovery situations where the secret " + + "in the backend was manually removed. If this flag is specified, " + + "-prefix is also required.", + }) + + f.BoolVar(&BoolVar{ + Name: "prefix", + Target: &c.flagPrefix, + Default: false, + Usage: "Treat the ID as a prefix instead of an exact lease ID. This can " + + "revoke multiple leases simultaneously.", + }) + + return set +} + +func (c *RevokeCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *RevokeCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *RevokeCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + leaseID, remaining, err := extractID(args) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if len(remaining) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + if c.flagForce && !c.flagPrefix { + c.UI.Error("Specifying -force requires also specifying -prefix") + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + switch { + case c.flagForce && c.flagPrefix: + c.UI.Warn(wrapAtLength("Warning! Force-removing leases can cause Vault " + + "to become out of sync with credential backends!")) + if err := client.Sys().RevokeForce(leaseID); err != nil { + c.UI.Error(fmt.Sprintf("Error force revoking leases with prefix %s: %s", leaseID, err)) + return 2 + } + c.UI.Output(fmt.Sprintf("Success! Force revoked any leases with prefix: %s", leaseID)) + return 0 + case c.flagPrefix: + if err := client.Sys().RevokePrefix(leaseID); err != nil { + c.UI.Error(fmt.Sprintf("Error revoking leases with prefix %s: %s", leaseID, err)) + return 2 + } + c.UI.Output(fmt.Sprintf("Success! Revoked any leases with prefix: %s", leaseID)) + return 0 + default: + if err := client.Sys().Revoke(leaseID); err != nil { + c.UI.Error(fmt.Sprintf("Error revoking lease %s: %s", leaseID, err)) + return 2 + } + c.UI.Output(fmt.Sprintf("Success! Revoked lease: %s", leaseID)) + return 0 + } +} diff --git a/command/revoke_test.go b/command/revoke_test.go index cb9febf6d0..1b2b94fd6e 100644 --- a/command/revoke_test.go +++ b/command/revoke_test.go @@ -1,46 +1,134 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" ) -func TestRevoke(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testRevokeCommand(tb testing.TB) (*cli.MockUi, *RevokeCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &RevokeCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &RevokeCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestRevokeCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "force_without_prefix", + []string{"-force"}, + "requires also specifying -prefix", + 1, + }, + { + "single", + nil, + "Success", + 0, + }, + { + "force_prefix", + []string{"-force", "-prefix"}, + "Success", + 0, + }, + { + "prefix", + []string{"-prefix"}, + "Success", + 0, }, } - client := testClient(t, addr, token) - _, err := client.Logical().Write("secret/foo", map[string]interface{}{ - "key": "value", - "lease": "1m", + 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("secret-leased", &api.MountInput{ + Type: "generic-leased", + }); err != nil { + t.Fatal(err) + } + + path := "secret-leased/revoke/" + tc.name + data := map[string]interface{}{ + "key": "value", + "lease": "1m", + } + if _, err := client.Logical().Write(path, data); err != nil { + t.Fatal(err) + } + secret, err := client.Logical().Read(path) + if err != nil { + t.Fatal(err) + } + + ui, cmd := testRevokeCommand(t) + cmd.client = client + + tc.args = append(tc.args, secret.LeaseID) + 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) + } + }) + } }) - if err != nil { - t.Fatalf("err: %s", err) - } - secret, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() - args := []string{ - "-address", addr, - secret.LeaseID, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testRevokeCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "foo/bar", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error revoking lease foo/bar: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testRevokeCommand(t) + assertNoTabs(t, cmd) + }) } From 0380caedd9ec93d171a4d0149c7e60a728619a02 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:04:12 -0400 Subject: [PATCH 068/281] Update rotate command --- command/rotate.go | 121 +++++++++++++++++++++++++--------------- command/rotate_test.go | 123 +++++++++++++++++++++++++++++++++++------ 2 files changed, 181 insertions(+), 63 deletions(-) diff --git a/command/rotate.go b/command/rotate.go index 9da387370b..eed0bc40e8 100644 --- a/command/rotate.go +++ b/command/rotate.go @@ -4,64 +4,95 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*RotateCommand)(nil) +var _ cli.CommandAutocomplete = (*RotateCommand)(nil) + // RotateCommand is a Command that rotates the encryption key being used type RotateCommand struct { - meta.Meta -} - -func (c *RotateCommand) Run(args []string) int { - flags := c.Meta.FlagSet("rotate", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - // Rotate the key - err = client.Sys().Rotate() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error with key rotation: %s", err)) - return 2 - } - - // Print the key status - status, err := client.Sys().KeyStatus() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error reading audits: %s", err)) - return 2 - } - - c.Ui.Output(fmt.Sprintf("Key Term: %d", status.Term)) - c.Ui.Output(fmt.Sprintf("Installation Time: %v", status.InstallTime)) - return 0 + *BaseCommand } func (c *RotateCommand) Synopsis() string { - return "Rotates the backend encryption key used to persist data" + return "Rotates the underlying encryption key" } func (c *RotateCommand) Help() string { helpText := ` Usage: vault rotate [options] - Rotates the backend encryption key which is used to secure data - written to the storage backend. This is done by installing a new key - which encrypts new data, while old keys are still used to decrypt - secrets written previously. This is an online operation and is not - disruptive. + Rotates the underlying encryption key which is used to secure data written + to the storage backend. This installs a new key in the key ring. This new + key is used to encrypted new data, while older keys in the ring are used to + decrypt older data. + + This is an online operation and does not cause downtime. This command is run + per-cluser (not per-server), since Vault servers in HA mode share the same + storeage backend. + + Rotate Vault's encryption key: + + $ vault rotate + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *RotateCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *RotateCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *RotateCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *RotateCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Rotate the key + err = client.Sys().Rotate() + if err != nil { + c.UI.Error(fmt.Sprintf("Error rotating key: %s", err)) + return 2 + } + + // Print the key status + status, err := client.Sys().KeyStatus() + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading key status: %s", err)) + return 2 + } + + c.UI.Output("Success! Rotated key") + c.UI.Output("") + c.UI.Output(printKeyStatus(status)) + return 0 +} diff --git a/command/rotate_test.go b/command/rotate_test.go index 257f280071..30790691cb 100644 --- a/command/rotate_test.go +++ b/command/rotate_test.go @@ -1,31 +1,118 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestRotate(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testRotateCommand(tb testing.TB) (*cli.MockUi, *RotateCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &RotateCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &RotateCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestRotateCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"abcd1234"}, + "Too many arguments", + 1, }, } - args := []string{ - "-address", addr, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + 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() + + ui, cmd := testRotateCommand(t) + + 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("default", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testRotateCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Rotated key" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + status, err := client.Sys().KeyStatus() + if err != nil { + t.Fatal(err) + } + if exp := 1; status.Term < exp { + t.Errorf("expected %d to be less than %d", status.Term, exp) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testRotateCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error rotating key: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testRotateCommand(t) + assertNoTabs(t, cmd) + }) } From f5b791108bcf63fb215db319102a0cb36992dff4 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:04:19 -0400 Subject: [PATCH 069/281] Update seal command --- command/seal.go | 100 +++++++++++++++++++++------------ command/seal_test.go | 131 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 171 insertions(+), 60 deletions(-) diff --git a/command/seal.go b/command/seal.go index 033c164587..97f102a58e 100644 --- a/command/seal.go +++ b/command/seal.go @@ -4,35 +4,17 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*SealCommand)(nil) +var _ cli.CommandAutocomplete = (*SealCommand)(nil) + // SealCommand is a Command that seals the vault. type SealCommand struct { - meta.Meta -} - -func (c *SealCommand) Run(args []string) int { - flags := c.Meta.FlagSet("seal", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if err := client.Sys().Seal(); err != nil { - c.Ui.Error(fmt.Sprintf("Error sealing: %s", err)) - return 1 - } - - c.Ui.Output("Vault is now sealed.") - return 0 + *BaseCommand } func (c *SealCommand) Synopsis() string { @@ -43,21 +25,65 @@ func (c *SealCommand) Help() string { helpText := ` Usage: vault seal [options] - Seal the vault. + Seals the Vault server. Sealing tells the Vault server to stop responding + to any operations until it is unsealed. When sealed, the Vault server + discards its in-memory master key to unlock the data, so it is physically + blocked from responding to operations unsealed. - Sealing a vault tells the Vault server to stop responding to any - access operations until it is unsealed again. A sealed vault throws away - its master key to unlock the data, so it is physically blocked from - responding to operations again until the vault is unsealed with - the "unseal" command or via the API. + If an unseal is in progress, sealing the Vault will reset the unsealing + process. Users will have to re-enter their portions of the master key again. - This command is idempotent, if the vault is already sealed it does nothing. + This command does nothing if the Vault server is already sealed. - If an unseal has started, sealing the vault will reset the unsealing - process. You'll have to re-enter every portion of the master key again. - This is the same as running "vault unseal -reset". + Seal the Vault server: + + $ vault seal + + For a full list of examples and why you might want to seal the Vault, please + see the documentation. + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *SealCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *SealCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *SealCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *SealCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if err := client.Sys().Seal(); err != nil { + c.UI.Error(fmt.Sprintf("Error sealing: %s", err)) + return 2 + } + + c.UI.Output("Success! Vault is sealed.") + return 0 +} diff --git a/command/seal_test.go b/command/seal_test.go index c224aee31e..a0cf373325 100644 --- a/command/seal_test.go +++ b/command/seal_test.go @@ -1,37 +1,122 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func Test_Seal(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testSealCommand(tb testing.TB) (*cli.MockUi, *SealCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &SealCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &SealCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestSealCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "args", + []string{"foo"}, + "Too many arguments", + 1, }, } - args := []string{"-address", addr} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - sealed, err := core.Sealed() - if err != nil { - t.Fatalf("err: %s", err) - } - if !sealed { - t.Fatal("should be sealed") - } + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testSealCommand(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("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testSealCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Vault is sealed." + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + sealStatus, err := client.Sys().SealStatus() + if err != nil { + t.Fatal(err) + } + if !sealStatus.Sealed { + t.Errorf("expected to be sealed") + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testSealCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error sealing: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testSealCommand(t) + assertNoTabs(t, cmd) + }) } From 9eb5978d1df50ae416be0ccf79e87d189907b244 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:04:32 -0400 Subject: [PATCH 070/281] Update ssh command --- command/ssh.go | 670 ++++++++++++++++++++++++-------------------- command/ssh_test.go | 196 +------------ 2 files changed, 381 insertions(+), 485 deletions(-) diff --git a/command/ssh.go b/command/ssh.go index 03e1933da6..675be788ff 100644 --- a/command/ssh.go +++ b/command/ssh.go @@ -9,43 +9,200 @@ import ( "os/exec" "os/user" "strings" + "syscall" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/builtin/logical/ssh" - "github.com/hashicorp/vault/meta" - homedir "github.com/mitchellh/go-homedir" + "github.com/mitchellh/cli" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*SSHCommand)(nil) +var _ cli.CommandAutocomplete = (*SSHCommand)(nil) + // SSHCommand is a Command that establishes a SSH connection with target by // generating a dynamic key type SSHCommand struct { - meta.Meta + *BaseCommand - // API - client *api.Client - sshClient *api.SSH + // Common SSH options + flagMode string + flagRole string + flagNoExec bool + flagMountPoint string + flagStrictHostKeyChecking string + flagUserKnownHostsFile string - // Common options - mode string - noExec bool - format string - mountPoint string - role string - username string - ip string - sshArgs []string + // SSH CA Mode options + flagPublicKeyPath string + flagPrivateKeyPath string + flagHostKeyMountPoint string + flagHostKeyHostnames string +} - // Key options - strictHostKeyChecking string - userKnownHostsFile string +func (c *SSHCommand) Synopsis() string { + return "Initiate an SSH session" +} - // SSH CA backend specific options - publicKeyPath string - privateKeyPath string - hostKeyMountPoint string - hostKeyHostnames string +func (c *SSHCommand) Help() string { + helpText := ` +Usage: vault ssh [options] username@ip [ssh options] + + Establishes an SSH connection with the target machine. + + This command uses one of the SSH authentication backends to authenticate and + automatically establish an SSH connection to a host. This operation requires + that the SSH backend is mounted and configured. + + SSH using the OTP mode (requires sshpass for full automation): + + $ vault ssh -mode=otp -role=my-role user@1.2.3.4 + + SSH using the CA mode: + + $ vault ssh -mode=ca -role=my-role user@1.2.3.4 + + SSH using CA mode with host key verification: + + $ vault ssh \ + -mode=ca \ + -role=my-role \ + -host-key-mount-point=host-signer \ + -host-key-hostnames=example.com \ + user@example.com + + For the full list of options and arguments, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *SSHCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + f := set.NewFlagSet("SSH Options") + + // TODO: doc field? + + // General + f.StringVar(&StringVar{ + Name: "mode", + Target: &c.flagMode, + Default: "", + EnvVar: "", + Completion: complete.PredictSet("ca", "dynamic", "otp"), + Usage: "Name of the role to use to generate the key.", + }) + + f.StringVar(&StringVar{ + Name: "role", + Target: &c.flagRole, + Default: "", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Name of the role to use to generate the key.", + }) + + f.BoolVar(&BoolVar{ + Name: "no-exec", + Target: &c.flagNoExec, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Print the generated credentials, but do not establish a " + + "connection.", + }) + + f.StringVar(&StringVar{ + Name: "mount-point", + Target: &c.flagMountPoint, + Default: "ssh/", + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Mount point to the SSH backend.", + }) + + f.StringVar(&StringVar{ + Name: "strict-host-key-checking", + Target: &c.flagStrictHostKeyChecking, + Default: "ask", + EnvVar: "VAULT_SSH_STRICT_HOST_KEY_CHECKING", + Completion: complete.PredictSet("ask", "no", "yes"), + Usage: "Value to use for the SSH configuration option " + + "\"StrictHostKeyChecking\".", + }) + + f.StringVar(&StringVar{ + Name: "user-known-hosts-file", + Target: &c.flagUserKnownHostsFile, + Default: "~/.ssh/known_hosts", + EnvVar: "VAULT_SSH_USER_KNOWN_HOSTS_FILE", + Completion: complete.PredictFiles("*"), + Usage: "Value to use for the SSH configuration option " + + "\"UserKnownHostsFile\".", + }) + + // SSH CA + f = set.NewFlagSet("CA Mode Options") + + f.StringVar(&StringVar{ + Name: "public-key-path", + Target: &c.flagPublicKeyPath, + Default: "~/.ssh/id_rsa.pub", + EnvVar: "g", + Completion: complete.PredictFiles("*"), + Usage: "Path to the SSH public key to send to Vault for signing.", + }) + + f.StringVar(&StringVar{ + Name: "private-key-path", + Target: &c.flagPrivateKeyPath, + Default: "~/.ssh/id_rsa", + EnvVar: "", + Completion: complete.PredictFiles("*"), + Usage: "Path to the SSH private key to use for authentication. This must " + + "be the corresponding private key to -public-key-path.", + }) + + f.StringVar(&StringVar{ + Name: "host-key-mount-point", + Target: &c.flagHostKeyMountPoint, + Default: "~/.ssh/id_rsa", + EnvVar: "VAULT_SSH_HOST_KEY_MOUNT_POINT", + Completion: complete.PredictAnything, + Usage: "Mount point to the SSH backend where host keys are signed. " + + "When given a value, Vault will generate a custom \"known_hosts\" file " + + "with delegation to the CA at the provided mount point to verify the " + + "SSH connection's host keys against the provided CA. By default, host " + + "keys are validated against the user's local \"known_hosts\" file. " + + "This flag forces strict key host checking and ignores a custom user " + + "known hosts file.", + }) + + f.StringVar(&StringVar{ + Name: "host-key-hostnames", + Target: &c.flagHostKeyHostnames, + Default: "*", + EnvVar: "VAULT_SSH_HOST_KEY_HOSTNAMES", + Completion: complete.PredictAnything, + Usage: "List of hostnames to delegate for the CA. The default value " + + "allows all domains and IPs. This is specified as a comma-separated " + + "list of values.", + }) + + return set +} + +func (c *SSHCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *SSHCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } // Structure to hold the fields returned when asked for a credential from SSHh backend. @@ -58,74 +215,35 @@ type SSHCredentialResp struct { } func (c *SSHCommand) Run(args []string) int { + f := c.Flags() - flags := c.Meta.FlagSet("ssh", meta.FlagSetDefault) - - envOrDefault := func(key string, def string) string { - if k := os.Getenv(key); k != "" { - return k - } - return def - } - - expandPath := func(p string) string { - e, err := homedir.Expand(p) - if err != nil { - return p - } - return e - } - - // Common options - flags.StringVar(&c.mode, "mode", "", "") - flags.BoolVar(&c.noExec, "no-exec", false, "") - flags.StringVar(&c.format, "format", "table", "") - flags.StringVar(&c.mountPoint, "mount-point", "ssh", "") - flags.StringVar(&c.role, "role", "", "") - - // Key options - flags.StringVar(&c.strictHostKeyChecking, "strict-host-key-checking", - envOrDefault("VAULT_SSH_STRICT_HOST_KEY_CHECKING", "ask"), "") - flags.StringVar(&c.userKnownHostsFile, "user-known-hosts-file", - envOrDefault("VAULT_SSH_USER_KNOWN_HOSTS_FILE", expandPath("~/.ssh/known_hosts")), "") - - // CA-specific options - flags.StringVar(&c.publicKeyPath, "public-key-path", - expandPath("~/.ssh/id_rsa.pub"), "") - flags.StringVar(&c.privateKeyPath, "private-key-path", - expandPath("~/.ssh/id_rsa"), "") - flags.StringVar(&c.hostKeyMountPoint, "host-key-mount-point", "", "") - flags.StringVar(&c.hostKeyHostnames, "host-key-hostnames", "*", "") - - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() + // Use homedir to expand any relative paths such as ~/.ssh + c.flagUserKnownHostsFile = expandPath(c.flagUserKnownHostsFile) + c.flagPublicKeyPath = expandPath(c.flagPublicKeyPath) + c.flagPrivateKeyPath = expandPath(c.flagPrivateKeyPath) + + args = f.Args() if len(args) < 1 { - c.Ui.Error("ssh expects at least one argument") + c.UI.Error(fmt.Sprintf("Not enough arguments, (expected 1-n, got %d)", len(args))) return 1 } - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing client: %v", err)) - return 1 - } - c.client = client - c.sshClient = client.SSHWithMountPoint(c.mountPoint) - // Extract the username and IP. - c.username, c.ip, err = c.userAndIP(args[0]) + username, ip, err := c.userAndIP(args[0]) if err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing user and IP: %s", err)) + c.UI.Error(fmt.Sprintf("Error parsing user and IP: %s", err)) return 1 } // The rest of the args are ssh args + sshArgs := []string{} if len(args) > 1 { - c.sshArgs = args[1:] + sshArgs = args[1:] } // Credentials are generated only against a registered role. If user @@ -134,100 +252,101 @@ func (c *SSHCommand) Run(args []string) int { // only one role associated with it, use it to establish the connection. // // TODO: remove in 0.9.0, convert to validation error - if c.role == "" { - c.Ui.Warn("" + - "WARNING: No -role specified. Use -role to tell Vault which ssh role\n" + - "to use for authentication. In the future, you will need to tell Vault\n" + - "which role to use. For now, Vault will attempt to guess based on a\n" + - "the API response.") + if c.flagRole == "" { + c.UI.Warn(wrapAtLength( + "WARNING: No -role specified. Use -role to tell Vault which ssh role " + + "to use for authentication. In the future, you will need to tell " + + "Vault which role to use. For now, Vault will attempt to guess based " + + "on a the API response. This will be removed in the next major " + + "version of Vault.")) - role, err := c.defaultRole(c.mountPoint, c.ip) + role, err := c.defaultRole(c.flagMountPoint, ip) if err != nil { - c.Ui.Error(fmt.Sprintf("Error choosing role: %v", err)) + c.UI.Error(fmt.Sprintf("Error choosing role: %v", err)) return 1 } // Print the default role chosen so that user knows the role name // if something doesn't work. If the role chosen is not allowed to // be used by the user (ACL enforcement), then user should see an // error message accordingly. - c.Ui.Output(fmt.Sprintf("Vault SSH: Role: %q", role)) - c.role = role + c.UI.Output(fmt.Sprintf("Vault SSH: Role: %q", role)) + c.flagRole = role } // If no mode was given, perform the old-school lookup. Keep this now for // backwards-compatability, but print a warning. // // TODO: remove in 0.9.0, convert to validation error - if c.mode == "" { - c.Ui.Warn("" + - "WARNING: No -mode specified. Use -mode to tell Vault which ssh\n" + - "authentication mode to use. In the future, you will need to tell\n" + - "Vault which mode to use. For now, Vault will attempt to guess based\n" + - "on the API response. This guess involves creating a temporary\n" + - "credential, reading its type, and then revoking it. To reduce the\n" + - "number of API calls and surface area, specify -mode directly.") - secret, cred, err := c.generateCredential() + if c.flagMode == "" { + c.UI.Warn(wrapAtLength( + "WARNING: No -mode specified. Use -mode to tell Vault which ssh " + + "authentication mode to use. In the future, you will need to tell " + + "Vault which mode to use. For now, Vault will attempt to guess based " + + "on the API response. This guess involves creating a temporary " + + "credential, reading its type, and then revoking it. To reduce the " + + "number of API calls and surface area, specify -mode directly. This " + + "will be removed in the next major version of Vault.")) + secret, cred, err := c.generateCredential(username, ip) if err != nil { // This is _very_ hacky, but is the only sane backwards-compatible way // to do this. If the error is "key type unknown", we just assume the // type is "ca". In the future, mode will be required as an option. if strings.Contains(err.Error(), "key type unknown") { - c.mode = ssh.KeyTypeCA + c.flagMode = ssh.KeyTypeCA } else { - c.Ui.Error(fmt.Sprintf("Error getting credential: %s", err)) + c.UI.Error(fmt.Sprintf("Error getting credential: %s", err)) return 1 } } else { - c.mode = cred.KeyType + c.flagMode = cred.KeyType } // Revoke the secret, since the child functions will generate their own // credential. Users wishing to avoid this should specify -mode. if secret != nil { if err := c.client.Sys().Revoke(secret.LeaseID); err != nil { - c.Ui.Warn(fmt.Sprintf("Failed to revoke temporary key: %s", err)) + c.UI.Warn(fmt.Sprintf("Failed to revoke temporary key: %s", err)) } } } - switch strings.ToLower(c.mode) { + switch strings.ToLower(c.flagMode) { case ssh.KeyTypeCA: - if err := c.handleTypeCA(); err != nil { - c.Ui.Error(err.Error()) - return 1 - } + return c.handleTypeCA(username, ip, sshArgs) case ssh.KeyTypeOTP: - if err := c.handleTypeOTP(); err != nil { - c.Ui.Error(err.Error()) - return 1 - } + return c.handleTypeOTP(username, ip, sshArgs) case ssh.KeyTypeDynamic: - if err := c.handleTypeDynamic(); err != nil { - c.Ui.Error(err.Error()) - return 1 - } + return c.handleTypeDynamic(username, ip, sshArgs) default: - c.Ui.Error(fmt.Sprintf("Unknown SSH mode: %s", c.mode)) + c.UI.Error(fmt.Sprintf("Unknown SSH mode: %s", c.flagMode)) return 1 } - - return 0 } // handleTypeCA is used to handle SSH logins using the "CA" key type. -func (c *SSHCommand) handleTypeCA() error { +func (c *SSHCommand) handleTypeCA(username, ip string, sshArgs []string) int { // Read the key from disk - publicKey, err := ioutil.ReadFile(c.publicKeyPath) + publicKey, err := ioutil.ReadFile(c.flagPublicKeyPath) if err != nil { - return errors.Wrap(err, "failed to read public key") + c.UI.Error(fmt.Sprintf("failed to read public key %s: %s", + c.flagPublicKeyPath, err)) + return 1 } + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + sshClient := client.SSHWithMountPoint(c.flagMountPoint) + // Attempt to sign the public key - secret, err := c.sshClient.SignKey(c.role, map[string]interface{}{ + secret, err := sshClient.SignKey(c.flagRole, map[string]interface{}{ // WARNING: publicKey is []byte, which is b64 encoded on JSON upload. We // have to convert it to a string. SV lost many hours to this... "public_key": string(publicKey), - "valid_principals": c.username, + "valid_principals": username, "cert_type": "user", // TODO: let the user configure these. In the interim, if users want to @@ -241,55 +360,62 @@ func (c *SSHCommand) handleTypeCA() error { }, }) if err != nil { - return errors.Wrap(err, "failed to sign public key") + c.UI.Error(fmt.Sprintf("failed to sign public key %s: %s", + c.flagPublicKeyPath, err)) + return 2 } if secret == nil || secret.Data == nil { - return fmt.Errorf("client signing returned empty credentials") + c.UI.Error("missing signed key") + return 2 } // Handle no-exec - if c.noExec { - // This is hacky, but OutputSecret returns an int, not an error :( - if i := OutputSecret(c.Ui, c.format, secret); i != 0 { - return fmt.Errorf("an error occurred outputting the secret") + if c.flagNoExec { + if c.flagFormat != "" { + return PrintRawField(c.UI, secret, c.flagField) } - return nil + return OutputSecret(c.UI, c.flagFormat, secret) } // Extract public key key, ok := secret.Data["signed_key"].(string) - if !ok { - return fmt.Errorf("missing signed key") + if !ok || key == "" { + c.UI.Error("signed key is empty") + return 2 } // Capture the current value - this could be overwritten later if the user // enabled host key signing verification. - userKnownHostsFile := c.userKnownHostsFile - strictHostKeyChecking := c.strictHostKeyChecking + userKnownHostsFile := c.flagUserKnownHostsFile + strictHostKeyChecking := c.flagStrictHostKeyChecking // Handle host key signing verification. If the user specified a mount point, // download the public key, trust it with the given domains, and use that // instead of the user's regular known_hosts file. - if c.hostKeyMountPoint != "" { - secret, err := c.client.Logical().Read(c.hostKeyMountPoint + "/config/ca") + if c.flagHostKeyMountPoint != "" { + secret, err := c.client.Logical().Read(c.flagHostKeyMountPoint + "/config/ca") if err != nil { - return errors.Wrap(err, "failed to get host signing key") + c.UI.Error(fmt.Sprintf("failed to get host signing key: %s", err)) + return 2 } if secret == nil || secret.Data == nil { - return fmt.Errorf("missing host signing key") + c.UI.Error("missing host signing key") + return 2 } publicKey, ok := secret.Data["public_key"].(string) - if !ok { - return fmt.Errorf("host signing key is empty") + if !ok || publicKey == "" { + c.UI.Error("host signing key is empty") + return 2 } // Write the known_hosts file - name := fmt.Sprintf("vault_ssh_ca_known_hosts_%s_%s", c.username, c.ip) - data := fmt.Sprintf("@cert-authority %s %s", c.hostKeyHostnames, publicKey) + name := fmt.Sprintf("vault_ssh_ca_known_hosts_%s_%s", username, ip) + data := fmt.Sprintf("@cert-authority %s %s", c.flagHostKeyHostnames, publicKey) knownHosts, err, closer := c.writeTemporaryFile(name, []byte(data), 0644) defer closer() if err != nil { - return errors.Wrap(err, "failed to write host public key") + c.UI.Error(fmt.Sprintf("failed to write host public key: %s", err)) + return 1 } // Update the variables @@ -298,20 +424,21 @@ func (c *SSHCommand) handleTypeCA() error { } // Write the signed public key to disk - name := fmt.Sprintf("vault_ssh_ca_%s_%s", c.username, c.ip) + name := fmt.Sprintf("vault_ssh_ca_%s_%s", username, ip) signedPublicKeyPath, err, closer := c.writeTemporaryKey(name, []byte(key)) defer closer() if err != nil { - return errors.Wrap(err, "failed to write signed public key") + c.UI.Error(fmt.Sprintf("failed to write signed public key: %s", err)) + return 2 } args := append([]string{ - "-i", c.privateKeyPath, + "-i", c.flagPrivateKeyPath, "-i", signedPublicKeyPath, "-o UserKnownHostsFile=" + userKnownHostsFile, "-o StrictHostKeyChecking=" + strictHostKeyChecking, - c.username + "@" + c.ip, - }, c.sshArgs...) + username + "@" + ip, + }, sshArgs...) cmd := exec.Command("ssh", args...) cmd.Stdin = os.Stdin @@ -319,61 +446,71 @@ func (c *SSHCommand) handleTypeCA() error { cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { - return errors.Wrap(err, "failed to run ssh command") + exitCode := 2 + + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.Success() { + return 0 + } + if ws, ok := exitError.Sys().(syscall.WaitStatus); ok { + exitCode = ws.ExitStatus() + } + } + + c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err)) + return exitCode } // There is no secret to revoke, since it's a certificate signing - - return nil + return 0 } // handleTypeOTP is used to handle SSH logins using the "otp" key type. -func (c *SSHCommand) handleTypeOTP() error { - secret, cred, err := c.generateCredential() +func (c *SSHCommand) handleTypeOTP(username, ip string, sshArgs []string) int { + secret, cred, err := c.generateCredential(username, ip) if err != nil { - return errors.Wrap(err, "failed to generate credential") + c.UI.Error(fmt.Sprintf("failed to generate credential: %s", err)) + return 2 } // Handle no-exec - if c.noExec { - // This is hacky, but OutputSecret returns an int, not an error :( - if i := OutputSecret(c.Ui, c.format, secret); i != 0 { - return fmt.Errorf("an error occurred outputting the secret") + if c.flagNoExec { + if c.flagFormat != "" { + return PrintRawField(c.UI, secret, c.flagField) } - return nil + return OutputSecret(c.UI, c.flagFormat, secret) } var cmd *exec.Cmd - // Check if the application 'sshpass' is installed in the client machine. - // If it is then, use it to automate typing in OTP to the prompt. Unfortunately, + // Check if the application 'sshpass' is installed in the client machine. If + // it is then, use it to automate typing in OTP to the prompt. Unfortunately, // it was not possible to automate it without a third-party application, with - // only the Go libraries. - // Feel free to try and remove this dependency. + // only the Go libraries. Feel free to try and remove this dependency. sshpassPath, err := exec.LookPath("sshpass") if err != nil { - c.Ui.Warn("" + - "Vault could not locate sshpass. The OTP code for the session will be\n" + - "displayed below. Enter this code in the SSH password prompt. If you\n" + - "install sshpass, Vault can automatically perform this step for you.") - c.Ui.Output("OTP for the session is " + cred.Key) + c.UI.Warn(wrapAtLength( + "Vault could not locate \"sshpass\". The OTP code for the session is " + + "displayed below. Enter this code in the SSH password prompt. If you " + + "install sshpass, Vault can automatically perform this step for you.")) + c.UI.Output("OTP for the session is: " + cred.Key) args := append([]string{ - "-o UserKnownHostsFile=" + c.userKnownHostsFile, - "-o StrictHostKeyChecking=" + c.strictHostKeyChecking, + "-o UserKnownHostsFile=" + c.flagUserKnownHostsFile, + "-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking, "-p", cred.Port, - c.username + "@" + c.ip, - }, c.sshArgs...) + username + "@" + ip, + }, sshArgs...) cmd = exec.Command("ssh", args...) } else { args := append([]string{ "-e", // Read password for SSHPASS environment variable "ssh", - "-o UserKnownHostsFile=" + c.userKnownHostsFile, - "-o StrictHostKeyChecking=" + c.strictHostKeyChecking, + "-o UserKnownHostsFile=" + c.flagUserKnownHostsFile, + "-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking, "-p", cred.Port, - c.username + "@" + c.ip, - }, c.sshArgs...) + username + "@" + ip, + }, sshArgs...) cmd = exec.Command(sshpassPath, args...) env := os.Environ() env = append(env, fmt.Sprintf("SSHPASS=%s", string(cred.Key))) @@ -385,49 +522,63 @@ func (c *SSHCommand) handleTypeOTP() error { cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { - return errors.Wrap(err, "failed to run ssh command") + exitCode := 2 + + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.Success() { + return 0 + } + if ws, ok := exitError.Sys().(syscall.WaitStatus); ok { + exitCode = ws.ExitStatus() + } + } + + c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err)) + return exitCode } // Revoke the key if it's longer than expected if err := c.client.Sys().Revoke(secret.LeaseID); err != nil { - return errors.Wrap(err, "failed to revoke key") + c.UI.Error(fmt.Sprintf("failed to revoke key: %s", err)) + return 2 } - return nil + return 0 } // handleTypeDynamic is used to handle SSH logins using the "dyanmic" key type. -func (c *SSHCommand) handleTypeDynamic() error { +func (c *SSHCommand) handleTypeDynamic(username, ip string, sshArgs []string) int { // Generate the credential - secret, cred, err := c.generateCredential() + secret, cred, err := c.generateCredential(username, ip) if err != nil { - return errors.Wrap(err, "failed to generate credential") + c.UI.Error(fmt.Sprintf("failed to generate credential: %s", err)) + return 2 } // Handle no-exec - if c.noExec { - // This is hacky, but OutputSecret returns an int, not an error :( - if i := OutputSecret(c.Ui, c.format, secret); i != 0 { - return fmt.Errorf("an error occurred outputting the secret") + if c.flagNoExec { + if c.flagFormat != "" { + return PrintRawField(c.UI, secret, c.flagField) } - return nil + return OutputSecret(c.UI, c.flagFormat, secret) } // Write the dynamic key to disk - name := fmt.Sprintf("vault_ssh_dynamic_%s_%s", c.username, c.ip) + name := fmt.Sprintf("vault_ssh_dynamic_%s_%s", username, ip) keyPath, err, closer := c.writeTemporaryKey(name, []byte(cred.Key)) defer closer() if err != nil { - return errors.Wrap(err, "failed to save dyanmic key") + c.UI.Error(fmt.Sprintf("failed to write dynamic key: %s", err)) + return 1 } args := append([]string{ "-i", keyPath, - "-o UserKnownHostsFile=" + c.userKnownHostsFile, - "-o StrictHostKeyChecking=" + c.strictHostKeyChecking, + "-o UserKnownHostsFile=" + c.flagUserKnownHostsFile, + "-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking, "-p", cred.Port, - c.username + "@" + c.ip, - }, c.sshArgs...) + username + "@" + ip, + }, sshArgs...) cmd := exec.Command("ssh", args...) cmd.Stdin = os.Stdin @@ -435,24 +586,44 @@ func (c *SSHCommand) handleTypeDynamic() error { cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { - return errors.Wrap(err, "failed to run ssh command") + exitCode := 2 + + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.Success() { + return 0 + } + if ws, ok := exitError.Sys().(syscall.WaitStatus); ok { + exitCode = ws.ExitStatus() + } + } + + c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err)) + return exitCode } // Revoke the key if it's longer than expected if err := c.client.Sys().Revoke(secret.LeaseID); err != nil { - return errors.Wrap(err, "failed to revoke key") + c.UI.Error(fmt.Sprintf("failed to revoke key: %s", err)) + return 2 } - return nil + return 0 } // generateCredential generates a credential for the given role and returns the // decoded secret data. -func (c *SSHCommand) generateCredential() (*api.Secret, *SSHCredentialResp, error) { +func (c *SSHCommand) generateCredential(username, ip string) (*api.Secret, *SSHCredentialResp, error) { + client, err := c.Client() + if err != nil { + return nil, nil, err + } + + sshClient := client.SSHWithMountPoint(c.flagMountPoint) + // Attempt to generate the credential. - secret, err := c.sshClient.Credential(c.role, map[string]interface{}{ - "username": c.username, - "ip": c.ip, + secret, err := sshClient.Credential(c.flagRole, map[string]interface{}{ + "username": username, + "ip": ip, }) if err != nil { return nil, nil, errors.Wrap(err, "failed to get credentials") @@ -540,9 +711,9 @@ func (c *SSHCommand) defaultRole(mountPoint, ip string) (string, error) { } roleNames = strings.TrimRight(roleNames, ", ") return "", fmt.Errorf("Roles:%q. "+` - Multiple roles are registered for this IP. - Select a role using '-role' option. - Note that all roles may not be permitted, based on ACLs.`, roleNames) + Multiple roles are registered for this IP. + Select a role using '-role' option. + Note that all roles may not be permitted, based on ACLs.`, roleNames) } } @@ -580,102 +751,3 @@ func (c *SSHCommand) userAndIP(s string) (string, string, error) { return username, ip, nil } - -func (c *SSHCommand) Synopsis() string { - return "Initiate an SSH session" -} - -func (c *SSHCommand) Help() string { - helpText := ` -Usage: vault ssh [options] username@ip [ssh options] - - Establishes an SSH connection with the target machine. - - This command uses one of the SSH authentication backends to authenticate and - automatically establish an SSH connection to a host. This operation requires - that the SSH backend is mounted and configured. - - SSH using the OTP mode (requires sshpass for full automation): - - $ vault ssh -mode=otp -role=my-role user@1.2.3.4 - - SSH using the CA mode: - - $ vault ssh -mode=ca -role=my-role user@1.2.3.4 - - SSH using CA mode with host key verification: - - $ vault ssh \ - -mode=ca \ - -role=my-role \ - -host-key-mount-point=host-signer \ - -host-key-hostnames=example.com \ - user@example.com - - For the full list of options and arguments, please see the documentation. - -General Options: -` + meta.GeneralOptionsUsage() + ` -SSH Options: - - -role Role to be used to create the key. Each IP is associated with - a role. To see the associated roles with IP, use "lookup" - endpoint. If you are certain that there is only one role - associated with the IP, you can skip mentioning the role. It - will be chosen by default. If there are no roles associated - with the IP, register the CIDR block of that IP using the - "roles/" endpoint. - - -no-exec Shows the credentials but does not establish connection. - - -mount-point Mount point of SSH backend. If the backend is mounted at - "ssh" (default), this parameter can be skipped. - - -format If the "no-exec" option is enabled, the credentials will be - printed out and SSH connection will not be established. The - format of the output can be "json" or "table" (default). - - -strict-host-key-checking This option corresponds to "StrictHostKeyChecking" - of SSH configuration. If "sshpass" is employed to enable - automated login, then if host key is not "known" to the - client, "vault ssh" command will fail. Set this option to - "no" to bypass the host key checking. Defaults to "ask". - Can also be specified with the - "VAULT_SSH_STRICT_HOST_KEY_CHECKING" environment variable. - - -user-known-hosts-file This option corresponds to "UserKnownHostsFile" of - SSH configuration. Assigns the file to use for storing the - host keys. If this option is set to "/dev/null" along with - "-strict-host-key-checking=no", both warnings and host key - checking can be avoided while establishing the connection. - Defaults to "~/.ssh/known_hosts". Can also be specified with - "VAULT_SSH_USER_KNOWN_HOSTS_FILE" environment variable. - -CA Mode Options: - - - public-key-path= - The path to the public key to send to Vault for signing. The default value - is ~/.ssh/id_rsa.pub. - - - private-key-path= - The path to the private key to use for authentication. This must be the - corresponding private key to -public-key-path. The default value is - ~/.ssh/id_rsa. - - - host-key-mount-point= - The mount point to the SSH backend where host keys are signed. When given - a value, Vault will generate a custom known_hosts file with delegation to - the CA at the provided mount point and verify the SSH connection's host - keys against the provided CA. By default, this command uses the users's - existing known_hosts file. When this flag is set, this command will force - strict host key checking and will override any values provided for a - custom -user-known-hosts-file. - - - host-key-hostnames= - The list of hostnames to delegate for this certificate authority. By - default, this is "*", which allows all domains and IPs. To restrict - validation to a series of hostnames, specify them as comma-separated - values here. -` - return strings.TrimSpace(helpText) -} diff --git a/command/ssh_test.go b/command/ssh_test.go index 70a58f5431..189ea2887f 100644 --- a/command/ssh_test.go +++ b/command/ssh_test.go @@ -1,199 +1,23 @@ package command import ( - "bytes" - "fmt" - "io" - "os" - "strings" "testing" - logicalssh "github.com/hashicorp/vault/builtin/logical/ssh" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -const ( - testCidr = "127.0.0.1/32" - testRoleName = "testRoleName" - testKey = "testKey" - testSharedPrivateKey = ` ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAvYvoRcWRxqOim5VZnuM6wHCbLUeiND0yaM1tvOl+Fsrz55DG -A0OZp4RGAu1Fgr46E1mzxFz1+zY4UbcEExg+u21fpa8YH8sytSWW1FyuD8ICib0A -/l8slmDMw4BkkGOtSlEqgscpkpv/TWZD1NxJWkPcULk8z6c7TOETn2/H9mL+v2RE -mbE6NDEwJKfD3MvlpIqCP7idR+86rNBAODjGOGgyUbtFLT+K01XmDRALkV3V/nh+ -GltyjL4c6RU4zG2iRyV5RHlJtkml+UzUMkzr4IQnkCC32CC/wmtoo/IsAprpcHVe -nkBn3eFQ7uND70p5n6GhN/KOh2j519JFHJyokwIDAQABAoIBAHX7VOvBC3kCN9/x -+aPdup84OE7Z7MvpX6w+WlUhXVugnmsAAVDczhKoUc/WktLLx2huCGhsmKvyVuH+ -MioUiE+vx75gm3qGx5xbtmOfALVMRLopjCnJYf6EaFA0ZeQ+NwowNW7Lu0PHmAU8 -Z3JiX8IwxTz14DU82buDyewO7v+cEr97AnERe3PUcSTDoUXNaoNxjNpEJkKREY6h -4hAY676RT/GsRcQ8tqe/rnCqPHNd7JGqL+207FK4tJw7daoBjQyijWuB7K5chSal -oPInylM6b13ASXuOAOT/2uSUBWmFVCZPDCmnZxy2SdnJGbsJAMl7Ma3MUlaGvVI+ -Tfh1aQkCgYEA4JlNOabTb3z42wz6mz+Nz3JRwbawD+PJXOk5JsSnV7DtPtfgkK9y -6FTQdhnozGWShAvJvc+C4QAihs9AlHXoaBY5bEU7R/8UK/pSqwzam+MmxmhVDV7G -IMQPV0FteoXTaJSikhZ88mETTegI2mik+zleBpVxvfdhE5TR+lq8Br0CgYEA2AwJ -CUD5CYUSj09PluR0HHqamWOrJkKPFPwa+5eiTTCzfBBxImYZh7nXnWuoviXC0sg2 -AuvCW+uZ48ygv/D8gcz3j1JfbErKZJuV+TotK9rRtNIF5Ub7qysP7UjyI7zCssVM -kuDd9LfRXaB/qGAHNkcDA8NxmHW3gpln4CFdSY8CgYANs4xwfercHEWaJ1qKagAe -rZyrMpffAEhicJ/Z65lB0jtG4CiE6w8ZeUMWUVJQVcnwYD+4YpZbX4S7sJ0B8Ydy -AhkSr86D/92dKTIt2STk6aCN7gNyQ1vW198PtaAWH1/cO2UHgHOy3ZUt5X/Uwxl9 -cex4flln+1Viumts2GgsCQKBgCJH7psgSyPekK5auFdKEr5+Gc/jB8I/Z3K9+g4X -5nH3G1PBTCJYLw7hRzw8W/8oALzvddqKzEFHphiGXK94Lqjt/A4q1OdbCrhiE68D -My21P/dAKB1UYRSs9Y8CNyHCjuZM9jSMJ8vv6vG/SOJPsnVDWVAckAbQDvlTHC9t -O98zAoGAcbW6uFDkrv0XMCpB9Su3KaNXOR0wzag+WIFQRXCcoTvxVi9iYfUReQPi -oOyBJU/HMVvBfv4g+OVFLVgSwwm6owwsouZ0+D/LasbuHqYyqYqdyPJQYzWA2Y+F -+B6f4RoPdSXj24JHPg/ioRxjaj094UXJxua2yfkcecGNEuBQHSs= ------END RSA PRIVATE KEY----- -` -) +func testSSHCommand(tb testing.TB) (*cli.MockUi, *SSHCommand) { + tb.Helper() -var testIP string -var testPort string -var testUserName string -var testAdminUser string - -// Starts the server and initializes the servers IP address, -// port and usernames to be used by the test cases. -func initTest() { - addr, err := vault.StartSSHHostTestServer() - if err != nil { - panic(fmt.Sprintf("Error starting mock server:%s", err)) + ui := cli.NewMockUi() + return ui, &SSHCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, } - input := strings.Split(addr, ":") - testIP = input[0] - testPort = input[1] - - testUserName := os.Getenv("VAULT_SSHTEST_USER") - if len(testUserName) == 0 { - panic("VAULT_SSHTEST_USER must be set to the desired user") - } - testAdminUser = testUserName } -// This test is broken. Hence temporarily disabling it. -func testSSH(t *testing.T) { - initTest() - // Add the SSH backend to the unsealed test core. - // This should be done before the unsealed core is created. - err := vault.AddTestLogicalBackend("ssh", logicalssh.Factory) - if err != nil { - t.Fatalf("err: %s", err) - } - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - mountCmd := &MountCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{"-address", addr, "ssh"} - - // Mount the SSH backend - if code := mountCmd.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - client, err := mountCmd.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatalf("err: %s", err) - } - - // Check if SSH backend is mounted or not - mount, ok := mounts["ssh/"] - if !ok { - t.Fatal("should have ssh mount") - } - if mount.Type != "ssh" { - t.Fatal("should have ssh type") - } - - writeCmd := &WriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - // Create a 'named' key in vault - args = []string{ - "-address", addr, - "ssh/keys/" + testKey, - "key=" + testSharedPrivateKey, - } - if code := writeCmd.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - // Create a role using the named key along with cidr, username and port - args = []string{ - "-address", addr, - "ssh/roles/" + testRoleName, - "key=" + testKey, - "admin_user=" + testUserName, - "cidr=" + testCidr, - "port=" + testPort, - } - if code := writeCmd.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - sshCmd := &SSHCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - // Get the dynamic key and establish an SSH connection with target. - // Inline command when supplied, runs on target and terminates the - // connection. Use whoami as the inline command in target and get - // the result. Compare the result with the username used to connect - // to target. Test succeeds if they match. - args = []string{ - "-address", addr, - "-role=" + testRoleName, - testUserName + "@" + testIP, - "/usr/bin/whoami", - } - - // Creating pipe to get the result of the inline command run in target machine. - stdout := os.Stdout - r, w, err := os.Pipe() - if err != nil { - t.Fatalf("err: %s", err) - } - os.Stdout = w - if code := sshCmd.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - bufChan := make(chan string) - go func() { - var buf bytes.Buffer - io.Copy(&buf, r) - bufChan <- buf.String() - }() - w.Close() - os.Stdout = stdout - userName := <-bufChan - userName = strings.TrimSpace(userName) - - // Comparing the username used to connect to target and - // the username on the target, thereby verifying successful - // execution - if userName != testUserName { - t.Fatalf("err: username mismatch") - } +func TestSSHCommand_Run(t *testing.T) { + t.Parallel() + t.Skip("Need a way to setup target infrastructure") } From bd33fe3c73aa572065a995c47661cce1f290fda8 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:04:39 -0400 Subject: [PATCH 071/281] Update status command --- command/status.go | 109 ++++++++++++++++++++++-------------- command/status_test.go | 122 +++++++++++++++++++++++++++++++++-------- 2 files changed, 167 insertions(+), 64 deletions(-) diff --git a/command/status.go b/command/status.go index 7b6cce348f..40cd1059d6 100644 --- a/command/status.go +++ b/command/status.go @@ -5,33 +5,79 @@ import ( "strings" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) -// StatusCommand is a Command that outputs the status of whether -// Vault is sealed or not as well as HA information. +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*StatusCommand)(nil) +var _ cli.CommandAutocomplete = (*StatusCommand)(nil) + +// StatusCommand is a Command that outputs the status of whether Vault is sealed +// or not as well as HA information. type StatusCommand struct { - meta.Meta + *BaseCommand +} + +func (c *StatusCommand) Synopsis() string { + return "Prints seal and HA status" +} + +func (c *StatusCommand) Help() string { + helpText := ` +Usage: vault status [options] + + Prints the current state of Vault including whether it is sealed and if HA + mode is enabled. This command prints regardless of whether the Vault is + sealed. + + The exit code reflects the seal status: + + - 0 - unsealed + - 1 - error + - 2 - sealed + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *StatusCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *StatusCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *StatusCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *StatusCommand) Run(args []string) int { - flags := c.Meta.FlagSet("status", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) + // We return 2 everywhere else, but 2 is reserved for "sealed" here return 1 } sealStatus, err := client.Sys().SealStatus() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error checking seal status: %s", err)) + c.UI.Error(fmt.Sprintf("Error checking seal status: %s", err)) return 1 } @@ -53,33 +99,32 @@ func (c *StatusCommand) Run(args []string) int { outStr = fmt.Sprintf("%s\nCluster Name: %s\nCluster ID: %s", outStr, sealStatus.ClusterName, sealStatus.ClusterID) } - c.Ui.Output(outStr) + c.UI.Output(outStr) - // Mask the 'Vault is sealed' error, since this means HA is enabled, - // but that we cannot query for the leader since we are sealed. + // Mask the 'Vault is sealed' error, since this means HA is enabled, but that + // we cannot query for the leader since we are sealed. leaderStatus, err := client.Sys().Leader() if err != nil && strings.Contains(err.Error(), "Vault is sealed") { leaderStatus = &api.LeaderResponse{HAEnabled: true} err = nil } if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error checking leader status: %s", err)) + c.UI.Error(fmt.Sprintf("Error checking leader status: %s", err)) return 1 } // Output if HA is enabled - c.Ui.Output("") - c.Ui.Output(fmt.Sprintf("High-Availability Enabled: %v", leaderStatus.HAEnabled)) + c.UI.Output("") + c.UI.Output(fmt.Sprintf("High-Availability Enabled: %v", leaderStatus.HAEnabled)) if leaderStatus.HAEnabled { if sealStatus.Sealed { - c.Ui.Output("\tMode: sealed") + c.UI.Output("\tMode: sealed") } else { mode := "standby" if leaderStatus.IsSelf { mode = "active" } - c.Ui.Output(fmt.Sprintf("\tMode: %s", mode)) + c.UI.Output(fmt.Sprintf("\tMode: %s", mode)) if leaderStatus.LeaderAddress == "" { leaderStatus.LeaderAddress = "" @@ -87,31 +132,13 @@ func (c *StatusCommand) Run(args []string) int { if leaderStatus.LeaderClusterAddress == "" { leaderStatus.LeaderClusterAddress = "" } - c.Ui.Output(fmt.Sprintf("\tLeader Cluster Address: %s", leaderStatus.LeaderClusterAddress)) + c.UI.Output(fmt.Sprintf("\tLeader Cluster Address: %s", leaderStatus.LeaderClusterAddress)) } } if sealStatus.Sealed { return 2 - } else { - return 0 } -} - -func (c *StatusCommand) Synopsis() string { - return "Outputs status of whether Vault is sealed and if HA mode is enabled" -} - -func (c *StatusCommand) Help() string { - helpText := ` -Usage: vault status [options] - - Outputs the state of the Vault, sealed or unsealed and if HA is enabled. - - This command outputs whether or not the Vault is sealed. The exit - code also reflects the seal status (0 unsealed, 2 sealed, 1 error). - -General Options: -` + meta.GeneralOptionsUsage() - return strings.TrimSpace(helpText) + + return 0 } diff --git a/command/status_test.go b/command/status_test.go index 92e7f74719..717d4afbc8 100644 --- a/command/status_test.go +++ b/command/status_test.go @@ -1,39 +1,115 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestStatus(t *testing.T) { - ui := new(cli.MockUi) - c := &StatusCommand{ - Meta: meta.Meta{ - Ui: ui, +func testStatusCommand(tb testing.TB) (*cli.MockUi, *StatusCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &StatusCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestStatusCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + sealed bool + out string + code int + }{ + { + "unsealed", + nil, + false, + "Sealed: false", + 0, + }, + { + "sealed", + nil, + true, + "Sealed: true", + 2, + }, + { + "args", + []string{"foo"}, + false, + "Too many arguments", + 1, }, } - core := vault.TestCore(t) - keys, _ := vault.TestCoreInit(t, core) - ln, addr := http.TestServer(t, core) - defer ln.Close() + t.Run("validations", func(t *testing.T) { + t.Parallel() - args := []string{"-address", addr} - if code := c.Run(args); code != 2 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + for _, tc := range cases { + tc := tc - for _, key := range keys { - if _, err := core.Unseal(key); err != nil { - t.Fatalf("err: %s", err) + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if tc.sealed { + if err := client.Sys().Seal(); err != nil { + t.Fatal(err) + } + } + + ui, cmd := testStatusCommand(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) + } + }) } - } + }) - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testStatusCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 1; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error checking seal status: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testStatusCommand(t) + assertNoTabs(t, cmd) + }) } From ba5712ef4f93a57137aae5306761998b171d9fb9 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:04:46 -0400 Subject: [PATCH 072/281] Update step-down command --- command/step-down.go | 55 ---------------------- command/step_down.go | 85 +++++++++++++++++++++++++++++++++ command/step_down_test.go | 99 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 55 deletions(-) delete mode 100644 command/step-down.go create mode 100644 command/step_down.go create mode 100644 command/step_down_test.go diff --git a/command/step-down.go b/command/step-down.go deleted file mode 100644 index be445a8389..0000000000 --- a/command/step-down.go +++ /dev/null @@ -1,55 +0,0 @@ -package command - -import ( - "fmt" - "strings" - - "github.com/hashicorp/vault/meta" -) - -// StepDownCommand is a Command that seals the vault. -type StepDownCommand struct { - meta.Meta -} - -func (c *StepDownCommand) Run(args []string) int { - flags := c.Meta.FlagSet("step-down", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if err := client.Sys().StepDown(); err != nil { - c.Ui.Error(fmt.Sprintf("Error stepping down: %s", err)) - return 1 - } - - return 0 -} - -func (c *StepDownCommand) Synopsis() string { - return "Force the Vault node to give up active duty" -} - -func (c *StepDownCommand) Help() string { - helpText := ` -Usage: vault step-down [options] - - Force the Vault node to step down from active duty. - - This causes the indicated node to give up active status. Note that while the - affected node will have a short delay before attempting to grab the lock - again, if no other node grabs the lock beforehand, it is possible for the - same node to re-grab the lock and become active again. - -General Options: -` + meta.GeneralOptionsUsage() - return strings.TrimSpace(helpText) -} diff --git a/command/step_down.go b/command/step_down.go new file mode 100644 index 0000000000..6b2f3114f4 --- /dev/null +++ b/command/step_down.go @@ -0,0 +1,85 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*StepDownCommand)(nil) +var _ cli.CommandAutocomplete = (*StepDownCommand)(nil) + +// StepDownCommand is a Command that tells the Vault server to give up its +// leadership +type StepDownCommand struct { + *BaseCommand +} + +func (c *StepDownCommand) Synopsis() string { + return "Forces Vault to resign active duty" +} + +func (c *StepDownCommand) Help() string { + helpText := ` +Usage: vault step-down [options] + + Forces the Vault server at the given address to step down from active duty. + While the affected node will have a delay before attempting to acquire the + leader lock again, if no other Vault nodes acquire the lock beforehand, it + is possible for the same node to re-acquire the lock and become active + again. + + Force Vault to step down as the leader: + + $ vault step-down + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *StepDownCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *StepDownCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *StepDownCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *StepDownCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if err := client.Sys().StepDown(); err != nil { + c.UI.Error(fmt.Sprintf("Error stepping down: %s", err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Stepped down: %s", client.Address())) + return 0 +} diff --git a/command/step_down_test.go b/command/step_down_test.go new file mode 100644 index 0000000000..6141e08a8a --- /dev/null +++ b/command/step_down_test.go @@ -0,0 +1,99 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func testStepDownCommand(tb testing.TB) (*cli.MockUi, *StepDownCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &StepDownCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestStepDownCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo"}, + "Too many arguments", + 1, + }, + { + "default", + nil, + "Success! Stepped down: ", + 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() + + ui, cmd := testStepDownCommand(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("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testStepDownCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error stepping down: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testStepDownCommand(t) + assertNoTabs(t, cmd) + }) +} From eee5edb102c6fc2c27cfd90cace3f229ca950829 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:04:54 -0400 Subject: [PATCH 073/281] Update token-create command --- command/token_create.go | 366 +++++++++++++++++++++-------------- command/token_create_test.go | 251 +++++++++++++++++++++--- 2 files changed, 449 insertions(+), 168 deletions(-) diff --git a/command/token_create.go b/command/token_create.go index f8d8c59265..ca5b7752f7 100644 --- a/command/token_create.go +++ b/command/token_create.go @@ -3,174 +3,250 @@ package command import ( "fmt" "strings" + "time" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/flag-kv" - "github.com/hashicorp/vault/helper/flag-slice" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*TokenCreateCommand)(nil) +var _ cli.CommandAutocomplete = (*TokenCreateCommand)(nil) + // TokenCreateCommand is a Command that mounts a new mount. type TokenCreateCommand struct { - meta.Meta -} + *BaseCommand -func (c *TokenCreateCommand) Run(args []string) int { - var format string - var id, displayName, lease, ttl, explicitMaxTTL, period, role string - var orphan, noDefaultPolicy, renewable bool - var metadata map[string]string - var numUses int - var policies []string - flags := c.Meta.FlagSet("mount", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - flags.StringVar(&displayName, "display-name", "", "") - flags.StringVar(&id, "id", "", "") - flags.StringVar(&lease, "lease", "", "") - flags.StringVar(&ttl, "ttl", "", "") - flags.StringVar(&explicitMaxTTL, "explicit-max-ttl", "", "") - flags.StringVar(&period, "period", "", "") - flags.StringVar(&role, "role", "", "") - flags.BoolVar(&orphan, "orphan", false, "") - flags.BoolVar(&renewable, "renewable", true, "") - flags.BoolVar(&noDefaultPolicy, "no-default-policy", false, "") - flags.IntVar(&numUses, "use-limit", 0, "") - flags.Var((*kvFlag.Flag)(&metadata), "metadata", "") - flags.Var((*sliceflag.StringFlag)(&policies), "policy", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } + flagID string + flagDisplayName string + flagTTL time.Duration + flagExplicitMaxTTL time.Duration + flagPeriod time.Duration + flagRenewable bool + flagOrphan bool + flagNoDefaultPolicy bool + flagUseLimit int + flagRole string + flagMetadata map[string]string + flagPolicies []string - args = flags.Args() - if len(args) != 0 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\ntoken-create expects no arguments")) - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if ttl == "" { - ttl = lease - } - - tcr := &api.TokenCreateRequest{ - ID: id, - Policies: policies, - Metadata: metadata, - TTL: ttl, - NoParent: orphan, - NoDefaultPolicy: noDefaultPolicy, - DisplayName: displayName, - NumUses: numUses, - Renewable: new(bool), - ExplicitMaxTTL: explicitMaxTTL, - Period: period, - } - *tcr.Renewable = renewable - - var secret *api.Secret - if role != "" { - secret, err = client.Auth().Token().CreateWithRole(tcr, role) - } else { - secret, err = client.Auth().Token().Create(tcr) - } - - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error creating token: %s", err)) - return 2 - } - - return OutputSecret(c.Ui, format, secret) + // Deprecated flags + flagLease time.Duration } func (c *TokenCreateCommand) Synopsis() string { - return "Create a new auth token" + return "Creates a new token" } func (c *TokenCreateCommand) Help() string { helpText := ` Usage: vault token-create [options] - Create a new auth token. + Creates a new token that can be used for authentication. This token will be + created as a child of the currently authenticated token. The generated token + will inherit all policies and permissions of the currently authenticated + token unless you explicitly define a subset list policies to assign to the + token. - This command creates a new token that can be used for authentication. - This token will be created as a child of your token. The created token - will inherit your policies, or can be assigned a subset of your policies. - - A lease can also be associated with the token. If a lease is not associated - with the token, then it cannot be renewed. If a lease is associated with + A ttl can also be associated with the token. If a ttl is not associated + with the token, then it cannot be renewed. If a ttl is associated with the token, it will expire after that amount of time unless it is renewed. - Metadata associated with the token (specified with "-metadata") is - written to the audit log when the token is used. + Metadata associated with the token (specified with "-metadata") is written + to the audit log when the token is used. If a role is specified, the role may override parameters specified here. -General Options: -` + meta.GeneralOptionsUsage() + ` -Token Options: +` + c.Flags().Help() - -id="7699125c-d8...." The token value that clients will use to authenticate - with Vault. If not provided this defaults to a 36 - character UUID. A root token is required to specify - the ID of a token. - - -display-name="name" A display name to associate with this token. This - is a non-security sensitive value used to help - identify created secrets, i.e. prefixes. - - -ttl="1h" Initial TTL to associate with the token; renewals can - extend this value. - - -explicit-max-ttl="1h" An explicit maximum lifetime for the token. Unlike - normal token TTLs, which can be renewed up until the - maximum TTL set on the auth/token mount or the system - configuration file, this lifetime is a hard limit set - on the token itself and cannot be exceeded. - - -period="1h" If specified, the token will be periodic; it will - have no maximum TTL (unless an "explicit-max-ttl" is - also set) but every renewal will use the given - period. Requires a root/sudo token to use. - - -renewable=true Whether or not the token is renewable to extend its - TTL up to Vault's configured maximum TTL for tokens. - This defaults to true; set to false to disable - renewal of this token. - - -metadata="key=value" Metadata to associate with the token. This shows - up in the audit log. This can be specified multiple - times. - - -orphan If specified, the token will have no parent. This - prevents the new token from being revoked with - your token. Requires a root/sudo token to use. - - -no-default-policy If specified, the token will not have the "default" - policy included in its policy set. - - -policy="name" Policy to associate with this token. This can be - specified multiple times. - - -use-limit=5 The number of times this token can be used until - it is automatically revoked. - - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. - - -role=name If set, the token will be created against the named - role. The role may override other parameters. This - requires the client to have permissions on the - appropriate endpoint (auth/token/create/). -` return strings.TrimSpace(helpText) } + +func (c *TokenCreateCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "id", + Target: &c.flagID, + Completion: complete.PredictAnything, + Usage: "Value for the token. By default, this is an auto-generated 36 " + + "character UUID. Specifying this value requires sudo permissions.", + }) + + f.StringVar(&StringVar{ + Name: "display-name", + Target: &c.flagDisplayName, + Completion: complete.PredictAnything, + Usage: "Name to associate with this token. This is a non-sensitive value " + + "that can be used to help identify created secrets (e.g. prefixes).", + }) + + f.DurationVar(&DurationVar{ + Name: "ttl", + Target: &c.flagTTL, + Completion: complete.PredictAnything, + Usage: "Initial TTL to associate with the token. Token renewals may be " + + "able to extend beyond this value, depending on the configured maximum" + + "TTLs. This is specified as a numeric string with suffix like \"30s\" " + + "or \"5m\".", + }) + + f.DurationVar(&DurationVar{ + Name: "explicit-max-ttl", + Target: &c.flagExplicitMaxTTL, + Completion: complete.PredictAnything, + Usage: "Explicit maximum lifetime for the token. Unlike normal TTLs, the " + + "maximum TTL is a hard limit and cannot be exceeded. This is specified " + + "as a numeric string with suffix like \"30s\" or \"5m\".", + }) + + f.DurationVar(&DurationVar{ + Name: "period", + Target: &c.flagPeriod, + Completion: complete.PredictAnything, + Usage: "If specified, every renewal will use the given period. Periodic " + + "tokens do not expire (unless -explicit-max-ttl is also provided). " + + "Setting this value requires sudo permissions. This is specified as a " + + "numeric string with suffix like \"30s\" or \"5m\".", + }) + + f.BoolVar(&BoolVar{ + Name: "renewable", + Target: &c.flagRenewable, + Default: true, + Usage: "Allow the token to be renewed up to it's maximum TTL.", + }) + + f.BoolVar(&BoolVar{ + Name: "orphan", + Target: &c.flagOrphan, + Default: false, + Usage: "Create the token with no parent. This prevents the token from " + + "being revoked when the token which created it expires. Setting this " + + "value requires sudo permissions.", + }) + + f.BoolVar(&BoolVar{ + Name: "no-default-policy", + Target: &c.flagNoDefaultPolicy, + Default: false, + Usage: "Detach the \"default\" policy from the policy set for this " + + "token.", + }) + + f.IntVar(&IntVar{ + Name: "use-limit", + Target: &c.flagUseLimit, + Default: 0, + Usage: "Number of times this token can be used. After the last use, the " + + "token is automatically revoked. By default, tokens can be used an " + + "unlimited number of times until their expiration.", + }) + + f.StringVar(&StringVar{ + Name: "role", + Target: &c.flagRole, + Default: "", + Usage: "Name of the role to create the token against. Specifying -role " + + "may override other arguments. The locally authenticated Vault token " + + "must have permission for \"auth/token/create/\".", + }) + + f.StringMapVar(&StringMapVar{ + Name: "metadata", + Target: &c.flagMetadata, + Completion: complete.PredictAnything, + Usage: "Arbitary key=value metadata to associate with the token. " + + "This metadata will show in the audit log when the token is used. " + + "This can be specified multiple times to add multiple pieces of " + + "metadata.", + }) + + f.StringSliceVar(&StringSliceVar{ + Name: "policy", + Target: &c.flagPolicies, + Completion: c.PredictVaultPolicies(), + Usage: "Name of a policy to associate with this token. This can be " + + "specified multiple times to attach multiple policies.", + }) + + // Deprecated flags + // TODO: remove in 0.9.0 + f.DurationVar(&DurationVar{ + Name: "lease", // prefer -ttl + Target: &c.flagLease, + Default: 0, + Hidden: true, + }) + + return set +} + +func (c *TokenCreateCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *TokenCreateCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *TokenCreateCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + if len(args) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) + return 1 + } + + // TODO: remove in 0.9.0 + if c.flagLease != 0 { + c.UI.Warn("The -lease flag is deprecated. Please use -ttl instead.") + c.flagTTL = c.flagLease + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + tcr := &api.TokenCreateRequest{ + ID: c.flagID, + Policies: c.flagPolicies, + Metadata: c.flagMetadata, + TTL: c.flagTTL.String(), + NoParent: c.flagOrphan, + NoDefaultPolicy: c.flagNoDefaultPolicy, + DisplayName: c.flagDisplayName, + NumUses: c.flagUseLimit, + Renewable: &c.flagRenewable, + ExplicitMaxTTL: c.flagExplicitMaxTTL.String(), + Period: c.flagPeriod.String(), + } + + var secret *api.Secret + if c.flagRole != "" { + secret, err = client.Auth().Token().CreateWithRole(tcr, c.flagRole) + } else { + secret, err = client.Auth().Token().Create(tcr) + } + if err != nil { + c.UI.Error(fmt.Sprintf("Error creating token: %s", err)) + return 2 + } + + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + return OutputSecret(c.UI, c.flagFormat, secret) +} diff --git a/command/token_create_test.go b/command/token_create_test.go index 9db2a26a29..ec0cc79fb5 100644 --- a/command/token_create_test.go +++ b/command/token_create_test.go @@ -1,38 +1,243 @@ package command import ( + "reflect" "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestTokenCreate(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testTokenCreateCommand(tb testing.TB) (*cli.MockUi, *TokenCreateCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &TokenCreateCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &TokenCreateCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestTokenCreateCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"abcd1234"}, + "Too many arguments", + 1, + }, + { + "default", + nil, + "token", + 0, + }, + { + "metadata", + []string{"-metadata", "foo=bar", "-metadata", "zip=zap"}, + "token", + 0, + }, + { + "policies", + []string{"-policy", "foo", "-policy", "bar"}, + "token", + 0, + }, + { + "field", + []string{ + "-field", "token_renewable", + }, + "false", + 0, + }, + { + "field_not_found", + []string{ + "-field", "not-a-real-field", + }, + "not present in secret", + 1, + }, + { + "format", + []string{ + "-format", "json", + }, + "{", + 0, + }, + { + "format_bad", + []string{ + "-format", "nope-not-real", + }, + "Invalid output format", + 1, }, } - args := []string{ - "-address", addr, - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Ensure we get lease info - output := ui.OutputWriter.String() - if !strings.Contains(output, "token_duration") { - t.Fatalf("bad: %#v", output) - } + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testTokenCreateCommand(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("default", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testTokenCreateCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-field", "token", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + token := strings.TrimSpace(ui.OutputWriter.String()) + secret, err := client.Auth().Token().Lookup(token) + if secret == nil || err != nil { + t.Fatal(err) + } + }) + + t.Run("metadata", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testTokenCreateCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-metadata", "foo=bar", + "-metadata", "zip=zap", + "-field", "token", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + token := strings.TrimSpace(ui.OutputWriter.String()) + secret, err := client.Auth().Token().Lookup(token) + if secret == nil || err != nil { + t.Fatal(err) + } + + meta, ok := secret.Data["meta"].(map[string]interface{}) + if !ok { + t.Fatalf("missing meta: %#v", secret) + } + if _, ok := meta["foo"]; !ok { + t.Errorf("missing meta.foo: %#v", meta) + } + if _, ok := meta["zip"]; !ok { + t.Errorf("missing meta.bar: %#v", meta) + } + }) + + t.Run("policies", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testTokenCreateCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-policy", "foo", + "-policy", "bar", + "-field", "token", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + token := strings.TrimSpace(ui.OutputWriter.String()) + secret, err := client.Auth().Token().Lookup(token) + if secret == nil || err != nil { + t.Fatal(err) + } + + raw, ok := secret.Data["policies"].([]interface{}) + if !ok { + t.Fatalf("missing policies: %#v", secret) + } + + policies := make([]string, len(raw)) + for i := range raw { + policies[i] = raw[i].(string) + } + + expected := []string{"bar", "default", "foo"} + if !reflect.DeepEqual(policies, expected) { + t.Errorf("expected %q to be %q", policies, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testTokenCreateCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error creating token: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testTokenCreateCommand(t) + assertNoTabs(t, cmd) + }) } From 618665bf8d2188cca09f59c30fd76001de600eec Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:01 -0400 Subject: [PATCH 074/281] Update token-lookup command --- command/token_lookup.go | 162 ++++++++++++--------- command/token_lookup_test.go | 265 ++++++++++++++++++++++------------- 2 files changed, 261 insertions(+), 166 deletions(-) diff --git a/command/token_lookup.go b/command/token_lookup.go index c1c62ef716..5d51e666eb 100644 --- a/command/token_lookup.go +++ b/command/token_lookup.go @@ -5,96 +5,126 @@ import ( "strings" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) -// TokenLookupCommand is a Command that outputs details about the -// provided. +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*TokenLookupCommand)(nil) +var _ cli.CommandAutocomplete = (*TokenLookupCommand)(nil) + +// TokenLookupCommand is a Command that outputs details about the provided. type TokenLookupCommand struct { - meta.Meta + *BaseCommand + + flagAccessor bool +} + +func (c *TokenLookupCommand) Synopsis() string { + return "Displays information about a token" +} + +func (c *TokenLookupCommand) Help() string { + helpText := ` +Usage: vault token-lookup [options] [TOKEN | ACCESSOR] + + Displays information about a token or accessor. If a TOKEN is not provided, + the locally authenticated token is used. + + Get information about the locally authenticated token (this uses the + /auth/token/lookup-self endpoint and permission): + + $ vault token-lookup + + Get information about a particular token (this uses the /auth/token/lookup + endpoint and permission): + + $ vault token-lookup 96ddf4bc-d217-f3ba-f9bd-017055595017 + + Get information about a token via its accessor: + + $ vault token-lookup -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *TokenLookupCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "accessor", + Target: &c.flagAccessor, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Treat the argument as an accessor intead of a token. When " + + "this option is selected, the output will NOT include the token.", + }) + + return set +} + +func (c *TokenLookupCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *TokenLookupCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *TokenLookupCommand) Run(args []string) int { - var format string - var accessor bool - flags := c.Meta.FlagSet("token-lookup", meta.FlagSetDefault) - flags.BoolVar(&accessor, "accessor", false, "") - flags.StringVar(&format, "format", "table", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) > 1 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\ntoken-lookup expects at most one argument")) + token := "" + + args = f.Args() + switch { + case c.flagAccessor && len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments with -accessor (expected 1, got %d)", len(args))) + return 1 + case c.flagAccessor && len(args) > 1: + c.UI.Error(fmt.Sprintf("Too many arguments with -accessor (expected 1, got %d)", len(args))) + return 1 + case len(args) == 0: + // Use the local token + case len(args) == 1: + token = strings.TrimSpace(args[0]) + case len(args) > 1: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-1, got %d)", len(args))) return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } var secret *api.Secret switch { - case !accessor && len(args) == 0: + case token == "": secret, err = client.Auth().Token().LookupSelf() - case !accessor && len(args) == 1: - secret, err = client.Auth().Token().Lookup(args[0]) - case accessor && len(args) == 1: - secret, err = client.Auth().Token().LookupAccessor(args[0]) + case c.flagAccessor: + secret, err = client.Auth().Token().LookupAccessor(token) default: - // This happens only when accessor is set and no argument is passed - c.Ui.Error(fmt.Sprintf("token-lookup expects an argument when accessor flag is set")) - return 1 + secret, err = client.Auth().Token().Lookup(token) } if err != nil { - c.Ui.Error(fmt.Sprintf( - "error looking up token: %s", err)) - return 1 - } - return OutputSecret(c.Ui, format, secret) -} - -func doTokenLookup(args []string, client *api.Client) (*api.Secret, error) { - if len(args) == 0 { - return client.Auth().Token().LookupSelf() + c.UI.Error(fmt.Sprintf("Error looking up token: %s", err)) + return 2 } - token := args[0] - return client.Auth().Token().Lookup(token) -} - -func (c *TokenLookupCommand) Synopsis() string { - return "Display information about the specified token" -} - -func (c *TokenLookupCommand) Help() string { - helpText := ` -Usage: vault token-lookup [options] [token|accessor] - - Displays information about the specified token. If no token is specified, the - operation is performed on the currently authenticated token i.e. lookup-self. - Information about the token can be retrieved using the token accessor via the - '-accessor' flag. - -General Options: -` + meta.GeneralOptionsUsage() + ` -Token Lookup Options: - -accessor A boolean flag, if set, treats the argument as an accessor of the token. - Note that the response of the command when this is set, will not contain - the token ID. Accessor is only meant for looking up the token properties - (and for revocation via '/auth/token/revoke-accessor/' endpoint). - - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. - -` - return strings.TrimSpace(helpText) + return OutputSecret(c.UI, c.flagFormat, secret) } diff --git a/command/token_lookup_test.go b/command/token_lookup_test.go index 143b9447d6..eeeaefd2b3 100644 --- a/command/token_lookup_test.go +++ b/command/token_lookup_test.go @@ -1,124 +1,189 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestTokenLookupAccessor(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testTokenLookupCommand(tb testing.TB) (*cli.MockUi, *TokenLookupCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &TokenLookupCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &TokenLookupCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - args := []string{ - "-address", addr, - } - c.Run(args) +} - // Create a new token for us to use - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) +func TestTokenLookupCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "accessor_no_args", + []string{"-accessor"}, + "Not enough arguments", + 1, + }, + { + "accessor_too_many_args", + []string{"-accessor", "abcd1234", "efgh5678"}, + "Too many arguments", + 1, + }, + { + "too_many_args", + []string{"abcd1234", "efgh5678"}, + "Too many arguments", + 1, + }, + { + "format", + []string{"-format", "json"}, + "{", + 0, + }, + { + "format_bad", + []string{"-format", "nope-not-real"}, + "Invalid output format", + 1, + }, } - resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", + + 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() + + ui, cmd := testTokenLookupCommand(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) + } + }) + } }) - if err != nil { - t.Fatalf("err: %s", err) - } - // Enable the accessor flag - args = append(args, "-accessor") + t.Run("token", func(t *testing.T) { + t.Parallel() - // Expect failure if no argument is passed when accessor flag is set - code := c.Run(args) - if code == 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServer(t) + defer closer() - // Add token accessor as arg - args = append(args, resp.Auth.Accessor) - code = c.Run(args) - if code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} + token, _ := testTokenAndAccessor(t, client) -func TestTokenLookupSelf(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() + ui, cmd := testTokenLookupCommand(t) + cmd.client = client - ui := new(cli.MockUi) - c := &TokenLookupCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } + code := cmd.Run([]string{ + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } - args := []string{ - "-address", addr, - } - - // Run it against itself - code := c.Run(args) - - // Verify it worked - if code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} - -func TestTokenLookup(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &TokenLookupCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - } - // Run it once for client - c.Run(args) - - // Create a new token for us to use - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", + expected := token + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } }) - if err != nil { - t.Fatalf("err: %s", err) - } - // Add token as arg for real test and run it - args = append(args, resp.Auth.ClientToken) - code := c.Run(args) + t.Run("self", func(t *testing.T) { + t.Parallel() - // Verify it worked - if code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testTokenLookupCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "display_name" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("accessor", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + _, accessor := testTokenAndAccessor(t, client) + + ui, cmd := testTokenLookupCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-accessor", + accessor, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := accessor + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testTokenLookupCommand(t) + cmd.client = client + + code := cmd.Run([]string{}) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error looking up token: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testTokenLookupCommand(t) + assertNoTabs(t, cmd) + }) } From c2a78c6cfe51add7e6729fdaa3615497fd3c77c0 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:09 -0400 Subject: [PATCH 075/281] Update token-renew command --- command/token_renew.go | 173 +++++++++++--------- command/token_renew_test.go | 305 ++++++++++++++++++++---------------- 2 files changed, 264 insertions(+), 214 deletions(-) diff --git a/command/token_renew.go b/command/token_renew.go index 8ec1a550bb..19efb5e079 100644 --- a/command/token_renew.go +++ b/command/token_renew.go @@ -6,110 +6,133 @@ import ( "time" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/parseutil" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*TokenRenewCommand)(nil) +var _ cli.CommandAutocomplete = (*TokenRenewCommand)(nil) + // TokenRenewCommand is a Command that mounts a new mount. type TokenRenewCommand struct { - meta.Meta + *BaseCommand + + flagIncrement time.Duration +} + +func (c *TokenRenewCommand) Synopsis() string { + return "Renews token leases" +} + +func (c *TokenRenewCommand) Help() string { + helpText := ` +Usage: vault token-renew [options] [TOKEN] + + Renews a token's lease, extending the amount of time it can be used. If a + TOKEN is not provided, the locally authenticated token is used. Lease renewal + will fail if the token is not renewable, the token has already been revoked, + or if the token has already reached its maximum TTL. + + Renew a token (this uses the /auth/token/renew endpoint and permission): + + $ vault token-renew 96ddf4bc-d217-f3ba-f9bd-017055595017 + + Renew the currently authenticated token (this uses the /auth/token/renew-self + endpoint and permission): + + $ vault token-renew + + Renew a token requesting a specific increment value: + + $ vault token-renew -increment 30m 96ddf4bc-d217-f3ba-f9bd-017055595017 + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *TokenRenewCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.DurationVar(&DurationVar{ + Name: "increment", + Aliases: []string{"i"}, + Target: &c.flagIncrement, + Default: 0, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "Request a specific increment for renewal. Vault is not required " + + "to honor this request. If not supplied, Vault will use the default " + + "TTL. This is specified as a numeric string with suffix like \"30s\" " + + "or \"5m\".", + }) + + return set +} + +func (c *TokenRenewCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *TokenRenewCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *TokenRenewCommand) Run(args []string) int { - var format, increment string - flags := c.Meta.FlagSet("token-renew", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - flags.StringVar(&increment, "increment", "", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) > 2 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\ntoken-renew expects at most two arguments")) - return 1 - } + token := "" + increment := c.flagIncrement - var token string - if len(args) > 0 { - token = args[0] - } + args = f.Args() + switch len(args) { + case 0: + // Use the local token + case 1: + token = strings.TrimSpace(args[0]) + case 2: + // TODO: remove in 0.9.0 - backwards compat + c.UI.Warn("Specifying increment as a second argument is deprecated. " + + "Please use -increment instead.") - var inc int - // If both are specified prefer the argument - if len(args) == 2 { - increment = args[1] - } - if increment != "" { - dur, err := parseutil.ParseDurationSecond(increment) + token = strings.TrimSpace(args[0]) + parsed, err := time.ParseDuration(appendDurationSuffix(args[1])) if err != nil { - c.Ui.Error(fmt.Sprintf("Invalid increment: %s", err)) + c.UI.Error(fmt.Sprintf("Invalid increment: %s", err)) return 1 } - - inc = int(dur / time.Second) + increment = parsed + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } - // If the given token is the same as the client's, use renew-self instead - // as this is far more likely to be allowed via policy var secret *api.Secret + inc := truncateToSeconds(increment) if token == "" { secret, err = client.Auth().Token().RenewSelf(inc) } else { secret, err = client.Auth().Token().Renew(token, inc) } if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error renewing token: %s", err)) - return 1 + c.UI.Error(fmt.Sprintf("Error renewing token: %s", err)) + return 2 } - return OutputSecret(c.Ui, format, secret) -} - -func (c *TokenRenewCommand) Synopsis() string { - return "Renew an auth token if there is an associated lease" -} - -func (c *TokenRenewCommand) Help() string { - helpText := ` -Usage: vault token-renew [options] [token] [increment] - - Renew an auth token, extending the amount of time it can be used. If a token - is given to the command, '/auth/token/renew' will be called with the given - token; otherwise, '/auth/token/renew-self' will be called with the client - token. - - This command is similar to "renew", but "renew" is only for leases; this - command is only for tokens. - - An optional increment can be given to request a certain number of seconds to - increment the lease. This request is advisory; Vault may not adhere to it at - all. If a token is being passed in on the command line, the increment can as - well; otherwise it must be passed in via the '-increment' flag. - -General Options: -` + meta.GeneralOptionsUsage() + ` -Token Renew Options: - - -increment=3600 The desired increment. If not supplied, Vault will - use the default TTL. If supplied, it may still be - ignored. This can be submitted as an integer number - of seconds or a string duration (e.g. "72h"). - - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. - -` - return strings.TrimSpace(helpText) + return OutputSecret(c.UI, c.flagFormat, secret) } diff --git a/command/token_renew_test.go b/command/token_renew_test.go index 270ee7ec71..2c36412932 100644 --- a/command/token_renew_test.go +++ b/command/token_renew_test.go @@ -1,177 +1,204 @@ package command import ( + "encoding/json" + "strconv" + "strings" "testing" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestTokenRenew(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testTokenRenewCommand(tb testing.TB) (*cli.MockUi, *TokenRenewCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &TokenRenewCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &TokenRenewCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - args := []string{ - "-address", addr, - } - - // Run it once for client - c.Run(args) - - // Create a token - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", - }) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Renew, passing in the token - args = append(args, resp.Auth.ClientToken) - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } } -func TestTokenRenewWithIncrement(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestTokenRenewCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) - c := &TokenRenewCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar", "baz"}, + "Too many arguments", + 1, + }, + { + "default", + nil, + "", + 0, + }, + { + "increment", + []string{"-increment", "60s"}, + "", + 0, + }, + { + "increment_no_suffix", + []string{"-increment", "60"}, + "", + 0, + }, + { + "format", + []string{"-format", "json"}, + "{", + 0, + }, + { + "format_bad", + []string{"-format", "nope-not-real"}, + "Invalid output format", + 1, }, } - args := []string{ - "-address", addr, - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Run it once for client - c.Run(args) + for _, tc := range cases { + tc := tc - // Create a token - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + // Login with the token so we can renew-self. + token, _ := testTokenAndAccessor(t, client) + client.SetToken(token) + + ui, cmd := testTokenRenewCommand(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) + } + }) + } }) - if err != nil { - t.Fatalf("err: %s", err) - } - // Renew, passing in the token - args = append(args, resp.Auth.ClientToken) - args = append(args, "72h") - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} + t.Run("token", func(t *testing.T) { + t.Parallel() -func TestTokenRenewSelf(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() + client, closer := testVaultServer(t) + defer closer() - ui := new(cli.MockUi) - c := &TokenRenewCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } + token, _ := testTokenAndAccessor(t, client) - args := []string{ - "-address", addr, - } + _, cmd := testTokenRenewCommand(t) + cmd.client = client - // Run it once for client - c.Run(args) + code := cmd.Run([]string{ + "-increment", "30m", + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } - // Create a token - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", + secret, err := client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + str := string(secret.Data["ttl"].(json.Number)) + ttl, err := strconv.ParseInt(str, 10, 64) + if err != nil { + t.Fatalf("bad ttl: %#v", secret.Data["ttl"]) + } + if exp := int64(1800); ttl > exp { + t.Errorf("expected %d to be <= to %d", ttl, exp) + } }) - if err != nil { - t.Fatalf("err: %s", err) - } - if resp.Auth.ClientToken == "" { - t.Fatal("returned client token is empty") - } - c.Meta.ClientToken = resp.Auth.ClientToken + t.Run("self", func(t *testing.T) { + t.Parallel() - // Renew using the self endpoint - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } -} + client, closer := testVaultServer(t) + defer closer() -func TestTokenRenewSelfWithIncrement(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() + token, _ := testTokenAndAccessor(t, client) - ui := new(cli.MockUi) - c := &TokenRenewCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } + // Get the old token and login as the new token. We need the old token + // to query after the lookup, but we need the new token on the client. + oldToken := client.Token() + client.SetToken(token) - args := []string{ - "-address", addr, - } + _, cmd := testTokenRenewCommand(t) + cmd.client = client - // Run it once for client - c.Run(args) + code := cmd.Run([]string{ + "-increment", "30m", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } - // Create a token - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Lease: "1h", + client.SetToken(oldToken) + secret, err := client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + str := string(secret.Data["ttl"].(json.Number)) + ttl, err := strconv.ParseInt(str, 10, 64) + if err != nil { + t.Fatalf("bad ttl: %#v", secret.Data["ttl"]) + } + if exp := int64(1800); ttl > exp { + t.Errorf("expected %d to be <= to %d", ttl, exp) + } }) - if err != nil { - t.Fatalf("err: %s", err) - } - if resp.Auth.ClientToken == "" { - t.Fatal("returned client token is empty") - } - c.Meta.ClientToken = resp.Auth.ClientToken + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() - args = append(args, "-increment=72h") - // Renew using the self endpoint - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testTokenRenewCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "foo/bar", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error renewing token: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testTokenRenewCommand(t) + assertNoTabs(t, cmd) + }) } From 621774e4255de567de08851aca26fe25cda0f5f0 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:14 -0400 Subject: [PATCH 076/281] Update token-revoke command --- command/token_revoke.go | 246 +++++++++++++++++------------- command/token_revoke_test.go | 282 +++++++++++++++++++++++++---------- 2 files changed, 343 insertions(+), 185 deletions(-) diff --git a/command/token_revoke.go b/command/token_revoke.go index 6e4105d0f2..65b0867a96 100644 --- a/command/token_revoke.go +++ b/command/token_revoke.go @@ -4,135 +4,173 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*TokenRevokeCommand)(nil) +var _ cli.CommandAutocomplete = (*TokenRevokeCommand)(nil) + // TokenRevokeCommand is a Command that mounts a new mount. type TokenRevokeCommand struct { - meta.Meta + *BaseCommand + + flagAccessor bool + flagSelf bool + flagMode string +} + +func (c *TokenRevokeCommand) Synopsis() string { + return "Revokes tokens and their children" +} + +func (c *TokenRevokeCommand) Help() string { + helpText := ` +Usage: vault token-revoke [options] [TOKEN | ACCESSOR] + + Revokes authentication tokens and their children. If a TOKEN is not provided, + the locally authenticated token is used. The "-mode" flag can be used to + control the behavior of the revocation. See the "-mode" flag documentation + for more information. + + Revoke a token and all the token's children: + + $ vault token-revoke 96ddf4bc-d217-f3ba-f9bd-017055595017 + + Revoke a token leaving the token's children: + + $ vault token-revoke -mode=orphan 96ddf4bc-d217-f3ba-f9bd-017055595017 + + Revoke a token by accessor: + + $ vault token-revoke -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *TokenRevokeCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "accessor", + Target: &c.flagAccessor, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Treat the argument as an accessor instead of a token.", + }) + + f.BoolVar(&BoolVar{ + Name: "self", + Target: &c.flagSelf, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Perform the revocation on the currently authenticated token.", + }) + + f.StringVar(&StringVar{ + Name: "mode", + Target: &c.flagMode, + Default: "", + EnvVar: "", + Completion: complete.PredictSet("orphan", "path"), + Usage: "Type of revocation to perform. If unspecified, Vault will revoke " + + "the token and all of the token's children. If \"orphan\", Vault will " + + "revoke only the token, leaving the children as orphans. If \"path\", " + + "tokens created from the given authentication path prefix are deleted " + + "along with their children.", + }) + + return set +} + +func (c *TokenRevokeCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *TokenRevokeCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *TokenRevokeCommand) Run(args []string) int { - var mode string - var accessor bool - var self bool - var token string - flags := c.Meta.FlagSet("token-revoke", meta.FlagSetDefault) - flags.BoolVar(&accessor, "accessor", false, "") - flags.BoolVar(&self, "self", false, "") - flags.StringVar(&mode, "mode", "", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - switch { - case len(args) == 1 && !self: - token = args[0] - case len(args) != 0 && self: - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\ntoken-revoke expects no arguments when revoking self")) + args = f.Args() + token := "" + if len(args) > 0 { + token = strings.TrimSpace(args[0]) + } + + switch c.flagMode { + case "", "orphan", "path": + default: + c.UI.Error(fmt.Sprintf("Invalid mode: %s", c.flagMode)) return 1 - case len(args) != 1 && !self: - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\ntoken-revoke expects one argument or the 'self' flag")) + } + + switch { + case c.flagSelf && len(args) > 0: + c.UI.Error(fmt.Sprintf("Too many arguments with -self (expected 0, got %d)", len(args))) + return 1 + case !c.flagSelf && len(args) > 1: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1 or -self, got %d)", len(args))) + return 1 + case !c.flagSelf && len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1 or -self, got %d)", len(args))) + return 1 + case c.flagSelf && c.flagAccessor: + c.UI.Error("Cannot use -self with -accessor!") + return 1 + case c.flagSelf && c.flagMode != "": + c.UI.Error("Cannot use -self with -mode!") + return 1 + case c.flagAccessor && c.flagMode == "orphan": + c.UI.Error("Cannot use -accessor with -mode=orphan!") + return 1 + case c.flagAccessor && c.flagMode == "path": + c.UI.Error("Cannot use -accessor with -mode=path!") return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } - var fn func(string) error + var revokeFn func(string) error // Handle all 6 possible combinations switch { - case !accessor && self && mode == "": - fn = client.Auth().Token().RevokeSelf - case !accessor && !self && mode == "": - fn = client.Auth().Token().RevokeTree - case !accessor && !self && mode == "orphan": - fn = client.Auth().Token().RevokeOrphan - case !accessor && !self && mode == "path": - fn = client.Sys().RevokePrefix - case accessor && !self && mode == "": - fn = client.Auth().Token().RevokeAccessor - case accessor && self: - c.Ui.Error("token-revoke cannot be run on self when 'accessor' flag is set") - return 1 - case self && mode != "": - c.Ui.Error("token-revoke cannot be run on self when 'mode' flag is set") - return 1 - case accessor && mode == "orphan": - c.Ui.Error("token-revoke cannot be run for 'orphan' mode when 'accessor' flag is set") - return 1 - case accessor && mode == "path": - c.Ui.Error("token-revoke cannot be run for 'path' mode when 'accessor' flag is set") - return 1 + case !c.flagAccessor && c.flagSelf && c.flagMode == "": + revokeFn = client.Auth().Token().RevokeSelf + case !c.flagAccessor && !c.flagSelf && c.flagMode == "": + revokeFn = client.Auth().Token().RevokeTree + case !c.flagAccessor && !c.flagSelf && c.flagMode == "orphan": + revokeFn = client.Auth().Token().RevokeOrphan + case !c.flagAccessor && !c.flagSelf && c.flagMode == "path": + revokeFn = client.Sys().RevokePrefix + case c.flagAccessor && !c.flagSelf && c.flagMode == "": + revokeFn = client.Auth().Token().RevokeAccessor } - if err := fn(token); err != nil { - c.Ui.Error(fmt.Sprintf( - "Error revoking token: %s", err)) + if err := revokeFn(token); err != nil { + c.UI.Error(fmt.Sprintf("Error revoking token: %s", err)) return 2 } - c.Ui.Output("Success! Token revoked if it existed.") + c.UI.Output("Success! Revoked token (if it existed)") return 0 } - -func (c *TokenRevokeCommand) Synopsis() string { - return "Revoke one or more auth tokens" -} - -func (c *TokenRevokeCommand) Help() string { - helpText := ` -Usage: vault token-revoke [options] [token|accessor] - - Revoke one or more auth tokens. - - This command revokes auth tokens. Use the "revoke" command for - revoking secrets. - - Depending on the flags used, auth tokens can be revoked in multiple ways - depending on the "-mode" flag: - - * Without any value, the token specified and all of its children - will be revoked. - - * With the "orphan" value, only the specific token will be revoked. - All of its children will be orphaned. - - * With the "path" value, tokens created from the given auth path - prefix will be deleted, along with all their children. In this case - the "token" arg above is actually a "path". This mode does *not* - work with token values or parts of token values. - - Token can be revoked using the token accessor. This can be done by - setting the '-accessor' flag. Note that when '-accessor' flag is set, - '-mode' should not be set for 'orphan' or 'path'. This is because, - a token accessor always revokes the token along with its child tokens. - -General Options: -` + meta.GeneralOptionsUsage() + ` -Token Options: - - -accessor A boolean flag, if set, treats the argument as an accessor of the token. - Note that accessor can also be used for looking up the token properties - via '/auth/token/lookup-accessor/' endpoint. - Accessor is used when there is no access to token ID. - - -self A boolean flag, if set, the operation is performed on the currently - authenticated token i.e. lookup-self. - - -mode=value The type of revocation to do. See the documentation - above for more information. - -` - return strings.TrimSpace(helpText) -} diff --git a/command/token_revoke_test.go b/command/token_revoke_test.go index 7265a106ee..60a1345c4b 100644 --- a/command/token_revoke_test.go +++ b/command/token_revoke_test.go @@ -1,102 +1,222 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestTokenRevokeAccessor(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testTokenRevokeCommand(tb testing.TB) (*cli.MockUi, *TokenRevokeCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &TokenRevokeCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &TokenRevokeCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - args := []string{ - "-address", addr, - } - - // Run it once for client - c.Run(args) - - // Create a token - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(nil) - if err != nil { - t.Fatalf("err: %s", err) - } - - // Treat the argument as accessor - args = append(args, "-accessor") - if code := c.Run(args); code == 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - // Verify it worked with proper accessor - args1 := append(args, resp.Auth.Accessor) - if code := c.Run(args1); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - // Fail if mode is set to 'orphan' when accessor is set - args2 := append(args, "-mode=\"orphan\"") - if code := c.Run(args2); code == 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - // Fail if mode is set to 'path' when accessor is set - args3 := append(args, "-mode=\"path\"") - if code := c.Run(args3); code == 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } } -func TestTokenRevoke(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestTokenRevokeCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) - c := &TokenRevokeCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + validations := []struct { + name string + args []string + out string + code int + }{ + { + "bad_mode", + []string{"-mode=banana"}, + "Invalid mode", + 1, + }, + { + "empty", + nil, + "Not enough arguments", + 1, + }, + { + "args_with_self", + []string{"-self", "abcd1234"}, + "Too many arguments", + 1, + }, + { + "too_many_args", + []string{"abcd1234", "efgh5678"}, + "Too many arguments", + 1, + }, + { + "self_and_accessor", + []string{"-self", "-accessor"}, + "Cannot use -self with -accessor", + 1, + }, + { + "self_and_mode", + []string{"-self", "-mode=orphan"}, + "Cannot use -self with -mode", + 1, + }, + { + "accessor_and_mode_orphan", + []string{"-accessor", "-mode=orphan", "abcd1234"}, + "Cannot use -accessor with -mode=orphan", + 1, + }, + { + "accessor_and_mode_path", + []string{"-accessor", "-mode=path", "abcd1234"}, + "Cannot use -accessor with -mode=path", + 1, }, } - args := []string{ - "-address", addr, - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Run it once for client - c.Run(args) + for _, tc := range validations { + tc := tc - // Create a token - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - resp, err := client.Auth().Token().Create(nil) - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - // Verify it worked - args = append(args, resp.Auth.ClientToken) - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + ui, cmd := testTokenRevokeCommand(t) + + 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("token", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + token, _ := testTokenAndAccessor(t, client) + + ui, cmd := testTokenRevokeCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Revoked token" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + secret, err := client.Auth().Token().Lookup(token) + if secret != nil || err == nil { + t.Errorf("expected token to be revoked: %#v", secret) + } + }) + + t.Run("self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testTokenRevokeCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-self", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Revoked token" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + secret, err := client.Auth().Token().LookupSelf() + if secret != nil || err == nil { + t.Errorf("expected token to be revoked: %#v", secret) + } + }) + + t.Run("accessor", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + token, accessor := testTokenAndAccessor(t, client) + + ui, cmd := testTokenRevokeCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-accessor", + accessor, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Revoked token" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + secret, err := client.Auth().Token().Lookup(token) + if secret != nil || err == nil { + t.Errorf("expected token to be revoked: %#v", secret) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testTokenRevokeCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "abcd1234", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error revoking token: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testTokenRevokeCommand(t) + assertNoTabs(t, cmd) + }) } From a84b6e4173e9be5a73cc6ca2d8fcd44d7f5fa848 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:20 -0400 Subject: [PATCH 077/281] Update unmount command --- command/unmount.go | 117 +++++++++++++++++----------- command/unmount_test.go | 167 ++++++++++++++++++++++++++++++++-------- 2 files changed, 208 insertions(+), 76 deletions(-) diff --git a/command/unmount.go b/command/unmount.go index b04e532a39..cb3bc6266a 100644 --- a/command/unmount.go +++ b/command/unmount.go @@ -4,64 +4,91 @@ import ( "fmt" "strings" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*UnmountCommand)(nil) +var _ cli.CommandAutocomplete = (*UnmountCommand)(nil) + // UnmountCommand is a Command that mounts a new mount. type UnmountCommand struct { - meta.Meta -} - -func (c *UnmountCommand) Run(args []string) int { - flags := c.Meta.FlagSet("mount", meta.FlagSetDefault) - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - args = flags.Args() - if len(args) != 1 { - flags.Usage() - c.Ui.Error(fmt.Sprintf( - "\nunmount expects one argument: the path to unmount")) - return 1 - } - - path := args[0] - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - if err := client.Sys().Unmount(path); err != nil { - c.Ui.Error(fmt.Sprintf( - "Unmount error: %s", err)) - return 2 - } - - c.Ui.Output(fmt.Sprintf( - "Successfully unmounted '%s' if it was mounted", path)) - - return 0 + *BaseCommand } func (c *UnmountCommand) Synopsis() string { - return "Unmount a secret backend" + return "Unmounts a secret backend" } func (c *UnmountCommand) Help() string { helpText := ` -Usage: vault unmount [options] path +Usage: vault unmount [options] PATH - Unmount a secret backend. + Unmounts a secret backend at the given PATH. The argument corresponds to + the PATH of the mount, not the TYPE! All secrets created by this backend + are revoked and its Vault data is removed. - This command unmounts a secret backend. All the secrets created - by this backend will be revoked and its Vault data will be deleted. + If no mount exists at the given path, the command will still return as + successful because unmounting is an idempotent operation. + + Unmount the secret backend mounted at aws/: + + $ vault unmount aws/ + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() -General Options: -` + meta.GeneralOptionsUsage() return strings.TrimSpace(helpText) } + +func (c *UnmountCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *UnmountCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultMounts() +} + +func (c *UnmountCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *UnmountCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + mountPath, remaining, err := extractPath(args) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + if len(remaining) > 0 { + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Append a trailing slash to indicate it's a path in output + mountPath = ensureTrailingSlash(mountPath) + + if err := client.Sys().Unmount(mountPath); err != nil { + c.UI.Error(fmt.Sprintf("Error unmounting %s: %s", mountPath, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Unmounted the secret backend (if it existed) at: %s", mountPath)) + return 0 +} diff --git a/command/unmount_test.go b/command/unmount_test.go index 1af5ef8b0f..47057458f5 100644 --- a/command/unmount_test.go +++ b/command/unmount_test.go @@ -1,47 +1,152 @@ package command import ( + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" ) -func TestUnmount(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testUnmountCommand(tb testing.TB) (*cli.MockUi, *UnmountCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &UnmountCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &UnmountCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestUnmountCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty", + nil, + "Missing PATH!", + 1, + }, + { + "slash", + []string{"/"}, + "Missing PATH!", + 1, + }, + { + "not_real", + []string{"not_real"}, + "Success! Unmounted the secret backend (if it existed) at: not_real/", + 0, + }, + { + "default", + []string{"secret"}, + "Success! Unmounted the secret backend (if it existed) at: secret/", + 0, }, } - args := []string{ - "-address", addr, - "secret", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + for _, tc := range cases { + tc := tc - mounts, err := client.Sys().ListMounts() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - _, ok := mounts["secret/"] - if ok { - t.Fatal("should not have mount") - } + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testUnmountCommand(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("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().Mount("integration_unmount/", &api.MountInput{ + Type: "generic", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testUnmountCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "integration_unmount/", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Unmounted the secret backend (if it existed) at: integration_unmount/" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + mounts, err := client.Sys().ListMounts() + if err != nil { + t.Fatal(err) + } + + if _, ok := mounts["integration_unmount"]; ok { + t.Errorf("expected mount to not exist: %#v", mounts) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testUnmountCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "pki/", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error unmounting pki/: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testUnmountCommand(t) + assertNoTabs(t, cmd) + }) } From 80c3d4f31967eb07fcd8683214c4529deb02ddfc Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:27 -0400 Subject: [PATCH 078/281] update unseal command --- command/unseal.go | 230 +++++++++++++++++++++++------------------ command/unseal_test.go | 172 ++++++++++++++++++++---------- 2 files changed, 251 insertions(+), 151 deletions(-) diff --git a/command/unseal.go b/command/unseal.go index 2dfb9476de..495bf6e6c1 100644 --- a/command/unseal.go +++ b/command/unseal.go @@ -2,98 +2,27 @@ package command import ( "fmt" + "io" "os" "strings" + "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/password" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*UnsealCommand)(nil) +var _ cli.CommandAutocomplete = (*UnsealCommand)(nil) + // UnsealCommand is a Command that unseals the vault. type UnsealCommand struct { - meta.Meta + *BaseCommand - // Key can be used to pre-seed the key. If it is set, it will not - // be asked with the `password` helper. - Key string -} + flagReset bool -func (c *UnsealCommand) Run(args []string) int { - var reset bool - flags := c.Meta.FlagSet("unseal", meta.FlagSetDefault) - flags.BoolVar(&reset, "reset", false, "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { - return 1 - } - - client, err := c.Client() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) - return 2 - } - - sealStatus, err := client.Sys().SealStatus() - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error checking seal status: %s", err)) - return 2 - } - - if !sealStatus.Sealed { - c.Ui.Output("Vault is already unsealed.") - return 0 - } - - args = flags.Args() - if reset { - sealStatus, err = client.Sys().ResetUnsealProcess() - } else { - value := c.Key - if len(args) > 0 { - value = args[0] - } - if value == "" { - fmt.Printf("Key (will be hidden): ") - value, err = password.Read(os.Stdin) - fmt.Printf("\n") - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error attempting to ask for password. The raw error message\n"+ - "is shown below, but the most common reason for this error is\n"+ - "that you attempted to pipe a value into unseal or you're\n"+ - "executing `vault unseal` from outside of a terminal.\n\n"+ - "You should use `vault unseal` from a terminal for maximum\n"+ - "security. If this isn't an option, the unseal key can be passed\n"+ - "in using the first parameter.\n\n"+ - "Raw error: %s", err)) - return 1 - } - } - sealStatus, err = client.Sys().Unseal(strings.TrimSpace(value)) - } - - if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error: %s", err)) - return 1 - } - - c.Ui.Output(fmt.Sprintf( - "Sealed: %v\n"+ - "Key Shares: %d\n"+ - "Key Threshold: %d\n"+ - "Unseal Progress: %d\n"+ - "Unseal Nonce: %v", - sealStatus.Sealed, - sealStatus.N, - sealStatus.T, - sealStatus.Progress, - sealStatus.Nonce, - )) - - return 0 + testOutput io.Writer // for tests } func (c *UnsealCommand) Synopsis() string { @@ -102,27 +31,132 @@ func (c *UnsealCommand) Synopsis() string { func (c *UnsealCommand) Help() string { helpText := ` -Usage: vault unseal [options] [key] +Usage: vault unseal [options] [KEY] - Unseal the vault by entering a portion of the master key. Once all - portions are entered, the vault will be unsealed. + Provide a portion of the master key to unseal a Vault server. Vault starts + in a sealed state. It cannot perform operations until it is unsealed. This + command accepts a portion of the master key (an "unseal key"). - Every Vault server initially starts as sealed. It cannot perform any - operation except unsealing until it is sealed. Secrets cannot be accessed - in any way until the vault is unsealed. This command allows you to enter - a portion of the master key to unseal the vault. + The unseal key can be supplied as an argument to the command, but this is + not recommended as the unseal key will be available in your history: - The unseal key can be specified via the command line, but this is - not recommended. The key may then live in your terminal history. This - only exists to assist in scripting. + $ vault unseal IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= -General Options: -` + meta.GeneralOptionsUsage() + ` -Unseal Options: + Instead, run the command with no arguments and it will prompt for the key: - -reset Reset the unsealing process by throwing away - prior keys in process to unseal the vault. + $ vault unseal + Key (will be hidden): IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() -` return strings.TrimSpace(helpText) } + +func (c *UnsealCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "reset", + Aliases: []string{}, + Target: &c.flagReset, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Discard any previously entered keys to the unseal process.", + }) + + return set +} + +func (c *UnsealCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *UnsealCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *UnsealCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + unsealKey := "" + + args = f.Args() + switch len(args) { + case 0: + // We will prompt for the unsealKey later + case 1: + unsealKey = strings.TrimSpace(args[0]) + default: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if c.flagReset { + status, err := client.Sys().ResetUnsealProcess() + if err != nil { + c.UI.Error(fmt.Sprintf("Error resetting unseal process: %s", err)) + return 2 + } + c.prettySealStatus(status) + return 0 + } + + if unsealKey == "" { + // Override the output + writer := (io.Writer)(os.Stdout) + if c.testOutput != nil { + writer = c.testOutput + } + + fmt.Fprintf(writer, "Key (will be hidden): ") + value, err := password.Read(os.Stdin) + fmt.Fprintf(writer, "\n") + if err != nil { + c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+ + "ask for an unseal key. The raw error message is shown below, but "+ + "usually this is because you attempted to pipe a value into the "+ + "unseal command or you are executing outside of a terminal (tty). "+ + "You should run the unseal command from a terminal for maximum "+ + "security. If this is not an option, the unseal can be provided as "+ + "the first argument to the unseal command. The raw error "+ + "was:\n\n%s", err))) + return 1 + } + unsealKey = strings.TrimSpace(value) + } + + status, err := client.Sys().Unseal(unsealKey) + if err != nil { + c.UI.Error(fmt.Sprintf("Error unsealing: %s", err)) + return 2 + } + + c.prettySealStatus(status) + return 0 +} + +func (c *UnsealCommand) prettySealStatus(status *api.SealStatusResponse) { + c.UI.Output(fmt.Sprintf("Sealed: %t", status.Sealed)) + c.UI.Output(fmt.Sprintf("Key Shares: %d", status.N)) + c.UI.Output(fmt.Sprintf("Key Threshold: %d", status.T)) + c.UI.Output(fmt.Sprintf("Unseal Progress: %d", status.Progress)) + if status.Nonce != "" { + c.UI.Output(fmt.Sprintf("Unseal Nonce: %s", status.Nonce)) + } +} diff --git a/command/unseal_test.go b/command/unseal_test.go index 699fdd8fb7..bc8d2e8d22 100644 --- a/command/unseal_test.go +++ b/command/unseal_test.go @@ -1,72 +1,138 @@ package command import ( - "encoding/hex" + "fmt" + "io/ioutil" + "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestUnseal(t *testing.T) { - core := vault.TestCore(t) - keys, _ := vault.TestCoreInit(t, core) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testUnsealCommand(tb testing.TB) (*cli.MockUi, *UnsealCommand) { + tb.Helper() - ui := new(cli.MockUi) - - for _, key := range keys { - c := &UnsealCommand{ - Key: hex.EncodeToString(key), - Meta: meta.Meta{ - Ui: ui, - }, - } - - args := []string{"-address", addr} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - } - - sealed, err := core.Sealed() - if err != nil { - t.Fatalf("err: %s", err) - } - if sealed { - t.Fatal("should not be sealed") + ui := cli.NewMockUi() + return ui, &UnsealCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, } } -func TestUnseal_arg(t *testing.T) { - core := vault.TestCore(t) - keys, _ := vault.TestCoreInit(t, core) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestUnsealCommand_Run(t *testing.T) { + t.Parallel() - ui := new(cli.MockUi) + t.Run("error_non_terminal", func(t *testing.T) { + t.Parallel() - for _, key := range keys { - c := &UnsealCommand{ - Meta: meta.Meta{ - Ui: ui, - }, + ui, cmd := testUnsealCommand(t) + cmd.testOutput = ioutil.Discard + + code := cmd.Run(nil) + if exp := 1; code != exp { + t.Errorf("expected %d to be %d", code, exp) } - args := []string{"-address", addr, hex.EncodeToString(key)} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + expected := "is not a terminal" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) } - } + }) - sealed, err := core.Sealed() - if err != nil { - t.Fatalf("err: %s", err) - } - if sealed { - t.Fatal("should not be sealed") - } + t.Run("reset", func(t *testing.T) { + t.Parallel() + + client, keys, closer := testVaultServerUnseal(t) + defer closer() + + // Seal so we can unseal + if err := client.Sys().Seal(); err != nil { + t.Fatal(err) + } + + // Enter an unseal key + if _, err := client.Sys().Unseal(keys[0]); err != nil { + t.Fatal(err) + } + + ui, cmd := testUnsealCommand(t) + cmd.client = client + cmd.testOutput = ioutil.Discard + + // Reset and check output + code := cmd.Run([]string{ + "-reset", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + expected := "Unseal Progress: 0" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("full", func(t *testing.T) { + t.Parallel() + + client, keys, closer := testVaultServerUnseal(t) + defer closer() + + // Seal so we can unseal + if err := client.Sys().Seal(); err != nil { + t.Fatal(err) + } + + for i, key := range keys { + ui, cmd := testUnsealCommand(t) + cmd.client = client + cmd.testOutput = ioutil.Discard + + // Reset and check output + code := cmd.Run([]string{ + key, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + expected := fmt.Sprintf("Unseal Progress: %d", (i+1)%3) // 1, 2, 0 + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testUnsealCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "abcd", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error unsealing: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testUnsealCommand(t) + assertNoTabs(t, cmd) + }) } From 01d4b5dd0945bf4221c0aa07275495668b4fd21f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:33 -0400 Subject: [PATCH 079/281] Update unwrap command --- command/unwrap.go | 137 ++++++++++++------------- command/unwrap_test.go | 223 ++++++++++++++++++++++++++--------------- 2 files changed, 214 insertions(+), 146 deletions(-) diff --git a/command/unwrap.go b/command/unwrap.go index 5a21920eb5..83d1cbdf83 100644 --- a/command/unwrap.go +++ b/command/unwrap.go @@ -1,104 +1,107 @@ package command import ( - "flag" "fmt" "strings" - "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" + "github.com/posener/complete" ) -// UnwrapCommand is a Command that behaves like ReadCommand but specifically -// for unwrapping cubbyhole-wrapped secrets +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*UnwrapCommand)(nil) +var _ cli.CommandAutocomplete = (*UnwrapCommand)(nil) + +// UnwrapCommand is a Command that behaves like ReadCommand but specifically for +// unwrapping cubbyhole-wrapped secrets type UnwrapCommand struct { - meta.Meta + *BaseCommand +} + +func (c *UnwrapCommand) Synopsis() string { + return "Unwraps a wrapped secret" +} + +func (c *UnwrapCommand) Help() string { + helpText := ` +Usage: vault unwrap [options] [TOKEN] + + Unwraps a wrapped secret from Vault by the given token. The result is the + same as the "vault read" operation on the non-wrapped secret. If no token + is given, the data in the currently authenticated token is unwrapped. + + Unwrap the data in the cubbyhole backend for a token: + + $ vault unwrap 3de9ece1-b347-e143-29b0-dc2dc31caafd + + Unwrap the data in the active token: + + $ vault auth 848f9ccf-7176-098c-5e2b-75a0689d41cd + $ vault unwrap # unwraps 848f9ccf... + + For a full list of examples and paths, please see the online documentation. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *UnwrapCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) +} + +func (c *UnwrapCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFiles() +} + +func (c *UnwrapCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *UnwrapCommand) Run(args []string) int { - var format string - var field string - var err error - var secret *api.Secret - var flags *flag.FlagSet - flags = c.Meta.FlagSet("unwrap", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - flags.StringVar(&field, "field", "", "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - var tokenID string - - args = flags.Args() + args = f.Args() + token := "" switch len(args) { case 0: + // Leave token as "", that will use the local token case 1: - tokenID = args[0] + token = strings.TrimSpace(args[0]) default: - c.Ui.Error("unwrap expects zero or one argument (the ID of the wrapping token)") - flags.Usage() + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-1, got %d)", len(args))) return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } - secret, err = client.Logical().Unwrap(tokenID) + secret, err := client.Logical().Unwrap(token) if err != nil { - c.Ui.Error(err.Error()) - return 1 + c.UI.Error(fmt.Sprintf("Error unwrapping: %s", err)) + return 2 } if secret == nil { - c.Ui.Error("Server gave empty response or secret returned was empty") - return 1 + c.UI.Error("Could not find wrapped response") + return 2 } // Handle single field output - if field != "" { - return PrintRawField(c.Ui, secret, field) + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) } - // Check if the original was a list response and format as a list if so - if secret.Data != nil && - len(secret.Data) == 1 && - secret.Data["keys"] != nil { - _, ok := secret.Data["keys"].([]interface{}) - if ok { - return OutputList(c.Ui, format, secret) - } + // Check if the original was a list response and format as a list + if _, ok := extractListData(secret); ok { + return OutputList(c.UI, c.flagFormat, secret) } - return OutputSecret(c.Ui, format, secret) -} - -func (c *UnwrapCommand) Synopsis() string { - return "Unwrap a wrapped secret" -} - -func (c *UnwrapCommand) Help() string { - helpText := ` -Usage: vault unwrap [options] - - Unwrap a wrapped secret. - - Unwraps the data wrapped by the given token ID. The returned result is the - same as a 'read' operation on a non-wrapped secret. - -General Options: -` + meta.GeneralOptionsUsage() + ` -Read Options: - - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. - - -field=field If included, the raw value of the specified field - will be output raw to stdout. - -` - return strings.TrimSpace(helpText) + return OutputSecret(c.UI, c.flagFormat, secret) } diff --git a/command/unwrap_test.go b/command/unwrap_test.go index e5dc0bfd33..2d90d47d42 100644 --- a/command/unwrap_test.go +++ b/command/unwrap_test.go @@ -4,104 +4,169 @@ import ( "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" ) -func TestUnwrap(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testUnwrapCommand(tb testing.TB) (*cli.MockUi, *UnwrapCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &UnwrapCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &UnwrapCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func testUnwrapWrappedToken(tb testing.TB, client *api.Client, data map[string]interface{}) string { + tb.Helper() + + wrapped, err := client.Logical().Write("sys/wrapping/wrap", data) + if err != nil { + tb.Fatal(err) + } + if wrapped == nil || wrapped.WrapInfo == nil || wrapped.WrapInfo.Token == "" { + tb.Fatalf("missing wrap info: %v", wrapped) + } + return wrapped.WrapInfo.Token +} + +func TestUnwrapCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "default", + nil, // Token comes in the test func + "bar", + 0, + }, + { + "field", + []string{"-field", "foo"}, + "bar", + 0, + }, + { + "field_not_found", + []string{"-field", "not-a-real-field"}, + "not present in secret", + 1, + }, + { + "format", + []string{"-format", "json"}, + "{", + 0, + }, + { + "format_bad", + []string{"-format", "nope-not-real"}, + "Invalid output format", + 1, }, } - args := []string{ - "-address", addr, - "-field", "zip", - } + t.Run("validations", func(t *testing.T) { + t.Parallel() - // Run once so the client is setup, ignore errors - c.Run(args) + for _, tc := range cases { + tc := tc - // Get the client so we can write data - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - wrapLookupFunc := func(method, path string) string { - if method == "GET" && path == "secret/foo" { - return "60s" + client, closer := testVaultServer(t) + defer closer() + + wrappedToken := testUnwrapWrappedToken(t, client, map[string]interface{}{ + "foo": "bar", + }) + + ui, cmd := testUnwrapCommand(t) + cmd.client = client + + tc.args = append(tc.args, wrappedToken) + 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) + } + }) } - if method == "LIST" && path == "secret" { - return "60s" + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testUnwrapCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "foo", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) } - return "" - } - client.SetWrappingLookupFunc(wrapLookupFunc) - data := map[string]interface{}{"zip": "zap"} - if _, err := client.Logical().Write("secret/foo", data); err != nil { - t.Fatalf("err: %s", err) - } + expected := "Error unwrapping: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) - outer, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } - if outer == nil { - t.Fatal("outer response was nil") - } - if outer.WrapInfo == nil { - t.Fatalf("outer wrapinfo was nil, response was %#v", *outer) - } + // This test needs its own client and server because it modifies the client + // to the wrapping token + t.Run("local_token", func(t *testing.T) { + t.Parallel() - args = append(args, outer.WrapInfo.Token) + client, closer := testVaultServer(t) + defer closer() - // Run the read - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + wrappedToken := testUnwrapWrappedToken(t, client, map[string]interface{}{ + "foo": "bar", + }) - output := ui.OutputWriter.String() - if output != "zap\n" { - t.Fatalf("unexpectd output:\n%s", output) - } + ui, cmd := testUnwrapCommand(t) + cmd.client = client + cmd.client.SetToken(wrappedToken) - // Now test with list handling, specifically that it will be called with - // the list output formatter - ui.OutputWriter.Reset() + // Intentionally don't pass the token here - it shoudl use the local token + code := cmd.Run([]string{}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } - outer, err = client.Logical().List("secret") - if err != nil { - t.Fatalf("err: %s", err) - } - if outer == nil { - t.Fatal("outer response was nil") - } - if outer.WrapInfo == nil { - t.Fatalf("outer wrapinfo was nil, response was %#v", *outer) - } + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, "bar") { + t.Errorf("expected %q to contain %q", combined, "bar") + } + }) - args = []string{ - "-address", addr, - outer.WrapInfo.Token, - } - // Run the read - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() - output = ui.OutputWriter.String() - if strings.TrimSpace(output) != "Keys\n----\nfoo" { - t.Fatalf("unexpected output:\n%s", output) - } + _, cmd := testUnwrapCommand(t) + assertNoTabs(t, cmd) + }) } From f161584f0d2599d567e6b80a66afa849a05740f9 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:41 -0400 Subject: [PATCH 080/281] Update version command --- command/version.go | 47 +++++++++++++++++++++++++++++++++++------ command/version_test.go | 41 +++++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/command/version.go b/command/version.go index 4665436e2e..b1ced4ee9b 100644 --- a/command/version.go +++ b/command/version.go @@ -1,18 +1,55 @@ package command import ( + "strings" + "github.com/hashicorp/vault/version" "github.com/mitchellh/cli" + "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*VersionCommand)(nil) +var _ cli.CommandAutocomplete = (*VersionCommand)(nil) + // VersionCommand is a Command implementation prints the version. type VersionCommand struct { + *BaseCommand + VersionInfo *version.VersionInfo - Ui cli.Ui +} + +func (c *VersionCommand) Synopsis() string { + return "Prints the Vault CLI version" } func (c *VersionCommand) Help() string { - return "" + helpText := ` +Usage: vault version + + Prints the version of this Vault CLI. This does not print the target Vault + server version. + + Print the version: + + $ vault version + + There are no arguments or flags to this command. Any additional arguments or + flags are ignored. +` + return strings.TrimSpace(helpText) +} + +func (c *VersionCommand) Flags() *FlagSets { + return nil +} + +func (c *VersionCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *VersionCommand) AutocompleteFlags() complete.Flags { + return nil } func (c *VersionCommand) Run(_ []string) int { @@ -20,10 +57,6 @@ func (c *VersionCommand) Run(_ []string) int { if version.CgoEnabled { out += " (cgo)" } - c.Ui.Output(out) + c.UI.Output(out) return 0 } - -func (c *VersionCommand) Synopsis() string { - return "Prints the Vault version" -} diff --git a/command/version_test.go b/command/version_test.go index 2a645690fa..2f132fed79 100644 --- a/command/version_test.go +++ b/command/version_test.go @@ -1,11 +1,48 @@ package command import ( + "strings" "testing" + "github.com/hashicorp/vault/version" "github.com/mitchellh/cli" ) -func TestVersionCommand_implements(t *testing.T) { - var _ cli.Command = &VersionCommand{} +func testVersionCommand(tb testing.TB) (*cli.MockUi, *VersionCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &VersionCommand{ + VersionInfo: &version.VersionInfo{}, + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestVersionCommand_Run(t *testing.T) { + t.Parallel() + + t.Run("output", func(t *testing.T) { + t.Parallel() + + ui, cmd := testVersionCommand(t) + code := cmd.Run(nil) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Vault" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to equal %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testVersionCommand(t) + assertNoTabs(t, cmd) + }) } From 9d1b0e640fdf1e54fbb73c5323813e04509976f7 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:47 -0400 Subject: [PATCH 081/281] Update write command --- command/write.go | 222 ++++++++++---------- command/write_test.go | 469 ++++++++++++++++++++---------------------- 2 files changed, 331 insertions(+), 360 deletions(-) diff --git a/command/write.go b/command/write.go index 6f7b495b40..3046e2b38d 100644 --- a/command/write.go +++ b/command/write.go @@ -6,149 +6,145 @@ import ( "os" "strings" - "github.com/hashicorp/vault/helper/kv-builder" - "github.com/hashicorp/vault/meta" + "github.com/mitchellh/cli" "github.com/posener/complete" ) +// Ensure we are implementing the right interfaces. +var _ cli.Command = (*WriteCommand)(nil) +var _ cli.CommandAutocomplete = (*WriteCommand)(nil) + // WriteCommand is a Command that puts data into the Vault. type WriteCommand struct { - meta.Meta + *BaseCommand - // The fields below can be overwritten for tests - testStdin io.Reader + flagForce bool + + testStdin io.Reader // for tests +} + +func (c *WriteCommand) Synopsis() string { + return "Writes data, configuration, and secrets" +} + +func (c *WriteCommand) Help() string { + helpText := ` +Usage: vault write [options] PATH [DATA K=V...] + + Writes data to Vault at the given path. The data can be credentials, secrets, + configuration, or arbitrary data. The specific behavior of this command is + determined at the backend mounted at the path. + + Data is specified as "key=value" pairs. If the value begins with an "@", then + it is loaded from a file. If the value is "-", Vault will read the value from + stdin. + + Persist data in the static secret backend: + + $ vault write secret/my-secret foo=bar + + Create a new encryption key in the transit backend: + + $ vault write -f transit/keys/my-key + + Upload an AWS IAM policy from a file on disk: + + $ vault write aws/roles/ops policy=@policy.json + + Configure access to Consul by providing an access token: + + $ echo $MY_TOKEN | vault write consul/config/access token=- + + For a full list of examples and paths, please see the documentation that + corresponds to the secret backend in use. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *WriteCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.BoolVar(&BoolVar{ + Name: "force", + Aliases: []string{"f"}, + Target: &c.flagForce, + Default: false, + EnvVar: "", + Completion: complete.PredictNothing, + Usage: "Allow the operation to continue with no key=value pairs. This " + + "allows writing to keys that do not need or expect data.", + }) + + return set +} + +func (c *WriteCommand) AutocompleteArgs() complete.Predictor { + // Return an anything predictor here. Without a way to access help + // information, we don't know what paths we could write to. + return complete.PredictAnything +} + +func (c *WriteCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *WriteCommand) Run(args []string) int { - var field, format string - var force bool - flags := c.Meta.FlagSet("write", meta.FlagSetDefault) - flags.StringVar(&format, "format", "table", "") - flags.StringVar(&field, "field", "", "") - flags.BoolVar(&force, "force", false, "") - flags.BoolVar(&force, "f", false, "") - flags.Usage = func() { c.Ui.Error(c.Help()) } - if err := flags.Parse(args); err != nil { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) return 1 } - args = flags.Args() - if len(args) < 1 { - c.Ui.Error("write requires a path") - flags.Usage() - return 1 - } - - if len(args) < 2 && !force { - c.Ui.Error("write expects at least two arguments; use -f to perform the write anyways") - flags.Usage() - return 1 - } - - path := args[0] - if path[0] == '/' { - path = path[1:] - } - - data, err := c.parseData(args[1:]) + path, kvs, err := extractPath(f.Args()) if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error loading data: %s", err)) + c.UI.Error(err.Error()) + return 1 + } + + if len(kvs) == 0 && !c.flagForce { + c.UI.Error("Missing DATA! Specify at least one K=V pair or use -force.") + return 1 + } + + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + data, err := parseArgsData(stdin, kvs) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err)) return 1 } client, err := c.Client() if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error initializing client: %s", err)) + c.UI.Error(err.Error()) return 2 } secret, err := client.Logical().Write(path, data) if err != nil { - c.Ui.Error(fmt.Sprintf( - "Error writing data to %s: %s", path, err)) - return 1 + c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) + return 2 } - if secret == nil { - // Don't output anything if people aren't using the "human" output - if format == "table" { - c.Ui.Output(fmt.Sprintf("Success! Data written to: %s", path)) + // Don't output anything unless using the "table" format + if c.flagFormat == "table" { + c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path)) } return 0 } // Handle single field output - if field != "" { - return PrintRawField(c.Ui, secret, field) + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) } - return OutputSecret(c.Ui, format, secret) -} - -func (c *WriteCommand) parseData(args []string) (map[string]interface{}, error) { - var stdin io.Reader = os.Stdin - if c.testStdin != nil { - stdin = c.testStdin - } - - builder := &kvbuilder.Builder{Stdin: stdin} - if err := builder.Add(args...); err != nil { - return nil, err - } - - return builder.Map(), nil -} - -func (c *WriteCommand) Synopsis() string { - return "Write secrets or configuration into Vault" -} - -func (c *WriteCommand) Help() string { - helpText := ` -Usage: vault write [options] path [data] - - Write data (secrets or configuration) into Vault. - - Write sends data into Vault at the given path. The behavior of the write is - determined by the backend at the given path. For example, writing to - "aws/policy/ops" will create an "ops" IAM policy for the AWS backend - (configuration), but writing to "consul/foo" will write a value directly into - Consul at that key. Check the documentation of the logical backend you're - using for more information on key structure. - - Data is sent via additional arguments in "key=value" pairs. If value begins - with an "@", then it is loaded from a file. Write expects data in the file to - be in JSON format. If you want to start the value with a literal "@", then - prefix the "@" with a slash: "\@". - -General Options: -` + meta.GeneralOptionsUsage() + ` -Write Options: - - -f | -force Force the write to continue without any data values - specified. This allows writing to keys that do not - need or expect any fields to be specified. - - -format=table The format for output. By default it is a whitespace- - delimited table. This can also be json or yaml. - - -field=field If included, the raw value of the specified field - will be output raw to stdout. - -` - return strings.TrimSpace(helpText) -} - -func (c *WriteCommand) AutocompleteArgs() complete.Predictor { - return complete.PredictNothing -} - -func (c *WriteCommand) AutocompleteFlags() complete.Flags { - return complete.Flags{ - "-force": complete.PredictNothing, - "-format": predictFormat, - "-field": complete.PredictNothing, - } + return OutputSecret(c.UI, c.flagFormat, secret) } diff --git a/command/write_test.go b/command/write_test.go index 5aa3c1e559..7139e7a514 100644 --- a/command/write_test.go +++ b/command/write_test.go @@ -2,271 +2,246 @@ package command import ( "io" - "io/ioutil" - "os" "strings" "testing" - "github.com/hashicorp/vault/http" - "github.com/hashicorp/vault/meta" - "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) -func TestWrite(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func testWriteCommand(tb testing.TB) (*cli.MockUi, *WriteCommand) { + tb.Helper() - ui := new(cli.MockUi) - c := &WriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + ui := cli.NewMockUi() + return ui, &WriteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, }, } - - args := []string{ - "-address", addr, - "secret/foo", - "value=bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - resp, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } - - if resp.Data["value"] != "bar" { - t.Fatalf("bad: %#v", resp) - } } -func TestWrite_arbitrary(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() +func TestWriteCommand_Run(t *testing.T) { + t.Parallel() - stdinR, stdinW := io.Pipe() - ui := new(cli.MockUi) - c := &WriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + cases := []struct { + name string + args []string + out string + code int + }{ + { + "empty_path", + nil, + "Missing PATH!", + 1, }, - - testStdin: stdinR, - } - - go func() { - stdinW.Write([]byte(`{"foo":"bar"}`)) - stdinW.Close() - }() - - args := []string{ - "-address", addr, - "secret/foo", - "-", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - resp, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } - - if resp.Data["foo"] != "bar" { - t.Fatalf("bad: %#v", resp) - } -} - -func TestWrite_escaped(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &WriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, + { + "empty_kvs", + []string{"secret/write/foo"}, + "Missing DATA!", + 1, + }, + { + "force_kvs", + []string{"-force", "auth/token/create"}, + "token", + 0, + }, + { + "force_f_kvs", + []string{"-f", "auth/token/create"}, + "token", + 0, + }, + { + "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, + }, + { + "field", + []string{ + "-field", "token_renewable", + "auth/token/create", "display_name=foo", + }, + "false", + 0, + }, + { + "field_not_found", + []string{ + "-field", "not-a-real-field", + "auth/token/create", "display_name=foo", + }, + "not present in secret", + 1, }, } - args := []string{ - "-address", addr, - "secret/foo", - "value=\\@bar", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testWriteCommand(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) + } + }) } - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } + t.Run("stdin_full", func(t *testing.T) { + t.Parallel() - resp, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } + client, closer := testVaultServer(t) + defer closer() - if resp.Data["value"] != "@bar" { - t.Fatalf("bad: %#v", resp) - } -} - -func TestWrite_file(t *testing.T) { - tf, err := ioutil.TempFile("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - tf.Write([]byte(`{"foo":"bar"}`)) - tf.Close() - defer os.Remove(tf.Name()) - - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &WriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "secret/foo", - "@" + tf.Name(), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - resp, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } - - if resp.Data["foo"] != "bar" { - t.Fatalf("bad: %#v", resp) - } -} - -func TestWrite_fileValue(t *testing.T) { - tf, err := ioutil.TempFile("", "vault") - if err != nil { - t.Fatalf("err: %s", err) - } - tf.Write([]byte("foo")) - tf.Close() - defer os.Remove(tf.Name()) - - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &WriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "secret/foo", - "value=@" + tf.Name(), - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - - client, err := c.Client() - if err != nil { - t.Fatalf("err: %s", err) - } - - resp, err := client.Logical().Read("secret/foo") - if err != nil { - t.Fatalf("err: %s", err) - } - - if resp.Data["value"] != "foo" { - t.Fatalf("bad: %#v", resp) - } -} - -func TestWrite_Output(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &WriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "auth/token/create", - "display_name=foo", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } - if !strings.Contains(ui.OutputWriter.String(), "Key") { - t.Fatalf("bad: %s", ui.OutputWriter.String()) - } -} - -func TestWrite_force(t *testing.T) { - core, _, token := vault.TestCoreUnsealed(t) - ln, addr := http.TestServer(t, core) - defer ln.Close() - - ui := new(cli.MockUi) - c := &WriteCommand{ - Meta: meta.Meta{ - ClientToken: token, - Ui: ui, - }, - } - - args := []string{ - "-address", addr, - "-force", - "sys/rotate", - } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) - } + stdinR, stdinW := io.Pipe() + go func() { + stdinW.Write([]byte(`{"foo":"bar"}`)) + stdinW.Close() + }() + + _, cmd := testWriteCommand(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 := testWriteCommand(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 := testWriteCommand(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("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testWriteCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "foo/bar", "a=b", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error writing data to foo/bar: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testWriteCommand(t) + assertNoTabs(t, cmd) + }) } From b96015a3868c7ccb9d458ac2c9561c290751f154 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 5 Sep 2017 00:05:53 -0400 Subject: [PATCH 082/281] Wire all commands together --- command/commands.go | 474 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 command/commands.go diff --git a/command/commands.go b/command/commands.go new file mode 100644 index 0000000000..43036e4d43 --- /dev/null +++ b/command/commands.go @@ -0,0 +1,474 @@ +package command + +import ( + "os" + "os/signal" + "syscall" + + "github.com/hashicorp/vault/audit" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/physical" + "github.com/hashicorp/vault/version" + "github.com/mitchellh/cli" + + "github.com/hashicorp/vault/builtin/logical/aws" + "github.com/hashicorp/vault/builtin/logical/cassandra" + "github.com/hashicorp/vault/builtin/logical/consul" + "github.com/hashicorp/vault/builtin/logical/database" + "github.com/hashicorp/vault/builtin/logical/mongodb" + "github.com/hashicorp/vault/builtin/logical/mssql" + "github.com/hashicorp/vault/builtin/logical/mysql" + "github.com/hashicorp/vault/builtin/logical/pki" + "github.com/hashicorp/vault/builtin/logical/postgresql" + "github.com/hashicorp/vault/builtin/logical/rabbitmq" + "github.com/hashicorp/vault/builtin/logical/ssh" + "github.com/hashicorp/vault/builtin/logical/totp" + "github.com/hashicorp/vault/builtin/logical/transit" + "github.com/hashicorp/vault/builtin/plugin" + + auditFile "github.com/hashicorp/vault/builtin/audit/file" + auditSocket "github.com/hashicorp/vault/builtin/audit/socket" + auditSyslog "github.com/hashicorp/vault/builtin/audit/syslog" + + credGcp "github.com/hashicorp/vault-plugin-auth-gcp/plugin" + credAppId "github.com/hashicorp/vault/builtin/credential/app-id" + credAppRole "github.com/hashicorp/vault/builtin/credential/approle" + credAws "github.com/hashicorp/vault/builtin/credential/aws" + credCert "github.com/hashicorp/vault/builtin/credential/cert" + credGitHub "github.com/hashicorp/vault/builtin/credential/github" + credLdap "github.com/hashicorp/vault/builtin/credential/ldap" + credOkta "github.com/hashicorp/vault/builtin/credential/okta" + credRadius "github.com/hashicorp/vault/builtin/credential/radius" + credToken "github.com/hashicorp/vault/builtin/credential/token" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" + + physAzure "github.com/hashicorp/vault/physical/azure" + physCassandra "github.com/hashicorp/vault/physical/cassandra" + physCockroachDB "github.com/hashicorp/vault/physical/cockroachdb" + physConsul "github.com/hashicorp/vault/physical/consul" + physCouchDB "github.com/hashicorp/vault/physical/couchdb" + physDynamoDB "github.com/hashicorp/vault/physical/dynamodb" + physEtcd "github.com/hashicorp/vault/physical/etcd" + physFile "github.com/hashicorp/vault/physical/file" + physGCS "github.com/hashicorp/vault/physical/gcs" + physInmem "github.com/hashicorp/vault/physical/inmem" + physMSSQL "github.com/hashicorp/vault/physical/mssql" + physMySQL "github.com/hashicorp/vault/physical/mysql" + physPostgreSQL "github.com/hashicorp/vault/physical/postgresql" + physS3 "github.com/hashicorp/vault/physical/s3" + physSwift "github.com/hashicorp/vault/physical/swift" + physZooKeeper "github.com/hashicorp/vault/physical/zookeeper" +) + +// Commands is the mapping of all the available commands. +var Commands map[string]cli.CommandFactory + +func init() { + ui := &cli.ColoredUi{ + ErrorColor: cli.UiColorRed, + WarnColor: cli.UiColorYellow, + Ui: &cli.BasicUi{ + Writer: os.Stdout, + ErrorWriter: os.Stderr, + }, + } + + authHandlers := map[string]AuthHandler{ + "aws": &credAws.CLIHandler{}, + "cert": &credCert.CLIHandler{}, + "github": &credGitHub.CLIHandler{}, + "ldap": &credLdap.CLIHandler{}, + "okta": &credOkta.CLIHandler{}, + "radius": &credUserpass.CLIHandler{ + DefaultMount: "radius", + }, + "token": &credToken.CLIHandler{}, + "userpass": &credUserpass.CLIHandler{ + DefaultMount: "userpass", + }, + } + + Commands = map[string]cli.CommandFactory{ + "audit-disable": func() (cli.Command, error) { + return &AuditDisableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "audit-enable": func() (cli.Command, error) { + return &AuditEnableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "audit-list": func() (cli.Command, error) { + return &AuditListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "auth": func() (cli.Command, error) { + return &AuthCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + Handlers: authHandlers, + }, nil + }, + "auth-disable": func() (cli.Command, error) { + return &AuthDisableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "auth-enable": func() (cli.Command, error) { + return &AuthEnableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "auth-help": func() (cli.Command, error) { + return &AuthHelpCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + Handlers: authHandlers, + }, nil + }, + "auth-list": func() (cli.Command, error) { + return &AuthListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "capabilities": func() (cli.Command, error) { + return &CapabilitiesCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "delete": func() (cli.Command, error) { + return &DeleteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "generate-root": func() (cli.Command, error) { + return &GenerateRootCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "init": func() (cli.Command, error) { + return &InitCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "key-status": func() (cli.Command, error) { + return &KeyStatusCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "list": func() (cli.Command, error) { + return &ListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "mount": func() (cli.Command, error) { + return &MountCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "mounts": func() (cli.Command, error) { + return &MountsCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "mount-tune": func() (cli.Command, error) { + return &MountTuneCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "path-help": func() (cli.Command, error) { + return &PathHelpCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "policies": func() (cli.Command, error) { + return &PolicyListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "policy-delete": func() (cli.Command, error) { + return &PolicyDeleteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "policy-write": func() (cli.Command, error) { + return &PolicyWriteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "read": func() (cli.Command, error) { + return &ReadCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "rekey": func() (cli.Command, error) { + return &RekeyCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "remount": func() (cli.Command, error) { + return &RemountCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "renew": func() (cli.Command, error) { + return &RenewCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "revoke": func() (cli.Command, error) { + return &RevokeCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "rotate": func() (cli.Command, error) { + return &RotateCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "seal": func() (cli.Command, error) { + return &SealCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "server": func() (cli.Command, error) { + return &ServerCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + AuditBackends: map[string]audit.Factory{ + "file": auditFile.Factory, + "socket": auditSocket.Factory, + "syslog": auditSyslog.Factory, + }, + CredentialBackends: map[string]logical.Factory{ + "app-id": credAppId.Factory, + "approle": credAppRole.Factory, + "aws": credAws.Factory, + "cert": credCert.Factory, + "gcp": credGcp.Factory, + "github": credGitHub.Factory, + "ldap": credLdap.Factory, + "okta": credOkta.Factory, + "plugin": plugin.Factory, + "radius": credRadius.Factory, + "userpass": credUserpass.Factory, + }, + LogicalBackends: map[string]logical.Factory{ + "aws": aws.Factory, + "cassandra": cassandra.Factory, + "consul": consul.Factory, + "database": database.Factory, + "mongodb": mongodb.Factory, + "mssql": mssql.Factory, + "mysql": mysql.Factory, + "pki": pki.Factory, + "plugin": plugin.Factory, + "postgresql": postgresql.Factory, + "rabbitmq": rabbitmq.Factory, + "ssh": ssh.Factory, + "totp": totp.Factory, + "transit": transit.Factory, + }, + PhysicalBackends: map[string]physical.Factory{ + "azure": physAzure.NewAzureBackend, + "cassandra": physCassandra.NewCassandraBackend, + "cockroachdb": physCockroachDB.NewCockroachDBBackend, + "consul": physConsul.NewConsulBackend, + "couchdb_transactional": physCouchDB.NewTransactionalCouchDBBackend, + "couchdb": physCouchDB.NewCouchDBBackend, + "dynamodb": physDynamoDB.NewDynamoDBBackend, + "etcd": physEtcd.NewEtcdBackend, + "file_transactional": physFile.NewTransactionalFileBackend, + "file": physFile.NewFileBackend, + "gcs": physGCS.NewGCSBackend, + "inmem_ha": physInmem.NewInmemHA, + "inmem_transactional_ha": physInmem.NewTransactionalInmemHA, + "inmem_transactional": physInmem.NewTransactionalInmem, + "inmem": physInmem.NewInmem, + "mssql": physMSSQL.NewMSSQLBackend, + "mysql": physMySQL.NewMySQLBackend, + "postgresql": physPostgreSQL.NewPostgreSQLBackend, + "s3": physS3.NewS3Backend, + "swift": physSwift.NewSwiftBackend, + "zookeeper": physZooKeeper.NewZooKeeperBackend, + }, + ShutdownCh: MakeShutdownCh(), + SighupCh: MakeSighupCh(), + }, nil + }, + "ssh": func() (cli.Command, error) { + return &SSHCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "status": func() (cli.Command, error) { + return &StatusCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "step-down": func() (cli.Command, error) { + return &StepDownCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "token-create": func() (cli.Command, error) { + return &TokenCreateCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "token-lookup": func() (cli.Command, error) { + return &TokenLookupCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "token-renew": func() (cli.Command, error) { + return &TokenRenewCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "token-revoke": func() (cli.Command, error) { + return &TokenRevokeCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "unseal": func() (cli.Command, error) { + return &UnsealCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "unmount": func() (cli.Command, error) { + return &UnmountCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "unwrap": func() (cli.Command, error) { + return &UnwrapCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "version": func() (cli.Command, error) { + return &VersionCommand{ + VersionInfo: version.GetVersion(), + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "write": func() (cli.Command, error) { + return &WriteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + } +} + +// MakeShutdownCh returns a channel that can be used for shutdown +// notifications for commands. This channel will send a message for every +// SIGINT or SIGTERM received. +func MakeShutdownCh() chan struct{} { + resultCh := make(chan struct{}) + + shutdownCh := make(chan os.Signal, 4) + signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM) + go func() { + <-shutdownCh + close(resultCh) + }() + return resultCh +} + +// MakeSighupCh returns a channel that can be used for SIGHUP +// reloading. This channel will send a message for every +// SIGHUP received. +func MakeSighupCh() chan struct{} { + resultCh := make(chan struct{}) + + signalCh := make(chan os.Signal, 4) + signal.Notify(signalCh, syscall.SIGHUP) + go func() { + for { + <-signalCh + resultCh <- struct{}{} + } + }() + return resultCh +} From 1794af3873513027cdf547cefc97b80c4b5d54d9 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Wed, 6 Sep 2017 10:02:15 -0400 Subject: [PATCH 083/281] Update credential help Use "vault login" instead of "vault auth" and use "method" consistently over provider. --- builtin/credential/aws/cli.go | 17 ++++++++--------- builtin/credential/cert/cli.go | 11 +++++------ builtin/credential/github/cli.go | 18 +++++++++--------- builtin/credential/ldap/cli.go | 11 +++++------ builtin/credential/okta/cli.go | 10 +++++----- builtin/credential/token/cli.go | 13 ++++++------- builtin/credential/userpass/cli.go | 17 ++++++++--------- 7 files changed, 46 insertions(+), 51 deletions(-) diff --git a/builtin/credential/aws/cli.go b/builtin/credential/aws/cli.go index 75fb08ff89..b01b9b4a3b 100644 --- a/builtin/credential/aws/cli.go +++ b/builtin/credential/aws/cli.go @@ -108,9 +108,9 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -Usage: vault auth -method=aws [CONFIG K=V...] +Usage: vault login -method=aws [CONFIG K=V...] - The AWS authentication provider allows users to authenticate with AWS IAM + The AWS authentication method allows users to authenticate with AWS IAM credentials. The AWS IAM credentials may be specified in a number of ways, listed in order of precedence below: @@ -124,11 +124,11 @@ Usage: vault auth -method=aws [CONFIG K=V...] Authenticate using locally stored credentials: - $ vault auth -method=aws + $ vault login -method=aws Authenticate by passing keys: - $ vault auth -method=aws aws_access_key_id=... aws_secret_access_key=... + $ vault login -method=aws aws_access_key_id=... aws_secret_access_key=... Configuration: @@ -145,14 +145,13 @@ Configuration: Value for the x-vault-aws-iam-server-id header in requests mount= - Path where the AWS credential provider is mounted. This is usually - provided via the -path flag in the "vault auth" command, but it can be - specified here as well. If specified here, it takes precedence over - the value for -path. The default value is "aws". + Path where the AWS credential method is mounted. This is usually provided + via the -path flag in the "vault login" command, but it can be specified + here as well. If specified here, it takes precedence over the value for + -path. The default value is "aws". role= Name of the role to request a token against - ` return strings.TrimSpace(help) diff --git a/builtin/credential/cert/cli.go b/builtin/credential/cert/cli.go index 5316efd8c1..bd066d466c 100644 --- a/builtin/credential/cert/cli.go +++ b/builtin/credential/cert/cli.go @@ -40,22 +40,21 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -Usage: vault auth -method=cert [CONFIG K=V...] +Usage: vault login -method=cert [CONFIG K=V...] - The certificate authentication provider allows uers to authenticate with a + The certificate authentication method allows uers to authenticate with a client certificate passed with the request. The -client-cert and -client-key - flags are included with the "vault auth" command, NOT as configuration to - the authentication provider. + flags are included with the "vault login" command, NOT as configuration to the + authentication method. Authenticate using a local client certificate: - $ vault auth -method=cert -client-cert=cert.pem -client-key=key.pem + $ vault login -method=cert -client-cert=cert.pem -client-key=key.pem Configuration: name= Certificate role to authenticate against. - ` return strings.TrimSpace(help) diff --git a/builtin/credential/github/cli.go b/builtin/credential/github/cli.go index 21f8d56a7d..05d4c6d40a 100644 --- a/builtin/credential/github/cli.go +++ b/builtin/credential/github/cli.go @@ -39,23 +39,23 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -Usage: vault auth -method=github [CONFIG K=V...] +Usage: vault login -method=github [CONFIG K=V...] - The GitHub authentication provider allows users to authenticate using a - GitHub personal access token. Users can generate a personal access token - from the settings page on their GitHub account. + The GitHub authentication method allows users to authenticate using a GitHub + personal access token. Users can generate a personal access token from the + settings page on their GitHub account. Authenticate using a GitHub token: - $ vault auth -method=github token=abcd1234 + $ vault login -method=github token=abcd1234 Configuration: mount= - Path where the GitHub credential provider is mounted. This is usually - provided via the -path flag in the "vault auth" command, but it can be - specified here as well. If specified here, it takes precedence over - the value for -path. The default value is "github". + Path where the GitHub credential method is mounted. This is usually + provided via the -path flag in the "vault login" command, but it can be + specified here as well. If specified here, it takes precedence over the + value for -path. The default value is "github". token= GitHub personal access token to use for authentication. diff --git a/builtin/credential/ldap/cli.go b/builtin/credential/ldap/cli.go index 2559f85c47..29e3834093 100644 --- a/builtin/credential/ldap/cli.go +++ b/builtin/credential/ldap/cli.go @@ -62,24 +62,24 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -Usage: vault auth -method=ldap [CONFIG K=V...] +Usage: vault login -method=ldap [CONFIG K=V...] - The LDAP authentication provider allows users to authenticate using LDAP or + The LDAP authentication method allows users to authenticate using LDAP or Active Directory. If MFA is enabled, a "method" and/or "passcode" may be required depending on - the MFA provider. To check which MFA is in use, run: + the MFA method. To check which MFA is in use, run: $ vault read auth//mfa_config Authenticate as "sally": - $ vault auth -method=ldap username=sally + $ vault login -method=ldap username=sally Password (will be hidden): Authenticate as "bob": - $ vault auth -method=ldap username=bob password=password + $ vault login -method=ldap username=bob password=password Configuration: @@ -95,7 +95,6 @@ Configuration: username= LDAP username to use for authentication. - ` return strings.TrimSpace(help) diff --git a/builtin/credential/okta/cli.go b/builtin/credential/okta/cli.go index 94f41d2f0f..77c8f0500f 100644 --- a/builtin/credential/okta/cli.go +++ b/builtin/credential/okta/cli.go @@ -53,18 +53,18 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro // Help method for okta cli func (h *CLIHandler) Help() string { help := ` -Usage: vault auth -method=okta [CONFIG K=V...] +Usage: vault login -method=okta [CONFIG K=V...] - The OKTA authentication provider allows users to authenticate using OKTA. + The OKTA authentication method allows users to authenticate using OKTA. Authenticate as "sally": - $ vault auth -method=okta username=sally + $ vault login -method=okta username=sally Password (will be hidden): Authenticate as "bob": - $ vault auth -method=okta username=bob password=password + $ vault login -method=okta username=bob password=password Configuration: @@ -74,7 +74,7 @@ Configuration: username= OKTA username to use for authentication. - ` +` return strings.TrimSpace(help) } diff --git a/builtin/credential/token/cli.go b/builtin/credential/token/cli.go index 5af0101dd8..b2252d6b7c 100644 --- a/builtin/credential/token/cli.go +++ b/builtin/credential/token/cli.go @@ -63,15 +63,15 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -Usage: vault auth TOKEN [CONFIG K=V...] +Usage: vault login TOKEN [CONFIG K=V...] - The token authentication provider allows logging in directly with a token. - This can be a token from the "token-create" command or API. There are no - configuration options for this authentication provider. + The token authentication method allows logging in directly with a token. This + can be a token from the "token-create" command or API. There are no + configuration options for this authentication method. Authenticate using a token: - $ vault auth 96ddf4bc-d217-f3ba-f9bd-017055595017 + $ vault login 96ddf4bc-d217-f3ba-f9bd-017055595017 This token usually comes from a different source such as the API or via the built-in "vault token-create" command. @@ -80,8 +80,7 @@ Configuration: token= The token to use for authentication. This is usually provided directly - via the "vault auth" command. - + via the "vault login" command. ` return strings.TrimSpace(help) diff --git a/builtin/credential/userpass/cli.go b/builtin/credential/userpass/cli.go index f4b79d1496..93b12879c6 100644 --- a/builtin/credential/userpass/cli.go +++ b/builtin/credential/userpass/cli.go @@ -66,24 +66,24 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro func (h *CLIHandler) Help() string { help := ` -Usage: vault auth -method=userpass [CONFIG K=V...] +Usage: vault login -method=userpass [CONFIG K=V...] - The userpass authentication provider allows users to authenticate using - Vault's internal user database. + The userpass authentication method allows users to authenticate using Vault's + internal user database. If MFA is enabled, a "method" and/or "passcode" may be required depending on - the MFA provider. To check which MFA is in use, run: + the MFA method. To check which MFA is in use, run: $ vault read auth//mfa_config Authenticate as "sally": - $ vault auth -method=userpass username=sally + $ vault login -method=userpass username=sally Password (will be hidden): Authenticate as "bob": - $ vault auth -method=userpass username=bob password=password + $ vault login -method=userpass username=bob password=password Configuration: @@ -94,12 +94,11 @@ Configuration: MFA OTP/passcode. password= - Password to use for authentication. If not provided, the CLI will - prompt for this on stdin. + Password to use for authentication. If not provided, the CLI will prompt + for this on stdin. username= Username to use for authentication. - ` return strings.TrimSpace(help) From 9f19e5de01b62341fde78c0f7d1088b5554b2692 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:55:36 -0400 Subject: [PATCH 084/281] Add more secret helpers for getting secret data --- api/secret.go | 69 +++++- api/secret_test.go | 522 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 590 insertions(+), 1 deletion(-) diff --git a/api/secret.go b/api/secret.go index 86117029db..d608d0b2c1 100644 --- a/api/secret.go +++ b/api/secret.go @@ -86,7 +86,7 @@ func (s *Secret) TokenAccessor() string { // TokenMeta returns the standardized token metadata for the given secret. // If the secret is nil or does not contain an accessor, this returns the empty // string. Metadata is usually modeled as an map[string]interface{}, but token -// metdata is always a map[string]string. This function handles the coercion. +// metadata is always a map[string]string. This function handles the coercion. func (s *Secret) TokenMeta() map[string]string { if s == nil { return nil @@ -176,6 +176,42 @@ func (s *Secret) TokenPolicies() []string { return policies } +// TokenMetadata returns the map of metadata associated with this token, if any +// exists. If the secret is nil or does not contain the "metadata" key, this +// returns nil. +func (s *Secret) TokenMetadata() map[string]string { + if s == nil { + return nil + } + + if s.Auth != nil && len(s.Auth.Metadata) > 0 { + return s.Auth.Metadata + } + + if s.Data == nil || (s.Data["metadata"] == nil && s.Data["meta"] == nil) { + return nil + } + + data, ok := s.Data["metadata"].(map[string]interface{}) + if !ok { + data, ok = s.Data["meta"].(map[string]interface{}) + if !ok { + return nil + } + } + + metadata := make(map[string]string, len(data)) + for k, v := range data { + typed, ok := v.(string) + if !ok { + return nil + } + metadata[k] = typed + } + + return metadata +} + // TokenIsRenewable returns the standardized token renewability for the given // secret. If the secret is nil or does not contain the "renewable" key, this // returns false. @@ -200,6 +236,37 @@ func (s *Secret) TokenIsRenewable() bool { return renewable } +// TokenTTLInt returns the token's TTL as an integer number of seconds. +func (s *Secret) TokenTTLInt() int { + if s == nil { + return 0 + } + + if s.Auth != nil && s.Auth.LeaseDuration > 0 { + return s.Auth.LeaseDuration + } + + if s.Data == nil || s.Data["ttl"] == nil { + return 0 + } + + ttlStr, ok := s.Data["ttl"].(json.Number) + if !ok { + return 0 + } + + if string(ttlStr) == "" { + return 0 + } + + i, err := strconv.ParseInt(string(ttlStr), 0, 64) + if err != nil { + return 0 + } + + return int(i) +} + // TokenTTL returns the standardized remaining token TTL for the given secret. // If the secret is nil or does not contain a TTL, this returns the 0. func (s *Secret) TokenTTL() time.Duration { diff --git a/api/secret_test.go b/api/secret_test.go index 4e4c8c3aef..246962e320 100644 --- a/api/secret_test.go +++ b/api/secret_test.go @@ -2,6 +2,7 @@ package api_test import ( "encoding/json" + "fmt" "reflect" "strings" "testing" @@ -11,6 +12,8 @@ import ( ) func TestParseSecret(t *testing.T) { + t.Parallel() + raw := strings.TrimSpace(` { "lease_id": "foo", @@ -1263,6 +1266,271 @@ func TestSecret_TokenPolicies(t *testing.T) { }) } +func TestSecret_TokenMetadata(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp map[string]string + }{ + { + "nil", + nil, + nil, + }, + { + "nil_auth", + &api.Secret{ + Auth: nil, + }, + nil, + }, + { + "nil_auth_metadata", + &api.Secret{ + Auth: &api.SecretAuth{ + Metadata: nil, + }, + }, + nil, + }, + { + "empty_auth_metadata", + &api.Secret{ + Auth: &api.SecretAuth{ + Metadata: map[string]string{}, + }, + }, + nil, + }, + { + "real_auth_metdata", + &api.Secret{ + Auth: &api.SecretAuth{ + Metadata: map[string]string{"foo": "bar"}, + }, + }, + map[string]string{"foo": "bar"}, + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + nil, + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + nil, + }, + { + "data_not_map", + &api.Secret{ + Data: map[string]interface{}{ + "metadata": 123, + }, + }, + nil, + }, + { + "data_map", + &api.Secret{ + Data: map[string]interface{}{ + "metadata": map[string]interface{}{"foo": "bar"}, + }, + }, + map[string]string{"foo": "bar"}, + }, + { + "data_map_bad_type", + &api.Secret{ + Data: map[string]interface{}{ + "metadata": map[string]interface{}{"foo": 123}, + }, + }, + nil, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenMetadata() + if !reflect.DeepEqual(act, tc.exp) { + t.Errorf("expected %#v to be %#v", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + metadata := map[string]string{"username": "test"} + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMetadata(), metadata) { + t.Errorf("expected %#v to be %#v", secret.TokenMetadata(), metadata) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + metadata := map[string]string{"username": "test"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Metadata: metadata, + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMetadata(), metadata) { + t.Errorf("expected %#v to be %#v", secret.TokenMetadata(), metadata) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + metadata := map[string]string{"username": "test"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Metadata: metadata, + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMetadata(), metadata) { + t.Errorf("expected %#v to be %#v", secret.TokenMetadata(), metadata) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + metadata := map[string]string{"username": "test"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Metadata: metadata, + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMetadata(), metadata) { + t.Errorf("expected %#v to be %#v", secret.TokenMetadata(), metadata) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + metadata := map[string]string{"username": "test"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Metadata: metadata, + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMetadata(), metadata) { + t.Errorf("expected %#v to be %#v", secret.TokenMetadata(), metadata) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + metadata := map[string]string{"username": "test"} + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Metadata: metadata, + Policies: []string{"default"}, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(secret.TokenMetadata(), metadata) { + t.Errorf("expected %#v to be %#v", secret.TokenMetadata(), metadata) + } + }) +} + func TestSecret_TokenIsRenewable(t *testing.T) { t.Parallel() @@ -1519,6 +1787,260 @@ func TestSecret_TokenIsRenewable(t *testing.T) { }) } +func TestSecret_TokenTTLInt(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + secret *api.Secret + exp int + }{ + { + "nil", + nil, + 0, + }, + { + "nil_auth", + &api.Secret{ + Auth: nil, + }, + 0, + }, + { + "nil_auth_lease_duration", + &api.Secret{ + Auth: &api.SecretAuth{ + LeaseDuration: 0, + }, + }, + 0, + }, + { + "real_auth_lease_duration", + &api.Secret{ + Auth: &api.SecretAuth{ + LeaseDuration: 3600, + }, + }, + 3600, + }, + { + "nil_data", + &api.Secret{ + Data: nil, + }, + 0, + }, + { + "empty_data", + &api.Secret{ + Data: map[string]interface{}{}, + }, + 0, + }, + { + "data_not_json_number", + &api.Secret{ + Data: map[string]interface{}{ + "ttl": 123, + }, + }, + 0, + }, + { + "data_json_number", + &api.Secret{ + Data: map[string]interface{}{ + "ttl": json.Number("3600"), + }, + }, + 3600, + }, + } + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + act := tc.secret.TokenTTLInt() + if act != tc.exp { + t.Errorf("expected %d to be %d", act, tc.exp) + } + }) + } + + t.Run("auth", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 3600 + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + "ttl": fmt.Sprintf("%ds", ttl), + "explicit_max_ttl": fmt.Sprintf("%ds", ttl), + }); err != nil { + t.Fatal(err) + } + + secret, err := client.Logical().Write("auth/userpass/login/test", map[string]interface{}{ + "password": "test", + }) + if err != nil || secret == nil { + t.Fatal(err) + } + + if secret.TokenTTLInt() == 0 || secret.TokenTTLInt() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTL(), ttl) + } + }) + + t.Run("token-create", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 3600 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: fmt.Sprintf("%ds", ttl), + ExplicitMaxTTL: fmt.Sprintf("%ds", ttl), + }) + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTLInt() == 0 || secret.TokenTTLInt() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTLInt(), ttl) + } + }) + + t.Run("token-lookup", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 3600 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: fmt.Sprintf("%ds", ttl), + ExplicitMaxTTL: fmt.Sprintf("%ds", ttl), + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTLInt() == 0 || secret.TokenTTLInt() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTLInt(), ttl) + } + }) + + t.Run("token-lookup-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 3600 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: fmt.Sprintf("%ds", ttl), + ExplicitMaxTTL: fmt.Sprintf("%ds", ttl), + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTLInt() == 0 || secret.TokenTTLInt() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTLInt(), ttl) + } + }) + + t.Run("token-renew", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 3600 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: fmt.Sprintf("%ds", ttl), + ExplicitMaxTTL: fmt.Sprintf("%ds", ttl), + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + secret, err = client.Auth().Token().Renew(token, 0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTLInt() == 0 || secret.TokenTTLInt() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTLInt(), ttl) + } + }) + + t.Run("token-renew-self", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ttl := 3600 + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: fmt.Sprintf("%ds", ttl), + ExplicitMaxTTL: fmt.Sprintf("%ds", ttl), + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + client.SetToken(token) + secret, err = client.Auth().Token().RenewSelf(0) + if err != nil { + t.Fatal(err) + } + + if secret.TokenTTLInt() == 0 || secret.TokenTTLInt() > ttl { + t.Errorf("expected %q to non-zero and less than %q", secret.TokenTTLInt(), ttl) + } + }) +} + func TestSecret_TokenTTL(t *testing.T) { t.Parallel() From fc535647fcbc90abda5cdf2461e1dc0aac5f1f71 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:56:39 -0400 Subject: [PATCH 085/281] Introduce auth as a subcommand --- command/auth.go | 491 ++++++--------------------------- command/auth_disable.go | 26 +- command/auth_disable_test.go | 4 +- command/auth_enable.go | 41 ++- command/auth_enable_test.go | 2 +- command/auth_help.go | 39 ++- command/auth_help_test.go | 10 +- command/auth_list.go | 28 +- command/auth_test.go | 515 ++++------------------------------- command/auth_tune.go | 120 ++++++++ command/auth_tune_test.go | 149 ++++++++++ command/token/helper.go | 2 +- 12 files changed, 473 insertions(+), 954 deletions(-) create mode 100644 command/auth_tune.go create mode 100644 command/auth_tune_test.go diff --git a/command/auth.go b/command/auth.go index 8ebf021bd1..fa8cc66268 100644 --- a/command/auth.go +++ b/command/auth.go @@ -1,452 +1,117 @@ package command import ( - "fmt" + "flag" "io" - "os" + "io/ioutil" "strings" - "github.com/hashicorp/vault/api" - "github.com/posener/complete" + "github.com/mitchellh/cli" ) -// AuthHandler is the interface that any auth handlers must implement -// to enable auth via the CLI. -type AuthHandler interface { - Auth(*api.Client, map[string]string) (*api.Secret, error) - Help() string -} +var _ cli.Command = (*AuthCommand)(nil) -// AuthCommand is a Command that handles authentication. type AuthCommand struct { *BaseCommand - Handlers map[string]AuthHandler - - flagMethod string - flagPath string - flagNoVerify bool - flagNoStore bool - flagOnlyToken bool - - // Deprecations - // TODO: remove in 0.9.0 - flagTokenOnly bool - flagMethods bool - flagMethodHelp bool + Handlers map[string]LoginHandler testStdin io.Reader // for tests } func (c *AuthCommand) Synopsis() string { - return "Authenticates users or machines" + return "Interact with auth methods" } func (c *AuthCommand) Help() string { - helpText := ` -Usage: vault auth [options] [AUTH K=V...] + return strings.TrimSpace(` +Usage: vault auth [options] [args] - Authenticates users or machines to Vault using the provided arguments. By - default, the authentication method is "token". If not supplied via the CLI, - Vault will prompt for input. If argument is "-", the configuration options - are read from stdin. + This command groups subcommands for interacting with Vault's auth methods. + Users can list, enable, disable, and get help for different auth methods. - The -method flag allows alternative authentication providers to be used, - such as userpass, github, or cert. For these, additional "key=value" pairs - may be required. For example, to authenticate to the userpass auth backend: + To authenticate to Vault as a user or machine, use the "vault login" command + instead. This command is for interacting with the auth methods themselves, not + authenticating to Vault. - $ vault auth -method=userpass username=my-username + List all enabled auth methods: - Use "vault auth-help TYPE" for more information about the list of - configuration parameters and examples for a particular provider. Use the - "vault auth-list" command to see a list of enabled authentication providers. + $ vault auth list - If an authentication provider is mounted at a different path, the -method - flag should by the canonical type, and the -path flag should be set to the - mount path. If a github authentication provider was mounted at "github-ent", - you would authenticate to this backend like this: + Enable a new auth method "userpass"; - $ vault auth -method=github -path=github-prod + $ vault auth enable userpass - If the authentication is requested with response wrapping (via -wrap-ttl), - the returned token is automatically unwrapped unless: + Get detailed help information about how to authenticate to a particular auth + method: - - The -only-token flag is used, in which case this command will output - the wrapping token + $ vault auth help github - - The -no-store flag is used, in which case this command will output - the details of the wrapping token. - - For a full list of examples, please see the documentation. - -` + c.Flags().Help() - - return strings.TrimSpace(helpText) -} - -func (c *AuthCommand) Flags() *FlagSets { - set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) - - f := set.NewFlagSet("Command Options") - - f.StringVar(&StringVar{ - Name: "method", - Target: &c.flagMethod, - Default: "token", - Completion: c.PredictVaultAvailableAuths(), - Usage: "Type of authentication to use such as \"userpass\" or " + - "\"ldap\". Note this corresponds to the TYPE, not the mount path. Use " + - "-path to specify the path where the authentication is mounted.", - }) - - f.StringVar(&StringVar{ - Name: "path", - Target: &c.flagPath, - Default: "", - Completion: c.PredictVaultAuths(), - Usage: "Mount point where the auth backend is enabled. This defaults to " + - "the TYPE of backend (e.g. userpass -> userpass/).", - }) - - f.BoolVar(&BoolVar{ - Name: "no-verify", - Target: &c.flagNoVerify, - Default: false, - Usage: "Do not verify the token after authentication. By default, Vault " + - "issue a request to get more metdata about the token. This request " + - "against the use-limit of the token. Set this to true to disable the " + - "post-authenication lookup.", - }) - - f.BoolVar(&BoolVar{ - Name: "no-store", - Target: &c.flagNoStore, - Default: false, - Usage: "Do not persist the token to the token helper (usually the " + - "local filesystem) after authentication for use in future requests. " + - "The token will only be displayed in the command output.", - }) - - f.BoolVar(&BoolVar{ - Name: "only-token", - Target: &c.flagOnlyToken, - Default: false, - Usage: "Output only the token with no verification. This flag is a " + - "shortcut for \"-field=token -no-store -no-verify\". Setting those " + - "flags to other values will have no affect.", - }) - - // Deprecations - // TODO: remove in Vault 0.9.0 - - f.BoolVar(&BoolVar{ - Name: "token-only", // Prefer only-token - Target: &c.flagTokenOnly, - Default: false, - Hidden: true, - }) - - f.BoolVar(&BoolVar{ - Name: "methods", // Prefer auth-list - Target: &c.flagMethods, - Default: false, - Hidden: true, - }) - - f.BoolVar(&BoolVar{ - Name: "method-help", // Prefer auth-help - Target: &c.flagMethodHelp, - Default: false, - Hidden: true, - }) - - return set -} - -func (c *AuthCommand) AutocompleteArgs() complete.Predictor { - return nil -} - -func (c *AuthCommand) AutocompleteFlags() complete.Flags { - return c.Flags().Completions() + Please see the individual subcommand help for detailed usage information. +`) } func (c *AuthCommand) Run(args []string) int { - f := c.Flags() - - if err := f.Parse(args); err != nil { - c.UI.Error(err.Error()) - return 1 - } - - args = f.Args() - - // Deprecations - do this before any argument validations + // If we entered the run method, none of the subcommands picked up. This + // means the user is still trying to use auth as "vault auth TOKEN" or + // similar, so direct them to vault login instead. + // + // This run command is a bit messy to maintain BC for a bit. In the future, + // it will just be a tiny function, but for now we have to maintain bc. + // + // Deprecation // TODO: remove in 0.9.0 - switch { - case c.flagMethods: - c.UI.Warn(wrapAtLength( - "WARNING! The -methods flag is deprecated. Please use " + - "\"vault auth-list\". This flag will be removed in the next major " + - "release of Vault.")) - cmd := &AuthListCommand{ - BaseCommand: &BaseCommand{ - UI: c.UI, - client: c.client, - }, - } - return cmd.Run(nil) - case c.flagMethodHelp: - c.UI.Warn(wrapAtLength( - "WARNING! The -method-help flag is deprecated. Please use " + - "\"vault auth-help\". This flag will be removed in the next major " + - "release of Vault.")) - cmd := &AuthHelpCommand{ - BaseCommand: &BaseCommand{ - UI: c.UI, - client: c.client, - }, - Handlers: c.Handlers, - } - return cmd.Run([]string{c.flagMethod}) - } - // TODO: remove in 0.9.0 - if c.flagTokenOnly { - c.UI.Warn(wrapAtLength( - "WARNING! The -token-only flag is deprecated. Plase use -only-token " + - "instead. This flag will be removed in the next major release of " + - "Vault.")) - c.flagOnlyToken = c.flagTokenOnly - } + // Parse the args for our deprecations and defer to the proper areas. + for _, arg := range args { + switch { + case strings.HasPrefix(arg, "-methods"): + c.UI.Warn(wrapAtLength( + "WARNING! The -methods flag is deprecated. Please use "+ + "\"vault auth list\" instead. This flag will be removed in the "+ + "next major release of Vault.") + "\n") + return (&AuthListCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + }).Run(nil) + case strings.HasPrefix(arg, "-method-help"): + c.UI.Warn(wrapAtLength( + "WARNING! The -method-help flag is deprecated. Please use "+ + "\"vault auth help\" instead. This flag will be removed in the "+ + "next major release of Vault.") + "\n") + // Parse the args to pull out the method, surpressing any errors because + // there could be other flags that we don't care about. + f := flag.NewFlagSet("", flag.ContinueOnError) + f.Usage = func() {} + f.SetOutput(ioutil.Discard) + flagMethod := f.String("method", "", "") + f.Parse(args) - // Set the right flags if the user requested only-token - this overrides - // any previously configured values, as documented. - if c.flagOnlyToken { - c.flagNoStore = true - c.flagNoVerify = true - c.flagField = "token" - } - - // Get the auth method - authMethod := sanitizePath(c.flagMethod) - if authMethod == "" { - authMethod = "token" - } - - // If no path is specified, we default the path to the backend type - // or use the plugin name if it's a plugin backend - authPath := c.flagPath - if authPath == "" { - authPath = ensureTrailingSlash(authMethod) - } - - // Get the handler function - authHandler, ok := c.Handlers[authMethod] - if !ok { - c.UI.Error(wrapAtLength(fmt.Sprintf( - "Unknown authentication method: %s. Use \"vault auth-list\" to see the "+ - "complete list of authentication providers. Additionally, some "+ - "authentication providers are only available via the HTTP API.", - authMethod))) - return 1 - } - - // Pull our fake stdin if needed - stdin := (io.Reader)(os.Stdin) - if c.testStdin != nil { - stdin = c.testStdin - } - - // If the user provided a token, pass it along to the auth provier. - if authMethod == "token" && len(args) == 1 { - args = []string{"token=" + args[0]} - } - - config, err := parseArgsDataString(stdin, args) - if err != nil { - c.UI.Error(fmt.Sprintf("Error parsing configuration: %s", err)) - return 1 - } - - // If the user did not specify a mount path, use the provided mount path. - if config["mount"] == "" && authPath != "" { - config["mount"] = authPath - } - - // Create the client - client, err := c.Client() - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - - // Authenticate delegation to the auth handler - secret, err := authHandler.Auth(client, config) - if err != nil { - c.UI.Error(wrapAtLength(fmt.Sprintf( - "Error authenticating: %s", err))) - return 2 - } - - // Unset any previous token wrapping functionality. If the original request - // was for a wrapped token, we don't want future requests to be wrapped. - client.SetWrappingLookupFunc(func(string, string) string { return "" }) - - // Recursively extract the token, handling wrapping - unwrap := !c.flagOnlyToken && !c.flagNoStore - secret, isWrapped, err := c.extractToken(client, secret, unwrap) - if err != nil { - c.UI.Error(fmt.Sprintf("Error extracting token: %s", err)) - return 2 - } - if secret == nil { - c.UI.Error("Vault returned an empty secret") - return 2 - } - - // Handle special cases if the token was wrapped - if isWrapped { - if c.flagOnlyToken { - return PrintRawField(c.UI, secret, "wrapping_token") - } - if c.flagNoStore { - return OutputSecret(c.UI, c.flagFormat, secret) + return (&AuthHelpCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + Handlers: c.Handlers, + }).Run([]string{*flagMethod}) } } - // If we got this far, verify we have authentication data before continuing - if secret.Auth == nil { - c.UI.Error(wrapAtLength( - "Vault returned a secret, but the secret has no authentication " + - "information attached. This should never happen and is likely a " + - "bug.")) - return 2 - } - - // Pull the token itself out, since we don't need the rest of the auth - // information anymore/. - token := secret.Auth.ClientToken - - tokenHelper, err := c.TokenHelper() - if err != nil { - c.UI.Error(fmt.Sprintf( - "Error initializing token helper: %s\n\n"+ - "Please verify that the token helper is available and properly\n"+ - "configured for your system. Please refer to the documentation\n"+ - "on token helpers for more information.", - err)) - return 1 - } - - if !c.flagNoVerify { - // Verify the token and pull it's list of policies - client.SetToken(token) - client.SetWrappingLookupFunc(func(string, string) string { return "" }) - - secret, err = client.Auth().Token().LookupSelf() - if err != nil { - c.UI.Error(fmt.Sprintf("Error verifying token: %s", err)) - return 2 - } - if secret == nil { - c.UI.Error("Empty response from lookup-self") - return 2 - } - } - - if !c.flagNoStore { - // Store the token in the local client - if err := tokenHelper.Store(token); err != nil { - c.UI.Error(fmt.Sprintf("Error storing token: %s", err)) - c.UI.Error(wrapAtLength( - "Authentication was successful, but the token was not persisted. The " + - "resulting token is shown below for your records.")) - OutputSecret(c.UI, c.flagFormat, secret) - return 2 - } - - // Warn if the VAULT_TOKEN environment variable is set, as that will take - // precedence. Don't output on token-only since we're likely piping output. - if c.flagField == "" && c.flagFormat == "table" { - if os.Getenv("VAULT_TOKEN") != "" { - c.UI.Warn(wrapAtLength("WARNING! The VAULT_TOKEN environment variable " + - "is set! This takes precedence over the value set by this command. To " + - "use the value set by this command, unset the VAULT_TOKEN environment " + - "variable or set it to the token displayed below.")) - } - } - } - - // If the user requested a particular field, print that out now since we - // are likely piping to another process. - if c.flagField != "" { - return PrintRawField(c.UI, secret, c.flagField) - } - - // Output the secret as json or yaml if requested. We have to maintain - // backwards compatiability - if c.flagFormat != "table" { - return OutputSecret(c.UI, c.flagFormat, secret) - } - - output := "Success! You are now authenticated. " - if c.flagNoVerify { - output += "The token was not verified for validity. " - } - if c.flagNoStore { - output += "The token was not stored in the token helper. " - } else { - output += "The token information displayed below is already stored in " + - "the token helper. You do NOT need to run \"vault auth\" again." - } - c.UI.Output(wrapAtLength(output) + "\n") - - // TODO make this consistent with other printed token secrets. - c.UI.Output(fmt.Sprintf("token: %s", secret.TokenID())) - c.UI.Output(fmt.Sprintf("accessor: %s", secret.TokenAccessor())) - - if ttl := secret.TokenTTL(); ttl != 0 { - c.UI.Output(fmt.Sprintf("duration: %s", ttl)) - } - - c.UI.Output(fmt.Sprintf("renewable: %t", secret.TokenIsRenewable())) - - if policies := secret.TokenPolicies(); len(policies) > 0 { - c.UI.Output(fmt.Sprintf("policies: %s", policies)) - } - - return 0 -} - -// extractToken extracts the token from the given secret, automatically -// unwrapping responses and handling error conditions if unwrap is true. The -// result also returns whether it was a wrapped resonse that was not unwrapped. -func (c *AuthCommand) extractToken(client *api.Client, secret *api.Secret, unwrap bool) (*api.Secret, bool, error) { - switch { - case secret == nil: - return nil, false, fmt.Errorf("empty response from auth helper") - - case secret.Auth != nil: - return secret, false, nil - - case secret.WrapInfo != nil: - if secret.WrapInfo.WrappedAccessor == "" { - return nil, false, fmt.Errorf("wrapped response does not contain a token") - } - - if !unwrap { - return secret, true, nil - } - - client.SetToken(secret.WrapInfo.Token) - secret, err := client.Logical().Unwrap("") - if err != nil { - return nil, false, err - } - return c.extractToken(client, secret, unwrap) - - default: - return nil, false, fmt.Errorf("no auth or wrapping info in response") - } + // If we got this far, we have an arg or a series of args that should be + // passed directly to the new "vault login" command. + c.UI.Warn(wrapAtLength( + "WARNING! The \"vault auth ARG\" command is deprecated and is now a "+ + "subcommand for interacting with auth methods. To "+ + "authenticate locally to Vault, use \"vault login\" instead. This "+ + "backwards compatability will be removed in the next major release of "+ + "Vault.") + "\n") + return (&LoginCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + Handlers: c.Handlers, + }).Run(args) } diff --git a/command/auth_disable.go b/command/auth_disable.go index 14cf092269..afcfe747df 100644 --- a/command/auth_disable.go +++ b/command/auth_disable.go @@ -8,35 +8,31 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*AuthDisableCommand)(nil) var _ cli.CommandAutocomplete = (*AuthDisableCommand)(nil) -// AuthDisableCommand is a Command that enables a new endpoint. type AuthDisableCommand struct { *BaseCommand } func (c *AuthDisableCommand) Synopsis() string { - return "Disables an auth provider" + return "Disables an auth method" } func (c *AuthDisableCommand) Help() string { helpText := ` -Usage: vault auth-disable [options] PATH +Usage: vault auth disable [options] PATH - Disables an existing authentication provider at the given PATH. The argument - corresponds to the PATH of the mount, not the TYPE!. Once the auth provider - is disabled its path can no longer be used to authenticate. All access tokens - generated via the disabled auth provider are revoked. + Disables an existing auth method at the given PATH. The argument corresponds + to the PATH of the mount, not the TYPE!. Once the auth method is disabled its + path can no longer be used to authenticate. - This command will block until all tokens are revoked. + All access tokens generated via the disabled auth method are immediately + revoked. This command will block until all tokens are revoked. - Disable the authentication provider at userpass/: + Disable the auth method at userpass/: - $ vault auth-disable userpass - - For a full list of examples, please see the documentation. + $ vault auth disable userpass/ ` + c.Flags().Help() @@ -82,10 +78,10 @@ func (c *AuthDisableCommand) Run(args []string) int { } if err := client.Sys().DisableAuth(path); err != nil { - c.UI.Error(fmt.Sprintf("Error disabling auth at %s: %s", path, err)) + c.UI.Error(fmt.Sprintf("Error disabling auth method at %s: %s", path, err)) return 2 } - c.UI.Output(fmt.Sprintf("Success! Disabled the auth provider (if it existed) at: %s", path)) + c.UI.Output(fmt.Sprintf("Success! Disabled the auth method (if it existed) at: %s", path)) return 0 } diff --git a/command/auth_disable_test.go b/command/auth_disable_test.go index 86b1f90560..dbe2e776f9 100644 --- a/command/auth_disable_test.go +++ b/command/auth_disable_test.go @@ -85,7 +85,7 @@ func TestAuthDisableCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Disabled the auth provider" + expected := "Success! Disabled the auth method" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -117,7 +117,7 @@ func TestAuthDisableCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error disabling auth at my-auth/: " + expected := "Error disabling auth method at my-auth/: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) diff --git a/command/auth_enable.go b/command/auth_enable.go index a5ba67f27d..85b0f6fbfb 100644 --- a/command/auth_enable.go +++ b/command/auth_enable.go @@ -9,11 +9,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*AuthEnableCommand)(nil) var _ cli.CommandAutocomplete = (*AuthEnableCommand)(nil) -// AuthEnableCommand is a Command that enables a new endpoint. type AuthEnableCommand struct { *BaseCommand @@ -24,30 +22,28 @@ type AuthEnableCommand struct { } func (c *AuthEnableCommand) Synopsis() string { - return "Enables a new auth provider" + return "Enables a new auth method" } func (c *AuthEnableCommand) Help() string { helpText := ` -Usage: vault auth-enable [options] TYPE +Usage: vault auth enable [options] TYPE - Enables a new authentication provider. An authentication provider is - responsible for authenticating users or machiens and assigning them - policies with which they can access Vault. + Enables a new auth method. An auth method is responsible for authenticating + users or machines and assigning them policies with which they can access + Vault. - Enable the userpass auth provider at userpass/: + Enable the userpass auth method at userpass/: - $ vault auth-enable userpass + $ vault auth enable userpass - Enable the LDAP auth provider at auth-prod/: + Enable the LDAP auth method at auth-prod/: - $ vault auth-enable -path=auth-prod ldap + $ vault auth enable -path=auth-prod ldap - Enable a custom auth plugin (after it is registered in the plugin registry): + Enable a custom auth plugin (after it's registered in the plugin registry): - $ vault auth-enable -path=my-auth -plugin-name=my-auth-plugin plugin - - For a full list of examples, please see the documentation. + $ vault auth enable -path=my-auth -plugin-name=my-auth-plugin plugin ` + c.Flags().Help() @@ -64,7 +60,7 @@ func (c *AuthEnableCommand) Flags() *FlagSets { Target: &c.flagDescription, Completion: complete.PredictAnything, Usage: "Human-friendly description for the purpose of this " + - "authentication provider.", + "auth method.", }) f.StringVar(&StringVar{ @@ -72,16 +68,17 @@ func (c *AuthEnableCommand) Flags() *FlagSets { Target: &c.flagPath, Default: "", // The default is complex, so we have to manually document Completion: complete.PredictAnything, - Usage: "Place where the auth provider will be accessible. This must be " + - "unique across all auth providers. This defaults to the \"type\" of " + - "the mount. The auth provider will be accessible at \"/auth/\".", + Usage: "Place where the auth method will be accessible. This must be " + + "unique across all auth methods. This defaults to the \"type\" of " + + "the auth method. The auth method will be accessible at " + + "\"/auth/\".", }) f.StringVar(&StringVar{ Name: "plugin-name", Target: &c.flagPluginName, Completion: complete.PredictAnything, - Usage: "Name of the auth provider plugin. This plugin name must already " + + Usage: "Name of the auth method plugin. This plugin name must already " + "exist in the Vault server's plugin catalog.", }) @@ -89,7 +86,7 @@ func (c *AuthEnableCommand) Flags() *FlagSets { Name: "local", Target: &c.flagLocal, Default: false, - Usage: "Mark the auth provider as local-only. Local auth providers are " + + Usage: "Mark the auth method as local-only. Local auth methods are " + "not replicated nor removed by replication.", }) @@ -156,7 +153,7 @@ func (c *AuthEnableCommand) Run(args []string) int { return 2 } - authThing := authType + " auth provider" + authThing := authType + " auth method" if authType == "plugin" { authThing = c.flagPluginName + " plugin" } diff --git a/command/auth_enable_test.go b/command/auth_enable_test.go index 00016d0138..e4308f9934 100644 --- a/command/auth_enable_test.go +++ b/command/auth_enable_test.go @@ -89,7 +89,7 @@ func TestAuthEnableCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Enabled userpass auth provider at:" + expected := "Success! Enabled userpass auth method at:" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) diff --git a/command/auth_help.go b/command/auth_help.go index 7fbd5e1443..d18d1bf095 100644 --- a/command/auth_help.go +++ b/command/auth_help.go @@ -8,42 +8,41 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*AuthHelpCommand)(nil) var _ cli.CommandAutocomplete = (*AuthHelpCommand)(nil) -// AuthHelpCommand is a Command that prints help output for a given auth -// provider type AuthHelpCommand struct { *BaseCommand - Handlers map[string]AuthHandler + Handlers map[string]LoginHandler } func (c *AuthHelpCommand) Synopsis() string { - return "Prints usage for an auth provider" + return "Prints usage for an auth method" } func (c *AuthHelpCommand) Help() string { helpText := ` -Usage: vault path-help [options] TYPE | PATH +Usage: vault auth help [options] TYPE | PATH - Prints usage and help for an authentication provider. If provided a TYPE, - this command retrieves the default help for the given authentication - provider of that type. If given a PATH, this command returns the help - output for the authentication provider mounted at that path. If given a - PATH argument, the path must exist and be mounted. + Prints usage and help for an auth method. - Get usage instructions for the userpass authentication provider: + - If given a TYPE, this command prints the default help for the + auth method of that type. - $ vault auth-help userpass + - If given a PATH, this command prints the help output for the + auth method enabled at that path. This path must already + exist. - Print usage for the authentication provider mounted at my-provider/ + Get usage instructions for the userpass auth method: - $ vault auth-help my-provider/: + $ vault auth help userpass - Each authentication provider produces its own help output. For additional - information, please view the online documentation. + Print usage for the auth method enabled at my-method/: + + $ vault auth help my-method/ + + Each auth method produces its own help output. ` + c.Flags().Help() @@ -98,7 +97,7 @@ func (c *AuthHelpCommand) Run(args []string) int { // There was no auth type by that name, see if it's a mount auths, err := client.Sys().ListAuth() if err != nil { - c.UI.Error(fmt.Sprintf("Error listing authentication providers: %s", err)) + c.UI.Error(fmt.Sprintf("Error listing auth methods: %s", err)) return 2 } @@ -106,14 +105,14 @@ func (c *AuthHelpCommand) Run(args []string) int { auth, ok := auths[authPath] if !ok { c.UI.Error(fmt.Sprintf( - "Error retrieving help: unknown authentication provider: %s", args[0])) + "Error retrieving help: unknown auth method: %s", authType)) return 1 } authHandler, ok = c.Handlers[auth.Type] if !ok { c.UI.Error(wrapAtLength(fmt.Sprintf( - "INTERNAL ERROR! Found an authentication provider mounted at %s, but "+ + "INTERNAL ERROR! Found an auth method enabled at %s, but "+ "its type %q is not registered in Vault. This is a bug and should "+ "be reported. Please open an issue at github.com/hashicorp/vault.", authPath, authType))) diff --git a/command/auth_help_test.go b/command/auth_help_test.go index b157552019..9457bea0ec 100644 --- a/command/auth_help_test.go +++ b/command/auth_help_test.go @@ -17,7 +17,7 @@ func testAuthHelpCommand(tb testing.TB) (*cli.MockUi, *AuthHelpCommand) { BaseCommand: &BaseCommand{ UI: ui, }, - Handlers: map[string]AuthHandler{ + Handlers: map[string]LoginHandler{ "userpass": &credUserpass.CLIHandler{ DefaultMount: "userpass", }, @@ -88,7 +88,7 @@ func TestAuthHelpCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Usage: vault auth -method=userpass" + expected := "Usage: vault login -method=userpass" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -101,7 +101,7 @@ func TestAuthHelpCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - // No mounted auth backends + // No mounted auth methods ui, cmd := testAuthHelpCommand(t) cmd.client = client @@ -113,7 +113,7 @@ func TestAuthHelpCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Usage: vault auth -method=userpass" + expected := "Usage: vault login -method=userpass" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -136,7 +136,7 @@ func TestAuthHelpCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error listing authentication providers: " + expected := "Error listing auth methods: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) diff --git a/command/auth_list.go b/command/auth_list.go index 7adda391e6..d67b5bac9e 100644 --- a/command/auth_list.go +++ b/command/auth_list.go @@ -11,12 +11,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*AuthListCommand)(nil) var _ cli.CommandAutocomplete = (*AuthListCommand)(nil) -// AuthListCommand is a Command that lists the enabled authentication methods -// and data about them. type AuthListCommand struct { *BaseCommand @@ -24,25 +21,24 @@ type AuthListCommand struct { } func (c *AuthListCommand) Synopsis() string { - return "Lists enabled auth providers" + return "Lists enabled auth methods" } func (c *AuthListCommand) Help() string { helpText := ` -Usage: vault auth-methods [options] +Usage: vault auth list [options] - Lists the enabled authentication providers on the Vault server. This command - also outputs information about the provider including configuration and - human-friendly descriptions. A TTL of "system" indicates that the system - default is in use. + Lists the enabled auth methods on the Vault server. This command also outputs + information about the method including configuration and human-friendly + descriptions. A TTL of "system" indicates that the system default is in use. - List all enabled authentication providers: + List all enabled auth methods: - $ vault auth-list + $ vault auth list - List all enabled authentication providers with detailed output: + List all enabled auth methods with detailed output: - $ vault auth-list -detailed + $ vault auth list -detailed ` + c.Flags().Help() @@ -59,7 +55,7 @@ func (c *AuthListCommand) Flags() *FlagSets { Target: &c.flagDetailed, Default: false, Usage: "Print detailed information such as configuration and replication " + - "status about each authentication provider.", + "status about each auth method.", }) return set @@ -100,11 +96,11 @@ func (c *AuthListCommand) Run(args []string) int { } if c.flagDetailed { - c.UI.Output(tableOutput(c.detailedMounts(auths))) + c.UI.Output(tableOutput(c.detailedMounts(auths), nil)) return 0 } - c.UI.Output(tableOutput(c.simpleMounts(auths))) + c.UI.Output(tableOutput(c.simpleMounts(auths), nil)) return 0 } diff --git a/command/auth_test.go b/command/auth_test.go index ca1e916284..5ec0cf60d3 100644 --- a/command/auth_test.go +++ b/command/auth_test.go @@ -6,7 +6,6 @@ import ( "github.com/mitchellh/cli" - "github.com/hashicorp/vault/api" credToken "github.com/hashicorp/vault/builtin/credential/token" credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" "github.com/hashicorp/vault/command/token" @@ -23,7 +22,7 @@ func testAuthCommand(tb testing.TB) (*cli.MockUi, *AuthCommand) { // Override to our own token helper tokenHelper: token.NewTestingTokenHelper(), }, - Handlers: map[string]AuthHandler{ + Handlers: map[string]LoginHandler{ "token": &credToken.CLIHandler{}, "userpass": &credUserpass.CLIHandler{}, }, @@ -33,55 +32,61 @@ func testAuthCommand(tb testing.TB) (*cli.MockUi, *AuthCommand) { func TestAuthCommand_Run(t *testing.T) { t.Parallel() - deprecations := []struct { - name string - args []string - out string - code int - }{ - { - "methods", - []string{"-methods"}, - "token/", - 0, - }, - { - "method_help", - []string{"-method", "userpass", "-method-help"}, - "Usage: vault auth -method=userpass", - 0, - }, - } - - t.Run("deprecations", func(t *testing.T) { + // TODO: remove in 0.9.0 + t.Run("deprecated_methods", func(t *testing.T) { t.Parallel() - for _, tc := range deprecations { - tc := tc + client, closer := testVaultServer(t) + defer closer() - t.Run(tc.name, func(t *testing.T) { - t.Parallel() + ui, cmd := testAuthCommand(t) + cmd.client = client - client, closer := testVaultServer(t) - defer closer() + // vault auth -methods -> vault auth list + code := cmd.Run([]string{"-methods"}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + stdout, stderr := ui.OutputWriter.String(), ui.ErrorWriter.String() - ui, cmd := testAuthCommand(t) - cmd.client = client + if expected := "WARNING!"; !strings.Contains(stderr, expected) { + t.Errorf("expected %q to contain %q", stderr, expected) + } - 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) - } - }) + if expected := "token/"; !strings.Contains(stdout, expected) { + t.Errorf("expected %q to contain %q", stdout, expected) } }) - t.Run("custom_path", func(t *testing.T) { + t.Run("deprecated_method_help", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testAuthCommand(t) + cmd.client = client + + // vault auth -method=foo -method-help -> vault auth help foo + code := cmd.Run([]string{ + "-method=userpass", + "-method-help", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + stdout, stderr := ui.OutputWriter.String(), ui.ErrorWriter.String() + + if expected := "WARNING!"; !strings.Contains(stderr, expected) { + t.Errorf("expected %q to contain %q", stderr, expected) + } + + if expected := "vault login"; !strings.Contains(stdout, expected) { + t.Errorf("expected %q to contain %q", stdout, expected) + } + }) + + t.Run("deprecated_login", func(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) @@ -100,11 +105,7 @@ func TestAuthCommand_Run(t *testing.T) { ui, cmd := testAuthCommand(t) cmd.client = client - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - + // vault auth ARGS -> vault login ARGS code := cmd.Run([]string{ "-method", "userpass", "-path", "my-auth", @@ -112,420 +113,16 @@ func TestAuthCommand_Run(t *testing.T) { "password=test", }) if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + stdout, stderr := ui.OutputWriter.String(), ui.ErrorWriter.String() + + if expected := "WARNING!"; !strings.Contains(stderr, expected) { + t.Errorf("expected %q to contain %q", stderr, expected) } - expected := "Success! You are now authenticated." - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to be %q", combined, expected) - } - - storedToken, err := tokenHelper.Get() - if err != nil { - t.Fatal(err) - } - - if l, exp := len(storedToken), 36; l != exp { - t.Errorf("expected token to be %d characters, was %d: %q", exp, l, storedToken) - } - }) - - t.Run("no_verify", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"default"}, - TTL: "30m", - NumUses: 1, - }) - if err != nil { - t.Fatal(err) - } - token := secret.Auth.ClientToken - - _, cmd := testAuthCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "-no-verify", - token, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - lookup, err := client.Auth().Token().Lookup(token) - if err != nil { - t.Fatal(err) - } - - // There was 1 use to start, make sure we didn't use it (verifying would - // use it). - uses := lookup.TokenRemainingUses() - if uses != 1 { - t.Errorf("expected %d to be %d", uses, 1) - } - }) - - t.Run("no_store", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"default"}, - TTL: "30m", - }) - if err != nil { - t.Fatal(err) - } - token := secret.Auth.ClientToken - - _, cmd := testAuthCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - // Ensure we have no token to start - if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { - t.Errorf("expected token helper to be empty: %s: %q", err, storedToken) - } - - code := cmd.Run([]string{ - "-no-store", - token, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - storedToken, err := tokenHelper.Get() - if err != nil { - t.Fatal(err) - } - - if exp := ""; storedToken != exp { - t.Errorf("expected %q to be %q", storedToken, exp) - } - }) - - t.Run("stores", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ - Policies: []string{"default"}, - TTL: "30m", - }) - if err != nil { - t.Fatal(err) - } - token := secret.Auth.ClientToken - - _, cmd := testAuthCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - token, - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - storedToken, err := tokenHelper.Get() - if err != nil { - t.Fatal(err) - } - - if storedToken != token { - t.Errorf("expected %q to be %q", storedToken, token) - } - }) - - t.Run("only_token", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testAuthCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "-only-token", - "-method", "userpass", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - // Verify only the token was printed - token := ui.OutputWriter.String() + ui.ErrorWriter.String() - if l, exp := len(token), 36; l != exp { - t.Errorf("expected token to be %d characters, was %d: %q", exp, l, token) - } - - // Verify the token was not stored - if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { - t.Fatalf("expted token to not be stored: %s: %q", err, storedToken) - } - }) - - t.Run("failure_no_store", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - ui, cmd := testAuthCommand(t) - cmd.client = client - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - - code := cmd.Run([]string{ - "not-a-real-token", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error verifying token: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) - } - - if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { - t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) - } - }) - - t.Run("wrap_auto_unwrap", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - _, cmd := testAuthCommand(t) - cmd.client = client - - // Set the wrapping ttl to 5s. We can't set this via the flag because we - // override the client object before that particular flag is parsed. - client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) - - code := cmd.Run([]string{ - "-method", "userpass", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - // Unset the wrapping - client.SetWrappingLookupFunc(func(string, string) string { return "" }) - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - token, err := tokenHelper.Get() - if err != nil || token == "" { - t.Fatalf("expected token from helper: %s: %q", err, token) - } - - // Ensure the resulting token is unwrapped - secret, err := client.Auth().Token().LookupSelf() - if err != nil { - t.Error(err) - } - - if secret.WrapInfo != nil { - t.Errorf("expected to be unwrapped: %#v", secret) - } - }) - - t.Run("wrap_only_token", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testAuthCommand(t) - cmd.client = client - - // Set the wrapping ttl to 5s. We can't set this via the flag because we - // override the client object before that particular flag is parsed. - client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) - - code := cmd.Run([]string{ - "-only-token", - "-method", "userpass", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - // Unset the wrapping - client.SetWrappingLookupFunc(func(string, string) string { return "" }) - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - storedToken, err := tokenHelper.Get() - if err != nil || storedToken != "" { - t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) - } - - token := ui.OutputWriter.String() - if token == "" { - t.Errorf("expected %q to not be %q", token, "") - } - if strings.Contains(token, "\n") { - t.Errorf("expected %q to not contain %q", token, "\n") - } - - // Ensure the resulting token is, in fact, still wrapped. - client.SetToken(token) - secret, err := client.Logical().Unwrap("") - if err != nil { - t.Error(err) - } - if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { - t.Fatalf("expected secret to have auth: %#v", secret) - } - }) - - t.Run("wrap_no_store", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { - t.Fatal(err) - } - if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ - "password": "test", - "policies": "default", - }); err != nil { - t.Fatal(err) - } - - ui, cmd := testAuthCommand(t) - cmd.client = client - - // Set the wrapping ttl to 5s. We can't set this via the flag because we - // override the client object before that particular flag is parsed. - client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) - - code := cmd.Run([]string{ - "-no-store", - "-method", "userpass", - "username=test", - "password=test", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - // Unset the wrapping - client.SetWrappingLookupFunc(func(string, string) string { return "" }) - - tokenHelper, err := cmd.TokenHelper() - if err != nil { - t.Fatal(err) - } - storedToken, err := tokenHelper.Get() - if err != nil || storedToken != "" { - t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) - } - - expected := "wrapping_token" - output := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(output, expected) { - t.Errorf("expected %q to contain %q", output, expected) - } - }) - - t.Run("communication_failure", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServerBad(t) - defer closer() - - ui, cmd := testAuthCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "token", - }) - if exp := 2; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - expected := "Error verifying token: " - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, expected) { - t.Errorf("expected %q to contain %q", combined, expected) + if expected := "Success! You are now authenticated."; !strings.Contains(stdout, expected) { + t.Errorf("expected %q to contain %q", stdout, expected) } }) diff --git a/command/auth_tune.go b/command/auth_tune.go new file mode 100644 index 0000000000..958d11bd1f --- /dev/null +++ b/command/auth_tune.go @@ -0,0 +1,120 @@ +package command + +import ( + "fmt" + "strings" + "time" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*AuthTuneCommand)(nil) +var _ cli.CommandAutocomplete = (*AuthTuneCommand)(nil) + +type AuthTuneCommand struct { + *BaseCommand + + flagDefaultLeaseTTL time.Duration + flagMaxLeaseTTL time.Duration +} + +func (c *AuthTuneCommand) Synopsis() string { + return "Tunes an auth method configuration" +} + +func (c *AuthTuneCommand) Help() string { + helpText := ` +Usage: vault auth tune [options] PATH + + Tunes the configuration options for the auth method at the given PATH. The + argument corresponds to the PATH where the auth method is enabled, not the + TYPE! + + Tune the default lease for the github auth method: + + $ vault auth tune -default-lease-ttl=72h github/ + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *AuthTuneCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.DurationVar(&DurationVar{ + Name: "default-lease-ttl", + Target: &c.flagDefaultLeaseTTL, + Default: 0, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "The default lease TTL for this auth method. If unspecified, this " + + "defaults to the Vault server's globally configured default lease TTL, " + + "or a previously configured value for the auth method.", + }) + + f.DurationVar(&DurationVar{ + Name: "max-lease-ttl", + Target: &c.flagMaxLeaseTTL, + Default: 0, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "The maximum lease TTL for this auth method. If unspecified, this " + + "defaults to the Vault server's globally configured maximum lease TTL, " + + "or a previously configured value for the auth method.", + }) + + return set +} + +func (c *AuthTuneCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultAuths() +} + +func (c *AuthTuneCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *AuthTuneCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Append /auth (since that's where auths live) and a trailing slash to + // indicate it's a path in output + mountPath := ensureTrailingSlash(sanitizePath(args[0])) + + if err := client.Sys().TuneMount("/auth/"+mountPath, api.MountConfigInput{ + DefaultLeaseTTL: ttlToAPI(c.flagDefaultLeaseTTL), + MaxLeaseTTL: ttlToAPI(c.flagMaxLeaseTTL), + }); err != nil { + c.UI.Error(fmt.Sprintf("Error tuning auth method %s: %s", mountPath, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Tuned the auth method at: %s", mountPath)) + return 0 +} diff --git a/command/auth_tune_test.go b/command/auth_tune_test.go new file mode 100644 index 0000000000..61a36441d7 --- /dev/null +++ b/command/auth_tune_test.go @@ -0,0 +1,149 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" +) + +func testAuthTuneCommand(tb testing.TB) (*cli.MockUi, *AuthTuneCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &AuthTuneCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestAuthTuneCommand_Run(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, + }, + } + + 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() + + ui, cmd := testAuthTuneCommand(t) + + 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("integration", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testAuthTuneCommand(t) + cmd.client = client + + // Mount + if err := client.Sys().EnableAuthWithOptions("my-auth", &api.EnableAuthOptions{ + Type: "userpass", + }); err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "-default-lease-ttl", "30m", + "-max-lease-ttl", "1h", + "my-auth/", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! Tuned the auth method at: my-auth/" + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatal(err) + } + + mountInfo, ok := auths["my-auth/"] + if !ok { + t.Fatalf("expected auth to exist") + } + if exp := "userpass"; mountInfo.Type != exp { + t.Errorf("expected %q to be %q", mountInfo.Type, exp) + } + if exp := 1800; mountInfo.Config.DefaultLeaseTTL != exp { + t.Errorf("expected %d to be %d", mountInfo.Config.DefaultLeaseTTL, exp) + } + if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp { + t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testAuthTuneCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "userpass/", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error tuning auth method userpass/: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testAuthTuneCommand(t) + assertNoTabs(t, cmd) + }) +} diff --git a/command/token/helper.go b/command/token/helper.go index db068beb29..cac79487fa 100644 --- a/command/token/helper.go +++ b/command/token/helper.go @@ -3,7 +3,7 @@ package token // TokenHelper is an interface that contains basic operations that must be // implemented by a token helper type TokenHelper interface { - // Path displays a backend-specific path; for the internal helper this + // Path displays a method-specific path; for the internal helper this // is the location of the token stored on disk; for the external helper // this is the location of the binary being invoked Path() string From 5c5d06ecd72c815c2d5670d9f28886ea08c0af20 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:56:53 -0400 Subject: [PATCH 086/281] Add login subcommand This replaces the "auth" part of "vault auth" --- command/login.go | 380 ++++++++++++++++++++++++++++++++ command/login_test.go | 493 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 873 insertions(+) create mode 100644 command/login.go create mode 100644 command/login_test.go diff --git a/command/login.go b/command/login.go new file mode 100644 index 0000000000..25fddb17bd --- /dev/null +++ b/command/login.go @@ -0,0 +1,380 @@ +package command + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/hashicorp/vault/api" + "github.com/posener/complete" +) + +// LoginHandler is the interface that any auth handlers must implement to enable +// auth via the CLI. +type LoginHandler interface { + Auth(*api.Client, map[string]string) (*api.Secret, error) + Help() string +} + +type LoginCommand struct { + *BaseCommand + + Handlers map[string]LoginHandler + + flagMethod string + flagPath string + flagNoStore bool + flagTokenOnly bool + + // Deprecations + // TODO: remove in 0.9.0 + flagNoVerify bool + + testStdin io.Reader // for tests +} + +func (c *LoginCommand) Synopsis() string { + return "Authenticate locally" +} + +func (c *LoginCommand) Help() string { + helpText := ` +Usage: vault login [options] [AUTH K=V...] + + Authenticates users or machines to Vault using the provided arguments. A + successful authentication results in a Vault token - conceptually similar to + a session token on a website. By default, this token is cached on the local + machine for future requests. + + The default authentication method is "token". If not supplied via the CLI, + Vault will prompt for input. If the argument is "-", the values are read + from stdin. + + The -method flag allows using other authentication methods, such as userpass, + github, or cert. For these, additional "K=V" pairs may be required. For + example, to authenticate to the userpass auth method: + + $ vault login -method=userpass username=my-username + + For more information about the list of configuration parameters available + for a given authentication method, use the "vault auth help TYPE". You can + also use "vault auth list" to see the list of enabled authentication methods. + + If an authentication method is enabled at a non-standard path, the -method + flag still refers to the canonical type, but the -path flag refers to the + enabled path. If a github authentication method was enabled at "github-ent", + authenticate like this: + + $ vault login -method=github -path=github-prod + + If the authentication is requested with response wrapping (via -wrap-ttl), + the returned token is automatically unwrapped unless: + + - The -token-only flag is used, in which case this command will output + the wrapping token. + + - The -no-store flag is used, in which case this command will output the + details of the wrapping token. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *LoginCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat) + + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "method", + Target: &c.flagMethod, + Default: "token", + Completion: c.PredictVaultAvailableAuths(), + Usage: "Type of authentication to use such as \"userpass\" or " + + "\"ldap\". Note this corresponds to the TYPE, not the enabled path. " + + "Use -path to specify the path where the authentication is enabled.", + }) + + f.StringVar(&StringVar{ + Name: "path", + Target: &c.flagPath, + Default: "", + Completion: c.PredictVaultAuths(), + Usage: "Remote path in Vault where the authentication method is enabled. " + + "This defaults to the TYPE of method (e.g. userpass -> userpass/).", + }) + + f.BoolVar(&BoolVar{ + Name: "no-store", + Target: &c.flagNoStore, + Default: false, + Usage: "Do not persist the token to the token helper (usually the " + + "local filesystem) after authentication for use in future requests. " + + "The token will only be displayed in the command output.", + }) + + f.BoolVar(&BoolVar{ + Name: "token-only", + Target: &c.flagTokenOnly, + Default: false, + Usage: "Output only the token with no verification. This flag is a " + + "shortcut for \"-field=token -no-store\". Setting those flags to other " + + "values will have no affect.", + }) + + // Deprecations + // TODO: remove in 0.9.0 + f.BoolVar(&BoolVar{ + Name: "no-verify", + Target: &c.flagNoVerify, + Hidden: true, + Default: false, + Usage: "", + }) + + return set +} + +func (c *LoginCommand) AutocompleteArgs() complete.Predictor { + return nil +} + +func (c *LoginCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *LoginCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + + // Deprecations + // TODO: remove in 0.9.0 + switch { + case c.flagNoVerify: + c.UI.Warn(wrapAtLength( + "WARNING! The -no-verify flag is deprecated. In the past, Vault " + + "performed a lookup on a token after authentication. This is no " + + "longer the case for all auth methods except \"token\". Vault will " + + "still attempt to perform a lookup when given a token directly " + + "because that is how it gets the list of policies, ttl, and other " + + "metadata. To disable this lookup, specify \"lookup=false\" as a " + + "configuration option to the token auth method, like this:")) + c.UI.Warn("") + c.UI.Warn(" $ vault auth token=ABCD lookup=false") + c.UI.Warn("") + c.UI.Warn("Or omit the token and Vault will prompt for it:") + c.UI.Warn("") + c.UI.Warn(" $ vault auth lookup=false") + c.UI.Warn(" Token (will be hidden): ...") + c.UI.Warn("") + c.UI.Warn(wrapAtLength( + "If you are not using token authentication, you can safely omit this " + + "flag. Vault will not perform a lookup after authentication.")) + c.UI.Warn("") + + // There's no point in passing this to other auth handlers... + if c.flagMethod == "token" { + args = append(args, "lookup=false") + } + } + + // Set the right flags if the user requested token-only - this overrides + // any previously configured values, as documented. + if c.flagTokenOnly { + c.flagNoStore = true + c.flagField = "token" + } + + // Get the auth method + authMethod := sanitizePath(c.flagMethod) + if authMethod == "" { + authMethod = "token" + } + + // If no path is specified, we default the path to the method type + // or use the plugin name if it's a plugin + authPath := c.flagPath + if authPath == "" { + authPath = ensureTrailingSlash(authMethod) + } + + // Get the handler function + authHandler, ok := c.Handlers[authMethod] + if !ok { + c.UI.Error(wrapAtLength(fmt.Sprintf( + "Unknown authentication method: %s. Use \"vault auth list\" to see the "+ + "complete list of authentication methods. Additionally, some "+ + "authentication methods are only available via the HTTP API.", + authMethod))) + return 1 + } + + // Pull our fake stdin if needed + stdin := (io.Reader)(os.Stdin) + if c.testStdin != nil { + stdin = c.testStdin + } + + // If the user provided a token, pass it along to the auth provier. + if authMethod == "token" && len(args) > 0 && !strings.Contains(args[0], "=") { + args = append([]string{"token=" + args[0]}, args[1:]...) + } + + config, err := parseArgsDataString(stdin, args) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing configuration: %s", err)) + return 1 + } + + // If the user did not specify a mount path, use the provided mount path. + if config["mount"] == "" && authPath != "" { + config["mount"] = authPath + } + + // Create the client + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Authenticate delegation to the auth handler + secret, err := authHandler.Auth(client, config) + if err != nil { + c.UI.Error(fmt.Sprintf("Error authenticating: %s", err)) + return 2 + } + + // Unset any previous token wrapping functionality. If the original request + // was for a wrapped token, we don't want future requests to be wrapped. + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + // Recursively extract the token, handling wrapping + unwrap := !c.flagTokenOnly && !c.flagNoStore + secret, isWrapped, err := c.extractToken(client, secret, unwrap) + if err != nil { + c.UI.Error(fmt.Sprintf("Error extracting token: %s", err)) + return 2 + } + if secret == nil { + c.UI.Error("Vault returned an empty secret") + return 2 + } + + // Handle special cases if the token was wrapped + if isWrapped { + if c.flagTokenOnly { + return PrintRawField(c.UI, secret, "wrapping_token") + } + if c.flagNoStore { + return OutputSecret(c.UI, c.flagFormat, secret) + } + } + + // If we got this far, verify we have authentication data before continuing + if secret.Auth == nil { + c.UI.Error(wrapAtLength( + "Vault returned a secret, but the secret has no authentication " + + "information attached. This should never happen and is likely a " + + "bug.")) + return 2 + } + + // Pull the token itself out, since we don't need the rest of the auth + // information anymore/. + token := secret.Auth.ClientToken + + if !c.flagNoStore { + // Grab the token helper so we can store + tokenHelper, err := c.TokenHelper() + if err != nil { + c.UI.Error(wrapAtLength(fmt.Sprintf( + "Error initializing token helper. Please verify that the token "+ + "helper is available and properly configured for your system. The "+ + "error was: %s", err))) + return 1 + } + + // Store the token in the local client + if err := tokenHelper.Store(token); err != nil { + c.UI.Error(fmt.Sprintf("Error storing token: %s", err)) + c.UI.Error(wrapAtLength( + "Authentication was successful, but the token was not persisted. The "+ + "resulting token is shown below for your records.") + "\n") + OutputSecret(c.UI, c.flagFormat, secret) + return 2 + } + + // Warn if the VAULT_TOKEN environment variable is set, as that will take + // precedence. We output as a warning, so piping should still work since it + // will be on a different stream. + if os.Getenv("VAULT_TOKEN") != "" { + c.UI.Warn(wrapAtLength("WARNING! The VAULT_TOKEN environment variable "+ + "is set! This takes precedence over the value set by this command. To "+ + "use the value set by this command, unset the VAULT_TOKEN environment "+ + "variable or set it to the token displayed below.") + "\n") + } + } else { + c.UI.Warn(wrapAtLength( + "The token was not stored in token helper. Set the VAULT_TOKEN "+ + "environment variable or pass the token below with each request to "+ + "Vault.") + "\n") + } + + // If the user requested a particular field, print that out now since we + // are likely piping to another process. + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) + } + + // Print some yay! text, but only in table mode. + if c.flagFormat == "table" { + c.UI.Output(wrapAtLength( + "Success! You are now authenticated. The token information displayed "+ + "below is already stored in the token helper. You do NOT need to run "+ + "\"vault login\" again. Future Vault requests will automatically use "+ + "this token.") + "\n") + } + + return OutputSecret(c.UI, c.flagFormat, secret) +} + +// extractToken extracts the token from the given secret, automatically +// unwrapping responses and handling error conditions if unwrap is true. The +// result also returns whether it was a wrapped resonse that was not unwrapped. +func (c *LoginCommand) extractToken(client *api.Client, secret *api.Secret, unwrap bool) (*api.Secret, bool, error) { + switch { + case secret == nil: + return nil, false, fmt.Errorf("empty response from auth helper") + + case secret.Auth != nil: + return secret, false, nil + + case secret.WrapInfo != nil: + if secret.WrapInfo.WrappedAccessor == "" { + return nil, false, fmt.Errorf("wrapped response does not contain a token") + } + + if !unwrap { + return secret, true, nil + } + + client.SetToken(secret.WrapInfo.Token) + secret, err := client.Logical().Unwrap("") + if err != nil { + return nil, false, err + } + return c.extractToken(client, secret, unwrap) + + default: + return nil, false, fmt.Errorf("no auth or wrapping info in response") + } +} diff --git a/command/login_test.go b/command/login_test.go new file mode 100644 index 0000000000..38ae744a2c --- /dev/null +++ b/command/login_test.go @@ -0,0 +1,493 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" + + "github.com/hashicorp/vault/api" + credToken "github.com/hashicorp/vault/builtin/credential/token" + credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/command/token" +) + +func testLoginCommand(tb testing.TB) (*cli.MockUi, *LoginCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &LoginCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + + // Override to our own token helper + tokenHelper: token.NewTestingTokenHelper(), + }, + Handlers: map[string]LoginHandler{ + "token": &credToken.CLIHandler{}, + "userpass": &credUserpass.CLIHandler{}, + }, + } +} + +func TestLoginCommand_Run(t *testing.T) { + t.Parallel() + + t.Run("custom_path", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/my-auth/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testLoginCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "-method", "userpass", + "-path", "my-auth", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Success! You are now authenticated." + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to be %q", combined, expected) + } + + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if l, exp := len(storedToken), 36; l != exp { + t.Errorf("expected token to be %d characters, was %d: %q", exp, l, storedToken) + } + }) + + t.Run("no_store", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + _, cmd := testLoginCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + // Ensure we have no token to start + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Errorf("expected token helper to be empty: %s: %q", err, storedToken) + } + + code := cmd.Run([]string{ + "-no-store", + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if exp := ""; storedToken != exp { + t.Errorf("expected %q to be %q", storedToken, exp) + } + }) + + t.Run("stores", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + _, cmd := testLoginCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + storedToken, err := tokenHelper.Get() + if err != nil { + t.Fatal(err) + } + + if storedToken != token { + t.Errorf("expected %q to be %q", storedToken, token) + } + }) + + t.Run("token_only", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testLoginCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "-token-only", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Verify only the token was printed + token := ui.OutputWriter.String() + if l, exp := len(token), 36; l != exp { + t.Errorf("expected token to be %d characters, was %d: %q", exp, l, token) + } + + // Verify the token was not stored + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Fatalf("expted token to not be stored: %s: %q", err, storedToken) + } + }) + + t.Run("failure_no_store", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testLoginCommand(t) + cmd.client = client + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + + code := cmd.Run([]string{ + "not-a-real-token", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error authenticating: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + + if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + }) + + t.Run("wrap_auto_unwrap", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + _, cmd := testLoginCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + token, err := tokenHelper.Get() + if err != nil || token == "" { + t.Fatalf("expected token from helper: %s: %q", err, token) + } + client.SetToken(token) + + // Ensure the resulting token is unwrapped + secret, err := client.Auth().Token().LookupSelf() + if err != nil { + t.Error(err) + } + if secret == nil { + t.Fatal("secret was nil") + } + + if secret.WrapInfo != nil { + t.Errorf("expected to be unwrapped: %#v", secret) + } + }) + + t.Run("wrap_token_only", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testLoginCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-token-only", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + storedToken, err := tokenHelper.Get() + if err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + + token := strings.TrimSpace(ui.OutputWriter.String()) + if token == "" { + t.Errorf("expected %q to not be %q", token, "") + } + + // Ensure the resulting token is, in fact, still wrapped. + client.SetToken(token) + secret, err := client.Logical().Unwrap("") + if err != nil { + t.Error(err) + } + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("expected secret to have auth: %#v", secret) + } + }) + + t.Run("wrap_no_store", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { + t.Fatal(err) + } + if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{ + "password": "test", + "policies": "default", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testLoginCommand(t) + cmd.client = client + + // Set the wrapping ttl to 5s. We can't set this via the flag because we + // override the client object before that particular flag is parsed. + client.SetWrappingLookupFunc(func(string, string) string { return "5m" }) + + code := cmd.Run([]string{ + "-no-store", + "-method", "userpass", + "username=test", + "password=test", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + // Unset the wrapping + client.SetWrappingLookupFunc(func(string, string) string { return "" }) + + tokenHelper, err := cmd.TokenHelper() + if err != nil { + t.Fatal(err) + } + storedToken, err := tokenHelper.Get() + if err != nil || storedToken != "" { + t.Fatalf("expected token to not be stored: %s: %q", err, storedToken) + } + + expected := "wrapping_token" + output := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(output, expected) { + t.Errorf("expected %q to contain %q", output, expected) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testLoginCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "token", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error authenticating: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + // Deprecations + // TODO: remove in 0.9.0 + t.Run("deprecated_no_verify", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{ + Policies: []string{"default"}, + TTL: "30m", + NumUses: 1, + }) + if err != nil { + t.Fatal(err) + } + token := secret.Auth.ClientToken + + _, cmd := testLoginCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-no-verify", + token, + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + lookup, err := client.Auth().Token().Lookup(token) + if err != nil { + t.Fatal(err) + } + + // There was 1 use to start, make sure we didn't use it (verifying would + // use it). + uses := lookup.TokenRemainingUses() + if uses != 1 { + t.Errorf("expected %d to be %d", uses, 1) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testLoginCommand(t) + assertNoTabs(t, cmd) + }) +} From 98b356d7f18fd09ba2a7c4595892ce8ac801d6f8 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:57:32 -0400 Subject: [PATCH 087/281] Make audit a subcommand --- command/audit.go | 42 +++++++++++++++++++++++++++++++++++ command/audit_disable.go | 35 +++++++++++++---------------- command/audit_disable_test.go | 18 +++++++-------- command/audit_enable.go | 39 ++++++++++++++------------------ command/audit_enable_test.go | 8 +++---- command/audit_list.go | 28 ++++++++++------------- 6 files changed, 100 insertions(+), 70 deletions(-) create mode 100644 command/audit.go diff --git a/command/audit.go b/command/audit.go new file mode 100644 index 0000000000..0e59357794 --- /dev/null +++ b/command/audit.go @@ -0,0 +1,42 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*AuditCommand)(nil) + +type AuditCommand struct { + *BaseCommand +} + +func (c *AuditCommand) Synopsis() string { + return "Interact with audit devices" +} + +func (c *AuditCommand) Help() string { + helpText := ` +Usage: vault audit [options] [args] + + This command groups subcommands for interacting with Vault's audit devices. + Users can list, enable, and disable audit devices. + + List all enabled audit devices: + + $ vault audit list + + Enable a new audit device "userpass"; + + $ vault audit enable file file_path=/var/log/audit.log + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (c *AuditCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/audit_disable.go b/command/audit_disable.go index 6bb9f68ff9..1025a0ba27 100644 --- a/command/audit_disable.go +++ b/command/audit_disable.go @@ -8,32 +8,30 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*AuditDisableCommand)(nil) var _ cli.CommandAutocomplete = (*AuditDisableCommand)(nil) -// AuditDisableCommand is a Command that mounts a new mount. type AuditDisableCommand struct { *BaseCommand } func (c *AuditDisableCommand) Synopsis() string { - return "Disables an audit backend" + return "Disables an audit device" } func (c *AuditDisableCommand) Help() string { helpText := ` -Usage: vault audit-disable [options] PATH +Usage: vault audit disable [options] PATH - Disables an audit backend. Once an audit backend is disabled, no future - audit logs are dispatched to it. The data associated with the audit backend - is not affected. + Disables an audit device. Once an audit device is disabled, no future audit + logs are dispatched to it. The data associated with the audit device is not + affected. - The argument corresponds to the PATH of the mount, not the TYPE! + The argument corresponds to the PATH of audit device, not the TYPE! - Disable the audit backend at file/: + Disable the audit device enabled at "file/": - $ vault audit-disable file/ + $ vault audit disable file/ ` + c.Flags().Help() @@ -61,18 +59,17 @@ func (c *AuditDisableCommand) Run(args []string) int { } args = f.Args() - path, kvs, err := extractPath(args) - if err != nil { - c.UI.Error(err.Error()) + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 - } - path = ensureTrailingSlash(path) - - if len(kvs) > 0 { + case len(args) > 1: c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) return 1 } + path := ensureTrailingSlash(sanitizePath(args[0])) + client, err := c.Client() if err != nil { c.UI.Error(err.Error()) @@ -80,11 +77,11 @@ func (c *AuditDisableCommand) Run(args []string) int { } if err := client.Sys().DisableAudit(path); err != nil { - c.UI.Error(fmt.Sprintf("Error disabling audit backend: %s", err)) + c.UI.Error(fmt.Sprintf("Error disabling audit device: %s", err)) return 2 } - c.UI.Output(fmt.Sprintf("Success! Disabled audit backend (if it was enabled) at: %s", path)) + c.UI.Output(fmt.Sprintf("Success! Disabled audit device (if it was enabled) at: %s", path)) return 0 } diff --git a/command/audit_disable_test.go b/command/audit_disable_test.go index 6980179abd..0a7e8e4dcd 100644 --- a/command/audit_disable_test.go +++ b/command/audit_disable_test.go @@ -29,27 +29,27 @@ func TestAuditDisableCommand_Run(t *testing.T) { code int }{ { - "empty", + "not_enough_args", nil, - "Missing PATH!", + "Not enough arguments", 1, }, { - "slash", - []string{"/"}, - "Missing PATH!", + "too_many_args", + []string{"foo", "bar", "baz"}, + "Too many arguments", 1, }, { "not_real", []string{"not_real"}, - "Success! Disabled audit backend (if it was enabled) at: not_real/", + "Success! Disabled audit device (if it was enabled) at: not_real/", 0, }, { "default", []string{"file"}, - "Success! Disabled audit backend (if it was enabled) at: file/", + "Success! Disabled audit device (if it was enabled) at: file/", 0, }, } @@ -112,7 +112,7 @@ func TestAuditDisableCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Disabled audit backend (if it was enabled) at: integration_audit_disable/" + expected := "Success! Disabled audit device (if it was enabled) at: integration_audit_disable/" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -144,7 +144,7 @@ func TestAuditDisableCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error disabling audit backend: " + expected := "Error disabling audit device: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) diff --git a/command/audit_enable.go b/command/audit_enable.go index 61fed20486..85b3bac9aa 100644 --- a/command/audit_enable.go +++ b/command/audit_enable.go @@ -11,11 +11,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*AuditEnableCommand)(nil) var _ cli.CommandAutocomplete = (*AuditEnableCommand)(nil) -// AuditEnableCommand is a Command that mounts a new mount. type AuditEnableCommand struct { *BaseCommand @@ -27,26 +25,23 @@ type AuditEnableCommand struct { } func (c *AuditEnableCommand) Synopsis() string { - return "Enables an audit backend" + return "Enables an audit device" } func (c *AuditEnableCommand) Help() string { helpText := ` -Usage: vault audit-enable [options] TYPE [CONFIG K=V...] +Usage: vault audit enable [options] TYPE [CONFIG K=V...] - Enables an audit backend at a given path. + Enables an audit device at a given path. - This command enables an audit backend of type "type". Additional - options for configuring the audit backend can be specified after the - type in the same format as the "vault write" command in key/value pairs. + This command enables an audit device of TYPE. Additional options for + configuring the audit device can be specified after the type in the same + format as the "vault write" command in key/value pairs. - For example, to configure the file audit backend to write audit logs at - the path /var/log/audit.log: + For example, to configure the file audit device to write audit logs at the + path "/var/log/audit.log": - $ vault audit-enable file file_path=/var/log/audit.log - - For information on available configuration options, please see the - documentation. + $ vault audit enable file file_path=/var/log/audit.log ` + c.Flags().Help() @@ -65,7 +60,7 @@ func (c *AuditEnableCommand) Flags() *FlagSets { EnvVar: "", Completion: complete.PredictAnything, Usage: "Human-friendly description for the purpose of this audit " + - "backend.", + "device.", }) f.StringVar(&StringVar{ @@ -74,9 +69,9 @@ func (c *AuditEnableCommand) Flags() *FlagSets { Default: "", // The default is complex, so we have to manually document EnvVar: "", Completion: complete.PredictAnything, - Usage: "Place where the audit backend will be accessible. This must be " + - "unique across all audit backends. This defaults to the \"type\" of the " + - "audit backend.", + Usage: "Place where the audit device will be accessible. This must be " + + "unique across all audit devices. This defaults to the \"type\" of the " + + "audit device.", }) f.BoolVar(&BoolVar{ @@ -84,8 +79,8 @@ func (c *AuditEnableCommand) Flags() *FlagSets { Target: &c.flagLocal, Default: false, EnvVar: "", - Usage: "Mark the audit backend as a local-only backned. Local backends " + - "are not replicated nor removed by replication.", + Usage: "Mark the audit device as a local-only device. Local devices " + + "are not replicated or removed by replication.", }) return set @@ -150,10 +145,10 @@ func (c *AuditEnableCommand) Run(args []string) int { Options: options, Local: c.flagLocal, }); err != nil { - c.UI.Error(fmt.Sprintf("Error enabling audit backend: %s", err)) + c.UI.Error(fmt.Sprintf("Error enabling audit device: %s", err)) return 2 } - c.UI.Output(fmt.Sprintf("Success! Enabled the %s audit backend at: %s", auditType, auditPath)) + c.UI.Output(fmt.Sprintf("Success! Enabled the %s audit device at: %s", auditType, auditPath)) return 0 } diff --git a/command/audit_enable_test.go b/command/audit_enable_test.go index 6be5c5c68a..c2fe43e84f 100644 --- a/command/audit_enable_test.go +++ b/command/audit_enable_test.go @@ -42,7 +42,7 @@ func TestAuditEnableCommand_Run(t *testing.T) { { "enable", []string{"file", "file_path=discard"}, - "Success! Enabled the file audit backend at: file/", + "Success! Enabled the file audit device at: file/", 0, }, { @@ -52,7 +52,7 @@ func TestAuditEnableCommand_Run(t *testing.T) { "file", "file_path=discard", }, - "Success! Enabled the file audit backend at: audit_path/", + "Success! Enabled the file audit device at: audit_path/", 0, }, } @@ -100,7 +100,7 @@ func TestAuditEnableCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Enabled the file audit backend at: audit_enable_integration/" + expected := "Success! Enabled the file audit device at: audit_enable_integration/" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -144,7 +144,7 @@ func TestAuditEnableCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error enabling audit backend: " + expected := "Error enabling audit device: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) diff --git a/command/audit_list.go b/command/audit_list.go index 048e3596c8..0012426e41 100644 --- a/command/audit_list.go +++ b/command/audit_list.go @@ -10,11 +10,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*AuditListCommand)(nil) var _ cli.CommandAutocomplete = (*AuditListCommand)(nil) -// AuditListCommand is a Command that lists the enabled audits. type AuditListCommand struct { *BaseCommand @@ -22,25 +20,23 @@ type AuditListCommand struct { } func (c *AuditListCommand) Synopsis() string { - return "Lists enabled audit backends" + return "Lists enabled audit devices" } func (c *AuditListCommand) Help() string { helpText := ` -Usage: vault audit-list [options] +Usage: vault audit list [options] - Lists the enabled audit backends in the Vault server. The output lists - the enabled audit backends and the options for those backends. + Lists the enabled audit devices in the Vault server. The output lists the + enabled audit devices and the options for those devices. - List all audit backends: + List all audit devices: - $ vault audit-list + $ vault audit list - List detailed output about the audit backends: + List detailed output about the audit devices: - $ vault audit-list -detailed - - For a full list of examples, please see the documentation. + $ vault audit list -detailed ` + c.Flags().Help() @@ -58,7 +54,7 @@ func (c *AuditListCommand) Flags() *FlagSets { Default: false, EnvVar: "", Usage: "Print detailed information such as options and replication " + - "status about each mount.", + "status about each auth device.", }) return set @@ -99,16 +95,16 @@ func (c *AuditListCommand) Run(args []string) int { } if len(audits) == 0 { - c.UI.Error(fmt.Sprintf("No audit backends are enabled.")) + c.UI.Output(fmt.Sprintf("No audit devices are enabled.")) return 0 } if c.flagDetailed { - c.UI.Output(tableOutput(c.detailedAudits(audits))) + c.UI.Output(tableOutput(c.detailedAudits(audits), nil)) return 0 } - c.UI.Output(tableOutput(c.simpleAudits(audits))) + c.UI.Output(tableOutput(c.simpleAudits(audits), nil)) return 0 } From b50d7d69bdbb9082619345730d235561481f5ccd Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:58:13 -0400 Subject: [PATCH 088/281] Add token as a subcommand --- command/token.go | 46 +++++++++++++++++++ ...{capabilities.go => token_capabilities.go} | 30 ++++++------ ...ies_test.go => token_capabilities_test.go} | 16 +++---- command/token_create.go | 8 ++-- command/token_lookup.go | 14 +++--- command/token_renew.go | 12 ++--- command/token_revoke.go | 12 ++--- 7 files changed, 87 insertions(+), 51 deletions(-) create mode 100644 command/token.go rename command/{capabilities.go => token_capabilities.go} (62%) rename command/{capabilities_test.go => token_capabilities_test.go} (88%) diff --git a/command/token.go b/command/token.go new file mode 100644 index 0000000000..20af230a5b --- /dev/null +++ b/command/token.go @@ -0,0 +1,46 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*TokenCommand)(nil) + +type TokenCommand struct { + *BaseCommand +} + +func (c *TokenCommand) Synopsis() string { + return "Interact with tokens" +} + +func (c *TokenCommand) Help() string { + helpText := ` +Usage: vault token [options] [args] + + This command groups subcommands for interacting with tokens. Users can + create, lookup, renew, and revoke tokens. + + Create a new token: + + $ vault token create + + Revoke a token: + + $ vault token revoke 96ddf4bc-d217-f3ba-f9bd-017055595017 + + Renew a token: + + $ vault token renew 96ddf4bc-d217-f3ba-f9bd-017055595017 + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (c *TokenCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/capabilities.go b/command/token_capabilities.go similarity index 62% rename from command/capabilities.go rename to command/token_capabilities.go index 6a49ebe395..3212546b43 100644 --- a/command/capabilities.go +++ b/command/token_capabilities.go @@ -9,35 +9,33 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*CapabilitiesCommand)(nil) -var _ cli.CommandAutocomplete = (*CapabilitiesCommand)(nil) +var _ cli.Command = (*TokenCapabilitiesCommand)(nil) +var _ cli.CommandAutocomplete = (*TokenCapabilitiesCommand)(nil) -// CapabilitiesCommand is a Command that enables a new endpoint. -type CapabilitiesCommand struct { +type TokenCapabilitiesCommand struct { *BaseCommand } -func (c *CapabilitiesCommand) Synopsis() string { - return "Fetchs the capabilities of a token" +func (c *TokenCapabilitiesCommand) Synopsis() string { + return "Print capabilities of a token on a path" } -func (c *CapabilitiesCommand) Help() string { +func (c *TokenCapabilitiesCommand) Help() string { helpText := ` -Usage: vault capabilities [options] [TOKEN] PATH +Usage: vault token capabilities [options] [TOKEN] PATH Fetches the capabilities of a token for a given path. If a TOKEN is provided as an argument, the "/sys/capabilities" endpoint and permission is used. If - no TOKEN is provided, the "/sys/capabilities-self" endpoint and permission + no TOKEN is provided, the "/sys/capabilities-self" endpoint and permission is used with the locally authenticated token. List capabilities for the local token on the "secret/foo" path: - $ vault capabilities secret/foo + $ vault token capabilities secret/foo List capabilities for a token on the "cubbyhole/foo" path: - $ vault capabilities 96ddf4bc-d217-f3ba-f9bd-017055595017 cubbyhole/foo + $ vault token capabilities 96ddf4bc-d217-f3ba-f9bd-017055595017 cubbyhole/foo For a full list of examples, please see the documentation. @@ -46,19 +44,19 @@ Usage: vault capabilities [options] [TOKEN] PATH return strings.TrimSpace(helpText) } -func (c *CapabilitiesCommand) Flags() *FlagSets { +func (c *TokenCapabilitiesCommand) Flags() *FlagSets { return c.flagSet(FlagSetHTTP) } -func (c *CapabilitiesCommand) AutocompleteArgs() complete.Predictor { +func (c *TokenCapabilitiesCommand) AutocompleteArgs() complete.Predictor { return nil } -func (c *CapabilitiesCommand) AutocompleteFlags() complete.Flags { +func (c *TokenCapabilitiesCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *CapabilitiesCommand) Run(args []string) int { +func (c *TokenCapabilitiesCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { diff --git a/command/capabilities_test.go b/command/token_capabilities_test.go similarity index 88% rename from command/capabilities_test.go rename to command/token_capabilities_test.go index 4ca2b1d285..74efd0cab5 100644 --- a/command/capabilities_test.go +++ b/command/token_capabilities_test.go @@ -8,18 +8,18 @@ import ( "github.com/mitchellh/cli" ) -func testCapabilitiesCommand(tb testing.TB) (*cli.MockUi, *CapabilitiesCommand) { +func testTokenCapabilitiesCommand(tb testing.TB) (*cli.MockUi, *TokenCapabilitiesCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &CapabilitiesCommand{ + return ui, &TokenCapabilitiesCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestCapabilitiesCommand_Run(t *testing.T) { +func TestTokenCapabilitiesCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -42,7 +42,7 @@ func TestCapabilitiesCommand_Run(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ui, cmd := testCapabilitiesCommand(t) + ui, cmd := testTokenCapabilitiesCommand(t) code := cmd.Run(tc.args) if code != tc.code { @@ -79,7 +79,7 @@ func TestCapabilitiesCommand_Run(t *testing.T) { } token := secret.Auth.ClientToken - ui, cmd := testCapabilitiesCommand(t) + ui, cmd := testTokenCapabilitiesCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -121,7 +121,7 @@ func TestCapabilitiesCommand_Run(t *testing.T) { client.SetToken(token) - ui, cmd := testCapabilitiesCommand(t) + ui, cmd := testTokenCapabilitiesCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -144,7 +144,7 @@ func TestCapabilitiesCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testCapabilitiesCommand(t) + ui, cmd := testTokenCapabilitiesCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -164,7 +164,7 @@ func TestCapabilitiesCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testCapabilitiesCommand(t) + _, cmd := testTokenCapabilitiesCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/token_create.go b/command/token_create.go index ca5b7752f7..a75a6066ff 100644 --- a/command/token_create.go +++ b/command/token_create.go @@ -10,11 +10,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*TokenCreateCommand)(nil) var _ cli.CommandAutocomplete = (*TokenCreateCommand)(nil) -// TokenCreateCommand is a Command that mounts a new mount. type TokenCreateCommand struct { *BaseCommand @@ -36,12 +34,12 @@ type TokenCreateCommand struct { } func (c *TokenCreateCommand) Synopsis() string { - return "Creates a new token" + return "Create a new token" } func (c *TokenCreateCommand) Help() string { helpText := ` -Usage: vault token-create [options] +Usage: vault token create [options] Creates a new token that can be used for authentication. This token will be created as a child of the currently authenticated token. The generated token @@ -159,7 +157,7 @@ func (c *TokenCreateCommand) Flags() *FlagSets { Name: "metadata", Target: &c.flagMetadata, Completion: complete.PredictAnything, - Usage: "Arbitary key=value metadata to associate with the token. " + + Usage: "Arbitrary key=value metadata to associate with the token. " + "This metadata will show in the audit log when the token is used. " + "This can be specified multiple times to add multiple pieces of " + "metadata.", diff --git a/command/token_lookup.go b/command/token_lookup.go index 5d51e666eb..2c885eaee9 100644 --- a/command/token_lookup.go +++ b/command/token_lookup.go @@ -9,11 +9,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*TokenLookupCommand)(nil) var _ cli.CommandAutocomplete = (*TokenLookupCommand)(nil) -// TokenLookupCommand is a Command that outputs details about the provided. type TokenLookupCommand struct { *BaseCommand @@ -21,12 +19,12 @@ type TokenLookupCommand struct { } func (c *TokenLookupCommand) Synopsis() string { - return "Displays information about a token" + return "Display information about a token" } func (c *TokenLookupCommand) Help() string { helpText := ` -Usage: vault token-lookup [options] [TOKEN | ACCESSOR] +Usage: vault token lookup [options] [TOKEN | ACCESSOR] Displays information about a token or accessor. If a TOKEN is not provided, the locally authenticated token is used. @@ -34,16 +32,16 @@ Usage: vault token-lookup [options] [TOKEN | ACCESSOR] Get information about the locally authenticated token (this uses the /auth/token/lookup-self endpoint and permission): - $ vault token-lookup + $ vault token lookup Get information about a particular token (this uses the /auth/token/lookup endpoint and permission): - $ vault token-lookup 96ddf4bc-d217-f3ba-f9bd-017055595017 + $ vault token lookup 96ddf4bc-d217-f3ba-f9bd-017055595017 Get information about a token via its accessor: - $ vault token-lookup -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da + $ vault token lookup -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da For a full list of examples, please see the documentation. @@ -63,7 +61,7 @@ func (c *TokenLookupCommand) Flags() *FlagSets { Default: false, EnvVar: "", Completion: complete.PredictNothing, - Usage: "Treat the argument as an accessor intead of a token. When " + + Usage: "Treat the argument as an accessor instead of a token. When " + "this option is selected, the output will NOT include the token.", }) diff --git a/command/token_renew.go b/command/token_renew.go index 19efb5e079..6505ce328d 100644 --- a/command/token_renew.go +++ b/command/token_renew.go @@ -10,11 +10,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*TokenRenewCommand)(nil) var _ cli.CommandAutocomplete = (*TokenRenewCommand)(nil) -// TokenRenewCommand is a Command that mounts a new mount. type TokenRenewCommand struct { *BaseCommand @@ -22,12 +20,12 @@ type TokenRenewCommand struct { } func (c *TokenRenewCommand) Synopsis() string { - return "Renews token leases" + return "Renew a token lease" } func (c *TokenRenewCommand) Help() string { helpText := ` -Usage: vault token-renew [options] [TOKEN] +Usage: vault token renew [options] [TOKEN] Renews a token's lease, extending the amount of time it can be used. If a TOKEN is not provided, the locally authenticated token is used. Lease renewal @@ -36,16 +34,16 @@ Usage: vault token-renew [options] [TOKEN] Renew a token (this uses the /auth/token/renew endpoint and permission): - $ vault token-renew 96ddf4bc-d217-f3ba-f9bd-017055595017 + $ vault token renew 96ddf4bc-d217-f3ba-f9bd-017055595017 Renew the currently authenticated token (this uses the /auth/token/renew-self endpoint and permission): - $ vault token-renew + $ vault token renew Renew a token requesting a specific increment value: - $ vault token-renew -increment 30m 96ddf4bc-d217-f3ba-f9bd-017055595017 + $ vault token renew -increment=30m 96ddf4bc-d217-f3ba-f9bd-017055595017 For a full list of examples, please see the documentation. diff --git a/command/token_revoke.go b/command/token_revoke.go index 65b0867a96..351ce639c3 100644 --- a/command/token_revoke.go +++ b/command/token_revoke.go @@ -8,11 +8,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*TokenRevokeCommand)(nil) var _ cli.CommandAutocomplete = (*TokenRevokeCommand)(nil) -// TokenRevokeCommand is a Command that mounts a new mount. type TokenRevokeCommand struct { *BaseCommand @@ -22,12 +20,12 @@ type TokenRevokeCommand struct { } func (c *TokenRevokeCommand) Synopsis() string { - return "Revokes tokens and their children" + return "Revoke a token and its children" } func (c *TokenRevokeCommand) Help() string { helpText := ` -Usage: vault token-revoke [options] [TOKEN | ACCESSOR] +Usage: vault token revoke [options] [TOKEN | ACCESSOR] Revokes authentication tokens and their children. If a TOKEN is not provided, the locally authenticated token is used. The "-mode" flag can be used to @@ -36,15 +34,15 @@ Usage: vault token-revoke [options] [TOKEN | ACCESSOR] Revoke a token and all the token's children: - $ vault token-revoke 96ddf4bc-d217-f3ba-f9bd-017055595017 + $ vault token revoke 96ddf4bc-d217-f3ba-f9bd-017055595017 Revoke a token leaving the token's children: - $ vault token-revoke -mode=orphan 96ddf4bc-d217-f3ba-f9bd-017055595017 + $ vault token revoke -mode=orphan 96ddf4bc-d217-f3ba-f9bd-017055595017 Revoke a token by accessor: - $ vault token-revoke -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da + $ vault token revoke -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da For a full list of examples, please see the documentation. From 36eccfb424d48b9148d3d9885f2bf1f3d9380c45 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:58:39 -0400 Subject: [PATCH 089/281] Predict "generic" as a secrets engine --- command/base_predict.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/base_predict.go b/command/base_predict.go index 7587a3544e..8f53c0444a 100644 --- a/command/base_predict.go +++ b/command/base_predict.go @@ -77,6 +77,7 @@ func (b *BaseCommand) PredictVaultAvailableMounts() complete.Predictor { "aws", "consul", "database", + "generic", "pki", "plugin", "rabbitmq", From 6b75e6e2bffffe0a39c3be2425b7d59e53a2478b Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:59:06 -0400 Subject: [PATCH 090/281] Update delete command --- command/delete.go | 16 +++++++--------- command/delete_test.go | 12 ++++++------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/command/delete.go b/command/delete.go index fbac3b0b32..12c7f96117 100644 --- a/command/delete.go +++ b/command/delete.go @@ -8,17 +8,15 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*DeleteCommand)(nil) var _ cli.CommandAutocomplete = (*DeleteCommand)(nil) -// DeleteCommand is a Command that puts data into the Vault. type DeleteCommand struct { *BaseCommand } func (c *DeleteCommand) Synopsis() string { - return "Deletes secrets and configuration" + return "Delete secrets and configuration" } func (c *DeleteCommand) Help() string { @@ -69,13 +67,11 @@ func (c *DeleteCommand) Run(args []string) int { } args = f.Args() - path, kvs, err := extractPath(args) - if err != nil { - c.UI.Error(err.Error()) + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 - } - - if len(kvs) > 0 { + case len(args) > 1: c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) return 1 } @@ -86,6 +82,8 @@ func (c *DeleteCommand) Run(args []string) int { return 2 } + path := sanitizePath(args[0]) + if _, err := client.Logical().Delete(path); err != nil { c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err)) return 2 diff --git a/command/delete_test.go b/command/delete_test.go index 6d06c7e972..44970f57af 100644 --- a/command/delete_test.go +++ b/command/delete_test.go @@ -28,15 +28,15 @@ func TestDeleteCommand_Run(t *testing.T) { code int }{ { - "empty", - nil, - "Missing PATH!", + "not_enough_args", + []string{}, + "Not enough arguments", 1, }, { - "slash", - []string{"/"}, - "Missing PATH!", + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", 1, }, } From 9a80d9a8f848553341f9da0134ce152e77d6c855 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:59:31 -0400 Subject: [PATCH 091/281] Add lease subcommand --- command/lease.go | 40 ++++++++++++++ command/{renew.go => lease_renew.go} | 24 ++++----- .../{renew_test.go => lease_renew_test.go} | 22 ++++---- command/{revoke.go => lease_revoke.go} | 52 +++++++++---------- .../{revoke_test.go => lease_revoke_test.go} | 12 ++--- 5 files changed, 93 insertions(+), 57 deletions(-) create mode 100644 command/lease.go rename command/{renew.go => lease_renew.go} (79%) rename command/{renew_test.go => lease_renew_test.go} (81%) rename command/{revoke.go => lease_revoke.go} (66%) rename command/{revoke_test.go => lease_revoke_test.go} (88%) diff --git a/command/lease.go b/command/lease.go new file mode 100644 index 0000000000..76f6cc174c --- /dev/null +++ b/command/lease.go @@ -0,0 +1,40 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*LeaseCommand)(nil) + +type LeaseCommand struct { + *BaseCommand +} + +func (c *LeaseCommand) Synopsis() string { + return "Interact with leases" +} + +func (c *LeaseCommand) Help() string { + helpText := ` +Usage: vault lease [options] [args] + + This command groups subcommands for interacting with leases. Users can revoke + or renew leases. + + Renew a lease: + + $ vault lease renew database/creds/readonly/2f6a614c... + + Revoke a lease: + + $ vault lease revoke database/creds/readonly/2f6a614c... +` + + return strings.TrimSpace(helpText) +} + +func (c *LeaseCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/renew.go b/command/lease_renew.go similarity index 79% rename from command/renew.go rename to command/lease_renew.go index c2130d122a..4dd2e1c573 100644 --- a/command/renew.go +++ b/command/lease_renew.go @@ -9,24 +9,22 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*RenewCommand)(nil) -var _ cli.CommandAutocomplete = (*RenewCommand)(nil) +var _ cli.Command = (*LeaseRenewCommand)(nil) +var _ cli.CommandAutocomplete = (*LeaseRenewCommand)(nil) -// RenewCommand is a Command that mounts a new mount. -type RenewCommand struct { +type LeaseRenewCommand struct { *BaseCommand flagIncrement time.Duration } -func (c *RenewCommand) Synopsis() string { +func (c *LeaseRenewCommand) Synopsis() string { return "Renews the lease of a secret" } -func (c *RenewCommand) Help() string { +func (c *LeaseRenewCommand) Help() string { helpText := ` -Usage: vault renew [options] ID +Usage: vault lease renew [options] ID Renews the lease on a secret, extending the time that it can be used before it is revoked by Vault. @@ -38,7 +36,7 @@ Usage: vault renew [options] ID Renew a secret: - $ vault renew database/creds/readonly/2f6a614c-4aa2-7b19-24b9-ad944a8d4de6 + $ vault lease renew database/creds/readonly/2f6a614c... Lease renewal will fail if the secret is not renewable, the secret has already been revoked, or if the secret has already reached its maximum TTL. @@ -50,7 +48,7 @@ Usage: vault renew [options] ID return strings.TrimSpace(helpText) } -func (c *RenewCommand) Flags() *FlagSets { +func (c *LeaseRenewCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) f := set.NewFlagSet("Command Options") @@ -67,15 +65,15 @@ func (c *RenewCommand) Flags() *FlagSets { return set } -func (c *RenewCommand) AutocompleteArgs() complete.Predictor { +func (c *LeaseRenewCommand) AutocompleteArgs() complete.Predictor { return complete.PredictAnything } -func (c *RenewCommand) AutocompleteFlags() complete.Flags { +func (c *LeaseRenewCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *RenewCommand) Run(args []string) int { +func (c *LeaseRenewCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { diff --git a/command/renew_test.go b/command/lease_renew_test.go similarity index 81% rename from command/renew_test.go rename to command/lease_renew_test.go index 5861cc16f0..166e5156be 100644 --- a/command/renew_test.go +++ b/command/lease_renew_test.go @@ -8,20 +8,20 @@ import ( "github.com/mitchellh/cli" ) -func testRenewCommand(tb testing.TB) (*cli.MockUi, *RenewCommand) { +func testLeaseRenewCommand(tb testing.TB) (*cli.MockUi, *LeaseRenewCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &RenewCommand{ + return ui, &LeaseRenewCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -// testRenewCommandMountAndLease mounts a leased secret backend and returns +// testLeaseRenewCommandMountAndLease mounts a leased secret backend and returns // the leaseID of an item. -func testRenewCommandMountAndLease(tb testing.TB, client *api.Client) string { +func testLeaseRenewCommandMountAndLease(tb testing.TB, client *api.Client) string { if err := client.Sys().Mount("testing", &api.MountInput{ Type: "generic-leased", }); err != nil { @@ -47,7 +47,7 @@ func testRenewCommandMountAndLease(tb testing.TB, client *api.Client) string { return secret.LeaseID } -func TestRenewCommand_Run(t *testing.T) { +func TestLeaseRenewCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -100,9 +100,9 @@ func TestRenewCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - leaseID := testRenewCommandMountAndLease(t, client) + leaseID := testLeaseRenewCommandMountAndLease(t, client) - ui, cmd := testRenewCommand(t) + ui, cmd := testLeaseRenewCommand(t) cmd.client = client if tc.args != nil { @@ -127,9 +127,9 @@ func TestRenewCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - leaseID := testRenewCommandMountAndLease(t, client) + leaseID := testLeaseRenewCommandMountAndLease(t, client) - _, cmd := testRenewCommand(t) + _, cmd := testLeaseRenewCommand(t) cmd.client = client code := cmd.Run([]string{leaseID}) @@ -144,7 +144,7 @@ func TestRenewCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testRenewCommand(t) + ui, cmd := testLeaseRenewCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -164,7 +164,7 @@ func TestRenewCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testRenewCommand(t) + _, cmd := testLeaseRenewCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/revoke.go b/command/lease_revoke.go similarity index 66% rename from command/revoke.go rename to command/lease_revoke.go index 8b4ba6118c..45f5dc3a76 100644 --- a/command/revoke.go +++ b/command/lease_revoke.go @@ -8,50 +8,48 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*ReadCommand)(nil) -var _ cli.CommandAutocomplete = (*ReadCommand)(nil) +var _ cli.Command = (*LeaseRevokeCommand)(nil) +var _ cli.CommandAutocomplete = (*LeaseRevokeCommand)(nil) -// RevokeCommand is a Command that mounts a new mount. -type RevokeCommand struct { +type LeaseRevokeCommand struct { *BaseCommand flagForce bool flagPrefix bool } -func (c *RevokeCommand) Synopsis() string { +func (c *LeaseRevokeCommand) Synopsis() string { return "Revokes leases and secrets" } -func (c *RevokeCommand) Help() string { +func (c *LeaseRevokeCommand) Help() string { helpText := ` -Usage: vault revoke [options] ID +Usage: vault lease revoke [options] ID Revokes secrets by their lease ID. This command can revoke a single secret or multiple secrets based on a path-matched prefix. Revoke a single lease: - $ vault revoke database/creds/readonly/2f6a614c... + $ vault lease revoke database/creds/readonly/2f6a614c... Revoke all leases for a role: - $ vault revoke -prefix aws/creds/deploy + $ vault lease revoke -prefix aws/creds/deploy - Force delete leases from Vault even if backend revocation fails: + Force delete leases from Vault even if secret engine revocation fails: - $ vault revoke -force -prefix consul/creds + $ vault lease revoke -force -prefix consul/creds For a full list of examples and paths, please see the documentation that - corresponds to the secret backend in use. + corresponds to the secret engine in use. ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *RevokeCommand) Flags() *FlagSets { +func (c *LeaseRevokeCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP) f := set.NewFlagSet("Command Options") @@ -60,10 +58,10 @@ func (c *RevokeCommand) Flags() *FlagSets { Aliases: []string{"f"}, Target: &c.flagForce, Default: false, - Usage: "Delete the lease from Vault even if the backend revocation " + + Usage: "Delete the lease from Vault even if the secret engine revocation " + "fails. This is meant for recovery situations where the secret " + - "in the backend was manually removed. If this flag is specified, " + - "-prefix is also required.", + "in the target secret engine was manually removed. If this flag is " + + "specified, -prefix is also required.", }) f.BoolVar(&BoolVar{ @@ -77,15 +75,15 @@ func (c *RevokeCommand) Flags() *FlagSets { return set } -func (c *RevokeCommand) AutocompleteArgs() complete.Predictor { +func (c *LeaseRevokeCommand) AutocompleteArgs() complete.Predictor { return c.PredictVaultFiles() } -func (c *RevokeCommand) AutocompleteFlags() complete.Flags { +func (c *LeaseRevokeCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *RevokeCommand) Run(args []string) int { +func (c *LeaseRevokeCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { @@ -94,13 +92,11 @@ func (c *RevokeCommand) Run(args []string) int { } args = f.Args() - leaseID, remaining, err := extractID(args) - if err != nil { - c.UI.Error(err.Error()) + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 - } - - if len(remaining) > 0 { + case len(args) > 1: c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) return 1 } @@ -116,10 +112,12 @@ func (c *RevokeCommand) Run(args []string) int { return 2 } + leaseID := strings.TrimSpace(args[0]) + switch { case c.flagForce && c.flagPrefix: c.UI.Warn(wrapAtLength("Warning! Force-removing leases can cause Vault " + - "to become out of sync with credential backends!")) + "to become out of sync with secret engines!")) if err := client.Sys().RevokeForce(leaseID); err != nil { c.UI.Error(fmt.Sprintf("Error force revoking leases with prefix %s: %s", leaseID, err)) return 2 diff --git a/command/revoke_test.go b/command/lease_revoke_test.go similarity index 88% rename from command/revoke_test.go rename to command/lease_revoke_test.go index 1b2b94fd6e..97904a4d7f 100644 --- a/command/revoke_test.go +++ b/command/lease_revoke_test.go @@ -8,18 +8,18 @@ import ( "github.com/mitchellh/cli" ) -func testRevokeCommand(tb testing.TB) (*cli.MockUi, *RevokeCommand) { +func testLeaseRevokeCommand(tb testing.TB) (*cli.MockUi, *LeaseRevokeCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &RevokeCommand{ + return ui, &LeaseRevokeCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestRevokeCommand_Run(t *testing.T) { +func TestLeaseRevokeCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -85,7 +85,7 @@ func TestRevokeCommand_Run(t *testing.T) { t.Fatal(err) } - ui, cmd := testRevokeCommand(t) + ui, cmd := testLeaseRevokeCommand(t) cmd.client = client tc.args = append(tc.args, secret.LeaseID) @@ -108,7 +108,7 @@ func TestRevokeCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testRevokeCommand(t) + ui, cmd := testLeaseRevokeCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -128,7 +128,7 @@ func TestRevokeCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testRevokeCommand(t) + _, cmd := testLeaseRevokeCommand(t) assertNoTabs(t, cmd) }) } From 67611bfcd3f4b117e44a2b44f5fef59129ff2759 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 21:59:46 -0400 Subject: [PATCH 092/281] Update list command --- command/list.go | 22 ++++++++++------------ command/list_test.go | 14 +++++++------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/command/list.go b/command/list.go index bdf5953cf9..ecfa0acb56 100644 --- a/command/list.go +++ b/command/list.go @@ -8,17 +8,15 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*ListCommand)(nil) var _ cli.CommandAutocomplete = (*ListCommand)(nil) -// ListCommand is a Command that lists data from the Vault. type ListCommand struct { *BaseCommand } func (c *ListCommand) Synopsis() string { - return "Lists data or secrets" + return "List data or secrets" } func (c *ListCommand) Help() string { @@ -27,14 +25,14 @@ func (c *ListCommand) Help() string { Usage: vault list [options] PATH Lists data from Vault at the given path. This can be used to list keys in a, - given backend. + given secret engine. - List values under the "my-app" folder: + List values under the "my-app" folder of the generic secret engine: $ vault list secret/my-app/ For a full list of examples and paths, please see the documentation that - corresponds to the secret backend in use. Not all backends support listing. + corresponds to the secret engine in use. Not all engines support listing. ` + c.Flags().Help() @@ -62,13 +60,11 @@ func (c *ListCommand) Run(args []string) int { } args = f.Args() - path, kvs, err := extractPath(args) - if err != nil { - c.UI.Error(err.Error()) + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 - } - - if len(kvs) > 0 { + case len(args) > 1: c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) return 1 } @@ -79,6 +75,8 @@ func (c *ListCommand) Run(args []string) int { return 2 } + path := ensureTrailingSlash(sanitizePath(args[0])) + secret, err := client.Logical().List(path) if err != nil { c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err)) diff --git a/command/list_test.go b/command/list_test.go index 712f1cae35..e5a77c532b 100644 --- a/command/list_test.go +++ b/command/list_test.go @@ -28,15 +28,15 @@ func TestListCommand_Run(t *testing.T) { code int }{ { - "empty", - nil, - "Missing PATH!", + "not_enough_args", + []string{}, + "Not enough arguments", 1, }, { - "slash", - []string{"/"}, - "Missing PATH!", + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", 1, }, { @@ -134,7 +134,7 @@ func TestListCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error listing secret/list: " + expected := "Error listing secret/list/: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) From d695dbf1114b88a2f3d91f99a4367abbd590d36f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:00:05 -0400 Subject: [PATCH 093/281] Update path-help command --- command/path_help.go | 30 +++++++++++++----------------- command/path_help_test.go | 12 ++++++------ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/command/path_help.go b/command/path_help.go index fb7b52c30f..2ce4a38bfd 100644 --- a/command/path_help.go +++ b/command/path_help.go @@ -8,7 +8,6 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*PathHelpCommand)(nil) var _ cli.CommandAutocomplete = (*PathHelpCommand)(nil) @@ -16,37 +15,34 @@ var pathHelpVaultSealedMessage = strings.TrimSpace(` Error: Vault is sealed. The "path-help" command requires the Vault to be unsealed so that the mount -points of the secret backends are known. +points of the secret engines are known. `) -// PathHelpCommand is a Command that lists the mounts. type PathHelpCommand struct { *BaseCommand } func (c *PathHelpCommand) Synopsis() string { - return "Retrieves API help for paths" + return "Retrieve API help for paths" } func (c *PathHelpCommand) Help() string { helpText := ` -Usage: vault path-help [options] path +Usage: vault path-help [options] PATH Retrieves API help for paths. All endpoints in Vault provide built-in help - in markdown format. This includes system paths, secret paths, and credential - providers. + in markdown format. This includes system paths, secret engines, and auth + methods. - A backend must be mounted before help is available: + Get help for the thing mounted at database/: - $ vault mount database $ vault path-help database/ The response object will return additional paths to retrieve help: $ vault path-help database/roles/ - Each backend produces different help output. For additional information, - please view the online documentation. + Each secret engine produces different help output. ` + c.Flags().Help() @@ -74,13 +70,11 @@ func (c *PathHelpCommand) Run(args []string) int { } args = f.Args() - path, kvs, err := extractPath(args) - if err != nil { - c.UI.Error(err.Error()) + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 - } - - if len(kvs) > 0 { + case len(args) > 1: c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) return 1 } @@ -91,6 +85,8 @@ func (c *PathHelpCommand) Run(args []string) int { return 2 } + path := sanitizePath(args[0]) + help, err := client.Help(path) if err != nil { if strings.Contains(err.Error(), "Vault is sealed") { diff --git a/command/path_help_test.go b/command/path_help_test.go index ff38c02250..4e788df130 100644 --- a/command/path_help_test.go +++ b/command/path_help_test.go @@ -28,15 +28,15 @@ func TestPathHelpCommand_Run(t *testing.T) { code int }{ { - "empty", - nil, - "Missing PATH!", + "not_enough_args", + []string{}, + "Not enough arguments", 1, }, { - "slash", - []string{"/"}, - "Missing PATH!", + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", 1, }, { From a34b2dae9f51bd21c4237103cea9ca95c3957b99 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:00:21 -0400 Subject: [PATCH 094/281] Add "policy" subcommand --- command/policies_deprecated.go | 52 +++++++ command/policies_deprecated_test.go | 96 +++++++++++++ command/policy.go | 47 ++++++ command/policy_delete.go | 14 +- command/policy_fmt.go | 109 ++++++++++++++ command/policy_fmt_test.go | 213 ++++++++++++++++++++++++++++ command/policy_list.go | 55 ++----- command/policy_list_test.go | 37 +---- command/policy_read.go | 87 ++++++++++++ command/policy_read_test.go | 128 +++++++++++++++++ command/policy_write.go | 20 ++- 11 files changed, 761 insertions(+), 97 deletions(-) create mode 100644 command/policies_deprecated.go create mode 100644 command/policies_deprecated_test.go create mode 100644 command/policy.go create mode 100644 command/policy_fmt.go create mode 100644 command/policy_fmt_test.go create mode 100644 command/policy_read.go create mode 100644 command/policy_read_test.go diff --git a/command/policies_deprecated.go b/command/policies_deprecated.go new file mode 100644 index 0000000000..b7b5f5cf17 --- /dev/null +++ b/command/policies_deprecated.go @@ -0,0 +1,52 @@ +package command + +import ( + "github.com/mitchellh/cli" +) + +// Deprecation +// TODO: remove in 0.9.0 + +var _ cli.Command = (*PoliciesDeprecatedCommand)(nil) + +type PoliciesDeprecatedCommand struct { + *BaseCommand +} + +func (c *PoliciesDeprecatedCommand) Synopsis() string { return "" } + +func (c *PoliciesDeprecatedCommand) Help() string { + return (&PolicyListCommand{ + BaseCommand: c.BaseCommand, + }).Help() +} + +func (c *PoliciesDeprecatedCommand) Run(args []string) int { + oargs := args + + f := c.flagSet(FlagSetHTTP) + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + args = f.Args() + + // Got an arg, this is trying to read a policy + if len(args) > 0 { + return (&PolicyReadCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + }).Run(oargs) + } + + // No args, probably ran "vault policies" and we want to translate that to + // "vault policy list" + return (&PolicyListCommand{ + BaseCommand: &BaseCommand{ + UI: c.UI, + client: c.client, + }, + }).Run(oargs) +} diff --git a/command/policies_deprecated_test.go b/command/policies_deprecated_test.go new file mode 100644 index 0000000000..de8ff7a329 --- /dev/null +++ b/command/policies_deprecated_test.go @@ -0,0 +1,96 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func testPoliciesDeprecatedCommand(tb testing.TB) (*cli.MockUi, *PoliciesDeprecatedCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &PoliciesDeprecatedCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestPoliciesDeprecatedCommand_Run(t *testing.T) { + t.Parallel() + + // TODO: remove in 0.9.0 + t.Run("deprecated_arg", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testPoliciesDeprecatedCommand(t) + cmd.client = client + + // vault policies ARG -> vault policy read ARG + code := cmd.Run([]string{"default"}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + stdout := ui.OutputWriter.String() + + if expected := "token/"; !strings.Contains(stdout, expected) { + t.Errorf("expected %q to contain %q", stdout, expected) + } + }) + + t.Run("deprecated_no_args", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testPoliciesDeprecatedCommand(t) + cmd.client = client + + // vault policies -> vault policy list + code := cmd.Run([]string{}) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + stdout := ui.OutputWriter.String() + + if expected := "root"; !strings.Contains(stdout, expected) { + t.Errorf("expected %q to contain %q", stdout, expected) + } + }) + + t.Run("deprecated_with_flags", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testPoliciesDeprecatedCommand(t) + cmd.client = client + + // vault policies -flag -> vault policy list + code := cmd.Run([]string{ + "-address", client.Address(), + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String()) + } + stdout := ui.OutputWriter.String() + + if expected := "root"; !strings.Contains(stdout, expected) { + t.Errorf("expected %q to contain %q", stdout, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testPoliciesDeprecatedCommand(t) + assertNoTabs(t, cmd) + }) +} diff --git a/command/policy.go b/command/policy.go new file mode 100644 index 0000000000..5d812cadeb --- /dev/null +++ b/command/policy.go @@ -0,0 +1,47 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*PolicyCommand)(nil) + +// PolicyCommand is a Command that holds the audit commands +type PolicyCommand struct { + *BaseCommand +} + +func (c *PolicyCommand) Synopsis() string { + return "Interact with policies" +} + +func (c *PolicyCommand) Help() string { + helpText := ` +Usage: vault policy [options] [args] + + This command groups subcommands for interacting with policies. Users can + Users can write, read, and list policies in Vault. + + List all enabled policies: + + $ vault policy list + + Create a policy named "my-policy" from contents on local disk: + + $ vault policy write my-policy ./my-policy.hcl + + Delete the policy named my-policy: + + $ vault policy delete my-policy + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (c *PolicyCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/policy_delete.go b/command/policy_delete.go index a2bbc25bda..e74030640f 100644 --- a/command/policy_delete.go +++ b/command/policy_delete.go @@ -8,11 +8,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*PolicyDeleteCommand)(nil) var _ cli.CommandAutocomplete = (*PolicyDeleteCommand)(nil) -// PolicyDeleteCommand is a Command that enables a new endpoint. type PolicyDeleteCommand struct { *BaseCommand } @@ -23,17 +21,17 @@ func (c *PolicyDeleteCommand) Synopsis() string { func (c *PolicyDeleteCommand) Help() string { helpText := ` -Usage: vault policy-delete [options] NAME +Usage: vault policy delete [options] NAME - Deletes a policy in the Vault server with the given name. Once the policy - is deleted, all tokens associated with the policy will be affected - immediately. + Deletes the policy named NAME in the Vault server. Once the policy is deleted, + all tokens associated with the policy are affected immediately. Delete the policy named "my-policy": - $ vault policy-delete my-policy + $ vault policy delete my-policy - For a full list of examples, please see the documentation. + Note that it is not possible to delete the "default" or "root" policies. + These are built-in policies. ` + c.Flags().Help() diff --git a/command/policy_fmt.go b/command/policy_fmt.go new file mode 100644 index 0000000000..4d655345b8 --- /dev/null +++ b/command/policy_fmt.go @@ -0,0 +1,109 @@ +package command + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/hashicorp/hcl/hcl/printer" + "github.com/hashicorp/vault/vault" + "github.com/mitchellh/cli" + homedir "github.com/mitchellh/go-homedir" + "github.com/posener/complete" +) + +var _ cli.Command = (*PolicyFmtCommand)(nil) +var _ cli.CommandAutocomplete = (*PolicyFmtCommand)(nil) + +type PolicyFmtCommand struct { + *BaseCommand +} + +func (c *PolicyFmtCommand) Synopsis() string { + return "Formats a policy on disk" +} + +func (c *PolicyFmtCommand) Help() string { + helpText := ` +Usage: vault policy fmt [options] PATH + + Formats a local policy file to the policy specification. This command will + overwrite the file at the given PATH with the properly-formatted policy + file contents. + + Format the local file "my-policy.hcl" as a policy file: + + $ vault policy fmt my-policy.hcl + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *PolicyFmtCommand) Flags() *FlagSets { + return c.flagSet(FlagSetNone) +} + +func (c *PolicyFmtCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictFiles("*.hcl") +} + +func (c *PolicyFmtCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *PolicyFmtCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + // Get the filepath, accounting for ~ and stuff + path, err := homedir.Expand(strings.TrimSpace(args[0])) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to expand path: %s", err)) + return 1 + } + + // Read the entire contents into memory - it would be nice if we could use + // a buffer, but hcl wants the full contents. + b, err := ioutil.ReadFile(path) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading source file: %s", err)) + return 1 + } + + // Actually parse the policy + if _, err := vault.Parse(string(b)); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + // Generate final contents + result, err := printer.Format(b) + if err != nil { + c.UI.Error(fmt.Sprintf("Error printing result: %s", err)) + return 1 + } + + // Write them back out + if err := ioutil.WriteFile(path, result, 0644); err != nil { + c.UI.Error(fmt.Sprintf("Error writing result: %s", err)) + return 1 + } + + c.UI.Output(fmt.Sprintf("Success! Formatted policy: %s", path)) + return 0 +} diff --git a/command/policy_fmt_test.go b/command/policy_fmt_test.go new file mode 100644 index 0000000000..93e8daa1bf --- /dev/null +++ b/command/policy_fmt_test.go @@ -0,0 +1,213 @@ +package command + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func testPolicyFmtCommand(tb testing.TB) (*cli.MockUi, *PolicyFmtCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &PolicyFmtCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestPolicyFmtCommand_Run(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, + }, + } + + 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() + + ui, cmd := testPolicyFmtCommand(t) + + 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("default", func(t *testing.T) { + t.Parallel() + + policy := strings.TrimSpace(` +path "secret" { + capabilities = ["create", "update","delete"] + +} +`) + + f, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + if _, err := f.Write([]byte(policy)); err != nil { + t.Fatal(err) + } + f.Close() + + _, cmd := testPolicyFmtCommand(t) + + code := cmd.Run([]string{ + f.Name(), + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := strings.TrimSpace(` +path "secret" { + capabilities = ["create", "update", "delete"] +} +`) + "\n" + + contents, err := ioutil.ReadFile(f.Name()) + if err != nil { + t.Fatal(err) + } + if string(contents) != expected { + t.Errorf("expected %q to be %q", string(contents), expected) + } + }) + + t.Run("bad_hcl", func(t *testing.T) { + t.Parallel() + + policy := `dafdaf` + + f, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + if _, err := f.Write([]byte(policy)); err != nil { + t.Fatal(err) + } + f.Close() + + ui, cmd := testPolicyFmtCommand(t) + + code := cmd.Run([]string{ + f.Name(), + }) + if exp := 1; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + stderr := ui.ErrorWriter.String() + expected := "Failed to parse policy" + if !strings.Contains(stderr, expected) { + t.Errorf("expected %q to include %q", stderr, expected) + } + }) + + t.Run("bad_policy", func(t *testing.T) { + t.Parallel() + + policy := `banana "foo" {}` + + f, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + if _, err := f.Write([]byte(policy)); err != nil { + t.Fatal(err) + } + f.Close() + + ui, cmd := testPolicyFmtCommand(t) + + code := cmd.Run([]string{ + f.Name(), + }) + if exp := 1; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + stderr := ui.ErrorWriter.String() + expected := "Failed to parse policy" + if !strings.Contains(stderr, expected) { + t.Errorf("expected %q to include %q", stderr, expected) + } + }) + + t.Run("bad_policy", func(t *testing.T) { + t.Parallel() + + policy := `path "secret/" { capabilities = ["bogus"] }` + + f, err := ioutil.TempFile("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + if _, err := f.Write([]byte(policy)); err != nil { + t.Fatal(err) + } + f.Close() + + ui, cmd := testPolicyFmtCommand(t) + + code := cmd.Run([]string{ + f.Name(), + }) + if exp := 1; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + stderr := ui.ErrorWriter.String() + expected := "Failed to parse policy" + if !strings.Contains(stderr, expected) { + t.Errorf("expected %q to include %q", stderr, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testPolicyFmtCommand(t) + assertNoTabs(t, cmd) + }) +} diff --git a/command/policy_list.go b/command/policy_list.go index 1169752854..1a61136ff1 100644 --- a/command/policy_list.go +++ b/command/policy_list.go @@ -8,11 +8,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*PolicyListCommand)(nil) var _ cli.CommandAutocomplete = (*PolicyListCommand)(nil) -// PolicyListCommand is a Command that enables a new endpoint. type PolicyListCommand struct { *BaseCommand } @@ -23,20 +21,9 @@ func (c *PolicyListCommand) Synopsis() string { func (c *PolicyListCommand) Help() string { helpText := ` -Usage: vault policies [options] [NAME] +Usage: vault policy list [options] - Lists the policies that are installed on the Vault server. If the optional - argument is given, this command returns the policy's contents. - - List all policies stored in Vault: - - $ vault policies - - Read the contents of the policy named "my-policy": - - $ vault policies my-policy - - For a full list of examples, please see the documentation. + Lists the names of the policies that are installed on the Vault server. ` + c.Flags().Help() @@ -48,7 +35,7 @@ func (c *PolicyListCommand) Flags() *FlagSets { } func (c *PolicyListCommand) AutocompleteArgs() complete.Predictor { - return c.PredictVaultPolicies() + return nil } func (c *PolicyListCommand) AutocompleteFlags() complete.Flags { @@ -64,10 +51,9 @@ func (c *PolicyListCommand) Run(args []string) int { } args = f.Args() - switch len(args) { - case 0, 1: - default: - c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-2, got %d)", len(args))) + switch { + case len(args) > 0: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args))) return 1 } @@ -77,28 +63,13 @@ func (c *PolicyListCommand) Run(args []string) int { return 2 } - switch len(args) { - case 0: - policies, err := client.Sys().ListPolicies() - if err != nil { - c.UI.Error(fmt.Sprintf("Error listing policies: %s", err)) - return 2 - } - for _, p := range policies { - c.UI.Output(p) - } - case 1: - name := strings.ToLower(strings.TrimSpace(args[0])) - rules, err := client.Sys().GetPolicy(name) - if err != nil { - c.UI.Error(fmt.Sprintf("Error reading policy %s: %s", name, err)) - return 2 - } - if rules == "" { - c.UI.Error(fmt.Sprintf("Error reading policy: no policy named: %s", name)) - return 2 - } - c.UI.Output(strings.TrimSpace(rules)) + policies, err := client.Sys().ListPolicies() + if err != nil { + c.UI.Error(fmt.Sprintf("Error listing policies: %s", err)) + return 2 + } + for _, p := range policies { + c.UI.Output(p) } return 0 diff --git a/command/policy_list_test.go b/command/policy_list_test.go index 1c140edf7a..70defe54ea 100644 --- a/command/policy_list_test.go +++ b/command/policy_list_test.go @@ -29,16 +29,10 @@ func TestPolicyListCommand_Run(t *testing.T) { }{ { "too_many_args", - []string{"foo", "bar"}, + []string{"foo"}, "Too many arguments", 1, }, - { - "no_policy_exists", - []string{"not-a-real-policy"}, - "no policy named", - 2, - }, } t.Run("validations", func(t *testing.T) { @@ -69,7 +63,7 @@ func TestPolicyListCommand_Run(t *testing.T) { } }) - t.Run("list", func(t *testing.T) { + t.Run("default", func(t *testing.T) { t.Parallel() client, closer := testVaultServer(t) @@ -90,33 +84,6 @@ func TestPolicyListCommand_Run(t *testing.T) { } }) - t.Run("read", func(t *testing.T) { - t.Parallel() - - client, closer := testVaultServer(t) - defer closer() - - policy := `path "secret/" {}` - if err := client.Sys().PutPolicy("my-policy", policy); err != nil { - t.Fatal(err) - } - - ui, cmd := testPolicyListCommand(t) - cmd.client = client - - code := cmd.Run([]string{ - "my-policy", - }) - if exp := 0; code != exp { - t.Errorf("expected %d to be %d", code, exp) - } - - combined := ui.OutputWriter.String() + ui.ErrorWriter.String() - if !strings.Contains(combined, policy) { - t.Errorf("expected %q to contain %q", combined, policy) - } - }) - t.Run("communication_failure", func(t *testing.T) { t.Parallel() diff --git a/command/policy_read.go b/command/policy_read.go new file mode 100644 index 0000000000..324615935a --- /dev/null +++ b/command/policy_read.go @@ -0,0 +1,87 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*PolicyReadCommand)(nil) +var _ cli.CommandAutocomplete = (*PolicyReadCommand)(nil) + +type PolicyReadCommand struct { + *BaseCommand +} + +func (c *PolicyReadCommand) Synopsis() string { + return "Prints the contents of a policy" +} + +func (c *PolicyReadCommand) Help() string { + helpText := ` +Usage: vault policy read [options] [NAME] + + Prints the contents and metadata of the Vault policy named NAME. If the policy + does not exist, an error is returned. + + Read the policy named "my-policy": + + $ vault policy read my-policy + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *PolicyReadCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *PolicyReadCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultPolicies() +} + +func (c *PolicyReadCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *PolicyReadCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + name := strings.ToLower(strings.TrimSpace(args[0])) + rules, err := client.Sys().GetPolicy(name) + if err != nil { + c.UI.Error(fmt.Sprintf("Error reading policy named %s: %s", name, err)) + return 2 + } + if rules == "" { + c.UI.Error(fmt.Sprintf("No policy named: %s", name)) + return 2 + } + c.UI.Output(strings.TrimSpace(rules)) + + return 0 +} diff --git a/command/policy_read_test.go b/command/policy_read_test.go new file mode 100644 index 0000000000..8cd7c066b8 --- /dev/null +++ b/command/policy_read_test.go @@ -0,0 +1,128 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func testPolicyReadCommand(tb testing.TB) (*cli.MockUi, *PolicyReadCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &PolicyReadCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestPolicyReadCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "no_policy_exists", + []string{"not-a-real-policy"}, + "No policy named", + 2, + }, + } + + 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() + + ui, cmd := testPolicyReadCommand(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("default", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + policy := `path "secret/" {}` + if err := client.Sys().PutPolicy("my-policy", policy); err != nil { + t.Fatal(err) + } + + ui, cmd := testPolicyReadCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-policy", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, policy) { + t.Errorf("expected %q to contain %q", combined, policy) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testPolicyReadCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "my-policy", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error reading policy named my-policy: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testPolicyReadCommand(t) + assertNoTabs(t, cmd) + }) +} diff --git a/command/policy_write.go b/command/policy_write.go index 73979458c6..9f6cb2222d 100644 --- a/command/policy_write.go +++ b/command/policy_write.go @@ -11,11 +11,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*PolicyWriteCommand)(nil) var _ cli.CommandAutocomplete = (*PolicyWriteCommand)(nil) -// PolicyWriteCommand is a Command uploads a policy type PolicyWriteCommand struct { *BaseCommand @@ -23,26 +21,24 @@ type PolicyWriteCommand struct { } func (c *PolicyWriteCommand) Synopsis() string { - return "Uploads a policy file" + return "Uploads a named policy from a file" } func (c *PolicyWriteCommand) Help() string { helpText := ` -Usage: vault policy-write [options] NAME PATH +Usage: vault policy write [options] NAME PATH - Uploads a policy with the given name from the contents of a local file or - stdin. If the path is "-", the policy is read from stdin. Otherwise, it is - loaded from the file at the given path. + Uploads a policy with name NAME from the contents of a local file PATH or + stdin. If PATH is "-", the policy is read from stdin. Otherwise, it is + loaded from the file at the given path on the local disk. - Upload a policy named "my-policy" from /tmp/policy.hcl on the local disk: + Upload a policy named "my-policy" from "/tmp/policy.hcl" on the local disk: - $ vault policy-write my-policy /tmp/policy.hcl + $ vault policy write my-policy /tmp/policy.hcl Upload a policy from stdin: - $ cat my-policy.hcl | vault policy-write my-policy - - - For a full list of examples, please see the documentation. + $ cat my-policy.hcl | vault policy write my-policy - ` + c.Flags().Help() From d4b68970f319616fcb2821c17af5cf7e3cde3313 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:00:30 -0400 Subject: [PATCH 095/281] Update read command --- command/read.go | 20 +++++++++----------- command/read_test.go | 12 ++++++------ 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/command/read.go b/command/read.go index 99bfdf5b99..6dc2b5337b 100644 --- a/command/read.go +++ b/command/read.go @@ -8,17 +8,15 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*ReadCommand)(nil) var _ cli.CommandAutocomplete = (*ReadCommand)(nil) -// ReadCommand is a command that reads data from the Vault. type ReadCommand struct { *BaseCommand } func (c *ReadCommand) Synopsis() string { - return "Reads data and retrieves secrets" + return "Read data and retrieves secrets" } func (c *ReadCommand) Help() string { @@ -28,12 +26,12 @@ Usage: vault read [options] PATH Reads data from Vault at the given path. This can be used to read secrets, generate dynamic credentials, get configuration details, and more. - Read a secret from the static secret backend: + Read a secret from the static secrets engine: $ vault read secret/my-secret For a full list of examples and paths, please see the documentation that - corresponds to the secret backend in use. + corresponds to the secrets engine in use. ` + c.Flags().Help() @@ -61,13 +59,11 @@ func (c *ReadCommand) Run(args []string) int { } args = f.Args() - path, kvs, err := extractPath(args) - if err != nil { - c.UI.Error(err.Error()) + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 - } - - if len(kvs) > 0 { + case len(args) > 1: c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) return 1 } @@ -78,6 +74,8 @@ func (c *ReadCommand) Run(args []string) int { return 2 } + path := sanitizePath(args[0]) + secret, err := client.Logical().Read(path) if err != nil { c.UI.Error(fmt.Sprintf("Error reading %s: %s", path, err)) diff --git a/command/read_test.go b/command/read_test.go index b880d96ba6..1a45ed28a6 100644 --- a/command/read_test.go +++ b/command/read_test.go @@ -28,15 +28,15 @@ func TestReadCommand_Run(t *testing.T) { code int }{ { - "empty", - nil, - "Missing PATH!", + "not_enough_args", + []string{}, + "Not enough arguments", 1, }, { - "slash", - []string{"/"}, - "Missing PATH!", + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", 1, }, { From 387cce957eb4915c1f682321585e85fe5b4ff3af Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:01:31 -0400 Subject: [PATCH 096/281] Rename mounts to secrets engines and add the subcommand --- command/mount_tune.go | 128 ------------------ command/secrets.go | 43 ++++++ command/secrets_disable.go | 84 ++++++++++++ ...nmount_test.go => secrets_disable_test.go} | 38 +++--- command/{mount.go => secrets_enable.go} | 109 ++++++++------- .../{mount_test.go => secrets_enable_test.go} | 29 ++-- command/{mounts.go => secrets_list.go} | 48 ++++--- .../{mounts_test.go => secrets_list_test.go} | 14 +- command/secrets_move.go | 89 ++++++++++++ .../{remount_test.go => secrets_move_test.go} | 22 +-- command/secrets_tune.go | 119 ++++++++++++++++ ...ount_tune_test.go => secrets_tune_test.go} | 30 ++-- command/unmount.go | 94 ------------- 13 files changed, 476 insertions(+), 371 deletions(-) delete mode 100644 command/mount_tune.go create mode 100644 command/secrets.go create mode 100644 command/secrets_disable.go rename command/{unmount_test.go => secrets_disable_test.go} (72%) rename command/{mount.go => secrets_enable.go} (51%) rename command/{mount_test.go => secrets_enable_test.go} (83%) rename command/{mounts.go => secrets_list.go} (66%) rename command/{mounts_test.go => secrets_list_test.go} (82%) create mode 100644 command/secrets_move.go rename command/{remount_test.go => secrets_move_test.go} (80%) create mode 100644 command/secrets_tune.go rename command/{mount_tune_test.go => secrets_tune_test.go} (83%) delete mode 100644 command/unmount.go diff --git a/command/mount_tune.go b/command/mount_tune.go deleted file mode 100644 index 962ebb5f5f..0000000000 --- a/command/mount_tune.go +++ /dev/null @@ -1,128 +0,0 @@ -package command - -import ( - "fmt" - "strings" - "time" - - "github.com/hashicorp/vault/api" - "github.com/mitchellh/cli" - "github.com/posener/complete" -) - -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*MountTuneCommand)(nil) -var _ cli.CommandAutocomplete = (*MountTuneCommand)(nil) - -// MountTuneCommand is a Command that remounts a mounted secret backend -// to a new endpoint. -type MountTuneCommand struct { - *BaseCommand - - flagDefaultLeaseTTL time.Duration - flagMaxLeaseTTL time.Duration -} - -func (c *MountTuneCommand) Synopsis() string { - return "Tunes an existing mount's configuration" -} - -func (c *MountTuneCommand) Help() string { - helpText := ` -Usage: vault mount-tune [options] PATH - - Tune the configuration options for a mounted secret backend at the given - path. The argument corresponds to the PATH of the mount, not the TYPE! - - Tune the default lease for the PKI secret backend: - - $ vault mount-tune -default-lease-ttl=72h pki/ - - For a full list of examples and paths, please see the documentation that - corresponds to the secret backend in use. - -` + c.Flags().Help() - - return strings.TrimSpace(helpText) -} - -func (c *MountTuneCommand) Flags() *FlagSets { - set := c.flagSet(FlagSetHTTP) - - f := set.NewFlagSet("Command Options") - - f.DurationVar(&DurationVar{ - Name: "default-lease-ttl", - Target: &c.flagDefaultLeaseTTL, - Default: 0, - EnvVar: "", - Completion: complete.PredictAnything, - Usage: "The default lease TTL for this backend. If unspecified, this " + - "defaults to the Vault server's globally configured default lease TTL, " + - "or a previously configured value for the backend.", - }) - - f.DurationVar(&DurationVar{ - Name: "max-lease-ttl", - Target: &c.flagMaxLeaseTTL, - Default: 0, - EnvVar: "", - Completion: complete.PredictAnything, - Usage: "The maximum lease TTL for this backend. If unspecified, this " + - "defaults to the Vault server's globally configured maximum lease TTL, " + - "or a previously configured value for the backend.", - }) - - return set -} - -func (c *MountTuneCommand) AutocompleteArgs() complete.Predictor { - return c.PredictVaultMounts() -} - -func (c *MountTuneCommand) AutocompleteFlags() complete.Flags { - return c.Flags().Completions() -} - -func (c *MountTuneCommand) Run(args []string) int { - f := c.Flags() - - if err := f.Parse(args); err != nil { - c.UI.Error(err.Error()) - return 1 - } - - args = f.Args() - mountPath, remaining, err := extractPath(args) - if err != nil { - c.UI.Error(err.Error()) - return 1 - } - - if len(remaining) > 0 { - c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) - return 1 - } - - client, err := c.Client() - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - - // Append a trailing slash to indicate it's a path in output - mountPath = ensureTrailingSlash(mountPath) - - mountConfig := api.MountConfigInput{ - DefaultLeaseTTL: c.flagDefaultLeaseTTL.String(), - MaxLeaseTTL: c.flagMaxLeaseTTL.String(), - } - - if err := client.Sys().TuneMount(mountPath, mountConfig); err != nil { - c.UI.Error(fmt.Sprintf("Error tuning mount %s: %s", mountPath, err)) - return 2 - } - - c.UI.Output(fmt.Sprintf("Success! Tuned the mount at: %s", mountPath)) - return 0 -} diff --git a/command/secrets.go b/command/secrets.go new file mode 100644 index 0000000000..06e63bec28 --- /dev/null +++ b/command/secrets.go @@ -0,0 +1,43 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*SecretsCommand)(nil) + +type SecretsCommand struct { + *BaseCommand +} + +func (c *SecretsCommand) Synopsis() string { + return "Interact with secrets engines" +} + +func (c *SecretsCommand) Help() string { + helpText := ` +Usage: vault secrets [options] [args] + + This command groups subcommands for interacting with Vault's secrets engines. + Each secret engine behaves differently. Please see the documentation for + more information. + + List all enabled secrets engines: + + $ vault secrets list + + Enable a new secrets engine: + + $ vault secrets enable database + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (c *SecretsCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/secrets_disable.go b/command/secrets_disable.go new file mode 100644 index 0000000000..7002874409 --- /dev/null +++ b/command/secrets_disable.go @@ -0,0 +1,84 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*SecretsDisableCommand)(nil) +var _ cli.CommandAutocomplete = (*SecretsDisableCommand)(nil) + +type SecretsDisableCommand struct { + *BaseCommand +} + +func (c *SecretsDisableCommand) Synopsis() string { + return "Disable a secret engine" +} + +func (c *SecretsDisableCommand) Help() string { + helpText := ` +Usage: vault secrets disable [options] PATH + + Disables a secrets engine at the given PATH. The argument corresponds to + the enabled PATH of the engine, not the TYPE! All secrets created by this + engine are revoked and its Vault data is removed. + + Disable the secrets engine enabled at aws/: + + $ vault secrets disable aws/ + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *SecretsDisableCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *SecretsDisableCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultMounts() +} + +func (c *SecretsDisableCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *SecretsDisableCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + path := ensureTrailingSlash(sanitizePath(args[0])) + + if err := client.Sys().Unmount(path); err != nil { + c.UI.Error(fmt.Sprintf("Error disabling secrets engine at %s: %s", path, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Disabled the secrets engine (if it existed) at: %s", path)) + return 0 +} diff --git a/command/unmount_test.go b/command/secrets_disable_test.go similarity index 72% rename from command/unmount_test.go rename to command/secrets_disable_test.go index 47057458f5..567c8956d6 100644 --- a/command/unmount_test.go +++ b/command/secrets_disable_test.go @@ -8,18 +8,18 @@ import ( "github.com/mitchellh/cli" ) -func testUnmountCommand(tb testing.TB) (*cli.MockUi, *UnmountCommand) { +func testSecretsDisableCommand(tb testing.TB) (*cli.MockUi, *SecretsDisableCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &UnmountCommand{ + return ui, &SecretsDisableCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestUnmountCommand_Run(t *testing.T) { +func TestSecretsDisableCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -29,27 +29,27 @@ func TestUnmountCommand_Run(t *testing.T) { code int }{ { - "empty", - nil, - "Missing PATH!", + "not_enough_args", + []string{}, + "Not enough arguments", 1, }, { - "slash", - []string{"/"}, - "Missing PATH!", + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", 1, }, { "not_real", []string{"not_real"}, - "Success! Unmounted the secret backend (if it existed) at: not_real/", + "Success! Disabled the secrets engine (if it existed) at: not_real/", 0, }, { "default", []string{"secret"}, - "Success! Unmounted the secret backend (if it existed) at: secret/", + "Success! Disabled the secrets engine (if it existed) at: secret/", 0, }, } @@ -66,7 +66,7 @@ func TestUnmountCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testUnmountCommand(t) + ui, cmd := testSecretsDisableCommand(t) cmd.client = client code := cmd.Run(tc.args) @@ -88,23 +88,23 @@ func TestUnmountCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - if err := client.Sys().Mount("integration_unmount/", &api.MountInput{ + if err := client.Sys().Mount("my-secret/", &api.MountInput{ Type: "generic", }); err != nil { t.Fatal(err) } - ui, cmd := testUnmountCommand(t) + ui, cmd := testSecretsDisableCommand(t) cmd.client = client code := cmd.Run([]string{ - "integration_unmount/", + "my-secret/", }) if exp := 0; code != exp { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Unmounted the secret backend (if it existed) at: integration_unmount/" + expected := "Success! Disabled the secrets engine (if it existed) at: my-secret/" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -126,7 +126,7 @@ func TestUnmountCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testUnmountCommand(t) + ui, cmd := testSecretsDisableCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -136,7 +136,7 @@ func TestUnmountCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error unmounting pki/: " + expected := "Error disabling secrets engine at pki/: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -146,7 +146,7 @@ func TestUnmountCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testUnmountCommand(t) + _, cmd := testSecretsDisableCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/mount.go b/command/secrets_enable.go similarity index 51% rename from command/mount.go rename to command/secrets_enable.go index c89115d782..8e5f178fb0 100644 --- a/command/mount.go +++ b/command/secrets_enable.go @@ -10,12 +10,10 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*MountCommand)(nil) -var _ cli.CommandAutocomplete = (*MountCommand)(nil) +var _ cli.Command = (*SecretsEnableCommand)(nil) +var _ cli.CommandAutocomplete = (*SecretsEnableCommand)(nil) -// MountCommand is a Command that mounts a new mount. -type MountCommand struct { +type SecretsEnableCommand struct { *BaseCommand flagDescription string @@ -27,45 +25,45 @@ type MountCommand struct { flagLocal bool } -func (c *MountCommand) Synopsis() string { - return "Mounts a secret backend at a path" +func (c *SecretsEnableCommand) Synopsis() string { + return "Enable a secrets engine" } -func (c *MountCommand) Help() string { +func (c *SecretsEnableCommand) Help() string { helpText := ` -Usage: vault mount [options] TYPE +Usage: vault secrets enable [options] TYPE - Mount a secret backend at a particular path. By default, secret backends are - mounted at the path corresponding to their "type", but users can customize - the mount point using the -path option. + Enables a secrets engine. By default, secrets engines are enabled at the path + corresponding to their TYPE, but users can customize the path using the + -path option. - Once mounted at a path, Vault will route all requests which begin with the - path to the secret backend. + Once enabled, Vault will route all requests which begin with the path to the + secrets engine. - Mount the AWS backend at aws/: + Enable the AWS secrets engine at aws/: - $ vault mount aws + $ vault secrets enable aws - Mount the SSH backend at ssh-prod/: + Enable the SSH secrets engine at ssh-prod/: - $ vault mount -path=ssh-prod ssh + $ vault secrets enable -path=ssh-prod ssh - Mount the database backend with an explicit maximum TTL of 30m: + Enable the database secrets engine with an explicit maximum TTL of 30m: - $ vault mount -max-lease-ttl=30m database + $ vault secrets enable -max-lease-ttl=30m database - Mount a custom plugin (after it is registered in the plugin registry): + Enable a custom plugin (after it is registered in the plugin registry): - $ vault mount -path=my-secrets -plugin-name=my-custom-plugin plugin + $ vault secrets enable -path=my-secrets -plugin-name=my-plugin plugin - For a full list of secret backends and examples, please see the documentation. + For a full list of secrets engines and examples, please see the documentation. ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *MountCommand) Flags() *FlagSets { +func (c *SecretsEnableCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP) f := set.NewFlagSet("Command Options") @@ -74,7 +72,7 @@ func (c *MountCommand) Flags() *FlagSets { Name: "description", Target: &c.flagDescription, Completion: complete.PredictAnything, - Usage: "Human-friendly description for the purpose of this mount.", + Usage: "Human-friendly description for the purpose of this engine.", }) f.StringVar(&StringVar{ @@ -82,31 +80,34 @@ func (c *MountCommand) Flags() *FlagSets { Target: &c.flagPath, Default: "", // The default is complex, so we have to manually document Completion: complete.PredictAnything, - Usage: "Place where the mount will be accessible. This must be " + - "unique across all mounts. This defaults to the \"type\" of the mount.", + Usage: "Place where the secrets engine will be accessible. This must be " + + "unique cross all secrets engines. This defaults to the \"type\" of the " + + "secrets engine.", }) f.DurationVar(&DurationVar{ Name: "default-lease-ttl", Target: &c.flagDefaultLeaseTTL, Completion: complete.PredictAnything, - Usage: "The default lease TTL for this backend. If unspecified, this " + - "defaults to the Vault server's globally configured default lease TTL.", + Usage: "The default lease TTL for this secrets engine. If unspecified, " + + "this defaults to the Vault server's globally configured default lease " + + "TTL.", }) f.DurationVar(&DurationVar{ Name: "max-lease-ttl", Target: &c.flagMaxLeaseTTL, Completion: complete.PredictAnything, - Usage: "The maximum lease TTL for this backend. If unspecified, this " + - "defaults to the Vault server's globally configured maximum lease TTL.", + Usage: "The maximum lease TTL for this secrets engine. If unspecified, " + + "this defaults to the Vault server's globally configured maximum lease " + + "TTL.", }) f.BoolVar(&BoolVar{ Name: "force-no-cache", Target: &c.flagForceNoCache, Default: false, - Usage: "Force the backend to disable caching. If unspecified, this " + + Usage: "Force the secrets engine to disable caching. If unspecified, this " + "defaults to the Vault server's globally configured cache settings. " + "This does not affect caching of the underlying encrypted data storage.", }) @@ -115,30 +116,30 @@ func (c *MountCommand) Flags() *FlagSets { Name: "plugin-name", Target: &c.flagPluginName, Completion: complete.PredictAnything, - Usage: "Name of the plugin to mount. This plugin name must already exist " + - "in the Vault server's plugin catalog.", + Usage: "Name of the secrets engine plugin. This plugin name must already " + + "exist in Vault's plugin catalog.", }) f.BoolVar(&BoolVar{ Name: "local", Target: &c.flagLocal, Default: false, - Usage: "Mark the mount as a local-only mount. Local mounts are not " + - "replicated nor removed by replication.", + Usage: "Mark the secrets engine as local-only. Local engines are not " + + "replicated or removed by replication.", }) return set } -func (c *MountCommand) AutocompleteArgs() complete.Predictor { +func (c *SecretsEnableCommand) AutocompleteArgs() complete.Predictor { return c.PredictVaultAvailableMounts() } -func (c *MountCommand) AutocompleteFlags() complete.Flags { +func (c *SecretsEnableCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *MountCommand) Run(args []string) int { +func (c *SecretsEnableCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { @@ -147,13 +148,11 @@ func (c *MountCommand) Run(args []string) int { } args = f.Args() - switch len(args) { - case 0: - c.UI.Error("Missing TYPE!") + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 - case 1: - // OK - default: + case len(args) > 1: c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) return 1 } @@ -164,17 +163,17 @@ func (c *MountCommand) Run(args []string) int { return 2 } - // Get the mount type (first arg) - mountType := strings.TrimSpace(args[0]) + // Get the engine type type (first arg) + engineType := strings.TrimSpace(args[0]) // If no path is specified, we default the path to the backend type // or use the plugin name if it's a plugin backend mountPath := c.flagPath if mountPath == "" { - if mountType == "plugin" { + if engineType == "plugin" { mountPath = c.flagPluginName } else { - mountPath = mountType + mountPath = engineType } } @@ -183,7 +182,7 @@ func (c *MountCommand) Run(args []string) int { // Build mount input mountInput := &api.MountInput{ - Type: mountType, + Type: engineType, Description: c.flagDescription, Local: c.flagLocal, Config: api.MountConfigInput{ @@ -195,15 +194,15 @@ func (c *MountCommand) Run(args []string) int { } if err := client.Sys().Mount(mountPath, mountInput); err != nil { - c.UI.Error(fmt.Sprintf("Error mounting: %s", err)) + c.UI.Error(fmt.Sprintf("Error enabling: %s", err)) return 2 } - mountThing := mountType + " secret backend" - if mountType == "plugin" { - mountThing = c.flagPluginName + " plugin" + thing := engineType + " secrets engine" + if engineType == "plugin" { + thing = c.flagPluginName + " plugin" } - c.UI.Output(fmt.Sprintf("Success! Mounted the %s at: %s", mountThing, mountPath)) + c.UI.Output(fmt.Sprintf("Success! Enabled the %s at: %s", thing, mountPath)) return 0 } diff --git a/command/mount_test.go b/command/secrets_enable_test.go similarity index 83% rename from command/mount_test.go rename to command/secrets_enable_test.go index dd7659127d..e241edfa65 100644 --- a/command/mount_test.go +++ b/command/secrets_enable_test.go @@ -7,17 +7,18 @@ import ( "github.com/mitchellh/cli" ) -func testMountCommand(tb testing.TB) (*cli.MockUi, *MountCommand) { +func testSecretsEnableCommand(tb testing.TB) (*cli.MockUi, *SecretsEnableCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &MountCommand{ + return ui, &SecretsEnableCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestMountCommand_Run(t *testing.T) { + +func TestSecretsEnableCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -27,9 +28,9 @@ func TestMountCommand_Run(t *testing.T) { code int }{ { - "empty", - nil, - "Missing TYPE!", + "not_enough_args", + []string{}, + "Not enough arguments", 1, }, { @@ -47,7 +48,7 @@ func TestMountCommand_Run(t *testing.T) { { "mount", []string{"transit"}, - "Success! Mounted the transit secret backend at: transit/", + "Success! Enabled the transit secrets engine at: transit/", 0, }, { @@ -56,7 +57,7 @@ func TestMountCommand_Run(t *testing.T) { "-path", "transit_mount_point", "transit", }, - "Success! Mounted the transit secret backend at: transit_mount_point/", + "Success! Enabled the transit secrets engine at: transit_mount_point/", 0, }, } @@ -70,7 +71,7 @@ func TestMountCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testMountCommand(t) + ui, cmd := testSecretsEnableCommand(t) cmd.client = client code := cmd.Run(tc.args) @@ -91,7 +92,7 @@ func TestMountCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testMountCommand(t) + ui, cmd := testSecretsEnableCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -106,7 +107,7 @@ func TestMountCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Mounted the pki secret backend at: mount_integration/" + expected := "Success! Enabled the pki secrets engine at: mount_integration/" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -144,7 +145,7 @@ func TestMountCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testMountCommand(t) + ui, cmd := testSecretsEnableCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -154,7 +155,7 @@ func TestMountCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error mounting: " + expected := "Error enabling: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -164,7 +165,7 @@ func TestMountCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testMountCommand(t) + _, cmd := testSecretsEnableCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/mounts.go b/command/secrets_list.go similarity index 66% rename from command/mounts.go rename to command/secrets_list.go index b25bfe4dcb..9bff70ff29 100644 --- a/command/mounts.go +++ b/command/secrets_list.go @@ -11,44 +11,42 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*MountsCommand)(nil) -var _ cli.CommandAutocomplete = (*MountsCommand)(nil) +var _ cli.Command = (*SecretsListCommand)(nil) +var _ cli.CommandAutocomplete = (*SecretsListCommand)(nil) -// MountsCommand is a Command that lists the mounts. -type MountsCommand struct { +type SecretsListCommand struct { *BaseCommand flagDetailed bool } -func (c *MountsCommand) Synopsis() string { - return "Lists mounted secret backends" +func (c *SecretsListCommand) Synopsis() string { + return "List enabled secrets engines" } -func (c *MountsCommand) Help() string { +func (c *SecretsListCommand) Help() string { helpText := ` -Usage: vault mounts [options] +Usage: vault secrets list [options] - Lists the mounted secret backends on the Vault server. This command also - outputs information about the mount point including configured TTLs and + Lists the enabled secret engines on the Vault server. This command also + outputs information about the enabled path including configured TTLs and human-friendly descriptions. A TTL of "system" indicates that the system default is in use. - List all mounts: + List all enabled secrets engines: - $ vault mounts + $ vault secrets list - List all mounts with detailed output: + List all enabled secrets engines with detailed output: - $ vault mounts -detailed + $ vault secrets list -detailed ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *MountsCommand) Flags() *FlagSets { +func (c *SecretsListCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP) f := set.NewFlagSet("Command Options") @@ -58,21 +56,21 @@ func (c *MountsCommand) Flags() *FlagSets { Target: &c.flagDetailed, Default: false, Usage: "Print detailed information such as TTLs and replication status " + - "about each mount.", + "about each secrets engine.", }) return set } -func (c *MountsCommand) AutocompleteArgs() complete.Predictor { +func (c *SecretsListCommand) AutocompleteArgs() complete.Predictor { return c.PredictVaultFiles() } -func (c *MountsCommand) AutocompleteFlags() complete.Flags { +func (c *SecretsListCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *MountsCommand) Run(args []string) int { +func (c *SecretsListCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { @@ -94,20 +92,20 @@ func (c *MountsCommand) Run(args []string) int { mounts, err := client.Sys().ListMounts() if err != nil { - c.UI.Error(fmt.Sprintf("Error listing mounts: %s", err)) + c.UI.Error(fmt.Sprintf("Error listing secrets engines: %s", err)) return 2 } if c.flagDetailed { - c.UI.Output(tableOutput(c.detailedMounts(mounts))) + c.UI.Output(tableOutput(c.detailedMounts(mounts), nil)) return 0 } - c.UI.Output(tableOutput(c.simpleMounts(mounts))) + c.UI.Output(tableOutput(c.simpleMounts(mounts), nil)) return 0 } -func (c *MountsCommand) simpleMounts(mounts map[string]*api.MountOutput) []string { +func (c *SecretsListCommand) simpleMounts(mounts map[string]*api.MountOutput) []string { paths := make([]string, 0, len(mounts)) for path := range mounts { paths = append(paths, path) @@ -123,7 +121,7 @@ func (c *MountsCommand) simpleMounts(mounts map[string]*api.MountOutput) []strin return out } -func (c *MountsCommand) detailedMounts(mounts map[string]*api.MountOutput) []string { +func (c *SecretsListCommand) detailedMounts(mounts map[string]*api.MountOutput) []string { paths := make([]string, 0, len(mounts)) for path := range mounts { paths = append(paths, path) diff --git a/command/mounts_test.go b/command/secrets_list_test.go similarity index 82% rename from command/mounts_test.go rename to command/secrets_list_test.go index 8f1c8b9c85..9edb628202 100644 --- a/command/mounts_test.go +++ b/command/secrets_list_test.go @@ -7,18 +7,18 @@ import ( "github.com/mitchellh/cli" ) -func testMountsCommand(tb testing.TB) (*cli.MockUi, *MountsCommand) { +func testSecretsListCommand(tb testing.TB) (*cli.MockUi, *SecretsListCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &MountsCommand{ + return ui, &SecretsListCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestMountsCommand_Run(t *testing.T) { +func TestSecretsListCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -59,7 +59,7 @@ func TestMountsCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testMountsCommand(t) + ui, cmd := testSecretsListCommand(t) cmd.client = client code := cmd.Run(tc.args) @@ -81,7 +81,7 @@ func TestMountsCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testMountsCommand(t) + ui, cmd := testSecretsListCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -89,7 +89,7 @@ func TestMountsCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error listing mounts: " + expected := "Error listing secrets engines: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -99,7 +99,7 @@ func TestMountsCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testMountsCommand(t) + _, cmd := testSecretsListCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/secrets_move.go b/command/secrets_move.go new file mode 100644 index 0000000000..542b14d3d2 --- /dev/null +++ b/command/secrets_move.go @@ -0,0 +1,89 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*SecretsMoveCommand)(nil) +var _ cli.CommandAutocomplete = (*SecretsMoveCommand)(nil) + +type SecretsMoveCommand struct { + *BaseCommand +} + +func (c *SecretsMoveCommand) Synopsis() string { + return "Move a secrets engine to a new path" +} + +func (c *SecretsMoveCommand) Help() string { + helpText := ` +Usage: vault secrets move [options] SOURCE DESTINATION + + Moves an existing secrets engine to a new path. Any leases from the old + secrets engine are revoked, but all configuration associated with the engine + is preserved. + + WARNING! Moving an existing secrets engine will revoke any leases from the + old engine. + + Move the existing secrets engine at secret/ to generic/: + + $ vault secrets move secret/ generic/ + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *SecretsMoveCommand) Flags() *FlagSets { + return c.flagSet(FlagSetHTTP) +} + +func (c *SecretsMoveCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultMounts() +} + +func (c *SecretsMoveCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *SecretsMoveCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + switch { + case len(args) < 2: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 2, got %d)", len(args))) + return 1 + case len(args) > 2: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 2, got %d)", len(args))) + return 1 + } + + // Grab the source and destination + source := ensureTrailingSlash(args[0]) + destination := ensureTrailingSlash(args[1]) + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if err := client.Sys().Remount(source, destination); err != nil { + c.UI.Error(fmt.Sprintf("Error moving secrets engine %s to %s: %s", source, destination, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Moved secrets engine %s to: %s", source, destination)) + return 0 +} diff --git a/command/remount_test.go b/command/secrets_move_test.go similarity index 80% rename from command/remount_test.go rename to command/secrets_move_test.go index 6a6815e520..0936a7dd30 100644 --- a/command/remount_test.go +++ b/command/secrets_move_test.go @@ -7,18 +7,18 @@ import ( "github.com/mitchellh/cli" ) -func testRemountCommand(tb testing.TB) (*cli.MockUi, *RemountCommand) { +func testSecretsMoveCommand(tb testing.TB) (*cli.MockUi, *SecretsMoveCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &RemountCommand{ + return ui, &SecretsMoveCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestRemountCommand_Run(t *testing.T) { +func TestSecretsMoveCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -29,7 +29,7 @@ func TestRemountCommand_Run(t *testing.T) { }{ { "not_enough_args", - nil, + []string{}, "Not enough arguments", 1, }, @@ -42,7 +42,7 @@ func TestRemountCommand_Run(t *testing.T) { { "non_existent", []string{"not_real", "over_here"}, - "Error remounting not_real/ to over_here/", + "Error moving secrets engine not_real/ to over_here/", 2, }, } @@ -56,7 +56,7 @@ func TestRemountCommand_Run(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ui, cmd := testRemountCommand(t) + ui, cmd := testSecretsMoveCommand(t) code := cmd.Run(tc.args) if code != tc.code { @@ -77,7 +77,7 @@ func TestRemountCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testRemountCommand(t) + ui, cmd := testSecretsMoveCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -87,7 +87,7 @@ func TestRemountCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Remounted secret/ to: generic/" + expected := "Success! Moved secrets engine secret/ to: generic/" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -109,7 +109,7 @@ func TestRemountCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testRemountCommand(t) + ui, cmd := testSecretsMoveCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -119,7 +119,7 @@ func TestRemountCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error remounting secret/ to generic/: " + expected := "Error moving secrets engine secret/ to generic/:" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -129,7 +129,7 @@ func TestRemountCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testRemountCommand(t) + _, cmd := testSecretsMoveCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/secrets_tune.go b/command/secrets_tune.go new file mode 100644 index 0000000000..b2029b7507 --- /dev/null +++ b/command/secrets_tune.go @@ -0,0 +1,119 @@ +package command + +import ( + "fmt" + "strings" + "time" + + "github.com/hashicorp/vault/api" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +var _ cli.Command = (*SecretsTuneCommand)(nil) +var _ cli.CommandAutocomplete = (*SecretsTuneCommand)(nil) + +type SecretsTuneCommand struct { + *BaseCommand + + flagDefaultLeaseTTL time.Duration + flagMaxLeaseTTL time.Duration +} + +func (c *SecretsTuneCommand) Synopsis() string { + return "Tune a secrets engine configuration" +} + +func (c *SecretsTuneCommand) Help() string { + helpText := ` +Usage: vault secrets tune [options] PATH + + Tunes the configuration options for the secrets engine at the given PATH. + The argument corresponds to the PATH where the secrets engine is enabled, + not the TYPE! + + Tune the default lease for the PKI secrets engine: + + $ vault secrets tune -default-lease-ttl=72h pki/ + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *SecretsTuneCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.DurationVar(&DurationVar{ + Name: "default-lease-ttl", + Target: &c.flagDefaultLeaseTTL, + Default: 0, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "The default lease TTL for this secrets engine. If unspecified, " + + "this defaults to the Vault server's globally configured default lease " + + "TTL, or a previously configured value for the secrets engine.", + }) + + f.DurationVar(&DurationVar{ + Name: "max-lease-ttl", + Target: &c.flagMaxLeaseTTL, + Default: 0, + EnvVar: "", + Completion: complete.PredictAnything, + Usage: "The maximum lease TTL for this secrets engine. If unspecified, " + + "this defaults to the Vault server's globally configured maximum lease " + + "TTL, or a previously configured value for the secrets engine.", + }) + + return set +} + +func (c *SecretsTuneCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultMounts() +} + +func (c *SecretsTuneCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *SecretsTuneCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.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(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // Append a trailing slash to indicate it's a path in output + mountPath := ensureTrailingSlash(sanitizePath(args[0])) + + if err := client.Sys().TuneMount(mountPath, api.MountConfigInput{ + DefaultLeaseTTL: ttlToAPI(c.flagDefaultLeaseTTL), + MaxLeaseTTL: ttlToAPI(c.flagMaxLeaseTTL), + }); err != nil { + c.UI.Error(fmt.Sprintf("Error tuning secrets engine %s: %s", mountPath, err)) + return 2 + } + + c.UI.Output(fmt.Sprintf("Success! Tuned the secrets engine at: %s", mountPath)) + return 0 +} diff --git a/command/mount_tune_test.go b/command/secrets_tune_test.go similarity index 83% rename from command/mount_tune_test.go rename to command/secrets_tune_test.go index b60c532f72..11d90263d3 100644 --- a/command/mount_tune_test.go +++ b/command/secrets_tune_test.go @@ -8,18 +8,18 @@ import ( "github.com/mitchellh/cli" ) -func testMountTuneCommand(tb testing.TB) (*cli.MockUi, *MountTuneCommand) { +func testSecretsTuneCommand(tb testing.TB) (*cli.MockUi, *SecretsTuneCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &MountTuneCommand{ + return ui, &SecretsTuneCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestMountTuneCommand_Run(t *testing.T) { +func TestSecretsTuneCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -29,15 +29,9 @@ func TestMountTuneCommand_Run(t *testing.T) { code int }{ { - "empty", - nil, - "Missing PATH!", - 1, - }, - { - "slash", - []string{"/"}, - "Missing PATH!", + "not_enough_args", + []string{}, + "Not enough arguments", 1, }, { @@ -57,7 +51,7 @@ func TestMountTuneCommand_Run(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ui, cmd := testMountTuneCommand(t) + ui, cmd := testSecretsTuneCommand(t) code := cmd.Run(tc.args) if code != tc.code { @@ -78,7 +72,7 @@ func TestMountTuneCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testMountTuneCommand(t) + ui, cmd := testSecretsTuneCommand(t) cmd.client = client // Mount @@ -97,7 +91,7 @@ func TestMountTuneCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Tuned the mount at: mount_tune_integration/" + expected := "Success! Tuned the secrets engine at: mount_tune_integration/" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -129,7 +123,7 @@ func TestMountTuneCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testMountTuneCommand(t) + ui, cmd := testSecretsTuneCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -139,7 +133,7 @@ func TestMountTuneCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Error tuning mount pki/: " + expected := "Error tuning secrets engine pki/: " combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) @@ -149,7 +143,7 @@ func TestMountTuneCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testMountTuneCommand(t) + _, cmd := testSecretsTuneCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/unmount.go b/command/unmount.go deleted file mode 100644 index cb3bc6266a..0000000000 --- a/command/unmount.go +++ /dev/null @@ -1,94 +0,0 @@ -package command - -import ( - "fmt" - "strings" - - "github.com/mitchellh/cli" - "github.com/posener/complete" -) - -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*UnmountCommand)(nil) -var _ cli.CommandAutocomplete = (*UnmountCommand)(nil) - -// UnmountCommand is a Command that mounts a new mount. -type UnmountCommand struct { - *BaseCommand -} - -func (c *UnmountCommand) Synopsis() string { - return "Unmounts a secret backend" -} - -func (c *UnmountCommand) Help() string { - helpText := ` -Usage: vault unmount [options] PATH - - Unmounts a secret backend at the given PATH. The argument corresponds to - the PATH of the mount, not the TYPE! All secrets created by this backend - are revoked and its Vault data is removed. - - If no mount exists at the given path, the command will still return as - successful because unmounting is an idempotent operation. - - Unmount the secret backend mounted at aws/: - - $ vault unmount aws/ - - For a full list of examples, please see the documentation. - -` + c.Flags().Help() - - return strings.TrimSpace(helpText) -} - -func (c *UnmountCommand) Flags() *FlagSets { - return c.flagSet(FlagSetHTTP) -} - -func (c *UnmountCommand) AutocompleteArgs() complete.Predictor { - return c.PredictVaultMounts() -} - -func (c *UnmountCommand) AutocompleteFlags() complete.Flags { - return c.Flags().Completions() -} - -func (c *UnmountCommand) Run(args []string) int { - f := c.Flags() - - if err := f.Parse(args); err != nil { - c.UI.Error(err.Error()) - return 1 - } - - args = f.Args() - mountPath, remaining, err := extractPath(args) - if err != nil { - c.UI.Error(err.Error()) - return 1 - } - - if len(remaining) > 0 { - c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) - return 1 - } - - client, err := c.Client() - if err != nil { - c.UI.Error(err.Error()) - return 2 - } - - // Append a trailing slash to indicate it's a path in output - mountPath = ensureTrailingSlash(mountPath) - - if err := client.Sys().Unmount(mountPath); err != nil { - c.UI.Error(fmt.Sprintf("Error unmounting %s: %s", mountPath, err)) - return 2 - } - - c.UI.Output(fmt.Sprintf("Success! Unmounted the secret backend (if it existed) at: %s", mountPath)) - return 0 -} From ef86e95effb9c1e80fd7334a01c725befaa0122a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:03:12 -0400 Subject: [PATCH 097/281] Add "operator" subcommand --- command/operator.go | 47 ++++++++++++ ...rate_root.go => operator_generate_root.go} | 56 +++++++-------- ...test.go => operator_generate_root_test.go} | 32 ++++----- command/{init.go => operator_init.go} | 40 +++++------ .../{init_test.go => operator_init_test.go} | 22 +++--- .../{key_status.go => operator_key_status.go} | 22 +++--- ...us_test.go => operator_key_status_test.go} | 14 ++-- command/{rekey.go => operator_rekey.go} | 71 ++++++++----------- .../{rekey_test.go => operator_rekey_test.go} | 38 +++++----- command/{seal.go => operator_seal.go} | 25 +++---- .../{seal_test.go => operator_seal_test.go} | 14 ++-- .../{step_down.go => operator_step_down.go} | 27 +++---- ...own_test.go => operator_step_down_test.go} | 12 ++-- command/{unseal.go => operator_unseal.go} | 30 ++++---- ...unseal_test.go => operator_unseal_test.go} | 16 ++--- command/rotate.go | 24 +++---- command/rotate_test.go | 14 ++-- 17 files changed, 255 insertions(+), 249 deletions(-) create mode 100644 command/operator.go rename command/{generate_root.go => operator_generate_root.go} (87%) rename command/{generate_root_test.go => operator_generate_root_test.go} (91%) rename command/{init.go => operator_init.go} (93%) rename command/{init_test.go => operator_init_test.go} (93%) rename command/{key_status.go => operator_key_status.go} (61%) rename command/{key_status_test.go => operator_key_status_test.go} (83%) rename command/{rekey.go => operator_rekey.go} (89%) rename command/{rekey_test.go => operator_rekey_test.go} (92%) rename command/{seal.go => operator_seal.go} (68%) rename command/{seal_test.go => operator_seal_test.go} (86%) rename command/{step_down.go => operator_step_down.go} (63%) rename command/{step_down_test.go => operator_step_down_test.go} (82%) rename command/{unseal.go => operator_unseal.go} (80%) rename command/{unseal_test.go => operator_unseal_test.go} (87%) diff --git a/command/operator.go b/command/operator.go new file mode 100644 index 0000000000..ad1bb439fc --- /dev/null +++ b/command/operator.go @@ -0,0 +1,47 @@ +package command + +import ( + "strings" + + "github.com/mitchellh/cli" +) + +var _ cli.Command = (*OperatorCommand)(nil) + +type OperatorCommand struct { + *BaseCommand +} + +func (c *OperatorCommand) Synopsis() string { + return "Perform operator-specific tasks" +} + +func (c *OperatorCommand) Help() string { + helpText := ` +Usage: vault operator [options] [args] + + This command groups subcommands for operators interacting with Vault. Most + users will not need to interact with these commands. Here are a few examples + of the operator commands: + + Initialize a new Vault cluster: + + $ vault operator init + + Force a Vault to resign leadership in a cluster: + + $ vault operator step-down + + Rotate Vault's underlying encryption key: + + $ vault operator rotate + + Please see the individual subcommand help for detailed usage information. +` + + return strings.TrimSpace(helpText) +} + +func (c *OperatorCommand) Run(args []string) int { + return cli.RunResultHelp +} diff --git a/command/generate_root.go b/command/operator_generate_root.go similarity index 87% rename from command/generate_root.go rename to command/operator_generate_root.go index 58f7442564..6eb17042a6 100644 --- a/command/generate_root.go +++ b/command/operator_generate_root.go @@ -18,12 +18,10 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*GenerateRootCommand)(nil) -var _ cli.CommandAutocomplete = (*GenerateRootCommand)(nil) +var _ cli.Command = (*OperatorGenerateRootCommand)(nil) +var _ cli.CommandAutocomplete = (*OperatorGenerateRootCommand)(nil) -// GenerateRootCommand is a Command that generates a new root token. -type GenerateRootCommand struct { +type OperatorGenerateRootCommand struct { *BaseCommand flagInit bool @@ -36,26 +34,26 @@ type GenerateRootCommand struct { flagGenerateOTP bool // Deprecation - // TODO: remove in 0.9.9 + // TODO: remove in 0.9.0 flagGenOTP bool testStdin io.Reader // for tests } -func (c *GenerateRootCommand) Synopsis() string { +func (c *OperatorGenerateRootCommand) Synopsis() string { return "Generates a new root token" } -func (c *GenerateRootCommand) Help() string { +func (c *OperatorGenerateRootCommand) Help() string { helpText := ` -Usage: vault generate-root [options] [KEY] +Usage: vault operator generate-root [options] [KEY] Generates a new root token by combining a quorum of share holders. One of the following must be provided to start the root token generation: - A base64-encoded one-time-password (OTP) provided via the "-otp" flag. Use the "-generate-otp" flag to generate a usable value. The resulting - token is XORed with this value when it is returend. Use the "-decode" + token is XORed with this value when it is returned. Use the "-decode" flag to output the final value. - A file containing a PGP key or a keybase username in the "-pgp-key" @@ -67,24 +65,22 @@ Usage: vault generate-root [options] [KEY] Generate an OTP code for the final token: - $ vault generate-root -generate-otp + $ vault operator generate-root -generate-otp Start a root token generation: - $ vault generate-root -init -otp="..." - $ vault generate-root -init -pgp-key="..." + $ vault operator generate-root -init -otp="..." + $ vault operator generate-root -init -pgp-key="..." Enter an unseal key to progress root token generation: - $ vault generate-root -otp="..." - - For a full list of examples, please see the documentation. + $ vault operator generate-root -otp="..." ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *GenerateRootCommand) Flags() *FlagSets { +func (c *OperatorGenerateRootCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP) f := set.NewFlagSet("Command Options") @@ -115,7 +111,7 @@ func (c *GenerateRootCommand) Flags() *FlagSets { Default: false, EnvVar: "", Completion: complete.PredictNothing, - Usage: "Print the status of the current attempt without provding an " + + Usage: "Print the status of the current attempt without providing an " + "unseal key.", }) @@ -183,15 +179,15 @@ func (c *GenerateRootCommand) Flags() *FlagSets { return set } -func (c *GenerateRootCommand) AutocompleteArgs() complete.Predictor { +func (c *OperatorGenerateRootCommand) AutocompleteArgs() complete.Predictor { return nil } -func (c *GenerateRootCommand) AutocompleteFlags() complete.Flags { +func (c *OperatorGenerateRootCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *GenerateRootCommand) Run(args []string) int { +func (c *OperatorGenerateRootCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { @@ -243,7 +239,7 @@ func (c *GenerateRootCommand) Run(args []string) int { } // verifyOTP verifies the given OTP code is exactly 16 bytes. -func (c *GenerateRootCommand) verifyOTP(otp string) error { +func (c *OperatorGenerateRootCommand) verifyOTP(otp string) error { if len(otp) == 0 { return fmt.Errorf("No OTP passed in") } @@ -259,7 +255,7 @@ func (c *GenerateRootCommand) verifyOTP(otp string) error { } // generateOTP generates a suitable OTP code for generating a root token. -func (c *GenerateRootCommand) generateOTP() int { +func (c *OperatorGenerateRootCommand) generateOTP() int { buf := make([]byte, 16) readLen, err := rand.Read(buf) if err != nil { @@ -276,7 +272,7 @@ func (c *GenerateRootCommand) generateOTP() int { } // decode decodes the given value using the otp. -func (c *GenerateRootCommand) decode(encoded, otp string) int { +func (c *OperatorGenerateRootCommand) decode(encoded, otp string) int { if encoded == "" { c.UI.Error("Missing encoded value: use -decode= to supply it") return 1 @@ -302,7 +298,7 @@ func (c *GenerateRootCommand) decode(encoded, otp string) int { } // init is used to start the generation process -func (c *GenerateRootCommand) init(client *api.Client, otp string, pgpKey string) int { +func (c *OperatorGenerateRootCommand) init(client *api.Client, otp string, pgpKey string) int { // Validate incoming fields. Either OTP OR PGP keys must be supplied. switch { case otp == "" && pgpKey == "": @@ -331,7 +327,7 @@ func (c *GenerateRootCommand) init(client *api.Client, otp string, pgpKey string // provide prompts the user for the seal key and posts it to the update root // endpoint. If this is the last unseal, this function outputs it. -func (c *GenerateRootCommand) provide(client *api.Client, key string) int { +func (c *OperatorGenerateRootCommand) provide(client *api.Client, key string) int { status, err := client.Sys().GenerateRootStatus() if err != nil { c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err)) @@ -413,7 +409,7 @@ func (c *GenerateRootCommand) provide(client *api.Client, key string) int { } // cancel cancels the root token generation -func (c *GenerateRootCommand) cancel(client *api.Client) int { +func (c *OperatorGenerateRootCommand) cancel(client *api.Client) int { if err := client.Sys().GenerateRootCancel(); err != nil { c.UI.Error(fmt.Sprintf("Error canceling root token generation: %s", err)) return 2 @@ -423,7 +419,7 @@ func (c *GenerateRootCommand) cancel(client *api.Client) int { } // status is used just to fetch and dump the status -func (c *GenerateRootCommand) status(client *api.Client) int { +func (c *OperatorGenerateRootCommand) status(client *api.Client) int { status, err := client.Sys().GenerateRootStatus() if err != nil { c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err)) @@ -433,7 +429,7 @@ func (c *GenerateRootCommand) status(client *api.Client) int { } // printStatus dumps the status to output -func (c *GenerateRootCommand) printStatus(status *api.GenerateRootStatusResponse) int { +func (c *OperatorGenerateRootCommand) printStatus(status *api.GenerateRootStatusResponse) int { out := []string{} out = append(out, fmt.Sprintf("Nonce | %s", status.Nonce)) out = append(out, fmt.Sprintf("Started | %t", status.Started)) @@ -446,7 +442,7 @@ func (c *GenerateRootCommand) printStatus(status *api.GenerateRootStatusResponse out = append(out, fmt.Sprintf("Root Token | %s", status.EncodedRootToken)) } - output := columnOutput(out) + output := columnOutput(out, nil) c.UI.Output(output) return 0 } diff --git a/command/generate_root_test.go b/command/operator_generate_root_test.go similarity index 91% rename from command/generate_root_test.go rename to command/operator_generate_root_test.go index ba13f6b47a..ad5e67e239 100644 --- a/command/generate_root_test.go +++ b/command/operator_generate_root_test.go @@ -11,18 +11,18 @@ import ( "github.com/mitchellh/cli" ) -func testGenerateRootCommand(tb testing.TB) (*cli.MockUi, *GenerateRootCommand) { +func testOperatorGenerateRootCommand(tb testing.TB) (*cli.MockUi, *OperatorGenerateRootCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &GenerateRootCommand{ + return ui, &OperatorGenerateRootCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestGenerateRootCommand_Run(t *testing.T) { +func TestOperatorGenerateRootCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -88,7 +88,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) code := cmd.Run(tc.args) if code != tc.code { @@ -106,7 +106,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { t.Run("generate_otp", func(t *testing.T) { t.Parallel() - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) code := cmd.Run([]string{ "-generate-otp", @@ -127,7 +127,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { encoded := "L9MaZ/4mQanpOV6QeWd84g==" otp := "dIeeezkjpDUv3fy7MYPOLQ==" - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) code := cmd.Run([]string{ "-decode", encoded, @@ -157,7 +157,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { t.Fatal(err) } - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -191,7 +191,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -227,7 +227,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -263,7 +263,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -297,7 +297,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { // Supply the first n-1 unseal keys for _, key := range keys[:len(keys)-1] { - _, cmd := testGenerateRootCommand(t) + _, cmd := testOperatorGenerateRootCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -309,7 +309,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { } } - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -364,7 +364,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { stdinW.Close() }() - _, cmd := testGenerateRootCommand(t) + _, cmd := testOperatorGenerateRootCommand(t) cmd.client = client cmd.testStdin = stdinR @@ -383,7 +383,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { stdinW.Close() }() - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) cmd.client = client cmd.testStdin = stdinR @@ -422,7 +422,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testGenerateRootCommand(t) + ui, cmd := testOperatorGenerateRootCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -442,7 +442,7 @@ func TestGenerateRootCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testGenerateRootCommand(t) + _, cmd := testOperatorGenerateRootCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/init.go b/command/operator_init.go similarity index 93% rename from command/init.go rename to command/operator_init.go index c2d1120816..cf3aaf31db 100644 --- a/command/init.go +++ b/command/operator_init.go @@ -16,12 +16,10 @@ import ( consulapi "github.com/hashicorp/consul/api" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*InitCommand)(nil) -var _ cli.CommandAutocomplete = (*InitCommand)(nil) +var _ cli.Command = (*OperatorInitCommand)(nil) +var _ cli.CommandAutocomplete = (*OperatorInitCommand)(nil) -// InitCommand is a Command that initializes a new Vault server. -type InitCommand struct { +type OperatorInitCommand struct { *BaseCommand flagStatus bool @@ -46,13 +44,13 @@ type InitCommand struct { flagCheck bool } -func (c *InitCommand) Synopsis() string { +func (c *OperatorInitCommand) Synopsis() string { return "Initializes a server" } -func (c *InitCommand) Help() string { +func (c *OperatorInitCommand) Help() string { helpText := ` -Usage: vault init [options] +Usage: vault operator init [options] Initializes a Vault server. Initialization is the process by which Vault's storage backend is prepared to receive data. Since Vault server's share the @@ -69,26 +67,24 @@ Usage: vault init [options] Start initialization with the default options: - $ vault init + $ vault operator init Initialize, but encrypt the unseal keys with pgp keys: - $ vault init \ + $ vault operator init \ -key-shares=3 \ -key-threshold=2 \ -pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo" Encrypt the initial root token using a pgp key: - $ vault init -root-token-pgp-key="keybase:hashicorp" - - For a complete list of examples, please see the documentation. + $ vault operator init -root-token-pgp-key="keybase:hashicorp" ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *InitCommand) Flags() *FlagSets { +func (c *OperatorInitCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) // Common Options @@ -231,15 +227,15 @@ func (c *InitCommand) Flags() *FlagSets { return set } -func (c *InitCommand) AutocompleteArgs() complete.Predictor { +func (c *OperatorInitCommand) AutocompleteArgs() complete.Predictor { return nil } -func (c *InitCommand) AutocompleteFlags() complete.Flags { +func (c *OperatorInitCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *InitCommand) Run(args []string) int { +func (c *OperatorInitCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { @@ -293,7 +289,7 @@ func (c *InitCommand) Run(args []string) int { } // consulAuto enables auto-joining via Consul. -func (c *InitCommand) consulAuto(client *api.Client, req *api.InitRequest) int { +func (c *OperatorInitCommand) consulAuto(client *api.Client, req *api.InitRequest) int { // Capture the client original address and reset it originalAddr := client.Address() defer client.SetAddress(originalAddr) @@ -432,7 +428,7 @@ func (c *InitCommand) consulAuto(client *api.Client, req *api.InitRequest) int { } } -func (c *InitCommand) init(client *api.Client, req *api.InitRequest) int { +func (c *OperatorInitCommand) init(client *api.Client, req *api.InitRequest) int { resp, err := client.Sys().Init(req) if err != nil { c.UI.Error(fmt.Sprintf("Error initializing: %s", err)) @@ -509,7 +505,7 @@ func (c *InitCommand) init(client *api.Client, req *api.InitRequest) int { } // initOutputYAML outputs the init output as YAML. -func (c *InitCommand) initOutputYAML(req *api.InitRequest, resp *api.InitResponse) int { +func (c *OperatorInitCommand) initOutputYAML(req *api.InitRequest, resp *api.InitResponse) int { b, err := yaml.Marshal(newMachineInit(req, resp)) if err != nil { c.UI.Error(fmt.Sprintf("Error marshaling YAML: %s", err)) @@ -519,7 +515,7 @@ func (c *InitCommand) initOutputYAML(req *api.InitRequest, resp *api.InitRespons } // initOutputJSON outputs the init output as JSON. -func (c *InitCommand) initOutputJSON(req *api.InitRequest, resp *api.InitResponse) int { +func (c *OperatorInitCommand) initOutputJSON(req *api.InitRequest, resp *api.InitResponse) int { b, err := json.Marshal(newMachineInit(req, resp)) if err != nil { c.UI.Error(fmt.Sprintf("Error marshaling JSON: %s", err)) @@ -530,7 +526,7 @@ func (c *InitCommand) initOutputJSON(req *api.InitRequest, resp *api.InitRespons // status inspects the init status of vault and returns an appropriate error // code and message. -func (c *InitCommand) status(client *api.Client) int { +func (c *OperatorInitCommand) status(client *api.Client) int { inited, err := client.Sys().InitStatus() if err != nil { c.UI.Error(fmt.Sprintf("Error checking init status: %s", err)) diff --git a/command/init_test.go b/command/operator_init_test.go similarity index 93% rename from command/init_test.go rename to command/operator_init_test.go index c0f88e58d9..f398dd3756 100644 --- a/command/init_test.go +++ b/command/operator_init_test.go @@ -13,18 +13,18 @@ import ( "github.com/mitchellh/cli" ) -func testInitCommand(tb testing.TB) (*cli.MockUi, *InitCommand) { +func testOperatorInitCommand(tb testing.TB) (*cli.MockUi, *OperatorInitCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &InitCommand{ + return ui, &OperatorInitCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestInitCommand_Run(t *testing.T) { +func TestOperatorInitCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -100,7 +100,7 @@ func TestInitCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testInitCommand(t) + ui, cmd := testOperatorInitCommand(t) cmd.client = client code := cmd.Run(tc.args) @@ -122,7 +122,7 @@ func TestInitCommand_Run(t *testing.T) { client, closer := testVaultServerUninit(t) defer closer() - ui, cmd := testInitCommand(t) + ui, cmd := testOperatorInitCommand(t) cmd.client = client // Verify the non-init response code @@ -142,7 +142,7 @@ func TestInitCommand_Run(t *testing.T) { } // Verify the init response code - ui, cmd = testInitCommand(t) + ui, cmd = testOperatorInitCommand(t) cmd.client = client code = cmd.Run([]string{ "-status", @@ -158,7 +158,7 @@ func TestInitCommand_Run(t *testing.T) { client, closer := testVaultServerUninit(t) defer closer() - ui, cmd := testInitCommand(t) + ui, cmd := testOperatorInitCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -217,7 +217,7 @@ func TestInitCommand_Run(t *testing.T) { client, closer := testVaultServerUninit(t) defer closer() - ui, cmd := testInitCommand(t) + ui, cmd := testOperatorInitCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -283,7 +283,7 @@ func TestInitCommand_Run(t *testing.T) { client, closer := testVaultServerUninit(t) defer closer() - ui, cmd := testInitCommand(t) + ui, cmd := testOperatorInitCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -335,7 +335,7 @@ func TestInitCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testInitCommand(t) + ui, cmd := testOperatorInitCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -355,7 +355,7 @@ func TestInitCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testInitCommand(t) + _, cmd := testOperatorInitCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/key_status.go b/command/operator_key_status.go similarity index 61% rename from command/key_status.go rename to command/operator_key_status.go index b870035b75..6558290ca7 100644 --- a/command/key_status.go +++ b/command/operator_key_status.go @@ -8,22 +8,20 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*KeyStatusCommand)(nil) -var _ cli.CommandAutocomplete = (*KeyStatusCommand)(nil) +var _ cli.Command = (*OperatorKeyStatusCommand)(nil) +var _ cli.CommandAutocomplete = (*OperatorKeyStatusCommand)(nil) -// KeyStatusCommand is a Command that provides information about the key status -type KeyStatusCommand struct { +type OperatorKeyStatusCommand struct { *BaseCommand } -func (c *KeyStatusCommand) Synopsis() string { +func (c *OperatorKeyStatusCommand) Synopsis() string { return "Provides information about the active encryption key" } -func (c *KeyStatusCommand) Help() string { +func (c *OperatorKeyStatusCommand) Help() string { helpText := ` -Usage: vault key-status [options] +Usage: vault operator key-status [options] Provides information about the active encryption key. Specifically, the current key term and the key installation time. @@ -33,19 +31,19 @@ Usage: vault key-status [options] return strings.TrimSpace(helpText) } -func (c *KeyStatusCommand) Flags() *FlagSets { +func (c *OperatorKeyStatusCommand) Flags() *FlagSets { return c.flagSet(FlagSetHTTP) } -func (c *KeyStatusCommand) AutocompleteArgs() complete.Predictor { +func (c *OperatorKeyStatusCommand) AutocompleteArgs() complete.Predictor { return nil } -func (c *KeyStatusCommand) AutocompleteFlags() complete.Flags { +func (c *OperatorKeyStatusCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *KeyStatusCommand) Run(args []string) int { +func (c *OperatorKeyStatusCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { diff --git a/command/key_status_test.go b/command/operator_key_status_test.go similarity index 83% rename from command/key_status_test.go rename to command/operator_key_status_test.go index 640cef7bb9..5c1aada3e6 100644 --- a/command/key_status_test.go +++ b/command/operator_key_status_test.go @@ -7,18 +7,18 @@ import ( "github.com/mitchellh/cli" ) -func testKeyStatusCommand(tb testing.TB) (*cli.MockUi, *KeyStatusCommand) { +func testOperatorKeyStatusCommand(tb testing.TB) (*cli.MockUi, *OperatorKeyStatusCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &KeyStatusCommand{ + return ui, &OperatorKeyStatusCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestKeyStatusCommand_Run(t *testing.T) { +func TestOperatorKeyStatusCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -44,7 +44,7 @@ func TestKeyStatusCommand_Run(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ui, cmd := testKeyStatusCommand(t) + ui, cmd := testOperatorKeyStatusCommand(t) code := cmd.Run(tc.args) if code != tc.code { @@ -65,7 +65,7 @@ func TestKeyStatusCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testKeyStatusCommand(t) + ui, cmd := testOperatorKeyStatusCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -86,7 +86,7 @@ func TestKeyStatusCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testKeyStatusCommand(t) + ui, cmd := testOperatorKeyStatusCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -104,7 +104,7 @@ func TestKeyStatusCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testKeyStatusCommand(t) + _, cmd := testOperatorKeyStatusCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/rekey.go b/command/operator_rekey.go similarity index 89% rename from command/rekey.go rename to command/operator_rekey.go index b4b5195ece..c808458204 100644 --- a/command/rekey.go +++ b/command/operator_rekey.go @@ -15,24 +15,12 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*RekeyCommand)(nil) -var _ cli.CommandAutocomplete = (*RekeyCommand)(nil) +var _ cli.Command = (*OperatorRekeyCommand)(nil) +var _ cli.CommandAutocomplete = (*OperatorRekeyCommand)(nil) -// RekeyCommand is a Command that rekeys the vault. -type RekeyCommand struct { +type OperatorRekeyCommand struct { *BaseCommand - // Key can be used to pre-seed the key. If it is set, it will not - // be asked with the `password` helper. - Key string - - // The nonce for the rekey request to send along - Nonce string - - // Whether to use the recovery key instead of barrier key, if available - RecoveryKey bool - flagCancel bool flagInit bool flagKeyShares int @@ -56,11 +44,11 @@ type RekeyCommand struct { testStdin io.Reader // for tests } -func (c *RekeyCommand) Synopsis() string { +func (c *OperatorRekeyCommand) Synopsis() string { return "Generates new unseal keys" } -func (c *RekeyCommand) Help() string { +func (c *OperatorRekeyCommand) Help() string { helpText := ` Usage: vault rekey [options] [KEY] @@ -75,14 +63,14 @@ Usage: vault rekey [options] [KEY] Initialize a rekey: - $ vault rekey \ + $ vault operator rekey \ -init \ -key-shares=15 \ -key-threshold=9 Rekey and encrypt the resulting unseal keys with PGP: - $ vault rekey \ + $ vault operator rekey \ -init \ -key-shares=3 \ -key-threshold=2 \ @@ -90,29 +78,27 @@ Usage: vault rekey [options] [KEY] Store encrypted PGP keys in Vault's core: - $ vault rekey \ + $ vault operator rekey \ -init \ -pgp-keys="..." \ -backup Retrieve backed-up unseal keys: - $ vault rekey -backup-retrieve + $ vault operator rekey -backup-retrieve Delete backed-up unseal keys: - $ vault rekey -backup-delete - - For a full list of examples, please see the documentation. + $ vault operator rekey -backup-delete ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *RekeyCommand) Flags() *FlagSets { +func (c *OperatorRekeyCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP) - f := set.NewFlagSet("Command Options") + f := set.NewFlagSet("Common Options") f.BoolVar(&BoolVar{ Name: "init", @@ -136,7 +122,7 @@ func (c *RekeyCommand) Flags() *FlagSets { Name: "status", Target: &c.flagStatus, Default: false, - Usage: "Print the status of the current attempt without provding an " + + Usage: "Print the status of the current attempt without providing an " + "unseal key.", }) @@ -188,8 +174,7 @@ func (c *RekeyCommand) Flags() *FlagSets { "public GPG keys OR a comma-separated list of Keybase usernames using " + "the format \"keybase:\". When supplied, the generated " + "unseal keys will be encrypted and base64-encoded in the order " + - "specified in this list. The number of entires must match -key-shares, " + - "unless -store-shares are used.", + "specified in this list.", }) f = set.NewFlagSet("Backup Options") @@ -216,7 +201,7 @@ func (c *RekeyCommand) Flags() *FlagSets { Name: "backup-retrieve", Target: &c.flagBackupRetrieve, Default: false, - Usage: "Retrieve the backed-up unseal keys. This option is only avaiable " + + Usage: "Retrieve the backed-up unseal keys. This option is only available " + "if the PGP keys were provided and the backup has not been deleted.", }) @@ -249,15 +234,15 @@ func (c *RekeyCommand) Flags() *FlagSets { return set } -func (c *RekeyCommand) AutocompleteArgs() complete.Predictor { +func (c *OperatorRekeyCommand) AutocompleteArgs() complete.Predictor { return complete.PredictAnything } -func (c *RekeyCommand) AutocompleteFlags() complete.Flags { +func (c *OperatorRekeyCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *RekeyCommand) Run(args []string) int { +func (c *OperatorRekeyCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { @@ -324,7 +309,7 @@ func (c *RekeyCommand) Run(args []string) int { } // init starts the rekey process. -func (c *RekeyCommand) init(client *api.Client) int { +func (c *OperatorRekeyCommand) init(client *api.Client) int { // Handle the different API requests var fn func(*api.RekeyInitRequest) (*api.RekeyStatusResponse, error) switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { @@ -377,7 +362,7 @@ func (c *RekeyCommand) init(client *api.Client) int { } // cancel is used to abort the rekey process. -func (c *RekeyCommand) cancel(client *api.Client) int { +func (c *OperatorRekeyCommand) cancel(client *api.Client) int { // Handle the different API requests var fn func() error switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { @@ -402,7 +387,7 @@ func (c *RekeyCommand) cancel(client *api.Client) int { // provide prompts the user for the seal key and posts it to the update root // endpoint. If this is the last unseal, this function outputs it. -func (c *RekeyCommand) provide(client *api.Client, key string) int { +func (c *OperatorRekeyCommand) provide(client *api.Client, key string) int { var statusFn func() (*api.RekeyStatusResponse, error) var updateFn func(string, string) (*api.RekeyUpdateResponse, error) @@ -504,7 +489,7 @@ func (c *RekeyCommand) provide(client *api.Client, key string) int { } // status is used just to fetch and dump the status. -func (c *RekeyCommand) status(client *api.Client) int { +func (c *OperatorRekeyCommand) status(client *api.Client) int { // Handle the different API requests var fn func() (*api.RekeyStatusResponse, error) switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { @@ -528,7 +513,7 @@ func (c *RekeyCommand) status(client *api.Client) int { } // backupRetrieve retrieves the stored backup keys. -func (c *RekeyCommand) backupRetrieve(client *api.Client) int { +func (c *OperatorRekeyCommand) backupRetrieve(client *api.Client) int { // Handle the different API requests var fn func() (*api.RekeyRetrieveResponse, error) switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { @@ -556,7 +541,7 @@ func (c *RekeyCommand) backupRetrieve(client *api.Client) int { } // backupDelete deletes the stored backup keys. -func (c *RekeyCommand) backupDelete(client *api.Client) int { +func (c *OperatorRekeyCommand) backupDelete(client *api.Client) int { // Handle the different API requests var fn func() error switch strings.ToLower(strings.TrimSpace(c.flagTarget)) { @@ -580,7 +565,7 @@ func (c *RekeyCommand) backupDelete(client *api.Client) int { } // printStatus dumps the status to output -func (c *RekeyCommand) printStatus(status *api.RekeyStatusResponse) int { +func (c *OperatorRekeyCommand) printStatus(status *api.RekeyStatusResponse) int { out := []string{} out = append(out, fmt.Sprintf("Nonce | %s", status.Nonce)) out = append(out, fmt.Sprintf("Started | %t", status.Started)) @@ -596,12 +581,12 @@ func (c *RekeyCommand) printStatus(status *api.RekeyStatusResponse) int { out = append(out, fmt.Sprintf("Backup | %t", status.Backup)) } - output := columnOutput(out) + output := columnOutput(out, nil) c.UI.Output(output) return 0 } -func (c *RekeyCommand) printUnsealKeys(status *api.RekeyStatusResponse, resp *api.RekeyUpdateResponse) int { +func (c *OperatorRekeyCommand) printUnsealKeys(status *api.RekeyStatusResponse, resp *api.RekeyUpdateResponse) int { // Space between the key prompt, if any, and the output c.UI.Output("") @@ -633,7 +618,7 @@ func (c *RekeyCommand) printUnsealKeys(status *api.RekeyStatusResponse, resp *ap c.UI.Output("") c.UI.Output(wrapAtLength(fmt.Sprintf( "The encrypted unseal keys are backed up to \"core/unseal-keys-backup\"" + - "in the physical backend. Remove these keys at any time using " + + "in the storage backend. Remove these keys at any time using " + "\"vault rekey -delete-backup\". Vault does not automatically remove " + "these keys.", ))) diff --git a/command/rekey_test.go b/command/operator_rekey_test.go similarity index 92% rename from command/rekey_test.go rename to command/operator_rekey_test.go index 7c08287d05..47154c73ec 100644 --- a/command/rekey_test.go +++ b/command/operator_rekey_test.go @@ -11,18 +11,18 @@ import ( "github.com/mitchellh/cli" ) -func testRekeyCommand(tb testing.TB) (*cli.MockUi, *RekeyCommand) { +func testOperatorRekeyCommand(tb testing.TB) (*cli.MockUi, *OperatorRekeyCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &RekeyCommand{ + return ui, &OperatorRekeyCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestRekeyCommand_Run(t *testing.T) { +func TestOperatorRekeyCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -75,7 +75,7 @@ func TestRekeyCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run(tc.args) @@ -97,7 +97,7 @@ func TestRekeyCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client // Verify the non-init response @@ -123,7 +123,7 @@ func TestRekeyCommand_Run(t *testing.T) { } // Verify the init response - ui, cmd = testRekeyCommand(t) + ui, cmd = testOperatorRekeyCommand(t) cmd.client = client code = cmd.Run([]string{ "-status", @@ -153,7 +153,7 @@ func TestRekeyCommand_Run(t *testing.T) { t.Fatal(err) } - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -185,7 +185,7 @@ func TestRekeyCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -221,7 +221,7 @@ func TestRekeyCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -270,7 +270,7 @@ func TestRekeyCommand_Run(t *testing.T) { // Supply the first n-1 unseal keys for _, key := range keys[:len(keys)-1] { - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -282,7 +282,7 @@ func TestRekeyCommand_Run(t *testing.T) { } } - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -338,7 +338,7 @@ func TestRekeyCommand_Run(t *testing.T) { stdinW.Close() }() - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client cmd.testStdin = stdinR @@ -357,7 +357,7 @@ func TestRekeyCommand_Run(t *testing.T) { stdinW.Close() }() - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client cmd.testStdin = stdinR @@ -399,7 +399,7 @@ func TestRekeyCommand_Run(t *testing.T) { client, keys, closer := testVaultServerUnseal(t) defer closer() - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -423,7 +423,7 @@ func TestRekeyCommand_Run(t *testing.T) { var combined string // Supply the unseal keys for _, key := range keys { - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -448,7 +448,7 @@ func TestRekeyCommand_Run(t *testing.T) { fingerprint, encryptedKey := match[0][1], match[0][2] // Get the backup - ui, cmd = testRekeyCommand(t) + ui, cmd = testOperatorRekeyCommand(t) cmd.client = client code = cmd.Run([]string{ @@ -467,7 +467,7 @@ func TestRekeyCommand_Run(t *testing.T) { } // Delete the backup - ui, cmd = testRekeyCommand(t) + ui, cmd = testOperatorRekeyCommand(t) cmd.client = client code = cmd.Run([]string{ @@ -489,7 +489,7 @@ func TestRekeyCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testRekeyCommand(t) + ui, cmd := testOperatorRekeyCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -509,7 +509,7 @@ func TestRekeyCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testRekeyCommand(t) + _, cmd := testOperatorRekeyCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/seal.go b/command/operator_seal.go similarity index 68% rename from command/seal.go rename to command/operator_seal.go index 97f102a58e..b5fe3b8eb7 100644 --- a/command/seal.go +++ b/command/operator_seal.go @@ -8,20 +8,18 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*SealCommand)(nil) -var _ cli.CommandAutocomplete = (*SealCommand)(nil) +var _ cli.Command = (*OperatorSealCommand)(nil) +var _ cli.CommandAutocomplete = (*OperatorSealCommand)(nil) -// SealCommand is a Command that seals the vault. -type SealCommand struct { +type OperatorSealCommand struct { *BaseCommand } -func (c *SealCommand) Synopsis() string { +func (c *OperatorSealCommand) Synopsis() string { return "Seals the Vault server" } -func (c *SealCommand) Help() string { +func (c *OperatorSealCommand) Help() string { helpText := ` Usage: vault seal [options] @@ -37,29 +35,26 @@ Usage: vault seal [options] Seal the Vault server: - $ vault seal - - For a full list of examples and why you might want to seal the Vault, please - see the documentation. + $ vault operator seal ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *SealCommand) Flags() *FlagSets { +func (c *OperatorSealCommand) Flags() *FlagSets { return c.flagSet(FlagSetHTTP) } -func (c *SealCommand) AutocompleteArgs() complete.Predictor { +func (c *OperatorSealCommand) AutocompleteArgs() complete.Predictor { return nil } -func (c *SealCommand) AutocompleteFlags() complete.Flags { +func (c *OperatorSealCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *SealCommand) Run(args []string) int { +func (c *OperatorSealCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { diff --git a/command/seal_test.go b/command/operator_seal_test.go similarity index 86% rename from command/seal_test.go rename to command/operator_seal_test.go index a0cf373325..86722d2e84 100644 --- a/command/seal_test.go +++ b/command/operator_seal_test.go @@ -7,18 +7,18 @@ import ( "github.com/mitchellh/cli" ) -func testSealCommand(tb testing.TB) (*cli.MockUi, *SealCommand) { +func testOperatorSealCommand(tb testing.TB) (*cli.MockUi, *OperatorSealCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &SealCommand{ + return ui, &OperatorSealCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestSealCommand_Run(t *testing.T) { +func TestOperatorSealCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -47,7 +47,7 @@ func TestSealCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testSealCommand(t) + ui, cmd := testOperatorSealCommand(t) cmd.client = client code := cmd.Run(tc.args) @@ -69,7 +69,7 @@ func TestSealCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testSealCommand(t) + ui, cmd := testOperatorSealCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -98,7 +98,7 @@ func TestSealCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testSealCommand(t) + ui, cmd := testOperatorSealCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -116,7 +116,7 @@ func TestSealCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testSealCommand(t) + _, cmd := testOperatorSealCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/step_down.go b/command/operator_step_down.go similarity index 63% rename from command/step_down.go rename to command/operator_step_down.go index 6b2f3114f4..63208faf04 100644 --- a/command/step_down.go +++ b/command/operator_step_down.go @@ -8,23 +8,20 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*StepDownCommand)(nil) -var _ cli.CommandAutocomplete = (*StepDownCommand)(nil) +var _ cli.Command = (*OperatorStepDownCommand)(nil) +var _ cli.CommandAutocomplete = (*OperatorStepDownCommand)(nil) -// StepDownCommand is a Command that tells the Vault server to give up its -// leadership -type StepDownCommand struct { +type OperatorStepDownCommand struct { *BaseCommand } -func (c *StepDownCommand) Synopsis() string { +func (c *OperatorStepDownCommand) Synopsis() string { return "Forces Vault to resign active duty" } -func (c *StepDownCommand) Help() string { +func (c *OperatorStepDownCommand) Help() string { helpText := ` -Usage: vault step-down [options] +Usage: vault operator step-down [options] Forces the Vault server at the given address to step down from active duty. While the affected node will have a delay before attempting to acquire the @@ -34,28 +31,26 @@ Usage: vault step-down [options] Force Vault to step down as the leader: - $ vault step-down - - For a full list of examples, please see the documentation. + $ vault operator step-down ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *StepDownCommand) Flags() *FlagSets { +func (c *OperatorStepDownCommand) Flags() *FlagSets { return c.flagSet(FlagSetHTTP) } -func (c *StepDownCommand) AutocompleteArgs() complete.Predictor { +func (c *OperatorStepDownCommand) AutocompleteArgs() complete.Predictor { return nil } -func (c *StepDownCommand) AutocompleteFlags() complete.Flags { +func (c *OperatorStepDownCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *StepDownCommand) Run(args []string) int { +func (c *OperatorStepDownCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { diff --git a/command/step_down_test.go b/command/operator_step_down_test.go similarity index 82% rename from command/step_down_test.go rename to command/operator_step_down_test.go index 6141e08a8a..93117a856b 100644 --- a/command/step_down_test.go +++ b/command/operator_step_down_test.go @@ -7,18 +7,18 @@ import ( "github.com/mitchellh/cli" ) -func testStepDownCommand(tb testing.TB) (*cli.MockUi, *StepDownCommand) { +func testOperatorStepDownCommand(tb testing.TB) (*cli.MockUi, *OperatorStepDownCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &StepDownCommand{ + return ui, &OperatorStepDownCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestStepDownCommand_Run(t *testing.T) { +func TestOperatorStepDownCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -53,7 +53,7 @@ func TestStepDownCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testStepDownCommand(t) + ui, cmd := testOperatorStepDownCommand(t) cmd.client = client code := cmd.Run(tc.args) @@ -75,7 +75,7 @@ func TestStepDownCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testStepDownCommand(t) + ui, cmd := testOperatorStepDownCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -93,7 +93,7 @@ func TestStepDownCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testStepDownCommand(t) + _, cmd := testOperatorStepDownCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/unseal.go b/command/operator_unseal.go similarity index 80% rename from command/unseal.go rename to command/operator_unseal.go index 495bf6e6c1..86c78005d4 100644 --- a/command/unseal.go +++ b/command/operator_unseal.go @@ -12,12 +12,10 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*UnsealCommand)(nil) -var _ cli.CommandAutocomplete = (*UnsealCommand)(nil) +var _ cli.Command = (*OperatorUnsealCommand)(nil) +var _ cli.CommandAutocomplete = (*OperatorUnsealCommand)(nil) -// UnsealCommand is a Command that unseals the vault. -type UnsealCommand struct { +type OperatorUnsealCommand struct { *BaseCommand flagReset bool @@ -25,13 +23,13 @@ type UnsealCommand struct { testOutput io.Writer // for tests } -func (c *UnsealCommand) Synopsis() string { +func (c *OperatorUnsealCommand) Synopsis() string { return "Unseals the Vault server" } -func (c *UnsealCommand) Help() string { +func (c *OperatorUnsealCommand) Help() string { helpText := ` -Usage: vault unseal [options] [KEY] +Usage: vault operator unseal [options] [KEY] Provide a portion of the master key to unseal a Vault server. Vault starts in a sealed state. It cannot perform operations until it is unsealed. This @@ -40,21 +38,19 @@ Usage: vault unseal [options] [KEY] The unseal key can be supplied as an argument to the command, but this is not recommended as the unseal key will be available in your history: - $ vault unseal IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= + $ vault operator unseal IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= Instead, run the command with no arguments and it will prompt for the key: - $ vault unseal + $ vault operator unseal Key (will be hidden): IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= - For a full list of examples, please see the documentation. - ` + c.Flags().Help() return strings.TrimSpace(helpText) } -func (c *UnsealCommand) Flags() *FlagSets { +func (c *OperatorUnsealCommand) Flags() *FlagSets { set := c.flagSet(FlagSetHTTP) f := set.NewFlagSet("Command Options") @@ -72,15 +68,15 @@ func (c *UnsealCommand) Flags() *FlagSets { return set } -func (c *UnsealCommand) AutocompleteArgs() complete.Predictor { +func (c *OperatorUnsealCommand) AutocompleteArgs() complete.Predictor { return c.PredictVaultFiles() } -func (c *UnsealCommand) AutocompleteFlags() complete.Flags { +func (c *OperatorUnsealCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *UnsealCommand) Run(args []string) int { +func (c *OperatorUnsealCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { @@ -151,7 +147,7 @@ func (c *UnsealCommand) Run(args []string) int { return 0 } -func (c *UnsealCommand) prettySealStatus(status *api.SealStatusResponse) { +func (c *OperatorUnsealCommand) prettySealStatus(status *api.SealStatusResponse) { c.UI.Output(fmt.Sprintf("Sealed: %t", status.Sealed)) c.UI.Output(fmt.Sprintf("Key Shares: %d", status.N)) c.UI.Output(fmt.Sprintf("Key Threshold: %d", status.T)) diff --git a/command/unseal_test.go b/command/operator_unseal_test.go similarity index 87% rename from command/unseal_test.go rename to command/operator_unseal_test.go index bc8d2e8d22..9091ae40e2 100644 --- a/command/unseal_test.go +++ b/command/operator_unseal_test.go @@ -9,24 +9,24 @@ import ( "github.com/mitchellh/cli" ) -func testUnsealCommand(tb testing.TB) (*cli.MockUi, *UnsealCommand) { +func testOperatorUnsealCommand(tb testing.TB) (*cli.MockUi, *OperatorUnsealCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &UnsealCommand{ + return ui, &OperatorUnsealCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestUnsealCommand_Run(t *testing.T) { +func TestOperatorUnsealCommand_Run(t *testing.T) { t.Parallel() t.Run("error_non_terminal", func(t *testing.T) { t.Parallel() - ui, cmd := testUnsealCommand(t) + ui, cmd := testOperatorUnsealCommand(t) cmd.testOutput = ioutil.Discard code := cmd.Run(nil) @@ -57,7 +57,7 @@ func TestUnsealCommand_Run(t *testing.T) { t.Fatal(err) } - ui, cmd := testUnsealCommand(t) + ui, cmd := testOperatorUnsealCommand(t) cmd.client = client cmd.testOutput = ioutil.Discard @@ -87,7 +87,7 @@ func TestUnsealCommand_Run(t *testing.T) { } for i, key := range keys { - ui, cmd := testUnsealCommand(t) + ui, cmd := testOperatorUnsealCommand(t) cmd.client = client cmd.testOutput = ioutil.Discard @@ -112,7 +112,7 @@ func TestUnsealCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testUnsealCommand(t) + ui, cmd := testOperatorUnsealCommand(t) cmd.client = client code := cmd.Run([]string{ @@ -132,7 +132,7 @@ func TestUnsealCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testUnsealCommand(t) + _, cmd := testOperatorUnsealCommand(t) assertNoTabs(t, cmd) }) } diff --git a/command/rotate.go b/command/rotate.go index eed0bc40e8..77bc0602b7 100644 --- a/command/rotate.go +++ b/command/rotate.go @@ -8,20 +8,18 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. -var _ cli.Command = (*RotateCommand)(nil) -var _ cli.CommandAutocomplete = (*RotateCommand)(nil) +var _ cli.Command = (*OperatorRotateCommand)(nil) +var _ cli.CommandAutocomplete = (*OperatorRotateCommand)(nil) -// RotateCommand is a Command that rotates the encryption key being used -type RotateCommand struct { +type OperatorRotateCommand struct { *BaseCommand } -func (c *RotateCommand) Synopsis() string { +func (c *OperatorRotateCommand) Synopsis() string { return "Rotates the underlying encryption key" } -func (c *RotateCommand) Help() string { +func (c *OperatorRotateCommand) Help() string { helpText := ` Usage: vault rotate [options] @@ -31,8 +29,8 @@ Usage: vault rotate [options] decrypt older data. This is an online operation and does not cause downtime. This command is run - per-cluser (not per-server), since Vault servers in HA mode share the same - storeage backend. + per-cluster (not per-server), since Vault servers in HA mode share the same + storage backend. Rotate Vault's encryption key: @@ -45,19 +43,19 @@ Usage: vault rotate [options] return strings.TrimSpace(helpText) } -func (c *RotateCommand) Flags() *FlagSets { +func (c *OperatorRotateCommand) Flags() *FlagSets { return c.flagSet(FlagSetHTTP) } -func (c *RotateCommand) AutocompleteArgs() complete.Predictor { +func (c *OperatorRotateCommand) AutocompleteArgs() complete.Predictor { return nil } -func (c *RotateCommand) AutocompleteFlags() complete.Flags { +func (c *OperatorRotateCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -func (c *RotateCommand) Run(args []string) int { +func (c *OperatorRotateCommand) Run(args []string) int { f := c.Flags() if err := f.Parse(args); err != nil { diff --git a/command/rotate_test.go b/command/rotate_test.go index 30790691cb..8f9756de06 100644 --- a/command/rotate_test.go +++ b/command/rotate_test.go @@ -7,18 +7,18 @@ import ( "github.com/mitchellh/cli" ) -func testRotateCommand(tb testing.TB) (*cli.MockUi, *RotateCommand) { +func testOperatorRotateCommand(tb testing.TB) (*cli.MockUi, *OperatorRotateCommand) { tb.Helper() ui := cli.NewMockUi() - return ui, &RotateCommand{ + return ui, &OperatorRotateCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } -func TestRotateCommand_Run(t *testing.T) { +func TestOperatorRotateCommand_Run(t *testing.T) { t.Parallel() cases := []struct { @@ -44,7 +44,7 @@ func TestRotateCommand_Run(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ui, cmd := testRotateCommand(t) + ui, cmd := testOperatorRotateCommand(t) code := cmd.Run(tc.args) if code != tc.code { @@ -65,7 +65,7 @@ func TestRotateCommand_Run(t *testing.T) { client, closer := testVaultServer(t) defer closer() - ui, cmd := testRotateCommand(t) + ui, cmd := testOperatorRotateCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -94,7 +94,7 @@ func TestRotateCommand_Run(t *testing.T) { client, closer := testVaultServerBad(t) defer closer() - ui, cmd := testRotateCommand(t) + ui, cmd := testOperatorRotateCommand(t) cmd.client = client code := cmd.Run([]string{}) @@ -112,7 +112,7 @@ func TestRotateCommand_Run(t *testing.T) { t.Run("no_tabs", func(t *testing.T) { t.Parallel() - _, cmd := testRotateCommand(t) + _, cmd := testOperatorRotateCommand(t) assertNoTabs(t, cmd) }) } From 22dd8a23d7131305581e114c168c6244b0201eb7 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:03:44 -0400 Subject: [PATCH 098/281] Update server command --- command/server.go | 181 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 175 insertions(+), 6 deletions(-) diff --git a/command/server.go b/command/server.go index 59c1c5770d..0160776dca 100644 --- a/command/server.go +++ b/command/server.go @@ -22,6 +22,7 @@ import ( colorable "github.com/mattn/go-colorable" log "github.com/mgutz/logxi/v1" + "github.com/mitchellh/cli" testing "github.com/mitchellh/go-testing-interface" "github.com/posener/complete" @@ -48,7 +49,9 @@ import ( "github.com/hashicorp/vault/version" ) -// ServerCommand is a Command that starts the Vault server. +var _ cli.Command = (*ServerCommand)(nil) +var _ cli.CommandAutocomplete = (*ServerCommand)(nil) + type ServerCommand struct { AuditBackends map[string]audit.Factory CredentialBackends map[string]logical.Factory @@ -69,6 +72,171 @@ type ServerCommand struct { reloadFuncsLock *sync.RWMutex reloadFuncs *map[string][]reload.ReloadFunc + startedCh chan (struct{}) // for tests + reloadedCh chan (struct{}) // for tests + + // new stuff + flagConfigs []string + flagLogLevel string + flagDev bool + flagDevRootTokenID string + flagDevListenAddr string + + flagDevPluginDir string + flagDevHA bool + flagDevTransactional bool + flagDevLeasedKV bool + flagDevThreeNode bool + flagTestVerifyOnly bool +} + +func (c *ServerCommand) Synopsis() string { + return "Start a Vault server" +} + +func (c *ServerCommand) Help() string { + helpText := ` +Usage: vault server [options] + + This command starts a Vault server that responds to API requests. By default, + Vault will start in a "sealed" state. The Vault cluster must be initialized + before use, usually by the "vault init" command. Each Vault server must also + be unsealed using the "vault unseal" command or the API before the server can + respond to requests. + + Start a server with a configuration file: + + $ vault server -config=/etc/vault/config.hcl + + Run in "dev" mode: + + $ vault server -dev -dev-root-token-id="root" + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *ServerCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + f.StringSliceVar(&StringSliceVar{ + Name: "config", + Target: &c.flagConfigs, + Completion: complete.PredictOr( + complete.PredictFiles("*.hcl"), + complete.PredictFiles("*.json"), + complete.PredictDirs("*"), + ), + Usage: "Path to a configuration file or directory of configuration " + + "files. This flag can be specified multiple times to load multiple " + + "configurations. If the path is a directory, all files which end in " + + ".hcl or .json are loaded.", + }) + + f.StringVar(&StringVar{ + Name: "log-level", + Target: &c.flagLogLevel, + Default: "info", + EnvVar: "VAULT_LOG", + Completion: complete.PredictSet("trace", "debug", "info", "warn", "err"), + Usage: "Log verbosity level. Supported values (in order of detail) are " + + "\"trace\", \"debug\", \"info\", \"warn\", and \"err\".", + }) + + f = set.NewFlagSet("Dev Options") + + f.BoolVar(&BoolVar{ + Name: "dev", + Target: &c.flagDev, + Usage: "Enable development mode. In this mode, Vault runs in-memory and " + + "starts unsealed. As the name implies, do not run \"dev\" mode in " + + "production.", + }) + + f.StringVar(&StringVar{ + Name: "dev-root-token-id", + Target: &c.flagDevRootTokenID, + Default: "", + EnvVar: "VAULT_DEV_ROOT_TOKEN_ID", + Usage: "Initial root token. This only applies when running in \"dev\" " + + "mode.", + }) + + f.StringVar(&StringVar{ + Name: "dev-listen-address", + Target: &c.flagDevListenAddr, + Default: "127.0.0.1:8200", + EnvVar: "VAULT_DEV_LISTEN_ADDRESS", + Usage: "Address to bind to in \"dev\" mode.", + }) + + // Internal-only flags to follow. + // + // Why hello there little source code reader! Welcome to the Vault source + // code. The remaining options are intentionally undocumented and come with + // no warranty or backwards-compatability promise. Do not use these flags + // in production. Do not build automation using these flags. Unless you are + // developing against Vault, you should not need any of these flags. + + f.StringVar(&StringVar{ + Name: "dev-plugin-dir", + Target: &c.flagDevPluginDir, + Default: "", + Completion: complete.PredictDirs("*"), + Hidden: true, + }) + + f.BoolVar(&BoolVar{ + Name: "dev-ha", + Target: &c.flagDevHA, + Default: false, + Hidden: true, + }) + + f.BoolVar(&BoolVar{ + Name: "dev-transactional", + Target: &c.flagDevTransactional, + Default: false, + Hidden: true, + }) + + f.BoolVar(&BoolVar{ + Name: "dev-leased-generic", + Target: &c.flagDevLeasedKV, + Default: false, + Hidden: true, + }) + + f.BoolVar(&BoolVar{ + Name: "dev-three-node", + Target: &c.flagDevThreeNode, + Default: false, + Hidden: true, + }) + + // TODO: should this be a public flag? + f.BoolVar(&BoolVar{ + Name: "test-verify-only", + Target: &c.flagTestVerifyOnly, + Default: false, + Hidden: true, + }) + + // End internal-only flags. + + return set +} + +func (c *ServerCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *ServerCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() } func (c *ServerCommand) Run(args []string) int { @@ -154,10 +322,11 @@ func (c *ServerCommand) Run(args []string) int { c.Ui.Output("At least one config path must be specified with -config") flags.Usage() return 1 - case devRootTokenID != "": - c.Ui.Output("Root token ID can only be specified with -dev") - flags.Usage() - return 1 + case c.flagDevRootTokenID != "": + c.UI.Warn(wrapAtLength( + "You cannot specify a custom root token ID outside of \"dev\" mode. " + + "Your request has been ignored.")) + c.flagDevRootTokenID = "" } } @@ -1217,7 +1386,7 @@ func (c *ServerCommand) Reload(lock *sync.RWMutex, reloadFuncs *map[string][]rel for _, relFunc := range relFuncs { if relFunc != nil { if err := relFunc(nil); err != nil { - reloadErrors = multierror.Append(reloadErrors, fmt.Errorf("Error encountered reloading file audit backend at path %s: %v", strings.TrimPrefix(k, "audit_file|"), err)) + reloadErrors = multierror.Append(reloadErrors, fmt.Errorf("Error encountered reloading file audit device at path %s: %v", strings.TrimPrefix(k, "audit_file|"), err)) } } } From 6b5685a91f59bced4da2280acaed66d1496bccd0 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:03:52 -0400 Subject: [PATCH 099/281] Update ssh command --- command/ssh.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/command/ssh.go b/command/ssh.go index 675be788ff..99939c0e75 100644 --- a/command/ssh.go +++ b/command/ssh.go @@ -19,12 +19,9 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*SSHCommand)(nil) var _ cli.CommandAutocomplete = (*SSHCommand)(nil) -// SSHCommand is a Command that establishes a SSH connection with target by -// generating a dynamic key type SSHCommand struct { *BaseCommand @@ -53,9 +50,9 @@ Usage: vault ssh [options] username@ip [ssh options] Establishes an SSH connection with the target machine. - This command uses one of the SSH authentication backends to authenticate and + This command uses one of the SSH secrets engines to authenticate and automatically establish an SSH connection to a host. This operation requires - that the SSH backend is mounted and configured. + that the SSH secrets engine is mounted and configured. SSH using the OTP mode (requires sshpass for full automation): @@ -123,7 +120,7 @@ func (c *SSHCommand) Flags() *FlagSets { Default: "ssh/", EnvVar: "", Completion: complete.PredictAnything, - Usage: "Mount point to the SSH backend.", + Usage: "Mount point to the SSH secrets engine.", }) f.StringVar(&StringVar{ @@ -153,7 +150,7 @@ func (c *SSHCommand) Flags() *FlagSets { Name: "public-key-path", Target: &c.flagPublicKeyPath, Default: "~/.ssh/id_rsa.pub", - EnvVar: "g", + EnvVar: "", Completion: complete.PredictFiles("*"), Usage: "Path to the SSH public key to send to Vault for signing.", }) @@ -171,10 +168,10 @@ func (c *SSHCommand) Flags() *FlagSets { f.StringVar(&StringVar{ Name: "host-key-mount-point", Target: &c.flagHostKeyMountPoint, - Default: "~/.ssh/id_rsa", + Default: "", EnvVar: "VAULT_SSH_HOST_KEY_MOUNT_POINT", Completion: complete.PredictAnything, - Usage: "Mount point to the SSH backend where host keys are signed. " + + Usage: "Mount point to the SSH secrets engine where host keys are signed. " + "When given a value, Vault will generate a custom \"known_hosts\" file " + "with delegation to the CA at the provided mount point to verify the " + "SSH connection's host keys against the provided CA. By default, host " + @@ -205,7 +202,8 @@ func (c *SSHCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } -// Structure to hold the fields returned when asked for a credential from SSHh backend. +// Structure to hold the fields returned when asked for a credential from SSH +// secrets engine. type SSHCredentialResp struct { KeyType string `mapstructure:"key_type"` Key string `mapstructure:"key"` From cf0c219668f4d02219b58416819108b7aa2f0698 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:03:57 -0400 Subject: [PATCH 100/281] Update status command --- command/status.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/command/status.go b/command/status.go index 40cd1059d6..ca81dba105 100644 --- a/command/status.go +++ b/command/status.go @@ -9,18 +9,15 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*StatusCommand)(nil) var _ cli.CommandAutocomplete = (*StatusCommand)(nil) -// StatusCommand is a Command that outputs the status of whether Vault is sealed -// or not as well as HA information. type StatusCommand struct { *BaseCommand } func (c *StatusCommand) Synopsis() string { - return "Prints seal and HA status" + return "Print seal and HA status" } func (c *StatusCommand) Help() string { From f8b71c9baa49d604eb929a738136252a7a53b6eb Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:04:02 -0400 Subject: [PATCH 101/281] Update version command --- command/version.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/version.go b/command/version.go index b1ced4ee9b..398036d3a1 100644 --- a/command/version.go +++ b/command/version.go @@ -8,7 +8,6 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*VersionCommand)(nil) var _ cli.CommandAutocomplete = (*VersionCommand)(nil) From 54b6254763f89fa85229a7a13f78e2b7c0fa9a16 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:04:07 -0400 Subject: [PATCH 102/281] Update unwrap command --- command/unwrap.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/command/unwrap.go b/command/unwrap.go index 83d1cbdf83..c454bacce6 100644 --- a/command/unwrap.go +++ b/command/unwrap.go @@ -8,7 +8,6 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*UnwrapCommand)(nil) var _ cli.CommandAutocomplete = (*UnwrapCommand)(nil) @@ -19,7 +18,7 @@ type UnwrapCommand struct { } func (c *UnwrapCommand) Synopsis() string { - return "Unwraps a wrapped secret" + return "Unwrap a wrapped secret" } func (c *UnwrapCommand) Help() string { @@ -30,7 +29,7 @@ Usage: vault unwrap [options] [TOKEN] same as the "vault read" operation on the non-wrapped secret. If no token is given, the data in the currently authenticated token is unwrapped. - Unwrap the data in the cubbyhole backend for a token: + Unwrap the data in the cubbyhole secrets engine for a token: $ vault unwrap 3de9ece1-b347-e143-29b0-dc2dc31caafd From d4e46e97f2234a65107b961343ecd4952a515481 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:04:16 -0400 Subject: [PATCH 103/281] Update write command --- command/write.go | 28 ++++++++++++++-------------- command/write_test.go | 41 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/command/write.go b/command/write.go index 3046e2b38d..ed25ba6ce7 100644 --- a/command/write.go +++ b/command/write.go @@ -10,7 +10,6 @@ import ( "github.com/posener/complete" ) -// Ensure we are implementing the right interfaces. var _ cli.Command = (*WriteCommand)(nil) var _ cli.CommandAutocomplete = (*WriteCommand)(nil) @@ -24,7 +23,7 @@ type WriteCommand struct { } func (c *WriteCommand) Synopsis() string { - return "Writes data, configuration, and secrets" + return "Write data, configuration, and secrets" } func (c *WriteCommand) Help() string { @@ -33,17 +32,17 @@ Usage: vault write [options] PATH [DATA K=V...] Writes data to Vault at the given path. The data can be credentials, secrets, configuration, or arbitrary data. The specific behavior of this command is - determined at the backend mounted at the path. + determined at the thing mounted at the path. Data is specified as "key=value" pairs. If the value begins with an "@", then it is loaded from a file. If the value is "-", Vault will read the value from stdin. - Persist data in the static secret backend: + Persist data in the generic secrets engine: $ vault write secret/my-secret foo=bar - Create a new encryption key in the transit backend: + Create a new encryption key in the transit secrets engine: $ vault write -f transit/keys/my-key @@ -56,7 +55,7 @@ Usage: vault write [options] PATH [DATA K=V...] $ echo $MY_TOKEN | vault write consul/config/access token=- For a full list of examples and paths, please see the documentation that - corresponds to the secret backend in use. + corresponds to the secret engines in use. ` + c.Flags().Help() @@ -99,14 +98,13 @@ func (c *WriteCommand) Run(args []string) int { return 1 } - path, kvs, err := extractPath(f.Args()) - if err != nil { - c.UI.Error(err.Error()) + args = f.Args() + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) return 1 - } - - if len(kvs) == 0 && !c.flagForce { - c.UI.Error("Missing DATA! Specify at least one K=V pair or use -force.") + case len(args) == 1 && !c.flagForce: + c.UI.Error("Must supply data or use -force") return 1 } @@ -116,7 +114,9 @@ func (c *WriteCommand) Run(args []string) int { stdin = c.testStdin } - data, err := parseArgsData(stdin, kvs) + path := sanitizePath(args[0]) + + data, err := parseArgsData(stdin, args[1:]) if err != nil { c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err)) return 1 diff --git a/command/write_test.go b/command/write_test.go index 7139e7a514..03aab4c79a 100644 --- a/command/write_test.go +++ b/command/write_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" ) @@ -29,15 +30,15 @@ func TestWriteCommand_Run(t *testing.T) { code int }{ { - "empty_path", - nil, - "Missing PATH!", + "not_enough_args", + []string{}, + "Not enough arguments", 1, }, { "empty_kvs", []string{"secret/write/foo"}, - "Missing DATA!", + "Must supply data or use -force", 1, }, { @@ -114,6 +115,38 @@ func TestWriteCommand_Run(t *testing.T) { }) } + t.Run("force", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + if err := client.Sys().Mount("transit/", &api.MountInput{ + Type: "transit", + }); err != nil { + t.Fatal(err) + } + + ui, cmd := testWriteCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "-force", + "transit/keys/my-key", + }) + if exp := 0; code != exp { + t.Fatalf("expected %d to be %d: %q", code, exp, ui.ErrorWriter.String()) + } + + secret, err := client.Logical().Read("transit/keys/my-key") + if err != nil { + t.Fatal(err) + } + if secret == nil || secret.Data == nil { + t.Fatal("expected secret to have data") + } + }) + t.Run("stdin_full", func(t *testing.T) { t.Parallel() From 30cd478c01399f663a21dc3af5d136c144c542a3 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:04:30 -0400 Subject: [PATCH 104/281] Update token cli to parse "verify" --- builtin/credential/token/cli.go | 57 ++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/builtin/credential/token/cli.go b/builtin/credential/token/cli.go index b2252d6b7c..1583044d77 100644 --- a/builtin/credential/token/cli.go +++ b/builtin/credential/token/cli.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "strconv" "strings" "github.com/hashicorp/vault/api" @@ -17,6 +18,20 @@ type CLIHandler struct { } func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) { + // Parse "lookup" first - we want to return an early error if the user + // supplied an invalid value here before we prompt them for a token. It would + // be annoying to type your token and then be told you supplied an invalid + // value that we could have known in advance. + lookup := true + if x, ok := m["lookup"]; ok { + parsed, err := strconv.ParseBool(x) + if err != nil { + return nil, fmt.Errorf("Failed to parse \"lookup\" as boolean: %s", err) + } + lookup = parsed + } + + // Parse the token. token, ok := m["token"] if !ok { // Override the output @@ -54,11 +69,44 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro "information.") } + // If the user declined verification, return now. Note that we will not have + // a lot of information about the token. + if !lookup { + return &api.Secret{ + Auth: &api.SecretAuth{ + ClientToken: token, + }, + }, nil + } + + // If we got this far, we want to lookup and lookup the token and pull it's + // list of policies an metadata. + c.SetToken(token) + c.SetWrappingLookupFunc(func(string, string) string { return "" }) + + secret, err := c.Auth().Token().LookupSelf() + if err != nil { + return nil, fmt.Errorf("Error looking up token: %s", err) + } + if secret == nil { + return nil, fmt.Errorf("Empty response from lookup-self") + } + + // Return an auth struct that "looks" like the response from an auth backend. + // lookup and lookup-self return their data in data, not auth. We try to + // mirror that data here. return &api.Secret{ Auth: &api.SecretAuth{ - ClientToken: token, + ClientToken: secret.TokenID(), + Accessor: secret.TokenAccessor(), + Policies: secret.TokenPolicies(), + Metadata: secret.TokenMetadata(), + + LeaseDuration: secret.TokenTTLInt(), + Renewable: secret.TokenIsRenewable(), }, }, nil + } func (h *CLIHandler) Help() string { @@ -73,6 +121,10 @@ Usage: vault login TOKEN [CONFIG K=V...] $ vault login 96ddf4bc-d217-f3ba-f9bd-017055595017 + Authenticate but do not lookup information about the token: + + $ vault login token=96ddf4bc-d217-f3ba-f9bd-017055595017 lookup=false + This token usually comes from a different source such as the API or via the built-in "vault token-create" command. @@ -81,6 +133,9 @@ Configuration: token= The token to use for authentication. This is usually provided directly via the "vault login" command. + + lookup= + Perform a lookup of the token's metadata and policies. ` return strings.TrimSpace(help) From b4d9d1517bd89d9931f16f4d6a4893da024d122c Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:04:48 -0400 Subject: [PATCH 105/281] Move more formatting into base_helpers --- command/base.go | 17 ---- command/base_helpers.go | 138 +++++++++++++++++-------------- command/format.go | 177 ++++++++++++++++++---------------------- 3 files changed, 156 insertions(+), 176 deletions(-) diff --git a/command/base.go b/command/base.go index ec0fa67c81..7dcca7671b 100644 --- a/command/base.go +++ b/command/base.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/command/token" - "github.com/kr/text" "github.com/mitchellh/cli" "github.com/pkg/errors" "github.com/posener/complete" @@ -274,22 +273,6 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { return c.flags } -// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking -// into account any provided left padding. -func wrapAtLengthWithPadding(s string, pad int) string { - wrapped := text.Wrap(s, maxLineLength-pad) - lines := strings.Split(wrapped, "\n") - for i, line := range lines { - lines[i] = strings.Repeat(" ", pad) + line - } - return strings.Join(lines, "\n") -} - -// wrapAtLength wraps the given text to maxLineLength. -func wrapAtLength(s string) string { - return wrapAtLengthWithPadding(s, 0) -} - // FlagSets is a group of flag sets. type FlagSets struct { flagSets []*FlagSet diff --git a/command/base_helpers.go b/command/base_helpers.go index 4c5bad6b88..75c0e80998 100644 --- a/command/base_helpers.go +++ b/command/base_helpers.go @@ -8,16 +8,13 @@ import ( "github.com/hashicorp/vault/api" kvbuilder "github.com/hashicorp/vault/helper/kv-builder" + "github.com/kr/text" homedir "github.com/mitchellh/go-homedir" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/ryanuber/columnize" ) -var ErrMissingID = fmt.Errorf("Missing ID!") -var ErrMissingPath = fmt.Errorf("Missing PATH!") -var ErrMissingThing = fmt.Errorf("Missing THING!") - // extractListData reads the secret and returns a typed list of data and a // boolean indicating whether the extraction was successful. func extractListData(secret *api.Secret) ([]interface{}, bool) { @@ -34,54 +31,9 @@ func extractListData(secret *api.Secret) ([]interface{}, bool) { return i, ok } -// extractPath extracts the path and list of arguments from the args. If there -// are no extra arguments, the remaining args will be nil. -func extractPath(args []string) (string, []string, error) { - str, remaining, err := extractThings(args) - if err == ErrMissingThing { - err = ErrMissingPath - } - return str, remaining, err -} - -// extractID extracts the path and list of arguments from the args. If there -// are no extra arguments, the remaining args will be nil. -func extractID(args []string) (string, []string, error) { - str, remaining, err := extractThings(args) - if err == ErrMissingThing { - err = ErrMissingID - } - return str, remaining, err -} - -func extractThings(args []string) (string, []string, error) { - if len(args) < 1 { - return "", nil, ErrMissingThing - } - - // Path is always the first argument after all flags - thing := args[0] - - // Strip leading and trailing slashes - thing = sanitizePath(thing) - - // Verify we have a thing - if thing == "" { - return "", nil, ErrMissingThing - } - - // Splice remaining args - var remaining []string - if len(args) > 1 { - remaining = args[1:] - } - - return thing, remaining, nil -} - // sanitizePath removes any leading or trailing things from a "path". func sanitizePath(s string) string { - return ensureNoTrailingSlash(ensureNoLeadingSlash(s)) + return ensureNoTrailingSlash(ensureNoLeadingSlash(strings.TrimSpace(s))) } // ensureTrailingSlash ensures the given string has a trailing slash. @@ -124,33 +76,45 @@ func ensureNoLeadingSlash(s string) string { } // columnOuput prints the list of items as a table with no headers. -func columnOutput(list []string) string { +func columnOutput(list []string, c *columnize.Config) string { if len(list) == 0 { return "" } - return columnize.Format(list, &columnize.Config{ - Glue: " ", - Empty: "n/a", - }) + if c == nil { + c = &columnize.Config{} + } + if c.Glue == "" { + c.Glue = " " + } + if c.Empty == "" { + c.Empty = "n/a" + } + + return columnize.Format(list, c) } // tableOutput prints the list of items as columns, where the first row is // the list of headers. -func tableOutput(list []string) string { +func tableOutput(list []string, c *columnize.Config) string { if len(list) == 0 { return "" } + delim := "|" + if c != nil && c.Delim != "" { + delim = c.Delim + } + underline := "" - headers := strings.Split(list[0], "|") + headers := strings.Split(list[0], delim) for i, h := range headers { h = strings.TrimSpace(h) u := strings.Repeat("-", len(h)) underline = underline + u if i != len(headers)-1 { - underline = underline + " | " + underline = underline + delim } } @@ -158,7 +122,7 @@ func tableOutput(list []string) string { copy(list[2:], list[1:]) list[1] = underline - return columnOutput(list) + return columnOutput(list, c) } // parseArgsData parses the given args in the format key=value into a map of @@ -207,7 +171,7 @@ func printKeyStatus(ks *api.KeyStatus) string { return columnOutput([]string{ fmt.Sprintf("Key Term | %d", ks.Term), fmt.Sprintf("Install Time | %s", ks.InstallTime.UTC().Format(time.RFC822)), - }) + }, nil) } // expandPath takes a filepath and returns the full expanded path, accounting @@ -223,3 +187,57 @@ func expandPath(s string) string { } return e } + +// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking +// into account any provided left padding. +func wrapAtLengthWithPadding(s string, pad int) string { + wrapped := text.Wrap(s, maxLineLength-pad) + lines := strings.Split(wrapped, "\n") + for i, line := range lines { + lines[i] = strings.Repeat(" ", pad) + line + } + return strings.Join(lines, "\n") +} + +// wrapAtLength wraps the given text to maxLineLength. +func wrapAtLength(s string) string { + return wrapAtLengthWithPadding(s, 0) +} + +// ttlToAPI converts a user-supplied ttl into an API-compatible string. If +// the TTL is 0, this returns the empty string. If the TTL is negative, this +// returns "system" to indicate to use the system values. Otherwise, the +// time.Duration ttl is used. +func ttlToAPI(d time.Duration) string { + if d == 0 { + return "" + } + + if d < 0 { + return "system" + } + + return d.String() +} + +// humanDuration prints the time duration without those pesky zeros. +func humanDuration(d time.Duration) string { + if d == 0 { + return "0s" + } + + s := d.String() + if strings.HasSuffix(s, "m0s") { + s = s[:len(s)-2] + } + if idx := strings.Index(s, "h0m"); idx > 0 { + s = s[:idx+1] + s[idx+3:] + } + return s +} + +// humanDurationInt prints the given int as if it were a time.Duration number +// of seconds. +func humanDurationInt(i int) string { + return humanDuration(time.Duration(i) * time.Second) +} diff --git a/command/format.go b/command/format.go index 5db41c7e11..35dcb482da 100644 --- a/command/format.go +++ b/command/format.go @@ -5,19 +5,19 @@ import ( "errors" "fmt" "sort" - "strconv" "strings" - "sync" - "time" "github.com/ghodss/yaml" "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" - "github.com/posener/complete" "github.com/ryanuber/columnize" ) -var predictFormat complete.Predictor = complete.PredictSet("json", "yaml") +const ( + // hopeDelim is the delimiter to use when splitting columns. We call it a + // hopeDelim because we hope that it's never contained in a secret. + hopeDelim = "♨" +) func OutputSecret(ui cli.Ui, format string, secret *api.Secret) int { return outputWithFormat(ui, format, secret, secret) @@ -89,7 +89,7 @@ type TableFormatter struct { func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error { // TODO: this should really use reflection like the other formatters do if s, ok := data.(*api.Secret); ok { - return t.OutputSecret(ui, secret, s) + return t.OutputSecret(ui, s) } if s, ok := data.([]interface{}); ok { return t.OutputList(ui, secret, s) @@ -98,132 +98,111 @@ func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) } func (t TableFormatter) OutputList(ui cli.Ui, secret *api.Secret, list []interface{}) error { - config := columnize.DefaultConfig() - config.Delim = "♨" - config.Glue = "\t" - config.Prefix = "" - - input := make([]string, 0, 5) + t.printWarnings(ui, secret) if len(list) > 0 { - input = append(input, "Keys") - input = append(input, "----") - - keys := make([]string, 0, len(list)) - for _, k := range list { - keys = append(keys, k.(string)) + keys := make([]string, len(list)) + for i, v := range list { + typed, ok := v.(string) + if !ok { + return fmt.Errorf("Error: %v is not a string", v) + } + keys[i] = typed } sort.Strings(keys) - for _, k := range keys { - input = append(input, fmt.Sprintf("%s", k)) - } + // Prepend the header + keys = append([]string{"Keys"}, keys...) + + ui.Output(tableOutput(keys, &columnize.Config{ + Delim: hopeDelim, + })) } - tableOutputStr := columnize.Format(input, config) - - // Print the warning separately because the length of first - // column in the output will be increased by the length of - // the longest warning string making the output look bad. - warningsInput := make([]string, 0, 5) - if len(secret.Warnings) != 0 { - warningsInput = append(warningsInput, "") - warningsInput = append(warningsInput, "The following warnings were returned from the Vault server:") - for _, warning := range secret.Warnings { - warningsInput = append(warningsInput, fmt.Sprintf("* %s", warning)) - } - } - - warningsOutputStr := columnize.Format(warningsInput, config) - - ui.Output(fmt.Sprintf("%s\n%s", tableOutputStr, warningsOutputStr)) - return nil } -func (t TableFormatter) OutputSecret(ui cli.Ui, secret, s *api.Secret) error { - config := columnize.DefaultConfig() - config.Delim = "♨" - config.Glue = "\t" - config.Prefix = "" +// printWarnings prints any warnings in the secret. +func (t TableFormatter) printWarnings(ui cli.Ui, secret *api.Secret) { + if secret != nil && len(secret.Warnings) > 0 { + ui.Warn("WARNING! The following warnings were returned from Vault:\n") + for _, warning := range secret.Warnings { + ui.Warn(wrapAtLengthWithPadding(fmt.Sprintf("* %s", warning), 2)) + } + ui.Warn("") + } +} - input := make([]string, 0, 5) - - onceHeader := &sync.Once{} - headerFunc := func() { - input = append(input, fmt.Sprintf("Key %s Value", config.Delim)) - input = append(input, fmt.Sprintf("--- %s -----", config.Delim)) +func (t TableFormatter) OutputSecret(ui cli.Ui, secret *api.Secret) error { + if secret == nil { + return nil } - if s.LeaseDuration > 0 { - onceHeader.Do(headerFunc) - if s.LeaseID != "" { - input = append(input, fmt.Sprintf("lease_id %s %s", config.Delim, s.LeaseID)) - input = append(input, fmt.Sprintf( - "lease_duration %s %s", config.Delim, (time.Second*time.Duration(s.LeaseDuration)).String())) + t.printWarnings(ui, secret) + + out := make([]string, 0, 8) + if secret.LeaseDuration > 0 { + if secret.LeaseID != "" { + out = append(out, fmt.Sprintf("lease_id %s %s", hopeDelim, secret.LeaseID)) + out = append(out, fmt.Sprintf("lease_duration %s %s", hopeDelim, humanDurationInt(secret.LeaseDuration))) + out = append(out, fmt.Sprintf("lease_renewable %s %t", hopeDelim, secret.Renewable)) } else { - input = append(input, fmt.Sprintf( - "refresh_interval %s %s", config.Delim, (time.Second*time.Duration(s.LeaseDuration)).String())) - } - if s.LeaseID != "" { - input = append(input, fmt.Sprintf( - "lease_renewable %s %s", config.Delim, strconv.FormatBool(s.Renewable))) + // This is probably the generic secret backend which has leases, but we + // print them as refresh_interval to reduce confusion. + out = append(out, fmt.Sprintf("refresh_interval %s %s", hopeDelim, humanDurationInt(secret.LeaseDuration))) } } - if s.Auth != nil { - onceHeader.Do(headerFunc) - input = append(input, fmt.Sprintf("token %s %s", config.Delim, s.Auth.ClientToken)) - input = append(input, fmt.Sprintf("token_accessor %s %s", config.Delim, s.Auth.Accessor)) - input = append(input, fmt.Sprintf("token_duration %s %s", config.Delim, (time.Second*time.Duration(s.Auth.LeaseDuration)).String())) - input = append(input, fmt.Sprintf("token_renewable %s %v", config.Delim, s.Auth.Renewable)) - input = append(input, fmt.Sprintf("token_policies %s %v", config.Delim, s.Auth.Policies)) - for k, v := range s.Auth.Metadata { - input = append(input, fmt.Sprintf("token_meta_%s %s %#v", k, config.Delim, v)) + if secret.Auth != nil { + out = append(out, fmt.Sprintf("token %s %s", hopeDelim, secret.Auth.ClientToken)) + out = append(out, fmt.Sprintf("token_accessor %s %s", hopeDelim, secret.Auth.Accessor)) + // If the lease duration is 0, it's likely a root token, so output the + // duration as "infinity" to clear things up. + if secret.Auth.LeaseDuration == 0 { + out = append(out, fmt.Sprintf("token_duration %s %s", hopeDelim, "∞")) + } else { + out = append(out, fmt.Sprintf("token_duration %s %s", hopeDelim, humanDurationInt(secret.Auth.LeaseDuration))) + } + out = append(out, fmt.Sprintf("token_renewable %s %t", hopeDelim, secret.Auth.Renewable)) + out = append(out, fmt.Sprintf("token_policies %s %v", hopeDelim, secret.Auth.Policies)) + for k, v := range secret.Auth.Metadata { + out = append(out, fmt.Sprintf("token_meta_%s %s %v", k, hopeDelim, v)) } } - if s.WrapInfo != nil { - onceHeader.Do(headerFunc) - input = append(input, fmt.Sprintf("wrapping_token: %s %s", config.Delim, s.WrapInfo.Token)) - input = append(input, fmt.Sprintf("wrapping_token_ttl: %s %s", config.Delim, (time.Second*time.Duration(s.WrapInfo.TTL)).String())) - input = append(input, fmt.Sprintf("wrapping_token_creation_time: %s %s", config.Delim, s.WrapInfo.CreationTime.String())) - input = append(input, fmt.Sprintf("wrapping_token_creation_path: %s %s", config.Delim, s.WrapInfo.CreationPath)) - if s.WrapInfo.WrappedAccessor != "" { - input = append(input, fmt.Sprintf("wrapped_accessor: %s %s", config.Delim, s.WrapInfo.WrappedAccessor)) + if secret.WrapInfo != nil { + out = append(out, fmt.Sprintf("wrapping_token: %s %s", hopeDelim, secret.WrapInfo.Token)) + out = append(out, fmt.Sprintf("wrapping_token_ttl: %s %s", hopeDelim, humanDurationInt(secret.WrapInfo.TTL))) + out = append(out, fmt.Sprintf("wrapping_token_creation_time: %s %s", hopeDelim, secret.WrapInfo.CreationTime.String())) + out = append(out, fmt.Sprintf("wrapping_token_creation_path: %s %s", hopeDelim, secret.WrapInfo.CreationPath)) + if secret.WrapInfo.WrappedAccessor != "" { + out = append(out, fmt.Sprintf("wrapped_accessor: %s %s", hopeDelim, secret.WrapInfo.WrappedAccessor)) } } - if s.Data != nil && len(s.Data) > 0 { - onceHeader.Do(headerFunc) - keys := make([]string, 0, len(s.Data)) - for k := range s.Data { + if len(secret.Data) > 0 { + keys := make([]string, 0, len(secret.Data)) + for k := range secret.Data { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { - input = append(input, fmt.Sprintf("%s %s %v", k, config.Delim, s.Data[k])) + out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, secret.Data[k])) } } - tableOutputStr := columnize.Format(input, config) - - // Print the warning separately because the length of first - // column in the output will be increased by the length of - // the longest warning string making the output look bad. - warningsInput := make([]string, 0, 5) - if len(s.Warnings) != 0 { - warningsInput = append(warningsInput, "") - warningsInput = append(warningsInput, "The following warnings were returned from the Vault server:") - for _, warning := range s.Warnings { - warningsInput = append(warningsInput, fmt.Sprintf("* %s", warning)) - } + // If we got this far and still don't have any data, there's nothing to print, + // sorry. + if len(out) == 0 { + return nil } - warningsOutputStr := columnize.Format(warningsInput, config) - - ui.Output(fmt.Sprintf("%s\n%s", tableOutputStr, warningsOutputStr)) + // Prepend the header + out = append([]string{"Key" + hopeDelim + "Value"}, out...) + ui.Output(tableOutput(out, &columnize.Config{ + Delim: hopeDelim, + })) return nil } From 3a84897213ed698e0bd260067b5acc873e4ea816 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:05:17 -0400 Subject: [PATCH 106/281] Add a custom flag for specifying "system" ttls --- command/base_flags.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/command/base_flags.go b/command/base_flags.go index 339247cff7..57c251d200 100644 --- a/command/base_flags.go +++ b/command/base_flags.go @@ -24,6 +24,11 @@ type FlagVisibility interface { Hidden() bool } +// FlagBool is an interface which boolean flags implement. +type FlagBool interface { + IsBoolFlag() bool +} + // -- BoolVar and boolValue type BoolVar struct { Name string @@ -512,6 +517,11 @@ func newDurationValue(def time.Duration, target *time.Duration, hidden bool) *du } func (d *durationValue) Set(s string) error { + // Maintain bc for people specifying "system" as the value. + if s == "system" { + s = "-1" + } + v, err := time.ParseDuration(appendDurationSuffix(s)) if err != nil { return err From bd703adacdcfac7e5515b4153faf20838196d255 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:05:54 -0400 Subject: [PATCH 107/281] Write all the deprecated commands together --- command/commands.go | 610 +++++++++++++++++++++++++++++++++++++++----- command/main.go | 155 ++++++----- 2 files changed, 636 insertions(+), 129 deletions(-) diff --git a/command/commands.go b/command/commands.go index 43036e4d43..06a57a53df 100644 --- a/command/commands.go +++ b/command/commands.go @@ -1,6 +1,7 @@ package command import ( + "fmt" "os" "os/signal" "syscall" @@ -60,6 +61,39 @@ import ( physZooKeeper "github.com/hashicorp/vault/physical/zookeeper" ) +// DeprecatedCommand is a command that wraps an existing command and prints a +// deprecation notice and points the user to the new command. Deprecated +// commands are always hidden from help output. +type DeprecatedCommand struct { + cli.Command + UI cli.Ui + + // Old is the old command name, New is the new command name. + Old, New string +} + +// Help wraps the embedded Help command and prints a warning about deprecations. +func (c *DeprecatedCommand) Help() string { + c.warn() + return c.Command.Help() +} + +// Run wraps the embedded Run command and prints a warning about deprecation. +func (c *DeprecatedCommand) Run(args []string) int { + c.warn() + return c.Command.Run(args) +} + +func (c *DeprecatedCommand) warn() { + c.UI.Warn(wrapAtLength(fmt.Sprintf( + "WARNING! The \"vault %s\" command is deprecated. Please use \"vault %s\" "+ + "instead. This command will be removed in the next major release of "+ + "Vault.", + c.Old, + c.New))) + c.UI.Warn("") +} + // Commands is the mapping of all the available commands. var Commands map[string]cli.CommandFactory @@ -73,7 +107,7 @@ func init() { }, } - authHandlers := map[string]AuthHandler{ + loginHandlers := map[string]LoginHandler{ "aws": &credAws.CLIHandler{}, "cert": &credCert.CLIHandler{}, "github": &credGitHub.CLIHandler{}, @@ -89,71 +123,78 @@ func init() { } Commands = map[string]cli.CommandFactory{ - "audit-disable": func() (cli.Command, error) { + "audit": func() (cli.Command, error) { + return &AuditCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "audit disable": func() (cli.Command, error) { return &AuditDisableCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "audit-enable": func() (cli.Command, error) { + "audit enable": func() (cli.Command, error) { return &AuditEnableCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "audit-list": func() (cli.Command, error) { + "audit list": func() (cli.Command, error) { return &AuditListCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, + "auth tune": func() (cli.Command, error) { + return &AuthTuneCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, "auth": func() (cli.Command, error) { return &AuthCommand{ BaseCommand: &BaseCommand{ UI: ui, }, - Handlers: authHandlers, + Handlers: loginHandlers, }, nil }, - "auth-disable": func() (cli.Command, error) { + "auth disable": func() (cli.Command, error) { return &AuthDisableCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "auth-enable": func() (cli.Command, error) { + "auth enable": func() (cli.Command, error) { return &AuthEnableCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "auth-help": func() (cli.Command, error) { + "auth help": func() (cli.Command, error) { return &AuthHelpCommand{ BaseCommand: &BaseCommand{ UI: ui, }, - Handlers: authHandlers, + Handlers: loginHandlers, }, nil }, - "auth-list": func() (cli.Command, error) { + "auth list": func() (cli.Command, error) { return &AuthListCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "capabilities": func() (cli.Command, error) { - return &CapabilitiesCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - }, nil - }, "delete": func() (cli.Command, error) { return &DeleteCommand{ BaseCommand: &BaseCommand{ @@ -161,22 +202,22 @@ func init() { }, }, nil }, - "generate-root": func() (cli.Command, error) { - return &GenerateRootCommand{ + "lease": func() (cli.Command, error) { + return &LeaseCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "init": func() (cli.Command, error) { - return &InitCommand{ + "lease renew": func() (cli.Command, error) { + return &LeaseRenewCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "key-status": func() (cli.Command, error) { - return &KeyStatusCommand{ + "lease revoke": func() (cli.Command, error) { + return &LeaseRevokeCommand{ BaseCommand: &BaseCommand{ UI: ui, }, @@ -189,22 +230,72 @@ func init() { }, }, nil }, - "mount": func() (cli.Command, error) { - return &MountCommand{ + "login": func() (cli.Command, error) { + return &LoginCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + Handlers: loginHandlers, + }, nil + }, + "operator": func() (cli.Command, error) { + return &OperatorCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "mounts": func() (cli.Command, error) { - return &MountsCommand{ + "operator generate-root": func() (cli.Command, error) { + return &OperatorGenerateRootCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "mount-tune": func() (cli.Command, error) { - return &MountTuneCommand{ + "operator init": func() (cli.Command, error) { + return &OperatorInitCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "operator key-status": func() (cli.Command, error) { + return &OperatorKeyStatusCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "operator rekey": func() (cli.Command, error) { + return &OperatorRekeyCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "operator rotate": func() (cli.Command, error) { + return &OperatorRotateCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "operator seal": func() (cli.Command, error) { + return &OperatorSealCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "operator step-down": func() (cli.Command, error) { + return &OperatorStepDownCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "operator unseal": func() (cli.Command, error) { + return &OperatorUnsealCommand{ BaseCommand: &BaseCommand{ UI: ui, }, @@ -217,21 +308,42 @@ func init() { }, }, nil }, - "policies": func() (cli.Command, error) { - return &PolicyListCommand{ + "policy": func() (cli.Command, error) { + return &PolicyCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "policy-delete": func() (cli.Command, error) { + "policy delete": func() (cli.Command, error) { return &PolicyDeleteCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "policy-write": func() (cli.Command, error) { + "policy fmt": func() (cli.Command, error) { + return &PolicyFmtCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "policy list": func() (cli.Command, error) { + return &PolicyListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "policy read": func() (cli.Command, error) { + return &PolicyReadCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "policy write": func() (cli.Command, error) { return &PolicyWriteCommand{ BaseCommand: &BaseCommand{ UI: ui, @@ -245,43 +357,43 @@ func init() { }, }, nil }, - "rekey": func() (cli.Command, error) { - return &RekeyCommand{ + "secrets": func() (cli.Command, error) { + return &SecretsCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "remount": func() (cli.Command, error) { - return &RemountCommand{ + "secrets disable": func() (cli.Command, error) { + return &SecretsDisableCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "renew": func() (cli.Command, error) { - return &RenewCommand{ + "secrets enable": func() (cli.Command, error) { + return &SecretsEnableCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "revoke": func() (cli.Command, error) { - return &RevokeCommand{ + "secrets list": func() (cli.Command, error) { + return &SecretsListCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "rotate": func() (cli.Command, error) { - return &RotateCommand{ + "secrets move": func() (cli.Command, error) { + return &SecretsMoveCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "seal": func() (cli.Command, error) { - return &SealCommand{ + "secrets tune": func() (cli.Command, error) { + return &SecretsTuneCommand{ BaseCommand: &BaseCommand{ UI: ui, }, @@ -367,55 +479,48 @@ func init() { }, }, nil }, - "step-down": func() (cli.Command, error) { - return &StepDownCommand{ + "token": func() (cli.Command, error) { + return &TokenCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "token-create": func() (cli.Command, error) { + "token create": func() (cli.Command, error) { return &TokenCreateCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "token-lookup": func() (cli.Command, error) { + "token capabilities": func() (cli.Command, error) { + return &TokenCapabilitiesCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, nil + }, + "token lookup": func() (cli.Command, error) { return &TokenLookupCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "token-renew": func() (cli.Command, error) { + "token renew": func() (cli.Command, error) { return &TokenRenewCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "token-revoke": func() (cli.Command, error) { + "token revoke": func() (cli.Command, error) { return &TokenRevokeCommand{ BaseCommand: &BaseCommand{ UI: ui, }, }, nil }, - "unseal": func() (cli.Command, error) { - return &UnsealCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - }, nil - }, - "unmount": func() (cli.Command, error) { - return &UnmountCommand{ - BaseCommand: &BaseCommand{ - UI: ui, - }, - }, nil - }, "unwrap": func() (cli.Command, error) { return &UnwrapCommand{ BaseCommand: &BaseCommand{ @@ -439,6 +544,381 @@ func init() { }, nil }, } + + initDeprecated(Commands, ui, loginHandlers) +} + +// This function contains all the deprecated commands in one place. This +// optimizes for backwards-compatability, but also provides a single function to +// delete in the next major release of Vault instead of riddling all these +// commands in separate files. As a result, it's a bit long. Sorry. +// +// Deprecations +// TODO: remove in 0.9.0 +func initDeprecated(commands map[string]cli.CommandFactory, ui cli.Ui, loginHandlers map[string]LoginHandler) { + commands["audit-disable"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "audit-disable", + New: "audit disable", + UI: ui, + Command: &AuditDisableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["audit-enable"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "audit-enable", + New: "audit enable", + UI: ui, + Command: &AuditEnableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["audit-list"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "audit-list", + New: "audit list", + UI: ui, + Command: &AuditListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["auth-disable"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "auth-disable", + New: "auth disable", + UI: ui, + Command: &AuthDisableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["auth-enable"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "auth-enable", + New: "auth enable", + UI: ui, + Command: &AuthEnableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["capabilities"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "capabilities", + New: "token capabilities", + UI: ui, + Command: &TokenCapabilitiesCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["generate-root"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "generate-root", + New: "operator generate-root", + UI: ui, + Command: &OperatorGenerateRootCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["init"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "init", + New: "operator init", + UI: ui, + Command: &OperatorInitCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["key-status"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "key-status", + New: "operator key-status", + UI: ui, + Command: &OperatorKeyStatusCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["renew"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "renew", + New: "lease renew", + UI: ui, + Command: &LeaseRenewCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["revoke"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "revoke", + New: "lease revoke", + UI: ui, + Command: &LeaseRevokeCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["mount"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "mount", + New: "secrets enable", + UI: ui, + Command: &SecretsEnableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["mount-tune"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "mount-tune", + New: "secrets tune", + UI: ui, + Command: &SecretsTuneCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["mounts"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "mounts", + New: "secrets list", + UI: ui, + Command: &SecretsListCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["policies"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "policies", + New: "policy read\" or \"vault policy list", // lol + UI: ui, + Command: &PoliciesDeprecatedCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["policy-delete"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "policy-delete", + New: "policy delete", + UI: ui, + Command: &PolicyDeleteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["policy-write"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "policy-write", + New: "policy write", + UI: ui, + Command: &PolicyWriteCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["rekey"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "rekey", + New: "operator rekey", + UI: ui, + Command: &OperatorRekeyCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["remount"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "remount", + New: "secrets move", + UI: ui, + Command: &SecretsMoveCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["rotate"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "rotate", + New: "operator rotate", + UI: ui, + Command: &OperatorRotateCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["seal"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "seal", + New: "operator seal", + UI: ui, + Command: &OperatorSealCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["step-down"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "step-down", + New: "operator step-down", + UI: ui, + Command: &OperatorStepDownCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["token-create"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "token-create", + New: "token create", + UI: ui, + Command: &TokenCreateCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["token-lookup"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "token-lookup", + New: "token lookup", + UI: ui, + Command: &TokenLookupCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["token-renew"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "token-renew", + New: "token renew", + UI: ui, + Command: &TokenRenewCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["token-revoke"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "token-revoke", + New: "token revoke", + UI: ui, + Command: &TokenRevokeCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["unmount"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "unmount", + New: "secrets disable", + UI: ui, + Command: &SecretsDisableCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } + + commands["unseal"] = func() (cli.Command, error) { + return &DeprecatedCommand{ + Old: "unseal", + New: "operator unseal", + UI: ui, + Command: &OperatorUnsealCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + }, + }, nil + } } // MakeShutdownCh returns a channel that can be used for shutdown diff --git a/command/main.go b/command/main.go index 731deb2273..99596df5fb 100644 --- a/command/main.go +++ b/command/main.go @@ -3,9 +3,12 @@ package command import ( "bytes" "fmt" + "io" + "log" "os" "sort" "strings" + "text/tabwriter" "github.com/mitchellh/cli" ) @@ -27,7 +30,14 @@ func Run(args []string) int { Name: "vault", Args: args, Commands: Commands, - HelpFunc: helpFunc, + + HelpFunc: FilterDeprecatedFunc( + FilterCommandFunc("version", + groupedHelpFunc( + cli.BasicHelpFunc("vault"), + ), + ), + ), Autocomplete: true, AutocompleteNoDefaultFlags: true, @@ -42,77 +52,94 @@ func Run(args []string) int { return exitCode } -// helpFunc is a cli.HelpFunc that can is used to output the help for Vault. -func helpFunc(commands map[string]cli.CommandFactory) string { - commonNames := map[string]struct{}{ - "delete": struct{}{}, - "read": struct{}{}, - "renew": struct{}{}, - "revoke": struct{}{}, - "server": struct{}{}, - "status": struct{}{}, - "unwrap": struct{}{}, - "write": struct{}{}, - } - - // Determine the maximum key length, and classify based on type - commonCommands := make(map[string]cli.CommandFactory) - otherCommands := make(map[string]cli.CommandFactory) - - commonKeyLen, otherKeyLen := 0, 0 - for key, f := range commands { - if _, ok := commonNames[key]; ok { - if len(key) > commonKeyLen { - commonKeyLen = len(key) +func FilterCommandFunc(name string, f cli.HelpFunc) cli.HelpFunc { + return func(commands map[string]cli.CommandFactory) string { + newCommands := make(map[string]cli.CommandFactory, len(commands)) + for k, v := range commands { + if k != name { + newCommands[k] = v } - commonCommands[key] = f - } else { - if len(key) > otherKeyLen { - otherKeyLen = len(key) - } - otherCommands[key] = f } + return f(newCommands) } - - var buf bytes.Buffer - buf.WriteString("Usage: vault [args]\n\n") - buf.WriteString("Common commands:\n\n") - buf.WriteString(listCommands(commonCommands, commonKeyLen)) - buf.WriteString("\n") - buf.WriteString("Other commands:\n\n") - buf.WriteString(listCommands(otherCommands, otherKeyLen)) - return strings.TrimSpace(buf.String()) } -// listCommands just lists the commands in the map with the -// given maximum key length. -func listCommands(commands map[string]cli.CommandFactory, maxKeyLen int) string { - var buf bytes.Buffer +// FilterDeprecatedFunc filters deprecated +func FilterDeprecatedFunc(f cli.HelpFunc) cli.HelpFunc { + return func(commands map[string]cli.CommandFactory) string { + newCommands := make(map[string]cli.CommandFactory) - // Get the list of keys so we can sort them, and also get the maximum - // key length so they can be aligned properly. - keys := make([]string, 0, len(commands)) - for key, _ := range commands { - keys = append(keys, key) - } - sort.Strings(keys) + for k, cmdFn := range commands { + command, err := cmdFn() + if err != nil { + log.Printf("[ERR] cli: Command %q failed to load: %s", k, err) + } - for _, key := range keys { - commandFunc, ok := commands[key] - if !ok { - // This should never happen since we JUST built the list of - // keys. - panic("command not found: " + key) + if _, ok := command.(*DeprecatedCommand); ok { + continue + } + + newCommands[k] = cmdFn } - command, err := commandFunc() - if err != nil { - panic(fmt.Sprintf("command '%s' failed to load: %s", key, err)) - } - - key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key))) - buf.WriteString(fmt.Sprintf(" %s %s\n", key, command.Synopsis())) + return f(newCommands) } - - return buf.String() +} + +var commonCommands = []string{ + "read", + "write", + "delete", + "list", + "login", + "server", + "status", + "unwrap", +} + +func groupedHelpFunc(f cli.HelpFunc) cli.HelpFunc { + return func(commands map[string]cli.CommandFactory) string { + var b bytes.Buffer + tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0) + + fmt.Fprintf(tw, "Usage: vault [args]\n\n") + fmt.Fprintf(tw, "Common commands:\n") + for _, v := range commonCommands { + printCommand(tw, v, commands[v]) + } + + otherCommands := make([]string, 0, len(commands)) + for k := range commands { + found := false + for _, v := range commonCommands { + if k == v { + found = true + break + } + } + + if !found { + otherCommands = append(otherCommands, k) + } + } + sort.Strings(otherCommands) + + fmt.Fprintf(tw, "\n") + fmt.Fprintf(tw, "Other commands:\n") + for _, v := range otherCommands { + printCommand(tw, v, commands[v]) + } + + tw.Flush() + + return strings.TrimSpace(b.String()) + } +} + +func printCommand(w io.Writer, name string, cmdFn cli.CommandFactory) { + cmd, err := cmdFn() + if err != nil { + panic(fmt.Sprintf("failed to load %q command: %s", name, err)) + } + fmt.Fprintf(w, " %s\t%s\n", name, cmd.Synopsis()) } From d5d4ef1a05d15089e0a4f5fb32f64a2fdd4c90bb Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:06:15 -0400 Subject: [PATCH 108/281] Allow quotes in meta description fields --- website/config.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/website/config.rb b/website/config.rb index a9617531df..96462ea704 100644 --- a/website/config.rb +++ b/website/config.rb @@ -37,7 +37,6 @@ helpers do # @return [String] def description_for(page) description = (page.data.description || "") - .gsub('"', '') .gsub(/\n+/, ' ') .squeeze(' ') From 7f7232d029c83437b3a0def85c868ff2e8c0377c Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:06:34 -0400 Subject: [PATCH 109/281] Add "audit disable" command documentation --- .../docs/commands/audit/disable.html.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 website/source/docs/commands/audit/disable.html.md diff --git a/website/source/docs/commands/audit/disable.html.md b/website/source/docs/commands/audit/disable.html.md new file mode 100644 index 0000000000..7af7568be2 --- /dev/null +++ b/website/source/docs/commands/audit/disable.html.md @@ -0,0 +1,34 @@ +--- +layout: "docs" +page_title: "audit disable - Command" +sidebar_current: "docs-commands-audit-disable" +description: |- + The "audit disable" command disables an audit device at a given path, if one + exists. This command is idempotent, meaning it succeeds even if no audit + device is enabled at the path. +--- + +# audit disable + +The `audit disable` command disables an audit device at a given path, if one +exists. This command is idempotent, meaning it succeeds even if no audit device +is enabled at the path. + +Once an audit device is disabled, no future audit logs are dispatched to it. The +data associated with the audit device is unaffected. For example, if you +disabled an audit device that was logging to a file, the file would still exist +and have stored contents. + +## Examples + +Disable the audit device enabled at "file/": + +```text +$ vault audit disable file/ +Success! Disabled audit device (if it was enabled) at: file/ +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 0b5c21168a54c24950b4e5c52457d6be1071a905 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:06:44 -0400 Subject: [PATCH 110/281] Add "audit enable" command documentation --- .../source/docs/commands/audit/enable.html.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 website/source/docs/commands/audit/enable.html.md diff --git a/website/source/docs/commands/audit/enable.html.md b/website/source/docs/commands/audit/enable.html.md new file mode 100644 index 0000000000..609b265ee5 --- /dev/null +++ b/website/source/docs/commands/audit/enable.html.md @@ -0,0 +1,41 @@ +--- +layout: "docs" +page_title: "audit enable - Command" +sidebar_current: "docs-commands-audit-enable" +description: |- + The "audit enable" command enables an audit device at a given path. +--- + +# audit enable + +The `audit enable` command enables an audit device at a given path. If an audit +device already exists at the given path, an error is returned. Additional +options for configuring the audit device are provided as `KEY=VALUE`. Each audit +device declares its own set of configuration options. + +Once an audit device is enabled, almost every request and response will be +logged to the device. + +## Examples + +Enable the audit device "file" enabled at "file/": + +```text +$ vault audit enable file file_path=/tmp/my-file.txt +Success! Enabled the file audit device at: file/ +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-description` `(string: "")` - Human-friendly description for the purpose of + this audit device. + +- `-local` `(bool: false)` - Mark the audit device as a local-only device. + Local devices are not replicated or removed by replication. + +- `-path` `(string: "")` - Place where the audit device will be accessible. This + must be unique across all audit devices. This defaults to the "type" of the + audit device. From f5be8ed04b3dbbb0236e6f7234831d6e469d786a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:06:48 -0400 Subject: [PATCH 111/281] Add "audit list" command documentation --- .../source/docs/commands/audit/list.html.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 website/source/docs/commands/audit/list.html.md diff --git a/website/source/docs/commands/audit/list.html.md b/website/source/docs/commands/audit/list.html.md new file mode 100644 index 0000000000..cbcf0b3deb --- /dev/null +++ b/website/source/docs/commands/audit/list.html.md @@ -0,0 +1,41 @@ +--- +layout: "docs" +page_title: "audit list - Command" +sidebar_current: "docs-commands-audit-list" +description: |- + The "audit list" command lists the audit devices enabled. The output lists the + enabled audit devices and options for those devices. +--- + +# audit list + +The `audit list` command lists the audit devices enabled. The output lists the +enabled audit devices and options for those devices. + +## Examples + +List all audit devices: + +```text +$ vault audit list +Path Type Description +---- ---- ----------- +file/ file n/a +``` + +List detailed audit device information: + +```text +$ vault audit list -detailed +Path Type Description Replication Options +---- ---- ----------- ----------- ------- +file/ file n/a replicated file_path=/var/log/audit.log +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-detailed` `(bool: false)` - Print detailed information such as options and + replication status about each auth device. From 629f1a789965242c62d9617da71661c35f23bbc8 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:06:55 -0400 Subject: [PATCH 112/281] Add "auth disable" command documentation --- .../source/docs/commands/auth/disable.html.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 website/source/docs/commands/auth/disable.html.md diff --git a/website/source/docs/commands/auth/disable.html.md b/website/source/docs/commands/auth/disable.html.md new file mode 100644 index 0000000000..23aa69bb0b --- /dev/null +++ b/website/source/docs/commands/auth/disable.html.md @@ -0,0 +1,33 @@ +--- +layout: "docs" +page_title: "auth disable - Command" +sidebar_current: "docs-commands-auth-disable" +description: |- + The "auth disable" command disables an auth method at a given path, if one + exists. This command is idempotent, meaning it succeeds even if no auth method + is enabled at the path. +--- + +# auth disable + +The `auth disable` command disables an auth method at a given path, if one +exists. This command is idempotent, meaning it succeeds even if no auth method +is enabled at the path. + +Once an auth method is disabled, it can no longer be used for authentication. +**All access tokens generated via the disabled auth method are immediately +revoked.** This command will block until all tokens are revoked. + +## Examples + +Disable the auth method enabled at "userpass/": + +```text +$ vault auth disable userpass/ +Success! Disabled the auth method (if it existed) at: userpass/ +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From b01c789140010bb42d1b1813fe2a53ec6c15c5cd Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:07:01 -0400 Subject: [PATCH 113/281] Add "auth enable" command documentation --- .../source/docs/commands/auth/enable.html.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 website/source/docs/commands/auth/enable.html.md diff --git a/website/source/docs/commands/auth/enable.html.md b/website/source/docs/commands/auth/enable.html.md new file mode 100644 index 0000000000..089358be88 --- /dev/null +++ b/website/source/docs/commands/auth/enable.html.md @@ -0,0 +1,58 @@ +--- +layout: "docs" +page_title: "auth enable - Command" +sidebar_current: "docs-commands-auth-enable" +description: |- + The "auth enable" command enables an auth method at a given path. If an auth + method already exists at the given path, an error is returned. After the auth + method is enabled, it usually needs configuration. +--- + +# auth enable + +The `auth enable` command enables an auth method at a given path. If an auth +method already exists at the given path, an error is returned. After the auth +method is enabled, it usually needs configuration. The configuration varies by +auth method. + +An auth method is responsible for authenticating users or machines and assigning +them policies and a token with which they can access Vault. Authentication is +usually mapped to policy. Please see the [policies +concepts](/docs/concepts/policies.html) page for more information. + +## Examples + +Enable the auth method "userpass" enabled at "userpass/": + +```text +$ vault auth enable userpass +Success! Enabled the userpass auth method at: userpass/ +``` + +Create a user: + +```text +$ vault write auth/userpass/users/sethvargo password=secret +Success! Data written to: auth/userpass/users/sethvargo +``` + +For more information on the specific configuration options and paths, please see +the [auth method](/docs/auth/index.html) documentation. + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-description` `(string: "")` - Human-friendly description for the purpose of + this auth method. + +- `-local` `(bool: false)` - Mark the auth method as local-only. Local auth + methods are not replicated nor removed by replication. + +- `-path` `(string: "")` - Place where the auth method will be accessible. This + must be unique across all auth methods. This defaults to the "type" of the + auth method. The auth method will be accessible at "/auth/". + +- `-plugin-name` `(string: "")` - Name of the auth method plugin. This plugin + name must already exist in the Vault server's plugin catalog. From 3f31c2b3fd69e90fe59aced01bddbbb389a5d675 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:07:07 -0400 Subject: [PATCH 114/281] Add "auth help" command documentation --- .../source/docs/commands/auth/help.html.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 website/source/docs/commands/auth/help.html.md diff --git a/website/source/docs/commands/auth/help.html.md b/website/source/docs/commands/auth/help.html.md new file mode 100644 index 0000000000..73a4cbe785 --- /dev/null +++ b/website/source/docs/commands/auth/help.html.md @@ -0,0 +1,45 @@ +--- +layout: "docs" +page_title: "auth help - Command" +sidebar_current: "docs-commands-auth-help" +description: |- + The "auth help" command prints usage and help for an auth method. +--- + +# auth help + +The `auth help` command prints usage and help for an auth method. + + - If given a TYPE, this command prints the default help for the auth method of + that type. + + - If given a PATH, this command prints the help output for the auth method + enabled at that path. This path must already exist. + +Each auth method produces its own help output. + +## Examples + +Get usage instructions for the userpass auth method: + +```text +$ vault auth help userpass +Usage: vault login -method=userpass [CONFIG K=V...] + + The userpass authentication method allows users to authenticate using Vault's + internal user database. + +# ... +``` + +Print usage for the auth method enabled at my-method/ + +```text +$ vault auth help my-method/ +# ... +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From eeefe935b13e1cf4f51e51a9a4eccf1e3e118620 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:07:10 -0400 Subject: [PATCH 115/281] Add "auth list" command documentation --- .../source/docs/commands/auth/list.html.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 website/source/docs/commands/auth/list.html.md diff --git a/website/source/docs/commands/auth/list.html.md b/website/source/docs/commands/auth/list.html.md new file mode 100644 index 0000000000..27d8d80363 --- /dev/null +++ b/website/source/docs/commands/auth/list.html.md @@ -0,0 +1,43 @@ +--- +layout: "docs" +page_title: "auth list - Command" +sidebar_current: "docs-commands-auth-list" +description: |- + The "auth list" command lists the auth methods enabled. The output lists the + enabled auth methods and options for those methods. +--- + +# auth list + +The `auth list` command lists the auth methods enabled. The output lists the +enabled auth methods and options for those methods. + +## Examples + +List all auth methods: + +```text +$ vault auth list +Path Type Description +---- ---- ----------- +token/ token token based credentials +userpass/ userpass n/a +``` + +List detailed auth method information: + +```text +$ vault auth list -detailed +Path Type Accessor Plugin Default TTL Max TTL Replication Description +---- ---- -------- ------ ----------- ------- ----------- ----------- +token/ token auth_token_b2166f9e n/a system system replicated token based credentials +userpass/ userpass auth_userpass_eea6507e n/a system system replicated n/a +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-detailed` `(bool: false)` - Print detailed information such as configuration + and replication status about each auth method. From 9c9e3a00faefe44171ca05ee1545d1dbdc710506 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:07:15 -0400 Subject: [PATCH 116/281] Add "auth tune" command documentation --- .../source/docs/commands/auth/tune.html.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 website/source/docs/commands/auth/tune.html.md diff --git a/website/source/docs/commands/auth/tune.html.md b/website/source/docs/commands/auth/tune.html.md new file mode 100644 index 0000000000..fc68093bdd --- /dev/null +++ b/website/source/docs/commands/auth/tune.html.md @@ -0,0 +1,38 @@ +--- +layout: "docs" +page_title: "auth tune - Command" +sidebar_current: "docs-commands-auth-tune" +description: |- + The "auth tune" command tunes the configuration options for the auth method at + the given PATH. +--- + +# auth tune + +The `auth tune` command tunes the configuration options for the auth method at +the given PATH. **The argument corresponds to the PATH where the auth method is +enabled, not the TYPE!** + +## Examples + +Tune the default lease for the auth method enabled at "github/": + +```text +$ vault auth tune -default-lease-ttl=72h github/ +Success! Tuned the auth method at: github/ +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-default-lease-ttl` `(duration: "")` - The default lease TTL for this auth + method. If unspecified, this defaults to the Vault server's globally + configured default lease TTL, or a previously configured value for the auth + method. + +- `-max-lease-ttl` `(duration: "")` - The maximum lease TTL for this auth + method. If unspecified, this defaults to the Vault server's globally + configured maximum lease TTL, or a previously configured value for the auth + method. From 9a23ee813f15080d4cd2fc3714cddd651351da93 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:07:21 -0400 Subject: [PATCH 117/281] Add "lease renew" command documentation --- .../source/docs/commands/lease/renew.html.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 website/source/docs/commands/lease/renew.html.md diff --git a/website/source/docs/commands/lease/renew.html.md b/website/source/docs/commands/lease/renew.html.md new file mode 100644 index 0000000000..09affc72b0 --- /dev/null +++ b/website/source/docs/commands/lease/renew.html.md @@ -0,0 +1,34 @@ +--- +layout: "docs" +page_title: "lease renew - Command" +sidebar_current: "docs-commands-lease-renew" +description: |- + The "lease renew" command renews the lease on a secret, extending the time + that it can be used before it is revoked by Vault. +--- + +# lease renew + +The `lease renew` command renews the lease on a secret, extending the time that +it can be used before it is revoked by Vault. + +Every secret in Vault has a lease associated with it. If the owner of the secret +wants to use it longer than the lease, then it must be renewed. Renewing the +lease does not change the contents of the secret. + +## Examples + +Renew a lease: + +```text +$ vault lease renew database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +Success! Revoked lease: database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-increment` `(duration: "")` - Request a specific increment in seconds. Vault + is not required to honor this request. From 276e1d2f98ddbfba94303a86ac11146d4973092e Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:07:28 -0400 Subject: [PATCH 118/281] Add "lease revoke" command documentation --- .../source/docs/commands/lease/revoke.html.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 website/source/docs/commands/lease/revoke.html.md diff --git a/website/source/docs/commands/lease/revoke.html.md b/website/source/docs/commands/lease/revoke.html.md new file mode 100644 index 0000000000..7c4d54c901 --- /dev/null +++ b/website/source/docs/commands/lease/revoke.html.md @@ -0,0 +1,43 @@ +--- +layout: "docs" +page_title: "lease revoke - Command" +sidebar_current: "docs-commands-lease-revoke" +description: |- + The "lease revoke" command revokes the lease on a secret, invalidating the + underlying secret. +--- + +# lease revoke + +The `lease revoke` command revokes the lease on a secret, invalidating the +underlying secret. + +## Examples + +Revoke a lease: + +```text +$ vault lease revoke database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +Success! Revoked lease: database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +``` + +Revoke a lease which starts with a prefix: + +```text +$ vault lease revoke -prefix database/creds +Success! Revoked any leases with prefix: database/creds +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-force` `(bool: false)` - Delete the lease from Vault even if the secret + engine revocation fails. This is meant for recovery situations where the + secret in the target secret engine was manually removed. If this flag is + specified, -prefix is also required. This is aliased as "-f". The default is + false. + +- `-prefix` `(bool: false)` - Treat the ID as a prefix instead of an exact lease + ID. This can revoke multiple leases simultaneously. The default is false. From 4f794cfdced6d3fe9558e592a3c36e27bf31e18d Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:07:39 -0400 Subject: [PATCH 119/281] Add "operator generate-root" command documentation --- .../commands/operator/generate-root.html.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 website/source/docs/commands/operator/generate-root.html.md diff --git a/website/source/docs/commands/operator/generate-root.html.md b/website/source/docs/commands/operator/generate-root.html.md new file mode 100644 index 0000000000..4a235aa3ce --- /dev/null +++ b/website/source/docs/commands/operator/generate-root.html.md @@ -0,0 +1,81 @@ +--- +layout: "docs" +page_title: "operator generate-root - Command" +sidebar_current: "docs-commands-operator-generate-root" +description: |- + The "operator generate-root" command generates a new root token by combining a + quorum of share holders. +--- + +# operator generate-root + +The `operator generate-root` command generates a new root token by combining a +quorum of share holders. One of the following must be provided to start the root +token generation: + +- A base64-encoded one-time-password (OTP) provided via the `-otp` flag. Use the + `-generate-otp` flag to generate a usable value. The resulting token is XORed + with this value when it is returned. Use the `-decode` flag to output the + final value. + +- A file containing a PGP key or a + [keybase](/docs/concepts/pgp-gpg-keybase.html) username in the `-pgp-key` + flag. The resulting token is encrypted with this public key. + +An unseal key may be provided directly on the command line as an argument to the +command. If key is specified as "-", the command will read from stdin. If a TTY +is available, the command will prompt for text. + +Please see the [generate root guide](/guides/generate-root.html) for +step-by-step instructions. + +## Examples + +Generate an OTP code for the final token: + +```text +$ vault operator generate-root -generate-otp +``` + +Start a root token generation: + +```text +$ vault operator generate-root -init -otp="..." +``` + +Enter an unseal key to progress root token generation: + +```text +$ vault operator generate-root -otp="..." +``` + + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-cancel` `(bool: false)` - Reset the root token generation progress. This + will discard any submitted unseal keys or configuration. + +- `-decode` `(string: "")` - Decode and output the generated root token. This + option requires the `-otp` flag be set to the OTP used during initialization. + +- `-generate-otp` `(bool: false)` - Generate and print a high-entropy + one-time-password (OTP) suitable for use with the "-init" flag. + +- `-init` `(bool: false)` - Start a root token generation. This can only be done + if there is not currently one in progress. + +- `-nonce` `(string; "")`- Nonce value provided at initialization. The same + nonce value must be provided with each unseal key. + +- `-otp` `(string: "")` - OTP code to use with `-decode` or `-init`. + +- `-pgp-key` `(keybase or pgp)`- Path to a file on disk containing a binary or + base64-encoded public GPG key. This can also be specified as a Keybase + username using the format "keybase:". When supplied, the generated + root token will be encrypted and base64-encoded with the given public key. + +- `-status` `(bool: false)` - Print the status of the current attempt without + providing an unseal key. The default is false. From 06e5d1f1df7a5d1a05445b09bce2860cebd3d360 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:07:45 -0400 Subject: [PATCH 120/281] Add "operator init" command documentation --- .../docs/commands/operator/init.html.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 website/source/docs/commands/operator/init.html.md diff --git a/website/source/docs/commands/operator/init.html.md b/website/source/docs/commands/operator/init.html.md new file mode 100644 index 0000000000..6f37d80928 --- /dev/null +++ b/website/source/docs/commands/operator/init.html.md @@ -0,0 +1,116 @@ +--- +layout: "docs" +page_title: "operator init - Command" +sidebar_current: "docs-commands-operator-init" +description: |- + The "operator init" command initializes a Vault server. Initialization is the + process by which Vault's storage backend is prepared to receive data. Since + Vault server's share the same storage backend in HA mode, you only need to + initialize one Vault to initialize the storage backend. +--- + +# operator init + +The `operator init` command initializes a Vault server. Initialization is the +process by which Vault's storage backend is prepared to receive data. Since +Vault server's share the same storage backend in HA mode, you only need to +initialize one Vault to initialize the storage backend. + +During initialization, Vault generates an in-memory master key and applies +Shamir's secret sharing algorithm to disassemble that master key into a +configuration number of key shares such that a configurable subset of those key +shares must come together to regenerate the master key. These keys are often +called "unseal keys" in Vault's documentation. + +This command cannot be run against already-initialized Vault cluster. + +For more information on sealing and unsealing, please the [seal concepts page](/docs/concepts/seal.html). + +## Examples + +Start initialization with the default options: + +```text +$ vault operator init +``` + +Initialize, but encrypt the unseal keys with pgp keys: + +```text +$ vault operator init \ + -key-shares=3 \ + -key-threshold=2 \ + -pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo" +``` + +Encrypt the initial root token using a pgp key: + +```text +$ vault operator init -root-token-pgp-key="keybase:hashicorp" +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-format` `(string: "")` - Print the output in the given format. Valid formats + are "table", "json", or "yaml". The default is table. This can also be + specified via the `VAULT_FORMAT` environment variable. + +### Common Options + +- `-key-shares` `(int: 5)` - Number of key shares to split the generated master + key into. This is the number of "unseal keys" to generate. This is aliased as + `-n`. + +- `-key-threshold` `(int: 3)` - Number of key shares required to reconstruct the + master key. This must be less than or equal to -key-shares. This is aliased as + `-t`. + +- `-pgp-keys` `(string: "...")` - Comma-separated list of paths to files on disk + containing public GPG keys OR a comma-separated list of Keybase usernames + using the format "keybase:". When supplied, the generated unseal + keys will be encrypted and base64-encoded in the order specified in this list. + The number of entires must match -key-shares, unless -store-shares are used. + +- `-root-token-pgp-key` `(string: "")` - Path to a file on disk containing a + binary or base64-encoded public GPG key. This can also be specified as a + Keybase username using the format "keybase:". When supplied, the + generated root token will be encrypted and base64-encoded with the given + public key. + +- `-status` `(bool": false)` - Print the current initialization status. An exit + code of 0 means the Vault is already initialized. An exit code of 1 means an + error occurred. An exit code of 2 means the mean is not initialized. + +### Consul Options + +- `-consul-auto` `(bool: false)` - Perform automatic service discovery using + Consul in HA mode. When all nodes in a Vault HA cluster are registered with + Consul, enabling this option will trigger automatic service discovery based on + the provided -consul-service value. When Consul is Vault's HA backend, this + functionality is automatically enabled. Ensure the proper Consul environment + variables are set (CONSUL_HTTP_ADDR, etc). When only one Vault server is + discovered, it will be initialized automatically. When more than one Vault + server is discovered, they will each be output for selection. The default is + false. + +- `-consul-service` `(string: "vault")` - Name of the service in Consul under + which the Vault servers are registered. + +### HSM Options + +- `-recovery-pgp-keys` `(string: "...")` - Behaves like `-pgp-keys`, but for the + recovery key shares. This is only used in HSM mode. + +- `-recovery-shares` `(int: 5)` - Number of key shares to split the recovery key + into. This is only used in HSM mode. + +- `-recovery-threshold` `(int: 3)` - Number of key shares required to + reconstruct the recovery key. This is only used in HSM mode. + +- `-stored-shares` `(int: 0)` - Number of unseal keys to store on an HSM. This + must be equal to `-key-shares`. From 4e7d5bb841324764bad1c59049e4bc5e843869d8 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:08:03 -0400 Subject: [PATCH 121/281] Add "operator key-status" command documentation --- .../docs/commands/operator/key-status.html.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 website/source/docs/commands/operator/key-status.html.md diff --git a/website/source/docs/commands/operator/key-status.html.md b/website/source/docs/commands/operator/key-status.html.md new file mode 100644 index 0000000000..6402355640 --- /dev/null +++ b/website/source/docs/commands/operator/key-status.html.md @@ -0,0 +1,28 @@ +--- +layout: "docs" +page_title: "operator key-status - Command" +sidebar_current: "docs-commands-operator-key-status" +description: |- + The "operator key-status" provides information about the active encryption + key. +--- + +# operator key-status + +The `operator key-status` provides information about the active encryption key. +Specifically, the current key term and the key installation time. + +## Examples + +Get the key status: + +```text +$ vault operator key-status +Key Term 2 +Install Time 01 Jan 17 12:30 UTC +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 15a0f6c58a63e5db4ce9b2e62dac794a5c8ef91a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:09:36 -0400 Subject: [PATCH 122/281] Add "operator rekey" command documentation --- .../docs/commands/operator/rekey.html.md | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 website/source/docs/commands/operator/rekey.html.md diff --git a/website/source/docs/commands/operator/rekey.html.md b/website/source/docs/commands/operator/rekey.html.md new file mode 100644 index 0000000000..04bd954183 --- /dev/null +++ b/website/source/docs/commands/operator/rekey.html.md @@ -0,0 +1,118 @@ +--- +layout: "docs" +page_title: "operator rekey - Command" +sidebar_current: "docs-commands-operator-rekey" +description: |- + The "operator rekey" command generates a new set of unseal keys. This can + optionally change the total number of key shares or the required threshold of + those key shares to reconstruct the master key. This operation is zero + downtime, but it requires the Vault is unsealed and a quorum of existing + unseal keys are provided. +--- + +# operator rekey + +The `operator rekey` command generates a new set of unseal keys. This can +optionally change the total number of key shares or the required threshold of +those key shares to reconstruct the master key. This operation is zero downtime, +but it requires the Vault is unsealed and a quorum of existing unseal keys are +provided. + +An unseal key may be provided directly on the command line as an argument to the +command. If key is specified as "-", the command will read from stdin. If a TTY +is available, the command will prompt for text. + +Please see the [rotating and rekeying](/guides/rekeying-and-rotating.html) for +step-by-step instructions. + +## Examples + +Initialize a rekey: + +```text +$ vault operator rekey \ + -init \ + -key-shares=15 \ + -key-threshold=9 +``` + +Rekey and encrypt the resulting unseal keys with PGP: + +```text +$ vault operator rekey \ + -init \ + -key-shares=3 \ + -key-threshold=2 \ + -pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo" +``` + +Store encrypted PGP keys in Vault's core: + +```text +$ vault operator rekey \ + -init \ + -pgp-keys="..." \ + -backup +``` + +Retrieve backed-up unseal keys: + +```text +$ vault operator rekey -backup-retrieve +``` + +Delete backed-up unseal keys: + +```text +$ vault operator rekey -backup-delete +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +## Common Options + +- `-cancel` `(bool: false)` - Reset the rekeying progress. This will discard any submitted unseal keys + or configuration. The default is false. + +- `-init` `(bool: false)` - Initialize the rekeying operation. This can only be + done if no rekeying operation is in progress. Customize the new number of key + shares and key threshold using the `-key-shares` and `-key-threshold flags`. + +- `-key-shares` `(int: 5)` - Number of key shares to split the generated master + key into. This is the number of "unseal keys" to generate. This is aliased as + `-n` + +- `-key-threshold` `(int: 3)` - Number of key shares required to reconstruct the + master key. This must be less than or equal to -key-shares. This is aliased as + `-t`. + +- `-nonce` `(string: "")` - Nonce value provided at initialization. The same + nonce value must be provided with each unseal key. + +- `-pgp-keys` `(string: "...")` - Comma-separated list of paths to files on disk + containing public GPG keys OR a comma-separated list of Keybase usernames + using the format "keybase:". When supplied, the generated unseal + keys will be encrypted and base64-encoded in the order specified in this list. + +- `-status` `(bool: false)` - Print the status of the current attempt without + providing an unseal key. The default is false. + +- `-target` `(string: "barrier")` - Target for rekeying. "recovery" only applies + when HSM support is enabled. + +### Backup Options + +- `-backup` `(bool: false)` - Store a backup of the current PGP encrypted unseal + keys in Vault's core. The encrypted values can be recovered in the event of + failure or discarded after success. See the -backup-delete and + -backup-retrieve options for more information. This option only applies when + the existing unseal keys were PGP encrypted. + +- `-backup-delete` `(bool: false)` - Delete any stored backup unseal keys. + +- `-backup-retrieve` `(bool: false)` - Retrieve the backed-up unseal keys. This + option is only available if the PGP keys were provided and the backup has not + been deleted. From cfc0940a23b03240217ce3ac5575d3e3ccf68646 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:09:40 -0400 Subject: [PATCH 123/281] Add "operator rotate" command documentation --- .../docs/commands/operator/rotate.html.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 website/source/docs/commands/operator/rotate.html.md diff --git a/website/source/docs/commands/operator/rotate.html.md b/website/source/docs/commands/operator/rotate.html.md new file mode 100644 index 0000000000..80f356e655 --- /dev/null +++ b/website/source/docs/commands/operator/rotate.html.md @@ -0,0 +1,36 @@ +--- +layout: "docs" +page_title: "operator rotate - Command" +sidebar_current: "docs-commands-operator-rotate" +description: |- + The "operator rotate" rotates the underlying encryption key which is used to + secure data written to the storage backend. This installs a new key in the key + ring. This new key is used to encrypted new data, while older keys in the ring + are used to decrypt older data. +--- + +# operator rotate + +The `operator rotate` rotates the underlying encryption key which is used to +secure data written to the storage backend. This installs a new key in the key +ring. This new key is used to encrypted new data, while older keys in the ring +are used to decrypt older data. + +This is an online operation and does not cause downtime. This command is run +per-cluster (not per-server), since Vault servers in HA mode share the same +storage backend. + +## Examples + +Rotate Vault's encryption key: + +```text +$ vault operator rotate +Key Term 3 +Install Time 01 May 17 10:30 UTC +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From eaf634ca34542f880b2b7a14bb375976e97117a9 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:09:46 -0400 Subject: [PATCH 124/281] Add "operator seal" command documentation --- .../docs/commands/operator/seal.html.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 website/source/docs/commands/operator/seal.html.md diff --git a/website/source/docs/commands/operator/seal.html.md b/website/source/docs/commands/operator/seal.html.md new file mode 100644 index 0000000000..4614eb3a6c --- /dev/null +++ b/website/source/docs/commands/operator/seal.html.md @@ -0,0 +1,39 @@ +--- +layout: "docs" +page_title: "operator seal - Command" +sidebar_current: "docs-commands-operator-seal" +description: |- + The "operator seal" seals the Vault server. Sealing tells the Vault server to + stop responding to any operations until it is unsealed. When sealed, the Vault + server discards its in-memory master key to unlock the data, so it is + physically blocked from responding to operations unsealed. +--- + +# operator seal + +The `operator seal` seals the Vault server. Sealing tells the Vault server to +stop responding to any operations until it is unsealed. When sealed, the Vault +server discards its in-memory master key to unlock the data, so it is physically +blocked from responding to operations unsealed. + +If an unseal is in progress, sealing the Vault will reset the unsealing process. +Users will have to re-enter their portions of the master key again. + +This command does nothing if the Vault server is already sealed. + +For more information on sealing and unsealing, please the [seal concepts +page](/docs/concepts/seal.html). + +## Examples + +Seal a Vault server: + +```text +$ vault operator seal +Success! Vault is sealed. +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From ccb3bec2a504d035d4bb8e18d3e04dcbf4e9b39d Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:10:34 -0400 Subject: [PATCH 125/281] Add "operator step-down" command documentation --- .../docs/commands/operator/step-down.html.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 website/source/docs/commands/operator/step-down.html.md diff --git a/website/source/docs/commands/operator/step-down.html.md b/website/source/docs/commands/operator/step-down.html.md new file mode 100644 index 0000000000..63764dc23d --- /dev/null +++ b/website/source/docs/commands/operator/step-down.html.md @@ -0,0 +1,30 @@ +--- +layout: "docs" +page_title: "operator step-down - Command" +sidebar_current: "docs-commands-operator-step-down" +description: |- + The "operator step-down" forces the Vault server at the given address to step + down from active duty. +--- + +# operator step-down + +The `operator step-down` forces the Vault server at the given address to step +down from active duty. While the affected node will have a delay before +attempting to acquire the leader lock again, if no other Vault nodes acquire the +lock beforehand, it is possible for the same node to re-acquire the lock and +become active again. + +## Examples + +Force a Vault server to step down as the leader: + +```text +$ vault operator step-down +Success! Stepped down: http://127.0.0.1:8200 +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From ca7a0a5d4db2f992761bd152782ad5952c1bbfaa Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:10:39 -0400 Subject: [PATCH 126/281] Add "operator unseal" command documentation --- .../docs/commands/operator/unseal.html.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 website/source/docs/commands/operator/unseal.html.md diff --git a/website/source/docs/commands/operator/unseal.html.md b/website/source/docs/commands/operator/unseal.html.md new file mode 100644 index 0000000000..ecdee34c61 --- /dev/null +++ b/website/source/docs/commands/operator/unseal.html.md @@ -0,0 +1,54 @@ +--- +layout: "docs" +page_title: "operator unseal - Command" +sidebar_current: "docs-commands-operator-unseal" +description: |- + The "operator unseal" allows the user to provide a portion of the master key + to unseal a Vault server. +--- + +# operator unseal + +The `operator unseal` allows the user to provide a portion of the master key to +unseal a Vault server. Vault starts in a sealed state. It cannot perform +operations until it is unsealed. This command accepts a portion of the master +key (an "unseal key"). + +The unseal key can be supplied as an argument to the command, but this is +not recommended as the unseal key will be available in your history: + +```text +$ vault operator unseal IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= +``` + +Instead, run the command with no arguments and it will prompt for the key: + +```text +$ vault operator unseal +Key (will be hidden): IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo= +``` + +For more information on sealing and unsealing, please the [seal concepts +page](/docs/concepts/seal.html). + + +## Examples + +Provide an unseal key: + +```text +$ vault operator unseal +Key (will be hidden): +Sealed: false +Key Shares: 1 +Key Threshold: 1 +Unseal Progress: 0 +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-reset` `(bool: false)` - Discard any previously entered keys to the unseal + process. From 2217c037d731295eb050b2a218febd7d8df45257 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:10:48 -0400 Subject: [PATCH 127/281] Add "policy delete" command documentation --- .../docs/commands/policy/delete.html.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 website/source/docs/commands/policy/delete.html.md diff --git a/website/source/docs/commands/policy/delete.html.md b/website/source/docs/commands/policy/delete.html.md new file mode 100644 index 0000000000..abcf284595 --- /dev/null +++ b/website/source/docs/commands/policy/delete.html.md @@ -0,0 +1,31 @@ +--- +layout: "docs" +page_title: "policy delete - Command" +sidebar_current: "docs-commands-policy-delete" +description: |- + The "policy delete" command deletes the policy named NAME in the Vault server. + Once the policy is deleted, all tokens associated with the policy are affected + immediately. +--- + +# policy delete + +The `policy delete` command deletes the policy named NAME in the Vault server. +Once the policy is deleted, all tokens associated with the policy are affected +immediately. + +Note that it is not possible to delete the "default" or "root" policies. These +are built-in policies. + +## Examples + +Delete the policy named "my-policy": + +```text +$ vault policy delete my-policy +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 0dc501cc9bd3e958002c6e86ee8d62b6cd3f3363 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:10:55 -0400 Subject: [PATCH 128/281] Add "policy fmt" command documentation --- .../source/docs/commands/policy/fmt.html.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 website/source/docs/commands/policy/fmt.html.md diff --git a/website/source/docs/commands/policy/fmt.html.md b/website/source/docs/commands/policy/fmt.html.md new file mode 100644 index 0000000000..e692f6172e --- /dev/null +++ b/website/source/docs/commands/policy/fmt.html.md @@ -0,0 +1,28 @@ +--- +layout: "docs" +page_title: "policy fmt - Command" +sidebar_current: "docs-commands-policy-fmt" +description: |- + The "policy fmt" formats a local policy file to the policy specification. This + command will overwrite the file at the given PATH with the properly-formatted + policy file contents. +--- + +# policy fmt + +The `policy fmt` formats a local policy file to the policy specification. This +command will overwrite the file at the given PATH with the properly-formatted +policy file contents. + +## Examples + +Format the local file "my-policy.hcl": + +```text +$ vault policy fmt my-policy.hcl +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 7d96e6cc4e60018b5b713f66d2263c67df7690a6 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:11:00 -0400 Subject: [PATCH 129/281] Add "policy list" command documentation --- .../source/docs/commands/policy/list.html.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 website/source/docs/commands/policy/list.html.md diff --git a/website/source/docs/commands/policy/list.html.md b/website/source/docs/commands/policy/list.html.md new file mode 100644 index 0000000000..4ba30291f2 --- /dev/null +++ b/website/source/docs/commands/policy/list.html.md @@ -0,0 +1,28 @@ +--- +layout: "docs" +page_title: "policy list - Command" +sidebar_current: "docs-commands-policy-list" +description: |- + The "policy list" command Lists the names of the policies that are installed + on the Vault server. +--- + +# policy list + +The `policy list` command Lists the names of the policies that are installed on +the Vault server. + +## Examples + +List the available policies: + +```text +$ vault policy list +default +root +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 88524e1f00bae91fa192af140ba4dbc315b181fa Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:11:03 -0400 Subject: [PATCH 130/281] Add "policy read" command documentation --- .../source/docs/commands/policy/read.html.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 website/source/docs/commands/policy/read.html.md diff --git a/website/source/docs/commands/policy/read.html.md b/website/source/docs/commands/policy/read.html.md new file mode 100644 index 0000000000..3f9e31956d --- /dev/null +++ b/website/source/docs/commands/policy/read.html.md @@ -0,0 +1,26 @@ +--- +layout: "docs" +page_title: "policy read - Command" +sidebar_current: "docs-commands-policy-read" +description: |- + The "policy read" command prints the contents and metadata of the Vault policy + named NAME. If the policy does not exist, an error is returned. +--- + +# policy read + +The `policy read` command prints the contents and metadata of the Vault policy +named NAME. If the policy does not exist, an error is returned. + +## Examples + +Read the policy named "my-policy": + +```text +$ vault policy read my-policy +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 44ea6f47d0c5d2359e0b1089ead9572524416bf4 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:11:11 -0400 Subject: [PATCH 131/281] Add "policy write" command documentation --- .../source/docs/commands/policy/write.html.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 website/source/docs/commands/policy/write.html.md diff --git a/website/source/docs/commands/policy/write.html.md b/website/source/docs/commands/policy/write.html.md new file mode 100644 index 0000000000..ff092699fb --- /dev/null +++ b/website/source/docs/commands/policy/write.html.md @@ -0,0 +1,37 @@ +--- +layout: "docs" +page_title: "policy write - Command" +sidebar_current: "docs-commands-policy-write" +description: |- + The "policy write" command uploads a policy with name NAME from the contents + of a local file PATH or stdin. If PATH is "-", the policy is read from stdin. + Otherwise, it is loaded from the file at the given path on the local disk. +--- + +# policy write + +The `policy write` command uploads a policy with name NAME from the contents of +a local file PATH or stdin. If PATH is "-", the policy is read from stdin. +Otherwise, it is loaded from the file at the given path on the local disk. + +For details on the policy syntax, please see the [policy +documentation](/docs/concepts/policies.html). + +## Examples + +Upload a policy named "my-policy" from "/tmp/policy.hcl" on the local disk: + +```text +$ vault policy write my-policy /tmp/policy.hcl +``` + +Upload a policy from stdin: + +```text +$ cat my-policy.hcl | vault policy write my-policy - +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 872ccb49cf07424299be54357276c3adec320e06 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:11:38 -0400 Subject: [PATCH 132/281] Add "secrets disable" command documentation --- .../docs/commands/secrets/disable.html.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 website/source/docs/commands/secrets/disable.html.md diff --git a/website/source/docs/commands/secrets/disable.html.md b/website/source/docs/commands/secrets/disable.html.md new file mode 100644 index 0000000000..27ef3e63fd --- /dev/null +++ b/website/source/docs/commands/secrets/disable.html.md @@ -0,0 +1,31 @@ +--- +layout: "docs" +page_title: "secrets disable - Command" +sidebar_current: "docs-commands-secrets-disable" +description: |- + The "secrets disable" command disables an secrets engine at a given PATH. The + argument corresponds to the enabled PATH of the engine, not the TYPE! All + secrets created by this engine are revoked and its Vault data is removed. +--- + +# secrets disable + +The `secrets disable` command disables an secrets engine at a given PATH. The +argument corresponds to the enabled PATH of the engine, not the TYPE! All +secrets created by this engine are revoked and its Vault data is removed. + +Once an secrets engine is disabled, **all secrets generated via the secrets +engine are immediately revoked.** + +## Examples + +Disable the secrets engine enabled at aws/: + +```text +$ vault secrets disable aws/ +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 7e9c0004b4e7f6da6eace04768957358e1fb98ba Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:11:47 -0400 Subject: [PATCH 133/281] Add "secrets enable" command documentation --- .../docs/commands/secrets/enable.html.md | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 website/source/docs/commands/secrets/enable.html.md diff --git a/website/source/docs/commands/secrets/enable.html.md b/website/source/docs/commands/secrets/enable.html.md new file mode 100644 index 0000000000..5d3542937a --- /dev/null +++ b/website/source/docs/commands/secrets/enable.html.md @@ -0,0 +1,88 @@ +--- +layout: "docs" +page_title: "secrets enable - Command" +sidebar_current: "docs-commands-secrets-enable" +description: |- + The "secrets enable" command enables an secrets engine at a given path. If an + secrets engine already exists at the given path, an error is returned. After + the secrets engine is enabled, it usually needs configuration. The + configuration varies by secrets engine. +--- + +# secrets enable + +The `secrets enable` command enables an secrets engine at a given path. If an +secrets engine already exists at the given path, an error is returned. After the +secrets engine is enabled, it usually needs configuration. The configuration +varies by secrets engine. + +By default, secrets engines are enabled at the path corresponding to their TYPE, +but users can customize the path using the `-path` option. + +Some secrets engines persist data, some act as data pass-through, and some +generate dynamic credentials. The secrets engine will likely require +configuration after it is mounted. For details on the specific configuration +options, please see the [secrets engine +documentation](/docs/secrets/index.html). + + +## Examples + +Enable the AWS secrets engine at "aws/": + +```text +$ vault secrets enable aws +Success! Enabled the aws secrets engine at: aws/ +``` + +Enable the SSH secrets engine at ssh-prod/: + +```text +$ vault secrets enable -path=ssh-prod ssh +``` + +Enable the database secrets engine with an explicit maximum TTL of 30m: + +```text +$ vault secrets enable -max-lease-ttl=30m database +``` + +Enable a custom plugin (after it is registered in the plugin registry): + +```text +$ vault secrets enable -path=my-secrets -plugin-name=my-plugin plugin +``` + +For more information on the specific configuration options and paths, please see +the [secrets engine](/docs/secrets/index.html) documentation. + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-default-lease-ttl` `(duration: "")` - The default lease TTL for this secrets + engine. If unspecified, this defaults to the Vault server's globally + configured default lease TTL. + +- `-description` `(string: "")` - Human-friendly description for the purpose of + this engine. + +- `-force-no-cache` `(bool: false)` - Force the secrets engine to disable + caching. If unspecified, this defaults to the Vault server's globally + configured cache settings. This does not affect caching of the underlying + encrypted data storage. + +- `-local` `(bool: false)` - Mark the secrets engine as local-only. Local + engines are not replicated or removed by replication. + +- `-max-lease-ttl` `(duration: "")` The maximum lease TTL for this secrets + engine. If unspecified, this defaults to the Vault server's globally + configured maximum lease TTL. + +- `-path` `(string: "")` Place where the secrets engine will be accessible. This + must be unique cross all secrets engines. This defaults to the "type" of the + secrets engine. + +- `-plugin-name` `(string: "")` - Name of the secrets engine plugin. This plugin + name must already exist in Vault's plugin catalog. From 4f5a073a627117c5adcf4079fdc8ae33a10daa05 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:11:52 -0400 Subject: [PATCH 134/281] Add "secrets list" command documentation --- .../source/docs/commands/secrets/list.html.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 website/source/docs/commands/secrets/list.html.md diff --git a/website/source/docs/commands/secrets/list.html.md b/website/source/docs/commands/secrets/list.html.md new file mode 100644 index 0000000000..f384dd95a3 --- /dev/null +++ b/website/source/docs/commands/secrets/list.html.md @@ -0,0 +1,49 @@ +--- +layout: "docs" +page_title: "secrets list - Command" +sidebar_current: "docs-commands-secrets-list" +description: |- + The "secrets list" command lists the enabled secret engines on the Vault + server. This command also outputs information about the enabled path including + configured TTLs and human-friendly descriptions. A TTL of "system" indicates + that the system default is in use. +--- + +# secrets list + +The `secrets list` command lists the enabled secret engines on the Vault server. +This command also outputs information about the enabled path including +configured TTLs and human-friendly descriptions. A TTL of "system" indicates +that the system default is in use. + +## Examples + +List all enabled secrets engines: + +```text +$ vault secrets list +Path Type Description +---- ---- ----------- +cubbyhole/ cubbyhole per-token private secret storage +secret/ generic generic secret storage +sys/ system system endpoints used for control, policy and debugging +``` + +List all enabled secrets engines with detailed output: + +```text +$ vault secrets list -detailed +Path Type Accessor Plugin Default TTL Max TTL Force No Cache Replication Description +---- ---- -------- ------ ----------- ------- -------------- ----------- ----------- +cubbyhole/ cubbyhole cubbyhole_10fbb584 n/a n/a n/a false local per-token private secret storage +secret/ generic generic_167ce199 n/a system system false replicated generic secret storage +sys/ system system_a9fd745d n/a n/a n/a false replicated system endpoints used for control, policy and debugging +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-detailed` `(bool: false)` - Print detailed information such as configuration + and replication status about each secrets engine. From bfaabc5cae1c62b2b0a8d22541167704bd7fe27c Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:12:03 -0400 Subject: [PATCH 135/281] Add "secrets move" command documentation --- .../source/docs/commands/secrets/move.html.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 website/source/docs/commands/secrets/move.html.md diff --git a/website/source/docs/commands/secrets/move.html.md b/website/source/docs/commands/secrets/move.html.md new file mode 100644 index 0000000000..11b6f8f07f --- /dev/null +++ b/website/source/docs/commands/secrets/move.html.md @@ -0,0 +1,31 @@ +--- +layout: "docs" +page_title: "secrets move - Command" +sidebar_current: "docs-commands-secrets-move" +description: |- + The "secrets move" command moves an existing secrets engine to a new path. Any + leases from the old secrets engine are revoked, but all configuration + associated with the engine is preserved. +--- + +# secrets move + +The `secrets move` command moves an existing secrets engine to a new path. Any +leases from the old secrets engine are revoked, but all configuration associated +with the engine is preserved. + +**Moving an existing secrets engine will revoke any leases from the old +engine.** + +## Examples + +Move the existing secrets engine at secret/ to generic/: + +```text +$ vault secrets move secret/ generic/ +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From ee6849c01c17ee1a3483c8645d652d7dfb7113e5 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:12:07 -0400 Subject: [PATCH 136/281] Add "secrets tune" command documentation --- .../source/docs/commands/secrets/tune.html.md | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 website/source/docs/commands/secrets/tune.html.md diff --git a/website/source/docs/commands/secrets/tune.html.md b/website/source/docs/commands/secrets/tune.html.md new file mode 100644 index 0000000000..ce1ab57247 --- /dev/null +++ b/website/source/docs/commands/secrets/tune.html.md @@ -0,0 +1,38 @@ +--- +layout: "docs" +page_title: "secrets tune - Command" +sidebar_current: "docs-commands-secrets-tune" +description: |- + The "secrets tune" command tunes the configuration options for the secrets + engine at the given PATH. The argument corresponds to the PATH where the + secrets engine is enabled, not the TYPE! +--- + +# secrets tune + +The `secrets tune` command tunes the configuration options for the secrets +engine at the given PATH. The argument corresponds to the PATH where the secrets +engine is enabled, not the TYPE! + +## Examples + +Tune the default lease for the PKI secrets engine: + +```text +$ vault secrets tune -default-lease-ttl=72h pki/ +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-default-lease-ttl` `(duration: "")` - The default lease TTL for this secrets + engine. If unspecified, this defaults to the Vault server's globally + configured default lease TTL, or a previously configured value for the secrets + engine. + +- `-max-lease-ttl` `(duration: "")` - The maximum lease TTL for this secrets + engine. If unspecified, this defaults to the Vault server's globally + configured maximum lease TTL, or a previously configured value for the secrets + engine. From 36b6563867e61c7abb218511e575cddb9bb6894d Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:12:32 -0400 Subject: [PATCH 137/281] Add "token capabilities" command documentation --- .../docs/commands/token/capabilities.html.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 website/source/docs/commands/token/capabilities.html.md diff --git a/website/source/docs/commands/token/capabilities.html.md b/website/source/docs/commands/token/capabilities.html.md new file mode 100644 index 0000000000..393e801afb --- /dev/null +++ b/website/source/docs/commands/token/capabilities.html.md @@ -0,0 +1,39 @@ +--- +layout: "docs" +page_title: "token capabilities - Command" +sidebar_current: "docs-commands-token-capabilities" +description: |- + The "token capabilities" command fetches the capabilities of a token for a + given path. +--- + +# token capabilities + +The `token capabilities` command fetches the capabilities of a token for a given +path. + +If a TOKEN is provided as an argument, this command uses the "/sys/capabilities" +endpoint and permission. If no TOKEN is provided, this command uses the +"/sys/capabilities-self" endpoint and permission with the locally authenticated +token. + +## Examples + +List capabilities for the local token on the "secret/foo" path: + +```text +$ vault token capabilities secret/foo +read +``` + +List capabilities for a token on the "cubbyhole/foo" path: + +```text +$ vault token capabilities 96ddf4bc-d217-f3ba-f9bd-017055595017 database/creds/readonly +deny +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From b8987e00c7ecb6da233219bed78e59407a3a991e Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:12:38 -0400 Subject: [PATCH 138/281] Add "token create" command documentation --- .../source/docs/commands/token/create.html.md | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 website/source/docs/commands/token/create.html.md diff --git a/website/source/docs/commands/token/create.html.md b/website/source/docs/commands/token/create.html.md new file mode 100644 index 0000000000..ea475ce648 --- /dev/null +++ b/website/source/docs/commands/token/create.html.md @@ -0,0 +1,121 @@ +--- +layout: "docs" +page_title: "token create - Command" +sidebar_current: "docs-commands-token-create" +description: |- + The "token create" command creates a new token that can be used for + authentication. This token will be created as a child of the currently + authenticated token. The generated token will inherit all policies and + permissions of the currently authenticated token unless you explicitly define + a subset list policies to assign to the token. +--- + +# token create + +The `token create` command creates a new token that can be used for +authentication. This token will be created as a child of the currently +authenticated token. The generated token will inherit all policies and +permissions of the currently authenticated token unless you explicitly define a +subset list policies to assign to the token. + +A ttl can also be associated with the token. If a ttl is not associated with the +token, then it cannot be renewed. If a ttl is associated with the token, it will +expire after that amount of time unless it is renewed. + +Metadata associated with the token (specified with `-metadata`) is written to +the audit log when the token is used. + +If a role is specified, the role may override parameters specified here. + +## Examples + +Create a token attached to specific policies: + +```text +$ vault token create -policy=my-policy -policy=other-policy +Key Value +--- ----- +token 95eba8ed-f6fc-958a-f490-c7fd0eda5e9e +token_accessor 882d4a40-3796-d06e-c4f0-604e8503750b +token_duration 768h +token_renewable true +token_policies [default my-policy other-policy] +``` + +Create a periodic token: + +```text +$ vault token create -period=30m +Key Value +--- ----- +token fdb90d58-af87-024f-fdcd-9f95039e353a +token_accessor 4cd9177c-034b-a004-c62d-54bc56c0e9bd +token_duration 30m +token_renewable true +token_policies [my-policy] +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-field` `(string: "")` - Print only the field with the given name. Specifying + this option will take precedence over other formatting directives. The result + will not have a trailing newline making it idea for piping to other processes. + +- `-format` `(string: "table")` - Print the output in the given format. Valid + formats are "table", "json", or "yaml". This can also be specified via the + `VAULT_FORMAT` environment variable. + +### Command Options + +- `-display-name` `(string: "")` - Name to associate with this token. This is a + non-sensitive value that can be used to help identify created secrets (e.g. + prefixes). + +- `-explicit-max-ttl` `(duration: "")` - Explicit maximum lifetime for the + token. Unlike normal TTLs, the maximum TTL is a hard limit and cannot be + exceeded. This is specified as a numeric string with suffix like "30s" or + "5m". + +- `-id` `(string: "")` - Value for the token. By default, this is an + auto-generated 36 character UUID. Specifying this value requires sudo + permissions. + +- `-metadata` `(k=v: "")` - Arbitrary key=value metadata to associate with the + token. This metadata will show in the audit log when the token is used. This + can be specified multiple times to add multiple pieces of metadata. + +- `-no-default-policy` `(bool: false)` - Detach the "default" policy from the + policy set for this token. + +- `-orphan` `(bool: false)` - Create the token with no parent. This prevents the + token from being revoked when the token which created it expires. Setting this + value requires sudo permissions. + +- `-period` `(duration: "")` - If specified, every renewal will use the given + period. Periodic tokens do not expire (unless `-explicit-max-ttl` is also + provided). Setting this value requires sudo permissions. This is specified as + a numeric string with suffix like "30s" or "5m". + +- `-policy` `(string: "")` - Name of a policy to associate with this token. This + can be specified multiple times to attach multiple policies. + +- `-renewable` `(bool: true)` - Allow the token to be renewed up to it's maximum + TTL. + +- `-role` `(string: "")` - Name of the role to create the token against. + Specifying -role may override other arguments. The locally authenticated Vault + token must have permission for "auth/token/create/". + +- `-ttl` `(duration: "")` - Initial TTL to associate with the token. Token + renewals may be able to extend beyond this value, depending on the configured + maximumTTLs. This is specified as a numeric string with suffix like "30s" or + "5m". + +- `-use-limit` `(int: 0)` - Number of times this token can be used. After the + last use, the token is automatically revoked. By default, tokens can be used + an unlimited number of times until their expiration. From 15b6cbf9e53ea269fb81fa49d94eed11b50c1b36 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:12:42 -0400 Subject: [PATCH 139/281] Add "token lookup" command documentation --- .../source/docs/commands/token/lookup.html.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 website/source/docs/commands/token/lookup.html.md diff --git a/website/source/docs/commands/token/lookup.html.md b/website/source/docs/commands/token/lookup.html.md new file mode 100644 index 0000000000..f9338103f7 --- /dev/null +++ b/website/source/docs/commands/token/lookup.html.md @@ -0,0 +1,51 @@ +--- +layout: "docs" +page_title: "token lookup - Command" +sidebar_current: "docs-commands-token-lookup" +description: |- + The "token lookup" displays information about a token or accessor. If a TOKEN + is not provided, the locally authenticated token is used. +--- + +# token lookup + +The `token lookup` displays information about a token or accessor. If a TOKEN is +not provided, the locally authenticated token is used. + +## Examples + +Get information about the locally authenticated token (this uses the +`/auth/token/lookup-self` endpoint and permission): + +```text +$ vault token lookup +``` + +Get information about a particular token (this uses the `/auth/token/lookup` +endpoint and permission): + +```text +$ vault token lookup 96ddf4bc-d217-f3ba-f9bd-017055595017 +``` + +Get information about a token via its accessor: + +```text +$ vault token lookup -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-format` `(default: "table")` - Print the output in the given format. Valid + formats are "table", "json", or "yaml". This can also be specified via the + `VAULT_FORMAT` environment variable. + +### Command Options + +- `-accessor` `(bool: false)` - Treat the argument as an accessor instead of a + token. When this option is selected, the output will NOT include the token. From 46b3f74988b33e4234c76955aff772d2c5bf32ab Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:12:46 -0400 Subject: [PATCH 140/281] Add "token renew" command documentation --- .../source/docs/commands/token/renew.html.md | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 website/source/docs/commands/token/renew.html.md diff --git a/website/source/docs/commands/token/renew.html.md b/website/source/docs/commands/token/renew.html.md new file mode 100644 index 0000000000..06e01fcd68 --- /dev/null +++ b/website/source/docs/commands/token/renew.html.md @@ -0,0 +1,56 @@ +--- +layout: "docs" +page_title: "token renew - Command" +sidebar_current: "docs-commands-token-renew" +description: |- + The "token renew" renews a token's lease, extending the amount of time it can + be used. If a TOKEN is not provided, the locally authenticated token is used. + Lease renewal will fail if the token is not renewable, the token has already + been revoked, or if the token has already reached its maximum TTL. +--- + +# token renew + +The `token renew` renews a token's lease, extending the amount of time it can be +used. If a TOKEN is not provided, the locally authenticated token is used. Lease +renewal will fail if the token is not renewable, the token has already been +revoked, or if the token has already reached its maximum TTL. + +## Examples + +Renew a token (this uses the `/auth/token/renew` endpoint and permission): + +```text +$ vault token renew 96ddf4bc-d217-f3ba-f9bd-017055595017 +``` + +Renew the currently authenticated token (this uses the `/auth/token/renew-self` +endpoint and permission): + +```text +$ vault token renew +``` + +Renew a token requesting a specific increment value: + +```text +$ vault token renew -increment=30m 96ddf4bc-d217-f3ba-f9bd-017055595017 +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-format` `(default: "table")` - Print the output in the given format. Valid + formats are "table", "json", or "yaml". This can also be specified via the + `VAULT_FORMAT` environment variable. + +### Command Options + +- `-increment` `(duration: "")` - Request a specific increment for renewal. + Vault is not required to honor this request. If not supplied, Vault will use + the default TTL. This is specified as a numeric string with suffix like "30s" + or "5m". This is aliased as "-i". From 0024eca6b1353591c8824c8fbda8939397e8b3dd Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:12:51 -0400 Subject: [PATCH 141/281] Add "token revoke" command documentation --- .../source/docs/commands/token/revoke.html.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 website/source/docs/commands/token/revoke.html.md diff --git a/website/source/docs/commands/token/revoke.html.md b/website/source/docs/commands/token/revoke.html.md new file mode 100644 index 0000000000..a99ac96151 --- /dev/null +++ b/website/source/docs/commands/token/revoke.html.md @@ -0,0 +1,53 @@ +--- +layout: "docs" +page_title: "token revoke - Command" +sidebar_current: "docs-commands-token-revoke" +description: |- + The "token revoke" revokes authentication tokens and their children. If a + TOKEN is not provided, the locally authenticated token is used. +--- + +# token revoke + +The `token revoke` revokes authentication tokens and their children. If a TOKEN +is not provided, the locally authenticated token is used. The `-mode` flag can +be used to control the behavior of the revocation. + +## Examples + +Revoke a token and all the token's children: + +```text +$ vault token revoke 96ddf4bc-d217-f3ba-f9bd-017055595017 +Success! Revoked token (if it existed) +``` + +Revoke a token leaving the token's children: + +```text +$ vault token revoke -mode=orphan 96ddf4bc-d217-f3ba-f9bd-017055595017 +Success! Revoked token (if it existed) +``` + +Revoke a token by accessor: + +```text +$ vault token revoke -accessor 9793c9b3-e04a-46f3-e7b8-748d7da248da +Success! Revoked token (if it existed) +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +- `-accessor` `(bool: false)` - Treat the argument as an accessor instead of a + token. + +- `-mode` `(string: "")` - Type of revocation to perform. If unspecified, Vault + will revoke the token and all of the token's children. If "orphan", Vault will + revoke only the token, leaving the children as orphans. If "path", tokens + created from the given authentication path prefix are deleted along with their + children. + +- `-self` - Perform the revocation on the currently authenticated token. From 0844c285b222df683449588ff2e845701dc525c6 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:13:11 -0400 Subject: [PATCH 142/281] Add "audit" command documentation --- website/source/docs/commands/audit.html.md | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 website/source/docs/commands/audit.html.md diff --git a/website/source/docs/commands/audit.html.md b/website/source/docs/commands/audit.html.md new file mode 100644 index 0000000000..802de73f02 --- /dev/null +++ b/website/source/docs/commands/audit.html.md @@ -0,0 +1,57 @@ +--- +layout: "docs" +page_title: "audit - Command" +sidebar_current: "docs-commands-audit" +description: |- + The "audit" command groups subcommands for interacting with Vault's audit + devices. Users can list, enable, and disable audit devices. +--- + +# audit + +The `audit` command groups subcommands for interacting with Vault's audit +devices. Users can list, enable, and disable audit devices. + +For more information, please see the [audit device +documentation](/docs/audit/index.html) + +## Examples + +Enable an audit device: + +```text +$ vault audit enable file file_path=/tmp/my-file.txt +Success! Enabled the file audit device at: file/ +``` + +List all audit devices: + +```text +$ vault audit list +Path Type Description +---- ---- ----------- +file/ file n/a +``` + +Disable an audit device: + +```text +$ vault audit disable file/ +Success! Disabled audit device (if it was enabled) at: file/ +``` + +## Usage + +```text +Usage: vault audit [options] [args] + + # ... + +Subcommands: + disable Disables an audit device + enable Enables an audit device + list Lists enabled audit devices +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar. From e776921f96e653dc80d83148259b7b0aa88028cd Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:13:15 -0400 Subject: [PATCH 143/281] Add "auth" command documentation --- website/source/docs/commands/auth.html.md | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 website/source/docs/commands/auth.html.md diff --git a/website/source/docs/commands/auth.html.md b/website/source/docs/commands/auth.html.md new file mode 100644 index 0000000000..d1617071cb --- /dev/null +++ b/website/source/docs/commands/auth.html.md @@ -0,0 +1,81 @@ +--- +layout: "docs" +page_title: "auth - Command" +sidebar_current: "docs-commands-auth" +description: |- + The "auth" command groups subcommands for interacting with Vault's auth + methods. Users can list, enable, disable, and get help for different auth + methods. +--- + +# auth + +The `auth` command groups subcommands for interacting with Vault's auth methods. +Users can list, enable, disable, and get help for different auth methods. + +For more information, please see the [auth method +documentation](/docs/auth/index.html) or the [authentication +concepts](/docs/concepts/auth.html) page. + +To authenticate to Vault as a user or machine, use the [`vault +login`](/docs/commands/login.html) command instead. This command is for +interacting with the auth methods themselves, not authenticating to Vault. + +## Examples + +Enable an auth method: + +```text +$ vault auth enable userpass +Success! Enabled userpass auth method at: userpass/ +``` + +List all auth methods: + +```text +$ vault auth list +Path Type Description +---- ---- ----------- +token/ token token based credentials +userpass/ userpass n/a +``` + +Get help about how to authenticate to a particular auth method: + +```text +$ vault auth help userpass/ +Usage: vault login -method=userpass [CONFIG K=V...] +# ... +``` + +Disable an auth method: + +```text +$ vault auth disable userpass/ +Success! Disabled the auth method (if it existed) at: userpass/ +``` + +Tune an auth method: + +```text +$ vault auth tune -max-lease-ttl=30m userpass/ +Success! Tuned the auth method at: userpass/ +``` + +## Usage + +```text +Usage: vault auth [options] [args] + + # ... + +Subcommands: + disable Disables an auth method + enable Enables a new auth method + help Prints usage for an auth method + list Lists enabled auth methods + tune Tunes an auth method configuration +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar. From 9d6cae1f5def94f6a794ad5558ec1dfb8fddb466 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:13:20 -0400 Subject: [PATCH 144/281] Add "delete" command documentation --- website/source/docs/commands/delete.html.md | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 website/source/docs/commands/delete.html.md diff --git a/website/source/docs/commands/delete.html.md b/website/source/docs/commands/delete.html.md new file mode 100644 index 0000000000..5f89bd3149 --- /dev/null +++ b/website/source/docs/commands/delete.html.md @@ -0,0 +1,40 @@ +--- +layout: "docs" +page_title: "delete - Command" +sidebar_current: "docs-commands-delete" +description: |- + The "delete" command deletes secrets and configuration from Vault at the given + path. The behavior of "delete" is delegated to the backend corresponding to + the given path. +--- + +# delete + +The `delete` command deletes secrets and configuration from Vault at the given +path. The behavior of "delete" is delegated to the backend corresponding to the +given path. + +## Examples + +Remove data in the status secret backend: + +```text +$ vault delete secret/my-secret +``` + +Uninstall an encryption key in the transit backend: + +```text +$ vault delete transit/keys/my-key +``` + +Delete an IAM role: + +```text +$ vault delete aws/roles/ops +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 53f3db41b68221769242c713df2b29872f114ac3 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:13:25 -0400 Subject: [PATCH 145/281] Add "lease" command documentation --- website/source/docs/commands/lease.html.md | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 website/source/docs/commands/lease.html.md diff --git a/website/source/docs/commands/lease.html.md b/website/source/docs/commands/lease.html.md new file mode 100644 index 0000000000..f4505e54ee --- /dev/null +++ b/website/source/docs/commands/lease.html.md @@ -0,0 +1,49 @@ +--- +layout: "docs" +page_title: "lease - Command" +sidebar_current: "docs-commands-lease" +description: |- + The "lease" command groups subcommands for interacting with leases attached to + secrets. +--- + +# lease + +The `lease` command groups subcommands for interacting with leases attached to +secrets. For leases attached to tokens, use the [`vault +token`](/docs/commands/token.html) subcommand. + +## Examples + +Renew a lease: + +```text +$ vault lease renew database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +Key Value +--- ----- +lease_id database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +lease_duration 5m +lease_renewable true +``` + +Revoke a lease: + +```text +$ vault lease revoke database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +Success! Revoked lease: database/creds/readonly/27e1b9a1-27b8-83d9-9fe0-d99d786bdc83 +``` + +## Usage + +```text +Usage: vault lease [options] [args] + + # ... + +Subcommands: + renew Renews the lease of a secret + revoke Revokes leases and secrets +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar. From 737540b9ba0386fc17f8759293940c69f09694f7 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:13:29 -0400 Subject: [PATCH 146/281] Add "list" command documentation --- website/source/docs/commands/list.html.md | 26 +++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 website/source/docs/commands/list.html.md diff --git a/website/source/docs/commands/list.html.md b/website/source/docs/commands/list.html.md new file mode 100644 index 0000000000..785ff73b50 --- /dev/null +++ b/website/source/docs/commands/list.html.md @@ -0,0 +1,26 @@ +--- +layout: "docs" +page_title: "list - Command" +sidebar_current: "docs-commands-list" +description: |- + The "list" command lists data from Vault at the given path. This can be used + to list keys in a, given secret engine. +--- + +# list + +The `list` command lists data from Vault at the given path. This can be used to +list keys in a, given secret engine. + +## Examples + +List values under the "my-app" folder of the generic secret engine: + +```text +$ vault list secret/my-app/ +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From afd289f65abbfb59b92a958bf0f9477054234feb Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:13:34 -0400 Subject: [PATCH 147/281] Add "login" command documentation --- website/source/docs/commands/login.html.md | 113 +++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 website/source/docs/commands/login.html.md diff --git a/website/source/docs/commands/login.html.md b/website/source/docs/commands/login.html.md new file mode 100644 index 0000000000..8284ff8dce --- /dev/null +++ b/website/source/docs/commands/login.html.md @@ -0,0 +1,113 @@ +--- +layout: "docs" +page_title: "login - Command" +sidebar_current: "docs-commands-login" +description: |- + The "login" command authenticates users or machines to Vault using the + provided arguments. A successful authentication results in a Vault token - + conceptually similar to a session token on a website. +--- + +# login + +The `login` command authenticates users or machines to Vault using the provided +arguments. A successful authentication results in a Vault token - conceptually +similar to a session token on a website. By default, this token is cached on the +local machine for future requests. + +The `-method` flag allows using other authentication methods, such as userpass, +github, or cert. For these, additional "K=V" pairs may be required. For more +information about the list of configuration parameters available for a given +authentication method, use the "vault auth help TYPE". You can also use "vault +auth list" to see the list of enabled authentication methods. + +If an authentication method is enabled at a non-standard path, the `-method` +flag still refers to the canonical type, but the `-path` flag refers to the +enabled path. + +If the authentication is requested with response wrapping (via `-wrap-ttl`), +the returned token is automatically unwrapped unless: + + - The `-token-only` flag is used, in which case this command will output + the wrapping token. + + - The `-no-store` flag is used, in which case this command will output the + details of the wrapping token. + +## Examples + +By default, login uses a "token" method: + +```text +$ vault login 10862232-fd55-701c-9013-d764b5bc3953 +Success! You are now authenticated. The token information below is already +stored in the token helper. You do NOT need to run "vault login" again. Future +requests will use this token automatically. + +token: 10862232-fd55-701c-9013-d764b5bc3953 +accessor: 121533e1-20e7-0b4e-04d6-a8c18b8566d5 +renewable: true +policies: [my-policy] +``` + +To login with a different method, use `-method`: + +```text +$ vault login -method=userpass username=my-username +Password (will be hidden): +Success! You are now authenticated. The token information below is already +stored in the token helper. You do NOT need to run "vault login" again. Future +requests will use this token automatically. + +token: a700ded8-28ed-907d-abf4-23514b783d52 +accessor: e0857619-3912-9981-4e03-8d6c4b2f6c56 +duration: 768h +renewable: true +policies: [default] +``` + +If a github authentication method was enabled at the path "github-ent": + +```text +$ vault login -method=github -path=github-prod +Success! You are now authenticated. The token information below is already +stored in the token helper. You do NOT need to run "vault login" again. Future +requests will use this token automatically. + +token: 7eab2aba-b476-af57-e0af-dfcab7c541f6 +accessor: 2ae9b1cd-6d17-3428-bd44-986e97f6d2f3 +renewable: 22bc4d76-aa3b-1c53-4349-b230b459b56b +policies: [root] +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-field` `(string: "")` - Print only the field with the given name. Specifying + this option will take precedence over other formatting directives. The result + will not have a trailing newline making it idea for piping to other processes. + +- `-format` `(string: "table")` - Print the output in the given format. Valid + formats are "table", "json", or "yaml". This can also be specified via the + `VAULT_FORMAT` environment variable. + +## Command Options + +- `-method` `(string "token")` - Type of authentication to use such as + "userpass" or "ldap". Note this corresponds to the TYPE, not the enabled path. + Use -path to specify the path where the authentication is enabled. + +- `-no-store` `(bool: false)` - Do not persist the token to the token helper + (usually the local filesystem) after authentication for use in future + requests. The token will only be displayed in the command output. + +- `-path` `(string: "")` - Remote path in Vault where the authentication method + is enabled. This defaults to the TYPE of method (e.g. userpass -> userpass/). + +- `-token-only` `(bool: false)` - Output only the token with no verification. + This flag is a shortcut for "-field=token -no-store". Setting those + flags to other values will have no affect. From f3fc20b64b9c990fad79984a323d662ecb4328c9 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:13:38 -0400 Subject: [PATCH 148/281] Add "operator" command documentation --- website/source/docs/commands/operator.html.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 website/source/docs/commands/operator.html.md diff --git a/website/source/docs/commands/operator.html.md b/website/source/docs/commands/operator.html.md new file mode 100644 index 0000000000..5892dccd59 --- /dev/null +++ b/website/source/docs/commands/operator.html.md @@ -0,0 +1,68 @@ +--- +layout: "docs" +page_title: "operator - Command" +sidebar_current: "docs-commands-operator" +description: |- + The "operator" command groups subcommands for operators interacting with + Vault. Most users will not need to interact with these commands. +--- + +# operator + +The `operator` command groups subcommands for operators interacting with Vault. +Most users will not need to interact with these commands. + +## Examples + +Initialize a new Vault cluster: + +```text +$ vault operator init +Unseal Key 1: sP/4C/fwIDjJmHEC2bi/1Pa43uKhsUQMmiB31GRzFc0R +Unseal Key 2: kHkw2xTBelbDFIMEgEC8NVX7NDSAZ+rdgBJ/HuJwxOX+ +Unseal Key 3: +1+1ZnkQDfJFHDZPRq0wjFxEuEEHxDDOQxa8JJ/AYWcb +Unseal Key 4: cewseNJTLovmFrgpyY+9Hi5OgJlJgGGCg7PZyiVdPwN0 +Unseal Key 5: wyd7rMGWX5fi0k36X4e+C4myt5CoTmJsHJ0rdYT7BQcF + +Initial Root Token: 6662bb4a-afd0-4b6b-faad-e237fb564568 + +# ... +``` + +Force a Vault to resign leadership in a cluster: + +```text +$ vault operator step-down +Success! Stepped down: https://vault.rocks +``` + +Rotate Vault's underlying encryption key: + +```text +$ vault operator rotate +Success! Rotated key + +Key Term 2 +Install Time 01 Jan 07 12:30 UTC +``` + +## Usage + +```text +Usage: vault operator [options] [args] + + # ... + +Subcommands: + generate-root Generates a new root token + init Initializes a server + key-status Provides information about the active encryption key + rekey Generates new unseal keys + rotate Rotates the underlying encryption key + seal Seals the Vault server + step-down Forces Vault to resign active duty + unseal Unseals the Vault server +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar. From 7a88b59414ed686997b28c0ef19c8d5309bcfc27 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:13:50 -0400 Subject: [PATCH 149/281] Update "path-help" documentation --- .../source/docs/commands/path-help.html.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 website/source/docs/commands/path-help.html.md diff --git a/website/source/docs/commands/path-help.html.md b/website/source/docs/commands/path-help.html.md new file mode 100644 index 0000000000..0e5f77e8af --- /dev/null +++ b/website/source/docs/commands/path-help.html.md @@ -0,0 +1,93 @@ +--- +layout: "docs" +page_title: "path-help - Command" +sidebar_current: "docs-commands-path-help" +description: |- + The "path-help" command retrieves API help for paths. All endpoints in Vault + provide built-in help in markdown format. This includes system paths, secret + engines, and auth methods. +--- + +# path-help + +The `path-help` command retrieves API help for paths. All endpoints in Vault +provide built-in help in markdown format. This includes system paths, secret +engines, and auth methods. + +The help system is the easiest way to learn how to use the various systems +in Vault, and also allows you to discover new paths. + +Before using `path-help`, it is important to understand "paths" within Vault. +Paths are the parameters used for `vault read`, `vault write`, etc. An example +path is `secret/foo`, or `aws/config/root`. The paths available depend on the +secrets engines in use. Because of this, the interactive help is an +indispensable tool to finding what paths are supported. + +To discover what paths are supported, use `vault path-help PATH`. For example, +if you enabled the AWS secrets engine, you can use `vault path-help aws` to find +the paths supported by that backend. The paths are shown with regular +expressions, which can make them hard to parse, but they are also extremely +exact. + +## Examples + +Get help output for the generic secrets engine: + +```text +$ vault path-help secret +## DESCRIPTION + +The generic backend reads and writes arbitrary secrets to the backend. +The secrets are encrypted/decrypted by Vault: they are never stored +unencrypted in the backend and the backend never has an opportunity to +see the unencrypted value. + +Leases can be set on a per-secret basis. These leases will be sent down +when that secret is read, and it is assumed that some outside process will +revoke and/or replace the secret at that path. + +## PATHS + +The following paths are supported by this backend. To view help for +any of the paths below, use the help command with any route matching +the path pattern. Note that depending on the policy of your auth token, +you may or may not be able to access certain paths. + + ^.*$ + Pass-through secret storage to the storage backend, allowing you to + read/write arbitrary data into secret storage. +``` + +Once you've found a path you like, you can learn more about it by using `vault +path-help ` where "path" is a path that matches one of the regular +expressions from the backend help. + +```text +$ vault path-help secret/password +Request: password +Matching Route: ^.*$ + +Pass-through secret storage to the storage backend, allowing you to +read/write arbitrary data into secret storage. + +## PARAMETERS + + lease (string) + Lease time for this key when read. Ex: 1h + +## DESCRIPTION + +The pass-through backend reads and writes arbitrary data into secret storage, +encrypting it along the way. + +A lease can be specified when writing with the "lease" field. If given, then +when the secret is read, Vault will report a lease with that duration. It +is expected that the consumer of this backend properly writes renewed keys +before the lease is up. In addition, revocation must be handled by the +user of this backend. +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From 66448e3f82ad78ce2981d727e21b397c331906ed Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:00 -0400 Subject: [PATCH 150/281] Add "policy" command documentation --- website/source/docs/commands/policy.html.md | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 website/source/docs/commands/policy.html.md diff --git a/website/source/docs/commands/policy.html.md b/website/source/docs/commands/policy.html.md new file mode 100644 index 0000000000..b1ab03b575 --- /dev/null +++ b/website/source/docs/commands/policy.html.md @@ -0,0 +1,53 @@ +--- +layout: "docs" +page_title: "policy - Command" +sidebar_current: "docs-commands-policy" +description: |- + The "policy" command groups subcommands for interacting with policies. Users + can Users can write, read, and list policies in Vault. +--- + +# policy + +The `policy` command groups subcommands for interacting with policies. Users can +Users can write, read, and list policies in Vault. + +For more information, please see the [policy +documentation](/docs/concepts/policies.html). + +## Examples + +List all enabled policies: + +```text +$ vault policy list +``` + +Create a policy named "my-policy" from contents on local disk: + +```text +$ vault policy write my-policy ./my-policy.hcl +``` + +Delete the policy named my-policy: + +```text +$ vault policy delete my-policy +``` + +## Usage + +```text +Usage: vault policy [options] [args] + + # ... + +Subcommands: + delete Deletes a policy by name + list Lists the installed policies + read Prints the contents of a policy + write Uploads a named policy from a file +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar. From 8e0eeade04dcb950e9f8b6550976c0279418ab79 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:04 -0400 Subject: [PATCH 151/281] Add "read" command documentation --- website/source/docs/commands/read.html.md | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 website/source/docs/commands/read.html.md diff --git a/website/source/docs/commands/read.html.md b/website/source/docs/commands/read.html.md new file mode 100644 index 0000000000..d37520ac17 --- /dev/null +++ b/website/source/docs/commands/read.html.md @@ -0,0 +1,44 @@ +--- +layout: "docs" +page_title: "read - Command" +sidebar_current: "docs-commands-read" +description: |- + The "read" command reads data from Vault at the given path. This can be used + to read secrets, generate dynamic credentials, get configuration details, and + more. +--- + +# read + +The `read` command reads data from Vault at the given path. This can be used to +read secrets, generate dynamic credentials, get configuration details, and more. + +For a full list of examples and paths, please see the documentation that +corresponds to the secrets engine in use. + +## Examples + +Read a secret from the static secrets engine: + +```text +$ vault read secret/my-secret +Key Value +--- ----- +refresh_interval 768h +foo bar +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-field` `(string: "")` - Print only the field with the given name. Specifying + this option will take precedence over other formatting directives. The result + will not have a trailing newline making it idea for piping to other processes. + +- `-format` `(string: "table")` - Print the output in the given format. Valid + formats are "table", "json", or "yaml". This can also be specified via the + `VAULT_FORMAT` environment variable. From b046a6bcddbb48ebc2fc57b55d2bd8aa67993e68 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:09 -0400 Subject: [PATCH 152/281] Add "secrets" command documentation --- website/source/docs/commands/secrets.html.md | 80 ++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 website/source/docs/commands/secrets.html.md diff --git a/website/source/docs/commands/secrets.html.md b/website/source/docs/commands/secrets.html.md new file mode 100644 index 0000000000..ab91581ab1 --- /dev/null +++ b/website/source/docs/commands/secrets.html.md @@ -0,0 +1,80 @@ +--- +layout: "docs" +page_title: "secrets - Command" +sidebar_current: "docs-commands-secrets" +description: |- + The "secrets" command groups subcommands for interacting with Vault's secrets + engines. +--- + +# secrets + +The `secrets` command groups subcommands for interacting with Vault's secrets +engines. Each secret engine behaves differently. Please see the documentation +for more information. + +Some secrets engines persist data, some act as data pass-through, and some +generate dynamic credentials. The secrets engine will likely require +configuration after it is mounted. For details on the specific configuration +options, please see the [secrets engine +documentation](/docs/secrets/index.html). + +## Examples + +Enable a secrets engine: + +```text +$ vault secrets enable database +Success! Enabled the database secrets engine at: database/ +``` + +List all secrets engines: + +```text +$ vault secrets list +Path Type Description +---- ---- ----------- +cubbyhole/ cubbyhole per-token private secret storage +database/ database n/a +secret/ generic generic secret storage +sys/ system system endpoints used for control, policy and debugging +``` + +Move a secrets engine to a new path: + +```text +$ vault secrets move database/ db-prod/ +Success! Moved secrets engine database/ to: db-prod/ +``` + +Tune a secrets engine: + +```text +$ vault secrets tune -max-lease-ttl=30m db-prod/ +Success! Tuned the secrets engine at: db-prod/ +``` + +Disable a secrets engine: + +```text +$ vault secrets disable db-prod/ +Success! Disabled the secrets engine (if it existed) at: db-prod/ +``` + +## Usage + +```text +Usage: vault secrets [options] [args] + + # ... + +Subcommands: + disable Disable a secret engine + enable Enable a secrets engine + list List enabled secrets engines + move Move a secrets engine to a new path + tune Tune a secrets engine configuration +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar. From 914321259a43695e635534a5ccedd01b23c044bc Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:12 -0400 Subject: [PATCH 153/281] Add "server" command documentation --- website/source/docs/commands/server.html.md | 72 +++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 website/source/docs/commands/server.html.md diff --git a/website/source/docs/commands/server.html.md b/website/source/docs/commands/server.html.md new file mode 100644 index 0000000000..97219fca8e --- /dev/null +++ b/website/source/docs/commands/server.html.md @@ -0,0 +1,72 @@ +--- +layout: "docs" +page_title: "server - Command" +sidebar_current: "docs-commands-server" +description: |- + The "server" command starts a Vault server that responds to API requests. By + default, Vault will start in a "sealed" state. The Vault cluster must be + initialized before use. +--- + +# server + +The `server` command starts a Vault server that responds to API requests. By +default, Vault will start in a "sealed" state. The Vault cluster must be +initialized before use, usually by the `vault operator init` command. Each Vault +server must also be unsealed using the `vault operator unseal` command or the +API before the server can respond to requests. + +For more information, please see: + +- [`operator init` command](/docs/commands/operator/init.html) for information + on initializing a Vault server. + +- [`operator unseal` command](/docs/commands/operator/unseal.html) for + information on providing unseal keys. + +- [Vault configuration](/docs/configuration/index.html) for the syntax and + various configuration options for a Vault server. + +## Examples + +Start a server with a configuration file: + +```text +$ vault server -config=/etc/vault/config.hcl +``` + +Run in "dev" mode with a custom initial root token: + +```text +$ vault server -dev -dev-root-token-id="root" +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Command Options + +- `-config` `(string: "")` - Path to a configuration file or directory of + configuration files. This flag can be specified multiple times to load + multiple configurations. If the path is a directory, all files which end in + .hcl or .json are loaded. + +- `-log-level` `(string: "info")` - Log verbosity level. Supported values (in + order of detail) are "trace", "debug", "info", "warn", and "err". This can + also be specified via the VAULT_LOG environment variable. + +### Dev Options + +- `-dev` `(bool: false)` - Enable development mode. In this mode, Vault runs + in-memory and starts unsealed. As the name implies, do not run "dev" mode in + production. + +- `-dev-listen-address` `(string: "127.0.0.1:8200")` - Address to bind to in + "dev" mode. This can also be specified via the `VAULT_DEV_LISTEN_ADDRESS` + environment variable. + +- `-dev-root-token-id` `(string: "")` - Initial root token. This only applies + when running in "dev" mode. This can also be specified via the + `VAULT_DEV_ROOT_TOKEN_ID` environment variable. From a0d67d8540ef89c76d8013d4ec8399dbd0227080 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:17 -0400 Subject: [PATCH 154/281] Add "ssh" command documentation --- website/source/docs/commands/ssh.html.md | 107 +++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 website/source/docs/commands/ssh.html.md diff --git a/website/source/docs/commands/ssh.html.md b/website/source/docs/commands/ssh.html.md new file mode 100644 index 0000000000..0f3b849fbf --- /dev/null +++ b/website/source/docs/commands/ssh.html.md @@ -0,0 +1,107 @@ +--- +layout: "docs" +page_title: "ssh - Command" +sidebar_current: "docs-commands-ssh" +description: |- + The "ssh" command establishes an SSH connection with the target machine using + credentials obtained from an SSH secrets engine. +--- + +# ssh + +The `ssh` command establishes an SSH connection with the target machine. + +This command uses one of the SSH secrets engines to authenticate and +automatically establish an SSH connection to a host. This operation requires +that the SSH secrets engine is mounted and configured. + +The user must have `ssh` installed locally - this command will exec out to it +with the proper commands to provide an "SSH-like" consistent experience. + +## Examples + +SSH using the OTP mode (requires [sshpass](https://linux.die.net/man/1/sshpass) +for full automation): + +```text +$ vault ssh -mode=otp -role=my-role user@1.2.3.4 +``` + +SSH using the CA mode: + +```text +$ vault ssh -mode=ca -role=my-role user@1.2.3.4 +``` + +SSH using CA mode with host key verification: + +```text +$ vault ssh \ + -mode=ca \ + -role=my-role \ + -host-key-mount-point=host-signer \ + -host-key-hostnames=example.com \ + user@example.com +``` + +For step-by-step guides and instructions for each of the available SSH +authentication methods, please see the corresponding [SSH secrets +engine](/docs/secrets/ssh/index.html). + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-field` `(string: "")` - Print only the field with the given name. Specifying + this option will take precedence over other formatting directives. The result + will not have a trailing newline making it idea for piping to other processes. + +- `-format` `(string: "table")` - Print the output in the given format. Valid + formats are "table", "json", or "yaml". This can also be specified via the + `VAULT_FORMAT` environment variable. + +### SSH Options + +- `-mode` `(string: "")` - Name of the role to use to generate the key. + +- `-mount-point` `(string: "ssh/")` - Mount point to the SSH secrets engine. + +- `-no-exec` `(bool: false)` - Print the generated credentials, but do not + establish a connection. + +- `-role` `(string: "")` - Name of the role to use to generate the key. + +- `-strict-host-key-checking` `(string: "")` - Value to use for the SSH + configuration option "StrictHostKeyChecking". The default is ask. This can + also be specified via the `VAULT_SSH_STRICT_HOST_KEY_CHECKING` environment + variable. + +- `-user-known-hosts-file` `(string: "~/.ssh/known_hosts")` - Value to use for + the SSH configuration option "UserKnownHostsFile". This can also be specified + via the `VAULT_SSH_USER_KNOWN_HOSTS_FILE` environment variable. + +### CA Mode Options + +- `-host-key-hostnames` `(string: "*")` - List of hostnames to delegate for the + CA. The default value allows all domains and IPs. This is specified as a + comma-separated list of values. This can also be specified via the + `VAULT_SSH_HOST_KEY_HOSTNAMES` environment variable. + +- `-host-key-mount-point` `(string: "")` - Mount point to the SSH + secrets engine where host keys are signed. When given a value, Vault will + generate a custom "known_hosts" file with delegation to the CA at the provided + mount point to verify the SSH connection's host keys against the provided CA. + By default, host keys are validated against the user's local "known_hosts" + file. This flag forces strict key host checking and ignores a custom user + known hosts file. This can also be specified via the + `VAULT_SSH_HOST_KEY_MOUNT_POINT` environment variable. + +- `-private-key-path` `(string: "~/.ssh/id_rsa")` - Path to the SSH private key + to use for authentication. This must be the corresponding private key to + `-public-key-path`. + +- `-public-key-path` `(string: "~/.ssh/id_rsa.pub")` - Path to the SSH public + key to send to Vault for signing. From 0783fe73fda47d27da6e6dcb48b3e50f22c2607f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:21 -0400 Subject: [PATCH 155/281] Add "status" command documentation --- website/source/docs/commands/status.html.md | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 website/source/docs/commands/status.html.md diff --git a/website/source/docs/commands/status.html.md b/website/source/docs/commands/status.html.md new file mode 100644 index 0000000000..abb2f2846a --- /dev/null +++ b/website/source/docs/commands/status.html.md @@ -0,0 +1,44 @@ +--- +layout: "docs" +page_title: "status - Command" +sidebar_current: "docs-commands-status" +description: |- + The "status" command prints the current state of Vault including whether it is + sealed and if HA mode is enabled. This command prints regardless of whether + the Vault is sealed. +--- + +# status + +The `status` command prints the current state of Vault including whether it is +sealed and if HA mode is enabled. This command prints regardless of whether the +Vault is sealed. + +The exit code reflects the seal status: + +- 0 - unsealed +- 1 - error +- 2 - sealed + +## Examples + +Check the status: + +```text +$ vault status +Sealed: false +Key Shares: 5 +Key Threshold: 3 +Unseal Progress: 0 +Unseal Nonce: +Version: x.y.z +Cluster Name: vault-cluster-49ffd45f +Cluster ID: d2dad792-fb99-1c8d-452e-528d073ba205 + +High-Availability Enabled: false +``` + +## Usage + +There are no flags beyond the [standard set of flags](/docs/commands/index.html) +included on all commands. From a282ac98f2169fdfe994c9b132056eed1fa9293b Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:26 -0400 Subject: [PATCH 156/281] Add "token" command documentation --- website/source/docs/commands/token.html.md | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 website/source/docs/commands/token.html.md diff --git a/website/source/docs/commands/token.html.md b/website/source/docs/commands/token.html.md new file mode 100644 index 0000000000..e1cbe25685 --- /dev/null +++ b/website/source/docs/commands/token.html.md @@ -0,0 +1,54 @@ +--- +layout: "docs" +page_title: "token - Command" +sidebar_current: "docs-commands-token" +description: |- + The "token" command groups subcommands for interacting with tokens. Users can + create, lookup, renew, and revoke tokens. +--- + +# token + +The `token` command groups subcommands for interacting with tokens. Users can +create, lookup, renew, and revoke tokens. + +For more information on tokens, please see the [token concepts +page](/docs/concepts/tokens.html). + +## Examples + +Create a new token: + +```text +$ vault token create +``` + +Revoke a token: + +```text +$ vault token revoke 96ddf4bc-d217-f3ba-f9bd-017055595017 +``` + +Renew a token: + +```text +$ vault token renew 96ddf4bc-d217-f3ba-f9bd-017055595017 +``` + +## Usage + +```text +Usage: vault token [options] [args] + + # ... + +Subcommands: + capabilities Print capabilities of a token on a path + create Create a new token + lookup Display information about a token + renew Renew a token lease + revoke Revoke a token and its children +``` + +For more information, examples, and usage about a subcommand, click on the name +of the subcommand in the sidebar. From 878f80e47f91dcd28ba6b890fa3e67c83a1a0b5c Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:30 -0400 Subject: [PATCH 157/281] Add "unwrap" command documentation --- website/source/docs/commands/unwrap.html.md | 46 +++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 website/source/docs/commands/unwrap.html.md diff --git a/website/source/docs/commands/unwrap.html.md b/website/source/docs/commands/unwrap.html.md new file mode 100644 index 0000000000..81e9d031b7 --- /dev/null +++ b/website/source/docs/commands/unwrap.html.md @@ -0,0 +1,46 @@ +--- +layout: "docs" +page_title: "unwrap - Command" +sidebar_current: "docs-commands-unwrap" +description: |- + The "unwrap" command unwraps a wrapped secret from Vault by the given token. + The result is the same as the "vault read" operation on the non-wrapped + secret. If no token is given, the data in the currently authenticated token is + unwrapped. +--- + +# unwrap + +The `unwrap` command unwraps a wrapped secret from Vault by the given token. The +result is the same as the "vault read" operation on the non-wrapped secret. If +no token is given, the data in the currently authenticated token is unwrapped. + +## Examples + +Unwrap the data in the cubbyhole secrets engine for a token: + +```text +$ vault unwrap 3de9ece1-b347-e143-29b0-dc2dc31caafd +``` + +Unwrap the data in the active token: + +```text +$ vault auth 848f9ccf-7176-098c-5e2b-75a0689d41cd +$ vault unwrap # unwraps 848f9ccf... +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-field` `(string: "")` - Print only the field with the given name. Specifying + this option will take precedence over other formatting directives. The result + will not have a trailing newline making it idea for piping to other processes. + +- `-format` `(string: "table")` - Print the output in the given format. Valid + formats are "table", "json", or "yaml". This can also be specified via the + `VAULT_FORMAT` environment variable. From aaeacc291a942588ec344c41d01d27e90ace5ac8 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:36 -0400 Subject: [PATCH 158/281] Add "write" command documentation --- website/source/docs/commands/write.html.md | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 website/source/docs/commands/write.html.md diff --git a/website/source/docs/commands/write.html.md b/website/source/docs/commands/write.html.md new file mode 100644 index 0000000000..aadc2e1237 --- /dev/null +++ b/website/source/docs/commands/write.html.md @@ -0,0 +1,69 @@ +--- +layout: "docs" +page_title: "write - Command" +sidebar_current: "docs-commands-write" +description: |- + The "write" command writes data to Vault at the given path. The data can be + credentials, secrets, configuration, or arbitrary data. The specific behavior + of this command is determined at the thing mounted at the path. +--- + +# write + +The `write` command writes data to Vault at the given path. The data can be +credentials, secrets, configuration, or arbitrary data. The specific behavior of +this command is determined at the thing mounted at the path. + +Data is specified as "key=value" pairs. If the value begins with an "@", then it +is loaded from a file. If the value is "-", Vault will read the value from +stdin. + +For a full list of examples and paths, please see the documentation that +corresponds to the secret engines in use. + +## Examples + +Persist data in the generic secrets engine: + +```text +$ vault write secret/my-secret foo=bar +``` + +Create a new encryption key in the transit secrets engine: + +```text +$ vault write -f transit/keys/my-key +``` + +Upload an AWS IAM policy from a file on disk: + +```text +$ vault write aws/roles/ops policy=@policy.json +``` + +Configure access to Consul by providing an access token: + +```text +$ echo $MY_TOKEN | vault write consul/config/access token=- +``` + +## Usage + +The following flags are available in addition to the [standard set of +flags](/docs/commands/index.html) included on all commands. + +### Output Options + +- `-field` `(string: "")` - Print only the field with the given name. Specifying + this option will take precedence over other formatting directives. The result + will not have a trailing newline making it idea for piping to other processes. + +- `-format` `(string: "table")` - Print the output in the given format. Valid + formats are "table", "json", or "yaml". This can also be specified via the + `VAULT_FORMAT` environment variable. + +### Command Options + +- `-force` `(bool: false)` - Allow the operation to continue with no key=value + pairs. This allows writing to keys that do not need or expect data. This is + aliased as "-f". From 9ae01f1e6a9645b5a556229122eed87de3955c6b Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:14:50 -0400 Subject: [PATCH 159/281] Absorb help and read-write into index --- website/redirects.txt | 4 + .../source/docs/commands/environment.html.md | 75 ------ website/source/docs/commands/index.html.md | 223 ++++++++++++++++-- .../source/docs/commands/read-write.html.md | 121 ---------- 4 files changed, 204 insertions(+), 219 deletions(-) delete mode 100644 website/source/docs/commands/environment.html.md delete mode 100644 website/source/docs/commands/read-write.html.md diff --git a/website/redirects.txt b/website/redirects.txt index fe5d0394ee..d268321460 100644 --- a/website/redirects.txt +++ b/website/redirects.txt @@ -98,3 +98,7 @@ /docs/vault-enterprise/mfa/mfa-okta.html /docs/enterprise/mfa/mfa-okta.html /docs/vault-enterprise/mfa/mfa-pingid.html /docs/enterprise/mfa/mfa-pingid.html /docs/vault-enterprise/mfa/mfa-totp.html /docs/enterprise/mfa/mfa-totp.html + +/docs/commands/environment.html /docs/commands/index.html#environment-variables +/docs/commands/read-write.html /docs/commands/index.html#reading-and-writing-data +/docs/commands/help.html /docs/commands/path-help.html diff --git a/website/source/docs/commands/environment.html.md b/website/source/docs/commands/environment.html.md deleted file mode 100644 index e19fc53a83..0000000000 --- a/website/source/docs/commands/environment.html.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -layout: "docs" -page_title: "Environment" -sidebar_current: "docs-commands-environment" -description: |- - Vault's behavior can be modified by certain environment variables. ---- - -# Environment variables - -The Vault CLI will read the following environment variables to set -behavioral defaults. These can be overridden in all cases using -command-line arguments; see the command-line help for details. - -The following table describes them: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Variable nameValue
VAULT_TOKENThe Vault authentication token. If not specified, the token located in $HOME/.vault-token will be used if it exists.
VAULT_ADDRThe address of the Vault server expressed as a URL and port, for example: http://127.0.0.1:8200
VAULT_CACERTPath to a PEM-encoded CA cert file to use to verify the Vault server SSL certificate.
VAULT_CAPATHPath to a directory of PEM-encoded CA cert files to verify the Vault server SSL certificate. If VAULT_CACERT is specified, its value will take precedence.
VAULT_CLIENT_CERTPath to a PEM-encoded client certificate for TLS authentication to the Vault server.
VAULT_CLIENT_KEYPath to an unencrypted PEM-encoded private key matching the client certificate.
VAULT_CLIENT_TIMEOUTTimeout variable for the vault client. Default value is 60 seconds.
VAULT_CLUSTER_ADDRThe address that should be used for other cluster members to connect to this node when in High Availability mode.
VAULT_MAX_RETRIESThe maximum number of retries when a `5xx` error code is encountered. Default is `2`, for three total tries; set to `0` or less to disable retrying.
VAULT_REDIRECT_ADDRThe address that should be used when clients are redirected to this node when in High Availability mode.
VAULT_SKIP_VERIFYIf set, do not verify Vault's presented certificate before communicating with it. Setting this variable is not recommended except during testing.
VAULT_TLS_SERVER_NAMEIf set, use the given name as the SNI host when connecting via TLS.
VAULT_MFA(Enterprise Only) MFA credentials in the format **mfa_method_name[:key[=value]]** (items in `[]` are optional). Note that when using the environment variable, only one credential can be supplied. If a MFA method expects multiple credential values, or if there are multiple MFA methods specified on a path, then the CLI flag `-mfa` should be used.
diff --git a/website/source/docs/commands/index.html.md b/website/source/docs/commands/index.html.md index 297f1e850d..a356dfe95a 100644 --- a/website/source/docs/commands/index.html.md +++ b/website/source/docs/commands/index.html.md @@ -3,40 +3,217 @@ layout: "docs" page_title: "Commands (CLI)" sidebar_current: "docs-commands" description: |- - Vault can be controlled via a command-line interface. This page documents all the commands Vault accepts. + In addition to a verbose HTTP API, Vault features a command-line interface + that wraps common functionality and formats output. The Vault CLI is a single + static binary. It is a thin wrapper around the HTTP API. Every CLI command + maps directly to the HTTP API internally. --- # Vault Commands (CLI) -Vault is controlled via a very easy to use command-line interface (CLI). -Vault is only a single command-line application: `vault`. This application -then takes a subcommand such as "read" or "write". The complete list of -subcommands is in the navigation to the left. +In addition to a verbose [HTTP API](/api/index.html), Vault features a +command-line interface that wraps common functionality and formats output. The +Vault CLI is a single static binary. It is a thin wrapper around the HTTP API. +Every CLI command maps directly to the HTTP API internally. -The Vault CLI is a well-behaved command line application. In erroneous cases, -a non-zero exit status will be returned. It also responds to `-h` and `--help` -as you'd most likely expect. +Each command is represented as a command or subcommand. Please see the sidebar +for more information about a particular command. This documentation corresponds +to the latest version of Vault. If you are running an older version, commands +may behave differently. Run `vault -h` or `vault -h` to see the help +output which corresponds to your version. -To view a list of the available commands at any time, just run Vault -with no arguments. To get help for any specific subcommand, run the subcommand -with the `-h` argument. +To get help, run: -The help output is very comprehensive, so we defer you to that for documentation. -We've included some guides to the left of common interactions with the -CLI. +```text +$ vault -h +``` + +To get help for a subcommand, run: + +```text +$ vault -h +``` + +## Exit Codes + +The Vault CLI aims to be consistent and well-behaved unless documented +otherwise. + + - Local errors such as incorrect flags, failed validations, or wrong numbers + of arguments return an exit code of 1. + + - Any remote errors such as API failures, bad TLS, or incorrect API parameters + return an exit status of 2 + +Some commands override this default where it makes sense. These commands +document this anomaly. ## Autocompletion -The `vault` command features opt-in subcommand autocompletion that you can -enable for your shell with `vault -autocomplete-install`. After doing so, -you can invoke a new shell and use the feature. +The `vault` command features opt-in autocompletion for flags, subcommands, and +arguments (where supported). -For example, assume a tab is typed at the end of each prompt line: +Enable autocompletion by running: + +```text +$ vault -autocomplete-install +``` + +~> Be sure to **restart your shell** after installing autocompletion! + +When you start tying a Vault command, press the `` character to show a +list of available completions. Type `-` to show available flag completions. + +If the `VAULT_*` environment variables are set, the autocompletion will +automatically query the Vault server and return helpful argument suggestions. + +## Reading and Writing Data + +The four most common operations in Vault are `read`, `write`, `delete`, and +`list`. These operations work on almost any path in Vault. Some paths will +contain secrets, other paths might contain configuration. Whatever it is, the +primary interface for reading and writing data to Vault is the same. + +### Writing Data + +To write data to Vault, use the `vault write` command: + +```text +$ vault write secret/password value=itsasecret +``` + +For some secrets engines, the key/value pairs are arbitrary. For others, they +are generally more strict. Vault's built-in help will guide you to these +restrictions where appropriate. + +#### stdin + +Some commands in Vault can read data from stdin using `-` as the value. If `-` +is the entire argument, Vault expects to read a JSON object from stdin: + +```text +$ echo -n '{"value":"itsasecret"}' | vault write secret/password - +``` + +In addition to reading full JSON objects, Vault can read just a value from +stdin: + +```text +$ echo -n "itsasecret" | vault write secret/password value=- +``` + +#### Files + +Some commands can also read data from a file on disk. The usage is similar to +stdin as documented above. If an argument starts with `@`, Vault will read it as +a file: + +```text +$ vault write secret/password @data.json +``` + +Or specify the contents of a file as a value: + +```text +$ vault write secret/password value=@data.txt +``` + +### Reading Data + +After data is persisted, read it back using `vault read`: ``` -$ vault au -audit-disable audit-enable audit-list auth auth-disable auth-enable - -$ vault s -seal server ssh status step-down +$ vault read secret/password +Key Value +--- ----- +refresh_interval 768h0m0s +value itsasecret ``` + +## Token Helper + +By default, the Vault CLI uses a "token helper" to cache the token after +authentication. This is conceptually similar to how a website securely stores +your session information as a cookie in the browser. Token helpers are +customizable, and you can even build your own. + +The default token helper stores the token in `~/.vault-token`. You can delete +this file at any time to "logout" of Vault. + +## Environment Variables + +The CLI reads the following environment variables to set behavioral defaults. +This can alleviate the need to repetitively type a flag. Flags always take +precedence over the environment variables. + +### `VAULT_TOKEN` + +Vault authentication token. Conceptually similar to a session token on a +website, the `VAULT_TOKEN` environment variable holds the contents of the token. +For more information, please see the [token +concepts](/docs/concepts/tokens.html) page. + +### `VAULT_ADDR` + +Address of the Vault server expressed as a URL and port, for example: +`https://vault.rocks:8200/`. + +### `VAULT_CACERT` + +Path to a PEM-encoded CA certificate _file_ on the local disk. This file is used +to verify the Vault server's SSL certificate. This environment variable takes +precedence over `VAULT_CAPATH`. + +### `VAULT_CAPATH` + +Path to a _directory_ of PEM-encoded CA certificate files on the local disk. +These certificates are used to verify the Vault server's SSL certificate. + +### `VAULT_CLIENT_CERT` + +Path to a PEM-encoded client certificate on the local disk. This file is used +for TLS communication with the Vault server. + +### `VAULT_CLIENT_KEY` + +Path to an unencrypted, PEM-encoded private key on disk which corresponds to the +matching client certificate. + +### `VAULT_CLIENT_TIMEOUT` + +Timeout variable. The default value is 60s. + +### `VAULT_CLUSTER_ADDR` + +Address that should be used for other cluster members to connect to this node +when in High Availability mode. + +### `VAULT_MAX_RETRIES` + +Maximum number of retries when a `5xx` error code is encountered. The default is +`2`, for three total attempts. Set this to `0` or less to disable retrying. + +### `VAULT_REDIRECT_ADDR` + +Address that should be used when clients are redirected to this node when in +High Availability mode. + +### `VAULT_SKIP_VERIFY` + +Do not verify Vault's presented certificate before communicating with it. +Setting this variable is not recommended and voids Vault's [security +model](/docs/internals/security.html). + +### `VAULT_TLS_SERVER_NAME` + +Name to use as the SNI host when connecting via TLS. + +### `VAULT_MFA` + +**ENTERPRISE ONLY** + +MFA credentials in the format `mfa_method_name[:key[=value]]` (items in `[]` are +optional). Note that when using the environment variable, only one credential +can be supplied. If a MFA method expects multiple credential values, or if there +are multiple MFA methods specified on a path, then the CLI flag `-mfa` should be +used. diff --git a/website/source/docs/commands/read-write.html.md b/website/source/docs/commands/read-write.html.md deleted file mode 100644 index b0553eda0c..0000000000 --- a/website/source/docs/commands/read-write.html.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -layout: "docs" -page_title: "Reading and Writing Data" -sidebar_current: "docs-commands-readwrite" -description: |- - The Vault CLI can be used to read, write, and delete secrets. This page documents how to do this. ---- - -# Reading and Writing Data with the CLI - -The Vault CLI can be used to read, write, and delete data from Vault. -This data might be raw secrets, it might be configuration for -a backend, etc. Whatever it is, the interface to read and write data -to Vault is the same. - -To determine what paths can be used to read and write data, -please use the built-in [help system](/docs/commands/help.html) -to discover the paths. - -## Writing Data - -To write data to Vault, you use `vault write`. It is very easy to use: - -``` -$ vault write secret/password \ - value=itsasecret -... -``` - -The above writes a value to `secret/password`. As mentioned in the getting -started guide, multiple values can also be written: - -``` -$ vault write secret/password \ - value=itsasecret \ - username=something -... -``` - -For the `secret/` backend, the key/value pairs are arbitrary and can be -anything. For other backends, they're generally more strict, and the -help system can tell you what data to send to Vault. - -In addition to writing key/value pairs, Vault can write from a variety -more sources. - -#### stdin - -`vault write` can read data to write from stdin by using "-" as the value. -If you use "-" as the entire argument, then Vault expects to read a JSON -object from stdin. The example below is equivalent to the first example -above. - -``` -$ echo -n '{"value":"itsasecret"}' | vault write secret/password - -... -``` - -You can also add more values in addition to "-" on the command-line. -Depending on their ordering will determine if they overwrite the values -from stdin: if they're after the "-" (positionally on the command-line), -then they will overwrite it, otherwise the values in stdin will overwrite -the command line values. - -In addition to reading full JSON objects, Vault can read just a JSON -value. The example below is also identical to the previous example. - -``` -$ echo -n "itsasecret" | vault write secret/password value=- -... -``` - -#### Files - -`vault write` can read data from files as well. The usage is very similar -to stdin as documented above, but the syntax is `@filename`. Example: - -``` -$ cat data.json -{ "value": "itsasecret" } - -$ vault write secret/password @data.json -... -``` - -And, just like stdin, you can also specify just values: - -``` -$ cat data.txt -itsasecret - -$ vault write secret/password value=@data.txt -``` - -Unlike stdin, you can specify multiple files, repeat files, etc. all -on the command line. Reading from files is very useful for complex data. - -## Reading Data - -Data can be read using `vault read`. This command is very simple: - -``` -$ vault read secret/password -Key Value ---- ----- -refresh_interval 768h0m0s -value itsasecret -``` - -You can use the `-format` flag to get various different formats out -from the command. Some formats are easier to use in different environments -than others. - -You can also use the `-field` flag to extract an individual field -from the secret data. - -``` -$ vault read -field=value secret/password -itsasecret -``` - From fc0ba28051a611a72eb6789f2f28a29abd82491e Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:19:44 -0400 Subject: [PATCH 160/281] Add new commands to the sidebar --- website/source/layouts/docs.erb | 166 ++++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 6 deletions(-) diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 9481cf3192..47659e6f1c 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -153,15 +153,169 @@ > Commands (CLI) From 965b8809e30ec6b448e2f27fc24293fe26d8eb79 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 7 Sep 2017 22:38:47 -0400 Subject: [PATCH 161/281] Audit backend -> device --- website/source/docs/audit/file.html.md | 111 +++++++---------------- website/source/docs/audit/index.html.md | 96 +++++++------------- website/source/docs/audit/socket.html.md | 104 ++++++++------------- website/source/docs/audit/syslog.html.md | 94 +++++++------------ website/source/layouts/docs.erb | 2 +- 5 files changed, 139 insertions(+), 268 deletions(-) diff --git a/website/source/docs/audit/file.html.md b/website/source/docs/audit/file.html.md index c4f76a2544..b2333de1b4 100644 --- a/website/source/docs/audit/file.html.md +++ b/website/source/docs/audit/file.html.md @@ -1,97 +1,56 @@ --- layout: "docs" -page_title: "Audit Backend: File" +page_title: "File - Audit Devices" sidebar_current: "docs-audit-file" description: |- - The "file" audit backend writes audit logs to a file. + The "file" audit device writes audit logs to a file. --- -# Audit Backend: File +# File Audit Device -The `file` audit backend writes audit logs to a file. This is a very simple audit -backend: it appends logs to a file. +The `file` audit device writes audit logs to a file. This is a very simple audit +device: it appends logs to a file. -## Rotation - -The backend does not currently assist with any log rotation. There are very +The device does not currently assist with any log rotation. There are very stable and feature-filled log rotation tools already, so we recommend using existing tools. -As of 0.6.2, sending a `SIGHUP` to the Vault process will cause `file` audit -backends to close and re-open their underlying file, which can assist with log -rotation needs. +Sending a `SIGHUP` to the Vault process will cause `file` audit devices to close +and re-open their underlying file, which can assist with log rotation needs. -## Format +## Examples -Each line in the audit log is a JSON object. The `type` field specifies what type of -object it is. Currently, only two types exist: `request` and `response`. The line contains -all of the information for any given request and response. By default, all the sensitive -information is first hashed before logging in the audit logs. +Enable at the default path: -## Enabling - -#### Via the CLI - -Audit `file` backend can be enabled by the following command. - -``` -$ vault audit-enable file file_path=/var/log/vault_audit.log +```text +$ vault audit enable file file_path=/var/log/vault_audit.log ``` -Any number of `file` audit logs can be created by enabling it with different `path`s. +Enable at a different path. It is possible to enable multiple copies of an audit +device: -``` -$ vault audit-enable -path="vault_audit_1" file file_path=/home/user/vault_audit.log +```text +$ vault audit enable -path="vault_audit_1" file file_path=/home/user/vault_audit.log ``` -Note the difference between `audit-enable` command options and the `file` backend -configuration options. Use `vault audit-enable -help` to see the command options. -Following are the configuration options available for the backend. +## Configuration -
-
Backend configuration options
-
-
    -
  • - file_path - required - The path to where the audit log will be written. If this - path exists, the audit backend will append to it. Specify `"stdout"` to write audit log to standard output; specify `"discard"` to discard output (useful in testing scenarios). -
  • -
  • - log_raw - optional - A string containing a boolean value ('true'/'false'), if set, logs - the security sensitive information without hashing, in the raw - format. Defaults to `false`. -
  • -
  • - hmac_accessor - optional - A string containing a boolean value ('true'/'false'), if set, - enables the hashing of token accessor. Defaults - to `true`. This option is useful only when `log_raw` is `false`. -
  • -
  • - mode - optional - A string containing an octal number representing the bit pattern - for the file mode, similar to `chmod`. This option defaults to - `0600`. -
  • -
  • - format - optional - Allows selecting the output format. Valid values are `json` (the - default) and `jsonx`, which formats the normal log entries as XML. -
  • -
  • - prefix - optional - Allows a customizable string prefix to write before the actual log - line. Defaults to an empty string. -
  • -
-
-
+- `file_path` `(string: "")` - The path to where the audit log will be written. + If this path exists, the audit device will append to it. Specify `"stdout"` to + write audit log to standard output. Specify `"discard"` to discard output + (useful in testing scenarios). +- `log_raw` `(bool: false)` - If enabled, logs the security sensitive + information without hashing, in the raw format. + +- `hmac_accessor` `(bool: true)` - If enabled, enables the hashing of token + accessor. + +- `mode` `(string: "0600")` - A string containing an octal number representing + the bit pattern for the file mode, similar to `chmod`. + +- `format` `(string: "json")` - Allows selecting the output format. Valid values + are `"json"` and `"jsonx"`, which formats the normal log entries as XML. + +- `prefix` `(string: "")` - A customizable string prefix to write before the + actual log line. diff --git a/website/source/docs/audit/index.html.md b/website/source/docs/audit/index.html.md index 7e4aa607c0..dd1ba57e94 100644 --- a/website/source/docs/audit/index.html.md +++ b/website/source/docs/audit/index.html.md @@ -1,22 +1,29 @@ --- layout: "docs" -page_title: "Audit Backends" +page_title: "Audit Devices" sidebar_current: "docs-audit" description: |- - Audit backends are mountable backends that log requests and responses in Vault. + Audit devices are mountable devices that log requests and responses in Vault. --- -# Audit Backends +# Audit Devices -Audit backends are the components in Vault that keep a detailed log +Audit devices are the components in Vault that keep a detailed log of all requests and response to Vault. Because _every_ operation with Vault is an API request/response, the audit log contains _every_ interaction with Vault, including errors. -Vault ships with multiple audit backends, depending on the location you want -the logs sent to. Multiple audit backends can be enabled and Vault will send -the audit logs to both. This allows you to not only have a redundant copy, -but also a second copy in case the first is tampered with. +Multiple audit devices can be enabled and Vault will send the audit logs to +both. This allows you to not only have a redundant copy, but also a second copy +in case the first is tampered with. + +## Format + +Each line in the audit log is a JSON object. The `type` field specifies what +type of object it is. Currently, only two types exist: `request` and `response`. +The line contains all of the information for any given request and response. By +default, all the sensitive information is first hashed before logging in the +audit logs. ## Sensitive Information @@ -28,80 +35,47 @@ hashed with a salt using HMAC-SHA256. The purpose of the hash is so that secrets aren't in plaintext within your audit logs. However, you're still able to check the value of secrets by -generating HMACs yourself; this can be done with the audit backend's hash +generating HMACs yourself; this can be done with the audit device's hash function and salt by using the `/sys/audit-hash` API endpoint (see the documentation for more details). -## Enabling/Disabling Audit Backends +## Enabling/Disabling Audit Devices When a Vault server is first initialized, no auditing is enabled. Audit -backends must be enabled by a root user using `vault audit-enable`. +devices must be enabled by a root user using `vault audit-enable`. -When enabling an audit backend, options can be passed to it to configure it. -For example, the command below enables the file audit backend: +When enabling an audit device, options can be passed to it to configure it. +For example, the command below enables the file audit device: -``` -$ vault audit-enable file file_path=/var/log/vault_audit.log -... +```text +$ vault audit enable file file_path=/var/log/vault_audit.log ``` In the command above, we passed the "file_path" parameter to specify the path -where the audit log will be written to. Each audit backend has its own +where the audit log will be written to. Each audit device has its own set of parameters. See the documentation to the left for more details. -When an audit backend is disabled, it will stop receiving logs immediately. +When an audit device is disabled, it will stop receiving logs immediately. The existing logs that it did store are untouched. -## Blocked Audit Backends +## Blocked Audit Devices -If there are any audit backends enabled, Vault requires that at least +If there are any audit devices enabled, Vault requires that at least one be able to persist the log before completing a Vault request. -If you have only one audit backend enabled, and it is blocking (network -block, etc.), then Vault will be _unresponsive_. Vault _will not_ complete -any requests until the audit backend can write. +!> If you have only one audit device enabled, and it is blocking (network +block, etc.), then Vault will be _unresponsive_. Vault **will not** complete +any requests until the audit device can write. -If you have more than one audit backend, then Vault will complete the request -as long as one audit backend persists the log. +If you have more than one audit device, then Vault will complete the request +as long as one audit device persists the log. -Vault will not respond to requests if audit backends are blocked because +Vault will not respond to requests if audit devices are blocked because audit logs are critically important and ignoring blocked requests opens -an avenue for attack. Be absolutely certain that your audit backends cannot +an avenue for attack. Be absolutely certain that your audit devices cannot block. ## API -### /sys/audit/[path] -#### POST - -
-
Description
-
- Enables audit backend at the specified path. -
- -
Method
-
POST
- -
-
    -
  • - type - required - The path to where the audit log will be written. If this - path exists, the audit backend will append to it. -
  • -
  • - description - optional - A description. -
  • -
  • - options - optional - Configuration options of the backend in JSON format. - Refer to `syslog`, `file` and `socket` audit backend options. -
  • -
-
-
+Audit devices also have a full HTTP API. Please see the [Audit device API +docs](/api/system/audit.html) for more details. diff --git a/website/source/docs/audit/socket.html.md b/website/source/docs/audit/socket.html.md index eb653d38a4..38782f0ae7 100644 --- a/website/source/docs/audit/socket.html.md +++ b/website/source/docs/audit/socket.html.md @@ -1,85 +1,55 @@ --- layout: "docs" -page_title: "Audit Backend: Socket" +page_title: "Socket - Audit Devices" sidebar_current: "docs-audit-socket" description: |- - The "socket" audit backend writes audit writes to a TCP or UDP socket. + The "socket" audit device writes audit writes to a TCP or UDP socket. --- -# Audit Backend: Socket +# Socket Audit Device -The `socket` audit backend writes to a TCP, UDP, or UNIX socket. +The `socket` audit device writes to a TCP, UDP, or UNIX socket. -~> **Warning:** Due to the nature of the underlying protocols used in this backend there exists a case when the connection to a socket is lost a single audit entry could be omitted from the logs and the request will still succeed. Using this backend in conjunction with another audit backend will help to improve accuracy, but the socket backend should not be used if strong guarantees are needed for audit logs. - -## Format - -Each line in the audit log is a JSON object. The `type` field specifies what type of -object it is. Currently, only two types exist: `request` and `response`. The line contains -all of the information for any given request and response. By default, all the sensitive -information is first hashed before logging in the audit logs. +~> **Warning:** Due to the nature of the underlying protocols used in this +device there exists a case when the connection to a socket is lost a single +audit entry could be omitted from the logs and the request will still succeed. +Using this device in conjunction with another audit device will help to improve +accuracy, but the socket device should not be used if strong guarantees are +needed for audit logs. ## Enabling -#### Via the CLI +Enable at the default path: -Audit `socket` backend can be enabled by the following command. - -``` -$ vault audit-enable socket +```text +$ vault audit enable socket ``` -Backend configuration options can also be provided from command-line. +Supply configuration parameters via K=V pairs: -``` -$ vault audit-enable socket address="127.0.0.1:9090" socket_type="tcp" +```text +$ vault audit enable socket address=127.0.0.1:9090 socket_type=tcp ``` -Following are the configuration options available for the backend. +## Configuration -
-
Backend configuration options
-
-
    -
  • - address - required - The socket server address to use. Example `127.0.0.1:9090` or `/tmp/audit.sock`. -
  • -
  • - socket_type - optional - The socket type to use, any type compatible with net.Dial is acceptable. Defaults to `tcp`. -
  • -
  • - log_raw - optional - A string containing a boolean value ('true'/'false'), if set, logs the security sensitive information without - hashing, in the raw format. Defaults to `false`. -
  • -
  • - hmac_accessor - optional - A string containing a boolean value ('true'/'false'), if set, enables the hashing of token accessor. Defaults - to `true`. This option is useful only when `log_raw` is `false`. -
  • -
  • - format - optional - Allows selecting the output format. Valid values are `json` (the - default) and `jsonx`, which formats the normal log entries as XML. -
  • -
  • - write_timeout - optional - Sets the timeout for writes to the socket. Defaults to "2s" (2 seconds). -
  • -
  • - prefix - optional - Allows a customizable string prefix to write before the actual log - line. Defaults to an empty string. -
  • -
-
-
+- `address` `(string: "")` - The socket server address to use. Example + `127.0.0.1:9090` or `/tmp/audit.sock`. + +- `socket_type` `(string: "tcp")` - The socket type to use, any type compatible + with net.Dial is acceptable. + +- `log_raw` `(bool: false)` - If enabled, logs the security sensitive + information without hashing, in the raw format. + +- `hmac_accessor` `(bool: true)` - If enabled, enables the hashing of token + accessor. + +- `mode` `(string: "0600")` - A string containing an octal number representing + the bit pattern for the file mode, similar to `chmod`. + +- `format` `(string: "json")` - Allows selecting the output format. Valid values + are `"json"` and `"jsonx"`, which formats the normal log entries as XML. + +- `prefix` `(string: "")` - A customizable string prefix to write before the + actual log line. diff --git a/website/source/docs/audit/syslog.html.md b/website/source/docs/audit/syslog.html.md index 22fa39ce81..c0beac47ec 100644 --- a/website/source/docs/audit/syslog.html.md +++ b/website/source/docs/audit/syslog.html.md @@ -1,82 +1,50 @@ --- layout: "docs" -page_title: "Audit Backend: Syslog" +page_title: "Syslog - Audit Devices" sidebar_current: "docs-audit-syslog" description: |- - The "syslog" audit backend writes audit logs to syslog. + The "syslog" audit device writes audit logs to syslog. --- -# Audit Backend: Syslog +# Syslog Audit Device -The `syslog` audit backend writes audit logs to syslog. +The `syslog` audit device writes audit logs to syslog. It currently does not support a configurable syslog destination, and always -sends to the local agent. This backend is only supported on Unix systems, +sends to the local agent. This device is only supported on Unix systems, and should not be enabled if any standby Vault instances do not support it. -## Format +## Examples -Each line in the audit log is a JSON object. The `type` field specifies what type of -object it is. Currently, only two types exist: `request` and `response`. The line contains -all of the information for any given request and response. By default, all the sensitive -information is first hashed before logging in the audit logs. +Audit `syslog` device can be enabled by the following command: -## Enabling - -#### Via the CLI - -Audit `syslog` backend can be enabled by the following command. - -``` -$ vault audit-enable syslog +```text +$ vault audit enable syslog ``` -Backend configuration options can also be provided from command-line. +Supply configuration parameters via K=V pairs: -``` -$ vault audit-enable syslog tag="vault" facility="AUTH" +```text +$ vault audit enable syslog tag="vault" facility="AUTH" ``` -Following are the configuration options available for the backend. +## Configuration -
-
Backend configuration options
-
-
    -
  • - facility - optional - The syslog facility to use. Defaults to `AUTH`. -
  • -
  • - tag - optional - The syslog tag to use. Defaults to `vault`. -
  • -
  • - log_raw - optional - A string containing a boolean value ('true'/'false'), if set, logs the security sensitive information without - hashing, in the raw format. Defaults to `false`. -
  • -
  • - hmac_accessor - optional - A string containing a boolean value ('true'/'false'), if set, enables the hashing of token accessor. Defaults - to `true`. This option is useful only when `log_raw` is `false`. -
  • -
  • - format - optional - Allows selecting the output format. Valid values are `json` (the - default) and `jsonx`, which formats the normal log entries as XML. -
  • -
  • - prefix - optional - Allows a customizable string prefix to write before the actual log - line. Defaults to an empty string. -
  • -
-
-
+- `facility` `(string: "AUTH")` - The syslog facility to use. + +- `tag` `(string: "vault")` - The syslog tag to use. + +- `log_raw` `(bool: false)` - If enabled, logs the security sensitive + information without hashing, in the raw format. + +- `hmac_accessor` `(bool: true)` - If enabled, enables the hashing of token + accessor. + +- `mode` `(string: "0600")` - A string containing an octal number representing + the bit pattern for the file mode, similar to `chmod`. + +- `format` `(string: "json")` - Allows selecting the output format. Valid values + are `"json"` and `"jsonx"`, which formats the normal log entries as XML. + +- `prefix` `(string: "")` - A customizable string prefix to write before the + actual log line. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 47659e6f1c..19288776e6 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -491,7 +491,7 @@ > - Audit Backends + Audit Devices