mirror of https://github.com/poanetwork/quorum.git
Add vaultService to allow different vendor impls & impl Hashicorp status
This commit is contained in:
parent
a40c72f8a5
commit
3d7bf16754
|
@ -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)
|
||||||
|
return cpy
|
||||||
|
}
|
||||||
|
|
||||||
//to parse a string url as an accounts.URL it must first be in json format
|
func (b *VaultBackend) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription {
|
||||||
toParse := fmt.Sprintf("\"%v\"", config.Client.Url)
|
return b.updateScope.Track(b.updateFeed.Subscribe(sink))
|
||||||
|
|
||||||
if err := url.UnmarshalJSON([]byte(toParse)); err != nil {
|
|
||||||
return vaultWallet{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return vaultWallet{Url: url}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue