diff --git a/builtin/token/disk/command.go b/builtin/token/disk/command.go new file mode 100644 index 0000000000..a0528dfabd --- /dev/null +++ b/builtin/token/disk/command.go @@ -0,0 +1,89 @@ +package disk + +import ( + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/mitchellh/go-homedir" +) + +// DefaultPath is the default path where the Vault token is stored. +const DefaultPath = "~/.vault-token" + +type Command struct{} + +func (c *Command) Run(args []string) int { + var path string + f := flag.NewFlagSet("token-disk", flag.ContinueOnError) + f.StringVar(&path, "path", DefaultPath, "") + f.Usage = func() { fmt.Fprintf(os.Stderr, c.Help()+"\n") } + if err := f.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "\n%s\n", err) + return 1 + } + + path, err := homedir.Expand(path) + if err != nil { + fmt.Fprintf(os.Stderr, "Error expanding directory: %s\n", err) + return 1 + } + + switch args[0] { + case "get": + f, err := os.Open(path) + if os.IsNotExist(err) { + return 0 + } + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return 1 + } + defer f.Close() + + if _, err := io.Copy(os.Stdout, f); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return 1 + } + case "store": + f, err := os.Create(path) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return 1 + } + defer f.Close() + + if _, err := io.Copy(f, os.Stdin); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return 1 + } + case "erase": + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "%s\n", err) + return 1 + } + } + + return 0 +} + +func (c *Command) Synopsis() string { + return "Stores Vault tokens on disk" +} + +func (c *Command) Help() string { + helpText := ` +Usage: vault token-disk [options] [operation] + + Vault token helper (see vault config "token_helper") that writes + authenticated tokens to disk unencrypted. + +Options: + + -path=path Path to store the token. + +` + return strings.TrimSpace(helpText) +} diff --git a/builtin/token/disk/command_test.go b/builtin/token/disk/command_test.go new file mode 100644 index 0000000000..0263141f60 --- /dev/null +++ b/builtin/token/disk/command_test.go @@ -0,0 +1,15 @@ +package disk + +import ( + "testing" + + "github.com/hashicorp/vault/command/token" +) + +func TestCommand(t *testing.T) { + token.TestProcess(t) +} + +func TestHelperProcess(t *testing.T) { + token.TestHelperProcessCLI(t, new(Command)) +} diff --git a/command/config.go b/command/config.go new file mode 100644 index 0000000000..1275cb59d1 --- /dev/null +++ b/command/config.go @@ -0,0 +1,11 @@ +package command + +// Config is the CLI configuration for Vault that can be specified via +// a `$HOME/.vault` file which is HCL-formatted (therefore HCL or JSON). +type Config struct { + // TokenHelper is the executable/command that is executed for storing + // and retrieving the authentication token for the Vault CLI. If this + // is not specified, then vault token-disk will be used, which stores + // the token on disk unencrypted. + TokenHelper string `hcl:"token_helper"` +} diff --git a/command/token/helper.go b/command/token/helper.go index f1c5290040..04ba75ed7d 100644 --- a/command/token/helper.go +++ b/command/token/helper.go @@ -2,6 +2,7 @@ package token import ( "bytes" + "fmt" "os/exec" ) @@ -24,16 +25,23 @@ type Helper struct { // Erase deletes the contents from the helper. func (h *Helper) Erase() error { cmd := h.cmd("erase") - return cmd.Run() + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf( + "Error: %s\n\n%s", err, string(output)) + } + + return nil } // Get gets the token value from the helper. func (h *Helper) Get() (string, error) { - var buf bytes.Buffer + var buf, stderr bytes.Buffer cmd := h.cmd("get") cmd.Stdout = &buf + cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - return "", err + return "", fmt.Errorf( + "Error: %s\n\n%s", err, stderr.String()) } return buf.String(), nil @@ -44,7 +52,12 @@ func (h *Helper) Store(v string) error { buf := bytes.NewBufferString(v) cmd := h.cmd("store") cmd.Stdin = buf - return cmd.Run() + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf( + "Error: %s\n\n%s", err, string(output)) + } + + return nil } func (h *Helper) cmd(op string) *exec.Cmd { diff --git a/command/token/helper_test.go b/command/token/helper_test.go index 9ae4b4905c..84baea15a8 100644 --- a/command/token/helper_test.go +++ b/command/token/helper_test.go @@ -11,31 +11,7 @@ import ( func TestHelper(t *testing.T) { h := testHelper(t) - if err := h.Store("foo"); err != nil { - t.Fatalf("err: %s", err) - } - - v, err := h.Get() - if err != nil { - t.Fatalf("err: %s", err) - } - - if v != "foo" { - t.Fatalf("bad: %#v", v) - } - - if err := h.Erase(); err != nil { - t.Fatalf("err: %s", err) - } - - v, err = h.Get() - if err != nil { - t.Fatalf("err: %s", err) - } - - if v != "" { - t.Fatalf("bad: %#v", v) - } + Test(t, h.Path) } func testHelper(t *testing.T) *Helper { diff --git a/command/token/testing.go b/command/token/testing.go new file mode 100644 index 0000000000..acb910669a --- /dev/null +++ b/command/token/testing.go @@ -0,0 +1,77 @@ +package token + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +// Test is a public function that can be used in other tests to +// test that a helper is functioning properly. +func Test(t *testing.T, path string) { + h := &Helper{Path: path} + if err := h.Store("foo"); err != nil { + t.Fatalf("err: %s", err) + } + + v, err := h.Get() + if err != nil { + t.Fatalf("err: %s", err) + } + + if v != "foo" { + t.Fatalf("bad: %#v", v) + } + + if err := h.Erase(); err != nil { + t.Fatalf("err: %s", err) + } + + v, err = h.Get() + if err != nil { + t.Fatalf("err: %s", err) + } + + if v != "" { + t.Fatalf("bad: %#v", v) + } +} + +// TestProcess is used to re-execute this test in order to use it as the +// helper process. For this to work, the TestHelperProcess function must +// exist. +func TestProcess(t *testing.T, s ...string) { + // Build the path to the CLI to execute + cs := []string{"-test.run=TestHelperProcess", "--"} + cs = append(cs, s...) + path := fmt.Sprintf( + "GO_WANT_HELPER_PROCESS=1 %s %s", + os.Args[0], + strings.Join(cs, " ")) + + // Run the tests + Test(t, path) +} + +// TestHelperProcessCLI can be called to implement TestHelperProcess +// for TestProcess that just executes a CLI command. +func TestHelperProcessCLI(t *testing.T, cmd cli.Command) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + args := os.Args + for len(args) > 0 { + if args[0] == "--" { + args = args[1:] + break + } + + args = args[1:] + } + + os.Exit(cmd.Run(args)) +} diff --git a/commands.go b/commands.go index cdd15a4bf4..81377728e0 100644 --- a/commands.go +++ b/commands.go @@ -5,13 +5,16 @@ import ( "github.com/hashicorp/vault/builtin/logical/aws" "github.com/hashicorp/vault/builtin/logical/consul" + tokenDisk "github.com/hashicorp/vault/builtin/token/disk" "github.com/hashicorp/vault/command" "github.com/hashicorp/vault/logical" "github.com/mitchellh/cli" ) -// Commands is the mapping of all the available Consul commands. +// Commands is the mapping of all the available Vault commands. CommandsInclude +// are the commands to include for help. var Commands map[string]cli.CommandFactory +var CommandsInclude []string func init() { ui := &cli.BasicUi{ @@ -97,4 +100,15 @@ func init() { }, nil }, } + + // Build the commands to include in the help now + CommandsInclude = make([]string, 0, len(Commands)) + for k, _ := range Commands { + CommandsInclude = append(CommandsInclude, k) + } + + // The commands below are hidden from the help output + Commands["token-disk"] = func() (cli.Command, error) { + return &tokenDisk.Command{}, nil + } } diff --git a/main.go b/main.go index 2ab9f1433a..05f95f3dcd 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,8 @@ func realMain() int { cli := &cli.CLI{ Args: args, Commands: Commands, - HelpFunc: cli.BasicHelpFunc("vault"), + HelpFunc: cli.FilteredHelpFunc( + CommandsInclude, cli.BasicHelpFunc("vault")), } exitCode, err := cli.Run()