diff --git a/accounts/vault/vault_backend.go b/accounts/vault/vault_backend.go index 6061f35a1..ef5210def 100644 --- a/accounts/vault/vault_backend.go +++ b/accounts/vault/vault_backend.go @@ -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 diff --git a/accounts/vault/vault_backend_test.go b/accounts/vault/vault_backend_test.go index 479659f41..080245d82 100644 --- a/accounts/vault/vault_backend_test.go +++ b/accounts/vault/vault_backend_test.go @@ -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") diff --git a/accounts/vault/vault_wallet.go b/accounts/vault/vault_wallet.go index a8cbb66b6..70b72c67d 100644 --- a/accounts/vault/vault_wallet.go +++ b/accounts/vault/vault_wallet.go @@ -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 +} diff --git a/accounts/vault/vault_wallet_test.go b/accounts/vault/vault_wallet_test.go new file mode 100644 index 000000000..ee901b5af --- /dev/null +++ b/accounts/vault/vault_wallet_test.go @@ -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) + } +}