Skip to content

Commit

Permalink
Adding support for AppRole vault auth backend
Browse files Browse the repository at this point in the history
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
  • Loading branch information
hairyhenderson committed Mar 12, 2017
1 parent 0d97dd7 commit 5fcaa7f
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 7 deletions.
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

0 comments on commit 5fcaa7f

Please sign in to comment.