mirror of https://github.com/hashicorp/terraform
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
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 @@
|
||||
main
|
||||
@ -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…
Reference in new issue