mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 02:02:43 +00:00
Move CLI token helper to api module (#25744)
* Move command/config + command/token to api/cliconfig + api/tokenhelper * Remove unused functions and unused import * Simplify and inline function copied from SDK * Delete unused duplicated/forwarding config implementation from command package * Delete unused code, unexport API surface that's only used internally to the package * Fix up license headers * Add changelog * Tweak .gitignore to track hcl files in testdata/ folders
This commit is contained in:
99
api/cliconfig/config.go
Normal file
99
api/cliconfig/config.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cliconfig
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/hcl"
|
||||
"github.com/hashicorp/hcl/hcl/ast"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultConfigPath is the default path to the configuration file
|
||||
defaultConfigPath = "~/.vault"
|
||||
|
||||
// configPathEnv is the environment variable that can be used to
|
||||
// override where the Vault configuration is.
|
||||
configPathEnv = "VAULT_CONFIG_PATH"
|
||||
)
|
||||
|
||||
// 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 defaultConfig 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's internal token store will be used, which
|
||||
// stores the token on disk unencrypted.
|
||||
TokenHelper string `hcl:"token_helper"`
|
||||
}
|
||||
|
||||
// loadConfig reads the configuration from the given path. If path is
|
||||
// empty, then the default path will be used, or the environment variable
|
||||
// if set.
|
||||
func loadConfig(path string) (*defaultConfig, error) {
|
||||
if path == "" {
|
||||
path = defaultConfigPath
|
||||
}
|
||||
if v := os.Getenv(configPathEnv); v != "" {
|
||||
path = v
|
||||
}
|
||||
|
||||
// NOTE: requires HOME env var to be set
|
||||
path, err := homedir.Expand(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error expanding config path %q: %w", path, err)
|
||||
}
|
||||
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf, err := parseConfig(string(contents))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing config file at %q: %w; ensure that the file is valid; Ansible Vault is known to conflict with it", path, err)
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// parseConfig parses the given configuration as a string.
|
||||
func parseConfig(contents string) (*defaultConfig, error) {
|
||||
root, err := hcl.Parse(contents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Top-level item should be the object list
|
||||
list, ok := root.Node.(*ast.ObjectList)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to parse config; does not contain a root object")
|
||||
}
|
||||
|
||||
valid := map[string]struct{}{
|
||||
"token_helper": {},
|
||||
}
|
||||
|
||||
var validationErrors error
|
||||
for _, item := range list.Items {
|
||||
key := item.Keys[0].Token.Value().(string)
|
||||
if _, ok := valid[key]; !ok {
|
||||
validationErrors = multierror.Append(validationErrors, fmt.Errorf("invalid key %q on line %d", key, item.Assign.Line))
|
||||
}
|
||||
}
|
||||
|
||||
if validationErrors != nil {
|
||||
return nil, validationErrors
|
||||
}
|
||||
|
||||
var c defaultConfig
|
||||
if err := hcl.DecodeObject(&c, list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
50
api/cliconfig/config_test.go
Normal file
50
api/cliconfig/config_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cliconfig
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadConfig(t *testing.T) {
|
||||
config, err := loadConfig(filepath.Join("testdata", "config.hcl"))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := &defaultConfig{
|
||||
TokenHelper: "foo",
|
||||
}
|
||||
if !reflect.DeepEqual(expected, config) {
|
||||
t.Fatalf("bad: %#v", config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_noExist(t *testing.T) {
|
||||
config, err := loadConfig("nope/not-once/.never")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if config.TokenHelper != "" {
|
||||
t.Errorf("expected %q to be %q", config.TokenHelper, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseConfig_badKeys(t *testing.T) {
|
||||
_, err := parseConfig(`
|
||||
token_helper = "/token"
|
||||
nope = "true"
|
||||
`)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), `invalid key "nope" on line 3`) {
|
||||
t.Errorf("bad error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
4
api/cliconfig/testdata/config.hcl
vendored
Normal file
4
api/cliconfig/testdata/config.hcl
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
token_helper = "foo"
|
||||
28
api/cliconfig/util.go
Normal file
28
api/cliconfig/util.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cliconfig
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/vault/api/tokenhelper"
|
||||
)
|
||||
|
||||
// DefaultTokenHelper returns the token helper that is configured for Vault.
|
||||
// This helper should only be used for non-server CLI commands.
|
||||
func DefaultTokenHelper() (tokenhelper.TokenHelper, error) {
|
||||
config, err := loadConfig("")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := config.TokenHelper
|
||||
if path == "" {
|
||||
return tokenhelper.NewInternalTokenHelper()
|
||||
}
|
||||
|
||||
path, err = tokenhelper.ExternalTokenHelperPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tokenhelper.ExternalTokenHelper{BinaryPath: path}, nil
|
||||
}
|
||||
@@ -20,7 +20,9 @@ require (
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2
|
||||
github.com/hashicorp/hcl v1.0.0
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/natefinch/atomic v1.0.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
|
||||
@@ -28,12 +30,11 @@ require (
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/google/go-cmp v0.5.7 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.6 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
|
||||
18
api/go.sum
18
api/go.sum
@@ -5,8 +5,9 @@ github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4r
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
|
||||
@@ -41,13 +42,14 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -55,6 +57,8 @@ github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUb
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
@@ -79,8 +83,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
||||
16
api/tokenhelper/helper.go
Normal file
16
api/tokenhelper/helper.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tokenhelper
|
||||
|
||||
// TokenHelper is an interface that contains basic operations that must be
|
||||
// implemented by a token helper
|
||||
type TokenHelper interface {
|
||||
// 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
|
||||
Erase() error
|
||||
Get() (string, error)
|
||||
Store(string) error
|
||||
}
|
||||
136
api/tokenhelper/helper_external.go
Normal file
136
api/tokenhelper/helper_external.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tokenhelper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExternalTokenHelperPath should only be used in dev mode.
|
||||
// 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 "", fmt.Errorf("unknown error getting the external helper path")
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
var _ TokenHelper = (*ExternalTokenHelper)(nil)
|
||||
|
||||
// ExternalTokenHelper should only be used in a dev mode. For all other cases,
|
||||
// InternalTokenHelper should be used.
|
||||
// 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 err
|
||||
}
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%q: %w", string(output), err)
|
||||
}
|
||||
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 "", err
|
||||
}
|
||||
cmd.Stdout = &buf
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", fmt.Errorf("%q: %w", stderr.String(), err)
|
||||
}
|
||||
|
||||
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 err
|
||||
}
|
||||
cmd.Stdin = buf
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("%q: %w", string(output), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ExternalTokenHelper) Path() string {
|
||||
return h.BinaryPath
|
||||
}
|
||||
|
||||
func (h *ExternalTokenHelper) cmd(op string) (*exec.Cmd, error) {
|
||||
script := strings.ReplaceAll(h.BinaryPath, "\\", "\\\\") + " " + 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
|
||||
}
|
||||
140
api/tokenhelper/helper_external_test.go
Normal file
140
api/tokenhelper/helper_external_test.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tokenhelper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExternalTokenHelperPath(t *testing.T) {
|
||||
cases := map[string]string{}
|
||||
|
||||
unixCases := map[string]string{
|
||||
"/foo": "/foo",
|
||||
}
|
||||
windowsCases := map[string]string{
|
||||
"C:/foo": "C:/foo",
|
||||
`C:\Program Files`: `C:\Program Files`,
|
||||
}
|
||||
|
||||
var runtimeCases map[string]string
|
||||
if runtime.GOOS == "windows" {
|
||||
runtimeCases = windowsCases
|
||||
} else {
|
||||
runtimeCases = unixCases
|
||||
}
|
||||
|
||||
for k, v := range runtimeCases {
|
||||
cases[k] = v
|
||||
}
|
||||
|
||||
// We don't expect those to actually exist, so we expect an error. For now,
|
||||
// I'm commenting out the rest of this code as we don't have real external
|
||||
// helpers to test with and the os.Stat will fail with our fake test cases.
|
||||
/*
|
||||
for k, v := range cases {
|
||||
actual, err := ExternalTokenHelperPath(k)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting external helper path: %v", err)
|
||||
}
|
||||
if actual != v {
|
||||
t.Fatalf(
|
||||
"input: %s, expected: %s, got: %s",
|
||||
k, v, actual)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
func TestExternalTokenHelper(t *testing.T) {
|
||||
test(t, testExternalTokenHelper())
|
||||
}
|
||||
|
||||
func testExternalTokenHelper() *ExternalTokenHelper {
|
||||
return &ExternalTokenHelper{BinaryPath: helperPath("helper"), Env: helperEnv()}
|
||||
}
|
||||
|
||||
func helperPath(s ...string) string {
|
||||
cs := []string{"-test.run=TestExternalTokenHelperProcess", "--"}
|
||||
cs = append(cs, s...)
|
||||
return fmt.Sprintf(
|
||||
"%s %s",
|
||||
os.Args[0],
|
||||
strings.Join(cs, " "))
|
||||
}
|
||||
|
||||
func helperEnv() []string {
|
||||
var env []string
|
||||
|
||||
tf, err := os.CreateTemp("", "vault")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tf.Close()
|
||||
|
||||
env = append(env, "GO_HELPER_PATH="+tf.Name(), "GO_WANT_HELPER_PROCESS=1")
|
||||
return env
|
||||
}
|
||||
|
||||
// This is not a real test. This is just a helper process kicked off by tests.
|
||||
func TestExternalTokenHelperProcess(*testing.T) {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
defer os.Exit(0)
|
||||
|
||||
args := os.Args
|
||||
for len(args) > 0 {
|
||||
if args[0] == "--" {
|
||||
args = args[1:]
|
||||
break
|
||||
}
|
||||
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No command\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
cmd, args := args[0], args[1:]
|
||||
switch cmd {
|
||||
case "helper":
|
||||
path := os.Getenv("GO_HELPER_PATH")
|
||||
|
||||
switch args[0] {
|
||||
case "erase":
|
||||
os.Remove(path)
|
||||
case "get":
|
||||
f, err := os.Open(path)
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Err: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(os.Stdout, f)
|
||||
case "store":
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Err: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
io.Copy(f, os.Stdin)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
104
api/tokenhelper/helper_internal.go
Normal file
104
api/tokenhelper/helper_internal.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tokenhelper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/natefinch/atomic"
|
||||
)
|
||||
|
||||
var _ TokenHelper = (*InternalTokenHelper)(nil)
|
||||
|
||||
// InternalTokenHelper fulfills the TokenHelper interface when no external
|
||||
// token-helper is configured, and avoids shelling out
|
||||
type InternalTokenHelper struct {
|
||||
tokenPath string
|
||||
homeDir string
|
||||
}
|
||||
|
||||
func NewInternalTokenHelper() (*InternalTokenHelper, error) {
|
||||
homeDir, err := homedir.Dir()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error getting user's home directory: %v", err))
|
||||
}
|
||||
return &InternalTokenHelper{homeDir: homeDir}, err
|
||||
}
|
||||
|
||||
// populateTokenPath figures out the token path using homedir to get the user's
|
||||
// home directory
|
||||
func (i *InternalTokenHelper) populateTokenPath() {
|
||||
i.tokenPath = filepath.Join(i.homeDir, ".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 strings.TrimSpace(buf.String()), nil
|
||||
}
|
||||
|
||||
// Store stores the value of the token to the file. We always overwrite any
|
||||
// existing file atomically to ensure that ownership and permissions are set
|
||||
// appropriately.
|
||||
func (i *InternalTokenHelper) Store(input string) error {
|
||||
i.populateTokenPath()
|
||||
tmpFile := i.tokenPath + ".tmp"
|
||||
f, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
defer os.Remove(tmpFile)
|
||||
|
||||
_, err = io.WriteString(f, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We don't care so much about atomic writes here. We're using this package
|
||||
// because we don't have a portable way of verifying that the target file
|
||||
// is owned by the correct user. The simplest way of ensuring that is
|
||||
// to simply re-write it, and the simplest way to ensure that we don't
|
||||
// damage an existing working file due to error is the write-rename pattern.
|
||||
// os.Rename on Windows will return an error if the target already exists.
|
||||
return atomic.ReplaceFile(tmpFile, i.tokenPath)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
64
api/tokenhelper/helper_internal_test.go
Normal file
64
api/tokenhelper/helper_internal_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tokenhelper
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCommand re-uses the existing Test function to ensure proper behavior of
|
||||
// the internal token helper
|
||||
func TestCommand(t *testing.T) {
|
||||
helper, err := NewInternalTokenHelper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
test(t, helper)
|
||||
}
|
||||
|
||||
func TestInternalHelperFilePerms(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", t.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
helper, err := NewInternalTokenHelper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
helper.homeDir = tmpDir
|
||||
|
||||
tmpFile := filepath.Join(tmpDir, ".vault-token")
|
||||
f, err := os.Create(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := os.Stat(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if fi.Mode().Perm()&0o04 != 0o04 {
|
||||
t.Fatalf("expected world-readable/writable permission bits, got: %o", fi.Mode().Perm())
|
||||
}
|
||||
|
||||
err = helper.Store("bogus_token")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fi, err = os.Stat(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if fi.Mode().Perm()&0o04 != 0 {
|
||||
t.Fatalf("expected no world-readable/writable permission bits, got: %o", fi.Mode().Perm())
|
||||
}
|
||||
}
|
||||
38
api/tokenhelper/testing.go
Normal file
38
api/tokenhelper/testing.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package tokenhelper
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 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, h TokenHelper) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user