Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for AppRole vault auth backend #105

Merged
merged 1 commit into from
Mar 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br/> 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.<br/> 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.<br/> 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.<br/> If the backend is mounted to a different location, set `$VAULT_AUTH_USERPASS_MOUNT`. |
Expand Down
13 changes: 13 additions & 0 deletions vault/app-id_strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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) {
Expand Down
104 changes: 104 additions & 0 deletions vault/approle_strategy.go
Original file line number Diff line number Diff line change
@@ -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())
}
87 changes: 87 additions & 0 deletions vault/approle_strategy_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 3 additions & 0 deletions vault/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
13 changes: 8 additions & 5 deletions vault/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func restoreLogFatal() {

func mockLogFatal(args ...interface{}) {
spyLogFatalMsg = (args[0]).(string)
panic(spyLogFatalMsg)
}

func setupMockLogFatal() {
Expand All @@ -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)
}

Expand Down
3 changes: 3 additions & 0 deletions vault/github_strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
6 changes: 4 additions & 2 deletions vault/userpass_strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down