diff --git a/README.md b/README.md
index 21ffadbc0..7fa6faf83 100644
--- a/README.md
+++ b/README.md
@@ -581,6 +581,7 @@ This table describes the currently-supported authentication mechanisms and how t
| auth backend | configuration |
| ---: |--|
+| [`approle`](https://www.vaultproject.io/docs/auth/approle.html) | Environment variables `$VAULT_ROLE_ID` and `$VAULT_SECRET_ID` must be set to the appropriate values.
If the backend is mounted to a different location, set `$VAULT_AUTH_APPROLE_MOUNT`. |
| [`app-id`](https://www.vaultproject.io/docs/auth/app-id.html) | Environment variables `$VAULT_APP_ID` and `$VAULT_USER_ID` must be set to the appropriate values.
If the backend is mounted to a different location, set `$VAULT_AUTH_APP_ID_MOUNT`. |
| [`github`](https://www.vaultproject.io/docs/auth/github.html) | Environment variable `$VAULT_AUTH_GITHUB_TOKEN` must be set to an appropriate value.
If the backend is mounted to a different location, set `$VAULT_AUTH_GITHUB_MOUNT`. |
| [`userpass`](https://www.vaultproject.io/docs/auth/userpass.html) | Environment variables `$VAULT_AUTH_USERNAME` and `$VAULT_AUTH_PASSWORD` must be set to the appropriate values.
If the backend is mounted to a different location, set `$VAULT_AUTH_USERPASS_MOUNT`. |
diff --git a/vault/app-id_strategy_test.go b/vault/app-id_strategy_test.go
index a5e9effc0..54c3d7450 100644
--- a/vault/app-id_strategy_test.go
+++ b/vault/app-id_strategy_test.go
@@ -9,6 +9,10 @@ import (
)
func TestNewAppIDAuthStrategy(t *testing.T) {
+ defer os.Unsetenv("VAULT_APP_ID")
+ defer os.Unsetenv("VAULT_USER_ID")
+ defer os.Unsetenv("VAULT_AUTH_APP_ID_MOUNT")
+
os.Unsetenv("VAULT_APP_ID")
os.Unsetenv("VAULT_USER_ID")
assert.Nil(t, NewAppIDAuthStrategy())
@@ -25,6 +29,15 @@ func TestNewAppIDAuthStrategy(t *testing.T) {
auth := NewAppIDAuthStrategy()
assert.Equal(t, "foo", auth.AppID)
assert.Equal(t, "bar", auth.UserID)
+ assert.Equal(t, "app-id", auth.Mount)
+
+ os.Setenv("VAULT_APP_ID", "baz")
+ os.Setenv("VAULT_USER_ID", "qux")
+ os.Setenv("VAULT_AUTH_APP_ID_MOUNT", "quux")
+ auth = NewAppIDAuthStrategy()
+ assert.Equal(t, "baz", auth.AppID)
+ assert.Equal(t, "qux", auth.UserID)
+ assert.Equal(t, "quux", auth.Mount)
}
func TestGetToken_AppIDErrorsGivenNetworkError(t *testing.T) {
diff --git a/vault/approle_strategy.go b/vault/approle_strategy.go
new file mode 100644
index 000000000..052c1cf73
--- /dev/null
+++ b/vault/approle_strategy.go
@@ -0,0 +1,104 @@
+package vault
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+)
+
+// AppRoleAuthStrategy - an AuthStrategy that uses Vault's approle authentication backend.
+type AppRoleAuthStrategy struct {
+ RoleID string `json:"role_id"`
+ SecretID string `json:"secret_id"`
+ Mount string `json:"-"`
+ hc *http.Client
+}
+
+// NewAppRoleAuthStrategy - create an AuthStrategy that uses Vault's approle auth
+// backend.
+func NewAppRoleAuthStrategy() *AppRoleAuthStrategy {
+ roleID := os.Getenv("VAULT_ROLE_ID")
+ secretID := os.Getenv("VAULT_SECRET_ID")
+ mount := os.Getenv("VAULT_AUTH_APPROLE_MOUNT")
+ if mount == "" {
+ mount = "approle"
+ }
+ if roleID != "" && secretID != "" {
+ return &AppRoleAuthStrategy{roleID, secretID, mount, nil}
+ }
+ return nil
+}
+
+// GetHTTPClient configures the HTTP client with a timeout
+func (a *AppRoleAuthStrategy) GetHTTPClient() *http.Client {
+ if a.hc == nil {
+ a.hc = &http.Client{Timeout: time.Second * 5}
+ }
+ return a.hc
+}
+
+// SetToken is a no-op for AppRoleAuthStrategy as a token hasn't been acquired yet
+func (a *AppRoleAuthStrategy) SetToken(req *http.Request) {
+ // no-op
+}
+
+// Do wraps http.Client.Do
+func (a *AppRoleAuthStrategy) Do(req *http.Request) (*http.Response, error) {
+ hc := a.GetHTTPClient()
+ return hc.Do(req)
+}
+
+// GetToken - log in to the approle auth backend and return the client token
+func (a *AppRoleAuthStrategy) GetToken(addr *url.URL) (string, error) {
+ buf := new(bytes.Buffer)
+ json.NewEncoder(buf).Encode(&a)
+
+ u := &url.URL{}
+ *u = *addr
+ u.Path = "/v1/auth/" + a.Mount + "/login"
+ res, err := requestAndFollow(a, "POST", u, buf.Bytes())
+ if err != nil {
+ return "", err
+ }
+ response := &AppRoleAuthResponse{}
+ err = json.NewDecoder(res.Body).Decode(response)
+ res.Body.Close()
+ if err != nil {
+ return "", err
+ }
+ if res.StatusCode != 200 {
+ err := fmt.Errorf("Unexpected HTTP status %d on AppRole login to %s: %s", res.StatusCode, u, response)
+ return "", err
+ }
+ return response.Auth.ClientToken, nil
+}
+
+// Revokable -
+func (a *AppRoleAuthStrategy) Revokable() bool {
+ return true
+}
+
+func (a *AppRoleAuthStrategy) String() string {
+ return fmt.Sprintf("role_id: %s, secret_id: %s, mount: %s", a.RoleID, a.SecretID, a.Mount)
+}
+
+// AppRoleAuthResponse - the Auth response from /v1/auth/approle/login
+type AppRoleAuthResponse struct {
+ Auth struct {
+ ClientToken string `json:"client_token"`
+ LeaseDuration int64 `json:"lease_duration"`
+ Metadata struct{} `json:"metadata"`
+ Policies []string `json:"policies"`
+ Renewable bool `json:"renewable"`
+ } `json:"auth"`
+}
+
+func (a *AppRoleAuthResponse) String() string {
+ buf := new(bytes.Buffer)
+ json.NewEncoder(buf).Encode(&a)
+ return string(buf.Bytes())
+}
diff --git a/vault/approle_strategy_test.go b/vault/approle_strategy_test.go
new file mode 100644
index 000000000..5e2463b76
--- /dev/null
+++ b/vault/approle_strategy_test.go
@@ -0,0 +1,87 @@
+package vault
+
+import (
+ "net/url"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewAppRoleAuthStrategy(t *testing.T) {
+ defer os.Unsetenv("VAULT_ROLE_ID")
+ defer os.Unsetenv("VAULT_SECRET_ID")
+ defer os.Unsetenv("VAULT_AUTH_APPROLE_MOUNT")
+
+ os.Unsetenv("VAULT_ROLE_ID")
+ os.Unsetenv("VAULT_SECRET_ID")
+ assert.Nil(t, NewAppRoleAuthStrategy())
+
+ os.Setenv("VAULT_ROLE_ID", "foo")
+ assert.Nil(t, NewAppRoleAuthStrategy())
+
+ os.Unsetenv("VAULT_ROLE_ID")
+ os.Setenv("VAULT_SECRET_ID", "bar")
+ assert.Nil(t, NewAppRoleAuthStrategy())
+
+ os.Setenv("VAULT_ROLE_ID", "foo")
+ os.Setenv("VAULT_SECRET_ID", "bar")
+ auth := NewAppRoleAuthStrategy()
+ assert.Equal(t, "foo", auth.RoleID)
+ assert.Equal(t, "bar", auth.SecretID)
+ assert.Equal(t, "approle", auth.Mount)
+
+ os.Setenv("VAULT_ROLE_ID", "baz")
+ os.Setenv("VAULT_SECRET_ID", "qux")
+ os.Setenv("VAULT_AUTH_APPROLE_MOUNT", "quux")
+ auth = NewAppRoleAuthStrategy()
+ assert.Equal(t, "baz", auth.RoleID)
+ assert.Equal(t, "qux", auth.SecretID)
+ assert.Equal(t, "quux", auth.Mount)
+}
+
+func TestGetToken_AppRoleErrorsGivenNetworkError(t *testing.T) {
+ server, client := setupErrorHTTP()
+ defer server.Close()
+
+ vaultURL, _ := url.Parse("http://vault:8200")
+
+ auth := &AppRoleAuthStrategy{"foo", "bar", "approle", client}
+ _, err := auth.GetToken(vaultURL)
+ assert.Error(t, err)
+}
+
+func TestGetToken_AppRoleErrorsGivenHTTPErrorStatus(t *testing.T) {
+ server, client := setupHTTP(500, "application/json; charset=utf-8", `{}`)
+ defer server.Close()
+
+ vaultURL, _ := url.Parse("http://vault:8200")
+
+ auth := &AppRoleAuthStrategy{"foo", "bar", "approle", client}
+ _, err := auth.GetToken(vaultURL)
+ assert.Error(t, err)
+}
+
+func TestGetToken_AppRoleErrorsGivenBadJSON(t *testing.T) {
+ server, client := setupHTTP(200, "application/json; charset=utf-8", `{`)
+ defer server.Close()
+
+ vaultURL, _ := url.Parse("http://vault:8200")
+
+ auth := &AppRoleAuthStrategy{"foo", "bar", "approle", client}
+ _, err := auth.GetToken(vaultURL)
+ assert.Error(t, err)
+}
+
+func TestGetToken_AppRole(t *testing.T) {
+ server, client := setupHTTP(200, "application/json; charset=utf-8", `{"auth": {"client_token": "baz"}}`)
+ defer server.Close()
+
+ vaultURL, _ := url.Parse("http://vault:8200")
+
+ auth := &AppRoleAuthStrategy{"foo", "bar", "approle", client}
+ token, err := auth.GetToken(vaultURL)
+ assert.NoError(t, err)
+
+ assert.Equal(t, "baz", token)
+}
diff --git a/vault/client.go b/vault/client.go
index 546b04494..42f1e2039 100644
--- a/vault/client.go
+++ b/vault/client.go
@@ -53,6 +53,9 @@ func getVaultAddr() *url.URL {
}
func getAuthStrategy() AuthStrategy {
+ if auth := NewAppRoleAuthStrategy(); auth != nil {
+ return auth
+ }
if auth := NewAppIDAuthStrategy(); auth != nil {
return auth
}
diff --git a/vault/client_test.go b/vault/client_test.go
index 9d37f2371..4437806dc 100644
--- a/vault/client_test.go
+++ b/vault/client_test.go
@@ -17,6 +17,7 @@ func restoreLogFatal() {
func mockLogFatal(args ...interface{}) {
spyLogFatalMsg = (args[0]).(string)
+ panic(spyLogFatalMsg)
}
func setupMockLogFatal() {
@@ -27,19 +28,21 @@ func TestNewClient_NoVaultAddr(t *testing.T) {
os.Unsetenv("VAULT_ADDR")
defer restoreLogFatal()
setupMockLogFatal()
- c := NewClient()
- assert.Nil(t, c.Addr)
+ assert.Panics(t, func() {
+ NewClient()
+ })
assert.Equal(t, "VAULT_ADDR is an unparseable URL!", spyLogFatalMsg)
}
func TestLogin_NoAuthStrategy(t *testing.T) {
os.Setenv("VAULT_ADDR", "https://localhost:8500")
- os.Unsetenv("VAULT_APP_ID")
- os.Unsetenv("VAULT_USER_ID")
+ defer os.Unsetenv("VAULT_ADDR")
os.Setenv("HOME", "/tmp")
defer restoreLogFatal()
setupMockLogFatal()
- _ = NewClient()
+ assert.Panics(t, func() {
+ NewClient()
+ })
assert.Equal(t, "No vault auth strategy configured", spyLogFatalMsg)
}
diff --git a/vault/github_strategy_test.go b/vault/github_strategy_test.go
index d7c33cb96..29020b03f 100644
--- a/vault/github_strategy_test.go
+++ b/vault/github_strategy_test.go
@@ -9,6 +9,9 @@ import (
)
func TestNewGitHubAuthStrategy(t *testing.T) {
+ defer os.Unsetenv("VAULT_AUTH_GITHUB_TOKEN")
+ defer os.Unsetenv("VAULT_AUTH_GITHUB_MOUNT")
+
os.Unsetenv("VAULT_AUTH_GITHUB_TOKEN")
assert.Nil(t, NewGitHubAuthStrategy())
diff --git a/vault/userpass_strategy_test.go b/vault/userpass_strategy_test.go
index 769832f79..48fcc2ab0 100644
--- a/vault/userpass_strategy_test.go
+++ b/vault/userpass_strategy_test.go
@@ -9,8 +9,10 @@ import (
)
func TestNewUserPassAuthStrategy(t *testing.T) {
- os.Unsetenv("VAULT_AUTH_USERNAME")
- os.Unsetenv("VAULT_AUTH_PASSWORD")
+ defer os.Unsetenv("VAULT_AUTH_USERNAME")
+ defer os.Unsetenv("VAULT_AUTH_PASSWORD")
+ defer os.Unsetenv("VAULT_AUTH_USERPASS_MOUNT")
+
assert.Nil(t, NewUserPassAuthStrategy())
os.Setenv("VAULT_AUTH_USERNAME", "foo")