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 package vault
import ( import (
"fmt"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "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. // 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 { func NewHashicorpBackend(walletConfigs []hashicorpWalletConfig) VaultBackend {
wallets := []accounts.Wallet{} wallets := []accounts.Wallet{}
for _, conf := range walletConfigs { for _, conf := range walletConfigs {
w, err := newHashicorpWallet(conf) w, err := newHashicorpWallet(conf)
if err != nil { 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) log.Error("unable to create Hashicorp wallet from config", "err", err)
continue continue
} }
@ -45,17 +33,14 @@ func NewHashicorpBackend(walletConfigs []hashicorpWalletConfig) VaultBackend {
} }
} }
func newHashicorpWallet(config hashicorpWalletConfig) (vaultWallet, error) { func (b *VaultBackend) Wallets() []accounts.Wallet {
var url accounts.URL cpy := make([]accounts.Wallet, len(b.wallets))
copy(cpy, b.wallets)
//to parse a string url as an accounts.URL it must first be in json format return cpy
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 // 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, "://") s := strings.Split(url, "://")
scheme, path := s[0], s[1] 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 { for _, u := range urls {
s := strings.Split(u, "://") s := strings.Split(u, "://")
scheme, path := s[0], s[1] 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 return wlts
@ -89,13 +89,13 @@ func TestVaultBackend_Wallets_ReturnsWallets(t *testing.T) {
func TestVaultBackend_Wallets_ReturnsCopy(t *testing.T) { func TestVaultBackend_Wallets_ReturnsCopy(t *testing.T) {
b := VaultBackend{ b := VaultBackend{
wallets: []accounts.Wallet{ wallets: []accounts.Wallet{
vaultWallet{Url: accounts.URL{Scheme: "http", Path: "url"}}, vaultWallet{url: accounts.URL{Scheme: "http", Path: "url"}},
}, },
} }
got := b.Wallets() 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) { if reflect.DeepEqual(b.wallets, got) {
t.Fatal("changes to returned slice should not affect slice in backend") t.Fatal("changes to returned slice should not affect slice in backend")

View File

@ -1,22 +1,54 @@
package vault package vault
import ( import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/hashicorp/vault/api"
"math/big" "math/big"
) )
type vaultWallet struct { 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 { 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) { 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 { func (w vaultWallet) Open(passphrase string) error {
@ -59,3 +91,43 @@ func (w vaultWallet) SignTxWithPassphrase(account accounts.Account, passphrase s
panic("implement me") 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)
}
}