Make TokenHelper an interface and split exisiting functionality

Functionality is split into ExternalTokenHelper, which is used if a path
is given in a configuration file, and InternalTokenHelper which is used
otherwise. The internal helper no longer shells out to the same Vault
binary, instead performing the same actions with internal code. This
avoids problems using dev mode when there are spaces in paths or when
the binary is built in a container without a shell.

Fixes #850 among others
This commit is contained in:
Jeff Mitchell 2015-12-14 16:23:04 -05:00
parent 49d2793acc
commit 1a324cf347
12 changed files with 257 additions and 280 deletions

View file

@ -1,100 +0,0 @@
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 {
Path string
}
func (c *Command) Run(args []string) int {
var path string
pathDefault := DefaultPath
if c.Path != "" {
pathDefault = c.Path
}
f := flag.NewFlagSet("token-disk", flag.ContinueOnError)
f.StringVar(&path, "path", pathDefault, "")
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
}
args = f.Args()
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.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
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
}
default:
fmt.Fprintf(os.Stderr, "Error: unknown subcommand: %s\n", args[0])
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)
}

View file

@ -1,15 +0,0 @@
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))
}

View file

@ -25,7 +25,6 @@ import (
"github.com/hashicorp/vault/builtin/logical/transit" "github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/audit"
tokenDisk "github.com/hashicorp/vault/builtin/token/disk"
"github.com/hashicorp/vault/command" "github.com/hashicorp/vault/command"
"github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
@ -275,11 +274,6 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
Ui: meta.Ui, Ui: meta.Ui,
}, nil }, nil
}, },
// The commands below are hidden from the help output
"token-disk": func() (cli.Command, error) {
return &tokenDisk.Command{}, nil
},
} }
} }

View file

@ -10,8 +10,6 @@ import (
"testing" "testing"
"github.com/hashicorp/vault/api" "github.com/hashicorp/vault/api"
tokenDisk "github.com/hashicorp/vault/builtin/token/disk"
"github.com/hashicorp/vault/command/token"
"github.com/hashicorp/vault/http" "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault" "github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
@ -188,14 +186,10 @@ func testAuthInit(t *testing.T) {
// Write a .vault config to use our custom token helper // Write a .vault config to use our custom token helper
config := fmt.Sprintf( config := fmt.Sprintf(
"token_helper = \"%s\"\n", token.TestProcessPath(t)) "token_helper = \"\"\n")
ioutil.WriteFile(filepath.Join(td, ".vault"), []byte(config), 0644) ioutil.WriteFile(filepath.Join(td, ".vault"), []byte(config), 0644)
} }
func TestHelperProcess(t *testing.T) {
token.TestHelperProcessCLI(t, &tokenDisk.Command{})
}
type testAuthHandler struct{} type testAuthHandler struct{}
func (h *testAuthHandler) Auth(c *api.Client, m map[string]string) (string, error) { func (h *testAuthHandler) Auth(c *api.Client, m map[string]string) (string, error) {

View file

@ -23,8 +23,8 @@ const (
type Config struct { type Config struct {
// TokenHelper is the executable/command that is executed for storing // TokenHelper is the executable/command that is executed for storing
// and retrieving the authentication token for the Vault CLI. If this // and retrieving the authentication token for the Vault CLI. If this
// is not specified, then vault token-disk will be used, which stores // is not specified, then vault's internal token store will be used, which
// the token on disk unencrypted. // stores the token on disk unencrypted.
TokenHelper string `hcl:"token_helper"` TokenHelper string `hcl:"token_helper"`
} }

View file

@ -194,7 +194,7 @@ func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet {
} }
// TokenHelper returns the token helper that is configured for Vault. // TokenHelper returns the token helper that is configured for Vault.
func (m *Meta) TokenHelper() (*token.Helper, error) { func (m *Meta) TokenHelper() (token.TokenHelper, error) {
config, err := m.Config() config, err := m.Config()
if err != nil { if err != nil {
return nil, err return nil, err
@ -202,11 +202,14 @@ func (m *Meta) TokenHelper() (*token.Helper, error) {
path := config.TokenHelper path := config.TokenHelper
if path == "" { if path == "" {
path = "disk" return &token.InternalTokenHelper{}, nil
} }
path = token.HelperPath(path) path, err = token.ExternalTokenHelperPath(path)
return &token.Helper{Path: path}, nil if err != nil {
return nil, err
}
return &token.ExternalTokenHelper{BinaryPath: path}, nil
} }
func (m *Meta) loadCACert(path string) (*x509.CertPool, error) { func (m *Meta) loadCACert(path string) (*x509.CertPool, error) {

View file

@ -1,132 +1,14 @@
package token package token
import ( // TokenHelper is an interface that contains basic operations that must be
"bytes" // implemented by a token helper
"fmt" type TokenHelper interface {
"os" // Path displays a backend-specific path; for the internal helper this
"os/exec" // is the location of the token stored on disk; for the external helper
"path/filepath" // this is the location of the binary being invoked
"runtime" Path() string
"strings"
"github.com/kardianos/osext" Erase() error
) Get() (string, error)
Store(string) error
var exePath string
func init() {
var err error
exePath, err = osext.Executable()
if err != nil {
panic("failed to detect self path: " + err.Error())
}
}
// HelperPath takes the configured path to a helper and expands it to
// a full absolute path that can be executed. If the path is relative then
// a prefix of "vault token-" will be prepended to the path.
func HelperPath(path string) string {
space := strings.Index(path, " ")
if space == -1 {
space = len(path)
}
// Get the binary name. If it isn't absolute, prepend "vault token-"
binary := path[0:space]
if !filepath.IsAbs(binary) {
binary = exePath + " token-" + binary
}
// Return the resulting string
return fmt.Sprintf("%s%s", binary, path[space:])
}
// Helper 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
// Path is executed within a shell with environment Env. The last argument
// appended will be the operation, which is:
//
// * "get" - Read the value of the token and write it to stdout.
// * "store" - Store the value of the token which is on stdin. Output
// nothing.
// * "erase" - Erase the contents stored. Output nothing.
//
// Any errors can be written on stdout. If the helper exits with a non-zero
// exit code then the stderr will be made part of the error value.
type Helper struct {
Path string
Env []string
}
// Erase deletes the contents from the helper.
func (h *Helper) Erase() error {
cmd, err := h.cmd("erase")
if err != nil {
return fmt.Errorf("Error: %s", err)
}
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, stderr bytes.Buffer
cmd, err := h.cmd("get")
if err != nil {
return "", fmt.Errorf("Error: %s", err)
}
cmd.Stdout = &buf
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf(
"Error: %s\n\n%s", err, stderr.String())
}
return buf.String(), nil
}
// Store stores the token value into the helper.
func (h *Helper) Store(v string) error {
buf := bytes.NewBufferString(v)
cmd, err := h.cmd("store")
if err != nil {
return fmt.Errorf("Error: %s", err)
}
cmd.Stdin = buf
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, error) {
script := strings.Replace(h.Path, "\\", "\\\\", -1) + " " + op
cmd, err := ExecScript(script)
if err != nil {
return nil, err
}
cmd.Env = h.Env
return cmd, nil
}
// ExecScript returns a command to execute a script
func ExecScript(script string) (*exec.Cmd, error) {
var shell, flag string
if runtime.GOOS == "windows" {
shell = "cmd"
flag = "/C"
} else {
shell = "/bin/sh"
flag = "-c"
}
if other := os.Getenv("SHELL"); other != "" {
shell = other
}
cmd := exec.Command(shell, flag, script)
return cmd, nil
} }

View file

@ -0,0 +1,131 @@
package token
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
// ExternalTokenHelperPath takes the configured path to a helper and expands it to
// a full absolute path that can be executed. As of 0.5, the default token
// helper is internal, to avoid problems running in dev mode (see GH-850 and
// GH-783), so special assumptions of prepending "vault token-" no longer
// apply.
//
// As an additional result, only absolute paths are now allowed. Looking in the
// path or a current directory for an arbitrary executable could allow someone
// to switch the expected binary for one further up the path (or in the current
// directory), potentially opening up execution of an arbitrary binary.
func ExternalTokenHelperPath(path string) (string, error) {
if !filepath.IsAbs(path) {
var err error
path, err = filepath.Abs(path)
if err != nil {
return "", err
}
}
if _, err := os.Stat(path); err != nil {
return path, nil
}
return "", fmt.Errorf("unknown error getting the external helper path")
}
// 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
// appended will be the operation, which is:
//
// * "get" - Read the value of the token and write it to stdout.
// * "store" - Store the value of the token which is on stdin. Output
// nothing.
// * "erase" - Erase the contents stored. Output nothing.
//
// Any errors can be written on stdout. If the helper exits with a non-zero
// exit code then the stderr will be made part of the error value.
type ExternalTokenHelper struct {
BinaryPath string
Env []string
}
// Erase deletes the contents from the helper.
func (h *ExternalTokenHelper) Erase() error {
cmd, err := h.cmd("erase")
if err != nil {
return fmt.Errorf("Error: %s", err)
}
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 *ExternalTokenHelper) Get() (string, error) {
var buf, stderr bytes.Buffer
cmd, err := h.cmd("get")
if err != nil {
return "", fmt.Errorf("Error: %s", err)
}
cmd.Stdout = &buf
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf(
"Error: %s\n\n%s", err, stderr.String())
}
return buf.String(), nil
}
// Store stores the token value into the helper.
func (h *ExternalTokenHelper) Store(v string) error {
buf := bytes.NewBufferString(v)
cmd, err := h.cmd("store")
if err != nil {
return fmt.Errorf("Error: %s", err)
}
cmd.Stdin = buf
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf(
"Error: %s\n\n%s", err, string(output))
}
return nil
}
func (h *ExternalTokenHelper) Path() string {
return h.BinaryPath
}
func (h *ExternalTokenHelper) cmd(op string) (*exec.Cmd, error) {
script := strings.Replace(h.BinaryPath, "\\", "\\\\", -1) + " " + op
cmd, err := ExecScript(script)
if err != nil {
return nil, err
}
cmd.Env = h.Env
return cmd, nil
}
// ExecScript returns a command to execute a script
func ExecScript(script string) (*exec.Cmd, error) {
var shell, flag string
if runtime.GOOS == "windows" {
shell = "cmd"
flag = "/C"
} else {
shell = "/bin/sh"
flag = "-c"
}
if other := os.Getenv("SHELL"); other != "" {
shell = other
}
cmd := exec.Command(shell, flag, script)
return cmd, nil
}

View file

@ -10,16 +10,13 @@ import (
"testing" "testing"
) )
func TestHelperPath(t *testing.T) { func TestExternalTokenHelperPath(t *testing.T) {
cases := map[string]string{ cases := map[string]string{}
"foo": exePath + " token-foo",
}
unixCases := map[string]string{ unixCases := map[string]string{
"/foo": "/foo", "/foo": "/foo",
} }
windowsCases := map[string]string{ windowsCases := map[string]string{
"/foo": exePath + " token-/foo",
"C:/foo": "C:/foo", "C:/foo": "C:/foo",
`C:\Program Files`: `C:\Program Files`, `C:\Program Files`: `C:\Program Files`,
} }
@ -36,7 +33,10 @@ func TestHelperPath(t *testing.T) {
} }
for k, v := range cases { for k, v := range cases {
actual := HelperPath(k) actual, err := ExternalTokenHelperPath(k)
if err != nil {
t.Fatalf("error getting external helper path: %v", err)
}
if actual != v { if actual != v {
t.Fatalf( t.Fatalf(
"input: %s, expected: %s, got: %s", "input: %s, expected: %s, got: %s",
@ -45,16 +45,16 @@ func TestHelperPath(t *testing.T) {
} }
} }
func TestHelper(t *testing.T) { func TestExternalTokenHelper(t *testing.T) {
Test(t, testHelper(t)) Test(t, testExternalTokenHelper(t))
} }
func testHelper(t *testing.T) *Helper { func testExternalTokenHelper(t *testing.T) *ExternalTokenHelper {
return &Helper{Path: helperPath("helper"), Env: helperEnv()} return &ExternalTokenHelper{BinaryPath: helperPath("helper"), Env: helperEnv()}
} }
func helperPath(s ...string) string { func helperPath(s ...string) string {
cs := []string{"-test.run=TestHelperProcess", "--"} cs := []string{"-test.run=TestExternalTokenHelperProcess", "--"}
cs = append(cs, s...) cs = append(cs, s...)
return fmt.Sprintf( return fmt.Sprintf(
"%s %s", "%s %s",
@ -76,7 +76,7 @@ func helperEnv() []string {
} }
// This is not a real test. This is just a helper process kicked off by tests. // This is not a real test. This is just a helper process kicked off by tests.
func TestHelperProcess(*testing.T) { func TestExternalTokenHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return return
} }

View file

@ -0,0 +1,77 @@
package token
import (
"bytes"
"fmt"
"io"
"os"
"github.com/mitchellh/go-homedir"
)
// InternalTokenHelper fulfills the TokenHelper interface when no external
// token-helper is configured, and avoids shelling out
type InternalTokenHelper struct {
tokenPath string
}
// populateTokenPath figures out the token path using homedir to get the user's
// home directory
func (i *InternalTokenHelper) populateTokenPath() {
homePath, err := homedir.Dir()
if err != nil {
panic(fmt.Errorf("error getting user's home directory: %v", err))
}
i.tokenPath = homePath + "/.vault-token"
}
func (i *InternalTokenHelper) Path() string {
return i.tokenPath
}
// Get gets the value of the stored token, if any
func (i *InternalTokenHelper) Get() (string, error) {
i.populateTokenPath()
f, err := os.Open(i.tokenPath)
if os.IsNotExist(err) {
return "", nil
}
if err != nil {
return "", err
}
defer f.Close()
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, f); err != nil {
return "", err
}
return buf.String(), nil
}
// Store stores the value of the token to the file
func (i *InternalTokenHelper) Store(input string) error {
i.populateTokenPath()
f, err := os.OpenFile(i.tokenPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer f.Close()
buf := bytes.NewBufferString(input)
if _, err := io.Copy(f, buf); err != nil {
return err
}
return nil
}
// Erase erases the value of the token
func (i *InternalTokenHelper) Erase() error {
i.populateTokenPath()
if err := os.Remove(i.tokenPath); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}

View file

@ -0,0 +1,11 @@
package token
import (
"testing"
)
// TestCommand re-uses the existing Test function to ensure proper behavior of
// the internal token helper
func TestCommand(t *testing.T) {
Test(t, &InternalTokenHelper{})
}

View file

@ -11,7 +11,7 @@ import (
// Test is a public function that can be used in other tests to // Test is a public function that can be used in other tests to
// test that a helper is functioning properly. // test that a helper is functioning properly.
func Test(t *testing.T, h *Helper) { func Test(t *testing.T, h TokenHelper) {
if err := h.Store("foo"); err != nil { if err := h.Store("foo"); err != nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
@ -40,16 +40,16 @@ func Test(t *testing.T, h *Helper) {
} }
// TestProcess is used to re-execute this test in order to use it as the // 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 // helper process. For this to work, the TestExternalTokenHelperProcess function must
// exist. // exist.
func TestProcess(t *testing.T, s ...string) { func TestProcess(t *testing.T, s ...string) {
h := &Helper{Path: TestProcessPath(t, s...)} h := &ExternalTokenHelper{BinaryPath: TestProcessPath(t, s...)}
Test(t, h) Test(t, h)
} }
// TestProcessPath returns the path to the test process. // TestProcessPath returns the path to the test process.
func TestProcessPath(t *testing.T, s ...string) string { func TestProcessPath(t *testing.T, s ...string) string {
cs := []string{"-test.run=TestHelperProcess", "--", "GO_WANT_HELPER_PROCESS"} cs := []string{"-test.run=TestExternalTokenHelperProcess", "--", "GO_WANT_HELPER_PROCESS"}
cs = append(cs, s...) cs = append(cs, s...)
return fmt.Sprintf( return fmt.Sprintf(
"%s %s", "%s %s",
@ -57,9 +57,9 @@ func TestProcessPath(t *testing.T, s ...string) string {
strings.Join(cs, " ")) strings.Join(cs, " "))
} }
// TestHelperProcessCLI can be called to implement TestHelperProcess // TestExternalTokenHelperProcessCLI can be called to implement TestExternalTokenHelperProcess
// for TestProcess that just executes a CLI command. // for TestProcess that just executes a CLI command.
func TestHelperProcessCLI(t *testing.T, cmd cli.Command) { func TestExternalTokenHelperProcessCLI(t *testing.T, cmd cli.Command) {
args := os.Args args := os.Args
for len(args) > 0 { for len(args) > 0 {
if args[0] == "--" { if args[0] == "--" {