diff --git a/accounts/vault/hashicorp_config.go b/accounts/vault/hashicorp_config.go new file mode 100644 index 000000000..db4105938 --- /dev/null +++ b/accounts/vault/hashicorp_config.go @@ -0,0 +1,25 @@ +package vault + +type hashicorpWalletConfig struct { + Client hashicorpClientConfig + Secrets []hashicorpSecretData +} + +type hashicorpClientConfig struct { + Url string `toml:",omitempty"` + Approle string `toml:",omitempty"` + CaCert string `toml:",omitempty"` + ClientCert string `toml:",omitempty"` + ClientKey string `toml:",omitempty"` + EnvVarPrefix string `toml:",omitempty"` + UseSecretCache bool `toml:",omitempty"` +} + +type hashicorpSecretData struct { + Name string `toml:",omitempty"` + SecretEngine string `toml:",omitempty"` + Version int `toml:",omitempty"` + AccountID string `toml:",omitempty"` + KeyID string `toml:",omitempty"` +} + diff --git a/accounts/vault/vault_backend.go b/accounts/vault/vault_backend.go index 8a4a7c789..6061f35a1 100644 --- a/accounts/vault/vault_backend.go +++ b/accounts/vault/vault_backend.go @@ -1,23 +1,74 @@ package vault import ( + "fmt" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "sort" ) -type vaultBackend struct { +type VaultBackend struct { wallets []accounts.Wallet updateScope event.SubscriptionScope updateFeed event.Feed - // 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 chance for concurrent access. + // 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 { +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 { +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 + } + wallets = append(wallets, w) + } + + sort.Sort(walletsByUrl(wallets)) + + return VaultBackend{ + wallets: wallets, + } +} + +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}, nil +} + +// walletsByUrl implements the sort interface to enable the sorting of a slice of wallets alphanumerically by their urls +type walletsByUrl []accounts.Wallet + +func (w walletsByUrl) Len() int { + return len(w) +} + +func (w walletsByUrl) Less(i, j int) bool { + return (w[i].URL()).Cmp(w[j].URL()) < 0 +} + +func (w walletsByUrl) Swap(i, j int) { + w[i], w[j] = w[j], w[i] +} diff --git a/accounts/vault/vault_backend_test.go b/accounts/vault/vault_backend_test.go index 8568c59f7..479659f41 100644 --- a/accounts/vault/vault_backend_test.go +++ b/accounts/vault/vault_backend_test.go @@ -1,27 +1,81 @@ package vault import ( - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/core/types" - "math/big" "reflect" + "strings" "testing" ) -func TestVaultBackend_Wallets(t *testing.T) { +func TestNewHashicorpBackend_CreatesWalletsFromConfig(t *testing.T) { + makeConfs := func (url string, urls... string) []hashicorpWalletConfig { + var confs []hashicorpWalletConfig + + confs = append(confs, hashicorpWalletConfig{Client: hashicorpClientConfig{Url: url}}) + + for _, u := range urls { + confs = append(confs, hashicorpWalletConfig{Client: hashicorpClientConfig{Url: u}}) + } + + return confs + } + + // makeWlts crudely splits the urls to get them as accounts.URLs so as to not use the same parsing method as in the production code. This is fine for simple urls but may not be suitable for tests that require more complex urls. + makeWlts := func(url string, urls... string) []accounts.Wallet { + var wlts []accounts.Wallet + + s := strings.Split(url, "://") + scheme, path := s[0], s[1] + + 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}}) + } + + return wlts + } + + tests := map[string]struct{ + in []hashicorpWalletConfig + want []accounts.Wallet + }{ + "no config": {in: []hashicorpWalletConfig{}, want: []accounts.Wallet{}}, + "single": {in: makeConfs("http://url:1"), want: makeWlts("http://url:1")}, + "multiple": {in: makeConfs("http://url:1", "http://url:2"), want: makeWlts("http://url:1", "http://url:2")}, + "orders by url": { + in: makeConfs("https://url:1", "https://a:9", "http://url:2", "http://url:1"), + want: makeWlts("http://url:1", "http://url:2", "https://a:9", "https://url:1")}, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + b := NewHashicorpBackend(tt.in) + + if !reflect.DeepEqual(tt.want, b.wallets) { + t.Fatalf("\nwant: %v, \ngot : %v", tt.want, b.wallets) + } + }) + } + +} + +func TestVaultBackend_Wallets_ReturnsWallets(t *testing.T) { tests := map[string]struct { in []accounts.Wallet want []accounts.Wallet }{ "empty": {in: []accounts.Wallet{}, want: []accounts.Wallet{}}, - "single": {in: []accounts.Wallet{VaultWallet{}}, want: []accounts.Wallet{VaultWallet{}}}, - "multiple": {in: []accounts.Wallet{VaultWallet{}, VaultWallet{}}, want: []accounts.Wallet{VaultWallet{}, VaultWallet{}}}, + "single": {in: []accounts.Wallet{vaultWallet{}}, want: []accounts.Wallet{vaultWallet{}}}, + "multiple": {in: []accounts.Wallet{vaultWallet{}, vaultWallet{}}, want: []accounts.Wallet{vaultWallet{}, vaultWallet{}}}, } for name, tt := range tests { t.Run(name, func(t *testing.T) { - b := vaultBackend{wallets: tt.in} + b := VaultBackend{wallets: tt.in} got := b.Wallets() @@ -33,11 +87,15 @@ func TestVaultBackend_Wallets(t *testing.T) { } func TestVaultBackend_Wallets_ReturnsCopy(t *testing.T) { - b := vaultBackend{wallets: []accounts.Wallet{VaultWallet{}}} + b := VaultBackend{ + wallets: []accounts.Wallet{ + vaultWallet{Url: accounts.URL{Scheme: "http", Path: "url"}}, + }, + } got := b.Wallets() - got[0] = OtherVaultWallet{} + 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") @@ -46,7 +104,7 @@ func TestVaultBackend_Wallets_ReturnsCopy(t *testing.T) { func TestVaultBackend_Subscribe_SubscriberReceivesEventsAddedToFeed(t *testing.T) { subscriber := make(chan accounts.WalletEvent, 1) - b := vaultBackend{} + b := VaultBackend{} b.Subscribe(subscriber) @@ -55,7 +113,7 @@ func TestVaultBackend_Subscribe_SubscriberReceivesEventsAddedToFeed(t *testing.T } // mock an event - event := accounts.WalletEvent{Wallet: VaultWallet{}, Kind: accounts.WalletOpened} + event := accounts.WalletEvent{Wallet: vaultWallet{}, Kind: accounts.WalletOpened} b.updateFeed.Send(event) if len(subscriber) != 1 { @@ -68,104 +126,3 @@ func TestVaultBackend_Subscribe_SubscriberReceivesEventsAddedToFeed(t *testing.T t.Fatalf("want: %v, got: %v", event, got) } } - -type OtherVaultWallet struct {} - -func (OtherVaultWallet) URL() accounts.URL { - panic("implement me") -} - -func (OtherVaultWallet) Status() (string, error) { - panic("implement me") -} - -func (OtherVaultWallet) Open(passphrase string) error { - panic("implement me") -} - -func (OtherVaultWallet) Close() error { - panic("implement me") -} - -func (OtherVaultWallet) Accounts() []accounts.Account { - panic("implement me") -} - -func (OtherVaultWallet) Contains(account accounts.Account) bool { - panic("implement me") -} - -func (OtherVaultWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { - panic("implement me") -} - -func (OtherVaultWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) { - panic("implement me") -} - -func (OtherVaultWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { - panic("implement me") -} - -func (OtherVaultWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int, isQuorum bool) (*types.Transaction, error) { - panic("implement me") -} - -func (OtherVaultWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { - panic("implement me") -} - -func (OtherVaultWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - panic("implement me") -} - -type VaultWallet struct {} - -func (VaultWallet) URL() accounts.URL { - panic("implement me") -} - -func (VaultWallet) Status() (string, error) { - panic("implement me") -} - -func (VaultWallet) Open(passphrase string) error { - panic("implement me") -} - -func (VaultWallet) Close() error { - panic("implement me") -} - -func (VaultWallet) Accounts() []accounts.Account { - panic("implement me") -} - -func (VaultWallet) Contains(account accounts.Account) bool { - panic("implement me") -} - -func (VaultWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { - panic("implement me") -} - -func (VaultWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) { - panic("implement me") -} - -func (VaultWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { - panic("implement me") -} - -func (VaultWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int, isQuorum bool) (*types.Transaction, error) { - panic("implement me") -} - -func (VaultWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { - panic("implement me") -} - -func (VaultWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - panic("implement me") -} - diff --git a/accounts/vault/vault_wallet.go b/accounts/vault/vault_wallet.go new file mode 100644 index 000000000..a8cbb66b6 --- /dev/null +++ b/accounts/vault/vault_wallet.go @@ -0,0 +1,61 @@ +package vault + +import ( + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/core/types" + "math/big" +) + +type vaultWallet struct { + Url accounts.URL +} + +func (w vaultWallet) URL() accounts.URL { + return w.Url +} + +func (w vaultWallet) Status() (string, error) { + panic("implement me") +} + +func (w vaultWallet) Open(passphrase string) error { + panic("implement me") +} + +func (w vaultWallet) Close() error { + panic("implement me") +} + +func (w vaultWallet) Accounts() []accounts.Account { + panic("implement me") +} + +func (w vaultWallet) Contains(account accounts.Account) bool { + panic("implement me") +} + +func (w vaultWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { + panic("implement me") +} + +func (w vaultWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) { + panic("implement me") +} + +func (w vaultWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { + panic("implement me") +} + +func (w vaultWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int, isQuorum bool) (*types.Transaction, error) { + panic("implement me") +} + +func (w vaultWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { + panic("implement me") +} + +func (w vaultWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + panic("implement me") +} +