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