Add vaultService to allow different vendor impls & impl Hashicorp status

This commit is contained in:
chris-j-h 2019-07-07 14:04:02 +01:00
parent a40c72f8a5
commit 3d7bf16754
4 changed files with 223 additions and 29 deletions

View File

@ -1,7 +1,6 @@
package vault
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
@ -15,23 +14,12 @@ type VaultBackend struct {
// Other backend impls require mutexes for safety as their wallets can change at any time (e.g. if a file/usb is added/removed). vaultWallets can only be created at startup so there is no danger of concurrent reads and writes.
}
func (b *VaultBackend) Wallets() []accounts.Wallet {
cpy := make([]accounts.Wallet, len(b.wallets))
copy(cpy, b.wallets)
return cpy
}
func (b *VaultBackend) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
return b.updateScope.Track(b.updateFeed.Subscribe(sink))
}
func NewHashicorpBackend(walletConfigs []hashicorpWalletConfig) VaultBackend {
wallets := []accounts.Wallet{}
for _, conf := range walletConfigs {
w, err := newHashicorpWallet(conf)
if err != nil {
// do something with error and do not append returned w to wallets
log.Error("unable to create Hashicorp wallet from config", "err", err)
continue
}
@ -45,17 +33,14 @@ func NewHashicorpBackend(walletConfigs []hashicorpWalletConfig) VaultBackend {
}
}
func newHashicorpWallet(config hashicorpWalletConfig) (vaultWallet, error) {
var url accounts.URL
func (b *VaultBackend) Wallets() []accounts.Wallet {
cpy := make([]accounts.Wallet, len(b.wallets))
copy(cpy, b.wallets)
return cpy
}
//to parse a string url as an accounts.URL it must first be in json format
toParse := fmt.Sprintf("\"%v\"", config.Client.Url)
if err := url.UnmarshalJSON([]byte(toParse)); err != nil {
return vaultWallet{}, err
}
return vaultWallet{Url: url}, nil
func (b *VaultBackend) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
return b.updateScope.Track(b.updateFeed.Subscribe(sink))
}
// walletsByUrl implements the sort interface to enable the sorting of a slice of wallets alphanumerically by their urls

View File

@ -27,13 +27,13 @@ func TestNewHashicorpBackend_CreatesWalletsFromConfig(t *testing.T) {
s := strings.Split(url, "://")
scheme, path := s[0], s[1]
wlts = append(wlts, vaultWallet{Url: accounts.URL{Scheme: scheme, Path: path}})
wlts = append(wlts, vaultWallet{url: accounts.URL{Scheme: scheme, Path: path}})
for _, u := range urls {
s := strings.Split(u, "://")
scheme, path := s[0], s[1]
wlts = append(wlts, vaultWallet{Url: accounts.URL{Scheme: scheme, Path: path}})
wlts = append(wlts, vaultWallet{url: accounts.URL{Scheme: scheme, Path: path}})
}
return wlts
@ -89,13 +89,13 @@ func TestVaultBackend_Wallets_ReturnsWallets(t *testing.T) {
func TestVaultBackend_Wallets_ReturnsCopy(t *testing.T) {
b := VaultBackend{
wallets: []accounts.Wallet{
vaultWallet{Url: accounts.URL{Scheme: "http", Path: "url"}},
vaultWallet{url: accounts.URL{Scheme: "http", Path: "url"}},
},
}
got := b.Wallets()
got[0] = vaultWallet{Url: accounts.URL{Scheme: "http", Path: "otherurl"}}
got[0] = vaultWallet{url: accounts.URL{Scheme: "http", Path: "otherurl"}}
if reflect.DeepEqual(b.wallets, got) {
t.Fatal("changes to returned slice should not affect slice in backend")

View File

@ -1,22 +1,54 @@
package vault
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/core/types"
"github.com/hashicorp/vault/api"
"math/big"
)
type vaultWallet struct {
Url accounts.URL
url accounts.URL
vault vaultService
}
// vault related behaviour that will be specific to each vault type
type vaultService interface {
status() (string, error)
}
func newHashicorpWallet(config hashicorpWalletConfig) (vaultWallet, error) {
var url accounts.URL
//to parse a string url as an accounts.URL it must first be in json format
toParse := fmt.Sprintf("\"%v\"", config.Client.Url)
if err := url.UnmarshalJSON([]byte(toParse)); err != nil {
return vaultWallet{}, err
}
return vaultWallet{url: url, vault: hashicorpService{}}, nil
}
func (w vaultWallet) URL() accounts.URL {
return w.Url
return w.url
}
const (
open = "open"
closed = "closed"
)
// the vault service should return open and nil error if status is good
func (w vaultWallet) Status() (string, error) {
panic("implement me")
if w.vault == nil {
return closed, nil
}
return w.vault.status()
}
func (w vaultWallet) Open(passphrase string) error {
@ -59,3 +91,43 @@ func (w vaultWallet) SignTxWithPassphrase(account accounts.Account, passphrase s
panic("implement me")
}
type hashicorpService struct {
client *api.Client
}
const (
hashicorpHealthcheckFailed = "Hashicorp Vault healthcheck failed"
hashicorpUninitialized = "Hashicorp Vault uninitialized"
hashicorpSealed = "Hashicorp Vault sealed"
)
var (
hashicorpSealedErr = errors.New(hashicorpSealed)
hashicorpUninitializedErr = errors.New(hashicorpUninitialized)
)
type hashicorpHealthcheckErr struct {
err error
}
func (e hashicorpHealthcheckErr) Error() string {
return fmt.Sprintf("%v: %v", hashicorpHealthcheckFailed, e.err)
}
func (c hashicorpService) status() (string, error) {
health, err := c.client.Sys().Health()
if err != nil {
return hashicorpHealthcheckFailed, hashicorpHealthcheckErr{err: err}
}
if !health.Initialized {
return hashicorpUninitialized, hashicorpUninitializedErr
}
if health.Sealed {
return hashicorpSealed, hashicorpSealedErr
}
return open, nil
}

View File

@ -0,0 +1,137 @@
package vault
import (
"encoding/json"
"github.com/ethereum/go-ethereum/accounts"
"github.com/hashicorp/vault/api"
"net/http"
"net/http/httptest"
"testing"
)
func TestVaultWallet_URL(t *testing.T) {
in := accounts.URL{Scheme: "http", Path: "url"}
w := vaultWallet{url: in}
got := w.URL()
if in.Cmp(got) != 0 {
t.Fatalf("want: %v, got: %v", in, got)
}
}
func TestVaultWallet_Status_ClosedWhenNoVaultService(t *testing.T) {
w := vaultWallet{}
status, err := w.Status()
if err != nil {
t.Fatal(err)
}
if status != closed {
t.Fatalf("want: %v, got: %v", closed, status)
}
}
// makeMockHashicorpService creates a new httptest.Server which responds with mockResponse for all requests. A default Hashicorp api.Client with URL updated with the httptest.Server's URL is returned. The Close() function for the httptest.Server and should be executed before test completion (probably best to defer as soon as it is returned)
func makeMockHashicorpClient(t *testing.T, mockResponse []byte) (*api.Client, func()) {
vaultServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(mockResponse)
}))
//create default client and update URL to use mock vault server
config := api.DefaultConfig()
config.Address = vaultServer.URL
client, err := api.NewClient(config)
if err != nil {
t.Fatalf("err creating client: %v", err)
}
return client, vaultServer.Close
}
func TestVaultWallet_Status_HashicorpHealthcheckSuccessful(t *testing.T) {
const (
uninitialised = "uninitialized"
sealed = "sealed"
open = "open"
)
makeMockHashicorpResponse := func(t *testing.T, vaultStatus string) []byte {
var vaultResponse api.HealthResponse
switch vaultStatus {
case uninitialised:
vaultResponse.Initialized = false
case sealed:
vaultResponse.Initialized = true
vaultResponse.Sealed = true
case open:
vaultResponse.Initialized = true
vaultResponse.Sealed = false
}
b, err := json.Marshal(vaultResponse)
if err != nil {
t.Fatalf("err marshalling mock response: %v", err)
}
return b
}
tests := []struct{
vaultStatus string
want string
wantErr error
}{
{vaultStatus: uninitialised, want: hashicorpUninitialized, wantErr: hashicorpUninitializedErr},
{vaultStatus: sealed, want: hashicorpSealed, wantErr: hashicorpSealedErr},
{vaultStatus: open, want: open, wantErr: nil},
}
for _, tt := range tests {
t.Run(tt.vaultStatus, func(t *testing.T) {
b := makeMockHashicorpResponse(t, tt.vaultStatus)
c, cleanup := makeMockHashicorpClient(t, b)
defer cleanup()
w := vaultWallet{
vault: hashicorpService{client: c},
}
status, err := w.Status()
if tt.wantErr != err {
t.Fatalf("want: %v, got: %v", tt.wantErr, err)
}
if tt.want != status {
t.Fatalf("want: %v, got: %v", tt.want, status)
}
})
}
}
func TestVaultWallet_Status_HashicorpHealthcheckFailed(t *testing.T) {
b := []byte("this is not the bytes for an api.HealthResponse and will cause a client error")
c, cleanup := makeMockHashicorpClient(t, b)
defer cleanup()
w := vaultWallet{
vault: hashicorpService{client: c},
}
status, err := w.Status()
if _, ok := err.(hashicorpHealthcheckErr); !ok {
t.Fatal("returned error should be of type hashicorpHealthcheckErr")
}
if status != hashicorpHealthcheckFailed {
t.Fatalf("want: %v, got: %v", hashicorpHealthcheckFailed, status)
}
}