svchost/auth: store and forget operations for helper programs

This introduces two new verbs to the credentials helper protocol to store
and forget credentials, and uses them to implement StoreForHost and
ForgetForHost.
This commit is contained in:
Martin Atkins 2019-07-30 16:05:27 -07:00
parent 821d0401bc
commit ec8dadcfa9
3 changed files with 124 additions and 14 deletions

View File

@ -7,6 +7,8 @@ import (
"os/exec"
"path/filepath"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/terraform/svchost"
)
@ -80,9 +82,68 @@ func (s *helperProgramCredentialsSource) ForHost(host svchost.Hostname) (HostCre
}
func (s *helperProgramCredentialsSource) StoreForHost(host svchost.Hostname, credentials HostCredentialsWritable) error {
return fmt.Errorf("credentials helper cannot currently store new credentials")
args := make([]string, len(s.args), len(s.args)+2)
copy(args, s.args)
args = append(args, "store")
args = append(args, string(host))
toStore := credentials.ToStore()
toStoreRaw, err := ctyjson.Marshal(toStore, toStore.Type())
if err != nil {
return fmt.Errorf("can't serialize credentials to store: %s", err)
}
inReader := bytes.NewReader(toStoreRaw)
errBuf := bytes.Buffer{}
cmd := exec.Cmd{
Path: s.executable,
Args: args,
Stdin: inReader,
Stderr: &errBuf,
Stdout: nil,
}
err = cmd.Run()
if _, isExitErr := err.(*exec.ExitError); isExitErr {
errText := errBuf.String()
if errText == "" {
// Shouldn't happen for a well-behaved helper program
return fmt.Errorf("error in %s, but it produced no error message", s.executable)
}
return fmt.Errorf("error in %s: %s", s.executable, errText)
} else if err != nil {
return fmt.Errorf("failed to run %s: %s", s.executable, err)
}
return nil
}
func (s *helperProgramCredentialsSource) ForgetForHost(host svchost.Hostname) error {
return fmt.Errorf("credentials helper cannot currently forget existing credentials")
args := make([]string, len(s.args), len(s.args)+2)
copy(args, s.args)
args = append(args, "forget")
args = append(args, string(host))
errBuf := bytes.Buffer{}
cmd := exec.Cmd{
Path: s.executable,
Args: args,
Stdin: nil,
Stderr: &errBuf,
Stdout: nil,
}
err := cmd.Run()
if _, isExitErr := err.(*exec.ExitError); isExitErr {
errText := errBuf.String()
if errText == "" {
// Shouldn't happen for a well-behaved helper program
return fmt.Errorf("error in %s, but it produced no error message", s.executable)
}
return fmt.Errorf("error in %s: %s", s.executable, errText)
} else if err != nil {
return fmt.Errorf("failed to run %s: %s", s.executable, err)
}
return nil
}

View File

@ -56,4 +56,28 @@ func TestHelperProgramCredentialsSource(t *testing.T) {
t.Error("completed successfully; want error")
}
})
t.Run("store happy path", func(t *testing.T) {
err := src.StoreForHost(svchost.Hostname("example.com"), HostCredentialsToken("example-token"))
if err != nil {
t.Fatal(err)
}
})
t.Run("store error", func(t *testing.T) {
err := src.StoreForHost(svchost.Hostname("fail.example.com"), HostCredentialsToken("example-token"))
if err == nil {
t.Error("completed successfully; want error")
}
})
t.Run("forget happy path", func(t *testing.T) {
err := src.ForgetForHost(svchost.Hostname("example.com"))
if err != nil {
t.Fatal(err)
}
})
t.Run("forget error", func(t *testing.T) {
err := src.ForgetForHost(svchost.Hostname("fail.example.com"))
if err == nil {
t.Error("completed successfully; want error")
}
})
}

View File

@ -1,7 +1,9 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
)
@ -15,21 +17,44 @@ func main() {
die("not enough arguments\n")
}
if args[1] != "get" {
die("unknown subcommand %q\n", args[1])
}
host := args[2]
switch args[1] {
case "get":
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
}
case "store":
dataSrc, err := ioutil.ReadAll(os.Stdin)
if err != nil {
die("invalid input: %s", err)
}
var data map[string]interface{}
err = json.Unmarshal(dataSrc, &data)
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")
switch host {
case "example.com":
if data["token"] != "example-token" {
die("incorrect token value to store")
}
default:
die("can't store credentials for %s", host)
}
case "forget":
switch host {
case "example.com":
// okay!
default:
die("can't forget credentials for %s", host)
}
default:
fmt.Print("{}") // no credentials available
die("unknown subcommand %q\n", args[1])
}
}