svchost/auth: CredentialsSource that runs an external program

This CredentialsSource can serve as an extension point to pass credentials
from an arbitrary external system to Terraform. For example, an external
helper program could fetch limited-time credentials from HashiCorp Vault
and return them, thus avoiding the need for any static configuration to
be maintained locally (except a Vault token!).

So far there are no real programs implementing this protocol, though this
commit includes a basic implementation that we use for unit tests.
pull/16407/head
Martin Atkins 9 years ago
parent 1b60e8fdb6
commit 981c95f699

@ -0,0 +1,80 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"github.com/hashicorp/terraform/svchost"
)
type helperProgramCredentialsSource struct {
executable string
args []string
}
// HelperProgramCredentialsSource returns a CredentialsSource that runs the
// given program with the given arguments in order to obtain credentials.
//
// The given executable path must be an absolute path; it is the caller's
// responsibility to validate and process a relative path or other input
// provided by an end-user. If the given path is not absolute, this
// function will panic.
//
// When credentials are requested, the program will be run in a child process
// with the given arguments along with two additional arguments added to the
// end of the list: the literal string "get", followed by the requested
// hostname in ASCII compatibility form (punycode form).
func HelperProgramCredentialsSource(executable string, args ...string) CredentialsSource {
if !filepath.IsAbs(executable) {
panic("NewCredentialsSourceHelperProgram requires absolute path to executable")
}
fullArgs := make([]string, len(args)+1)
fullArgs[0] = executable
copy(fullArgs[1:], args)
return &helperProgramCredentialsSource{
executable: executable,
args: fullArgs,
}
}
func (s *helperProgramCredentialsSource) ForHost(host svchost.Hostname) (HostCredentials, error) {
args := make([]string, len(s.args), len(s.args)+2)
copy(args, s.args)
args = append(args, "get")
args = append(args, string(host))
outBuf := bytes.Buffer{}
errBuf := bytes.Buffer{}
cmd := exec.Cmd{
Path: s.executable,
Args: args,
Stdin: nil,
Stdout: &outBuf,
Stderr: &errBuf,
}
err := cmd.Run()
if _, isExitErr := err.(*exec.ExitError); isExitErr {
errText := errBuf.String()
if errText == "" {
// Shouldn't happen for a well-behaved helper program
return nil, fmt.Errorf("error in %s, but it produced no error message", s.executable)
}
return nil, fmt.Errorf("error in %s: %s", s.executable, errText)
} else if err != nil {
return nil, fmt.Errorf("failed to run %s: %s", s.executable, err)
}
var m map[string]interface{}
err = json.Unmarshal(outBuf.Bytes(), &m)
if err != nil {
return nil, fmt.Errorf("malformed output from %s: %s", s.executable, err)
}
return HostCredentialsFromMap(m), nil
}

@ -0,0 +1,59 @@
package auth
import (
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/svchost"
)
func TestHelperProgramCredentialsSource(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
program := filepath.Join(wd, "test-helper/test-helper")
t.Logf("testing with helper at %s", program)
src := HelperProgramCredentialsSource(program)
t.Run("happy path", func(t *testing.T) {
creds, err := src.ForHost(svchost.Hostname("example.com"))
if err != nil {
t.Fatal(err)
}
if tokCreds, isTok := creds.(HostCredentialsToken); isTok {
if got, want := string(tokCreds), "example-token"; got != want {
t.Errorf("wrong token %q; want %q", got, want)
}
} else {
t.Errorf("wrong type of credentials %T", creds)
}
})
t.Run("no credentials", func(t *testing.T) {
creds, err := src.ForHost(svchost.Hostname("nothing.example.com"))
if err != nil {
t.Fatal(err)
}
if creds != nil {
t.Errorf("got credentials; want nil")
}
})
t.Run("unsupported credentials type", func(t *testing.T) {
creds, err := src.ForHost(svchost.Hostname("other-cred-type.example.com"))
if err != nil {
t.Fatal(err)
}
if creds != nil {
t.Errorf("got credentials; want nil")
}
})
t.Run("lookup error", func(t *testing.T) {
_, err := src.ForHost(svchost.Hostname("fail.example.com"))
if err == nil {
t.Error("completed successfully; want error")
}
})
}

@ -0,0 +1,39 @@
package main
import (
"fmt"
"os"
)
// This is a simple program that implements the "helper program" protocol
// for the svchost/auth package for unit testing purposes.
func main() {
args := os.Args
if len(args) < 3 {
die("not enough arguments\n")
}
if args[1] != "get" {
die("unknown subcommand %q\n", args[1])
}
host := args[2]
switch host {
case "example.com":
fmt.Print(`{"token":"example-token"}`)
case "other-cred-type.example.com":
fmt.Print(`{"username":"alfred"}`) // unrecognized by main program
case "fail.example.com":
die("failing because you told me to fail\n")
default:
fmt.Print("{}") // no credentials available
}
}
func die(f string, args ...interface{}) {
fmt.Fprintf(os.Stderr, fmt.Sprintf(f, args...))
os.Exit(1)
}

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -eu
cd "$( dirname "${BASH_SOURCE[0]}" )"
[ -x main ] || go build -o main .
exec ./main "$@"
Loading…
Cancel
Save