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.
This commit is contained in:
Martin Atkins 2017-10-17 18:05:51 -07:00
parent 1b60e8fdb6
commit 981c95f699
5 changed files with 186 additions and 0 deletions

View File

@ -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
}

View File

@ -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")
}
})
}

1
svchost/auth/test-helper/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
main

View File

@ -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)
}

View File

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