Skip to content

Commit

Permalink
Ability to initialize API credentials via --api flag
Browse files Browse the repository at this point in the history
  • Loading branch information
dimroc committed Jul 20, 2018
1 parent 9fe28cb commit 3d23112
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 19 deletions.
60 changes: 58 additions & 2 deletions cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
"io/ioutil"
"net/http"
"path"
"strings"

"github.com/gin-gonic/gin"
"github.com/smartcontractkit/chainlink/logger"
"github.com/smartcontractkit/chainlink/services"
"github.com/smartcontractkit/chainlink/store"
"github.com/smartcontractkit/chainlink/store/models"
Expand Down Expand Up @@ -250,8 +252,7 @@ func NewPromptingAPIInitializer(prompter Prompter) APIInitializer {
return &promptingAPIInitializer{prompter: prompter}
}

// Initialize uses the terminal to get credentials from the user that it then saves in the
// store.
// Initialize uses the terminal to get credentials that it then saves in the store.
func (t *promptingAPIInitializer) Initialize(store *store.Store) (models.User, error) {
if user, err := store.FindUser(); err == nil {
return user, err
Expand All @@ -271,3 +272,58 @@ func (t *promptingAPIInitializer) Initialize(store *store.Store) (models.User, e
return user, err
}
}

type fileAPIInitializer struct {
file string
}

// NewFileAPIInitializer creates a concrete instance of APIInitializer
// that pulls API user credentials from the passed file path.
func NewFileAPIInitializer(file string) APIInitializer {
return fileAPIInitializer{file: file}
}

func (f fileAPIInitializer) Initialize(store *store.Store) (models.User, error) {
if user, err := store.FindUser(); err == nil {
return user, err
}

request, err := credentialsFromFile(f.file)
if err != nil {
return models.User{}, err
}

user, err := models.NewUser(request.email, request.password)
if err != nil {
return user, err
}
return user, store.Save(&user)
}

var errNoCredentialFile = errors.New("No API user credential file was passed")

func credentialsFromFile(file string) (userCredentials, error) {
if len(file) == 0 {
return userCredentials{}, errNoCredentialFile
}

logger.Debug("Initializing API credentials from ", file)
dat, err := ioutil.ReadFile(file)
if err != nil {
return userCredentials{}, err
}
lines := strings.Split(string(dat), "\n")
if len(lines) < 2 {
return userCredentials{}, fmt.Errorf("Malformed API credentials file does not have at least two lines at %s", file)
}
credentials := userCredentials{
email: strings.TrimSpace(lines[0]),
password: strings.TrimSpace(lines[1]),
}
return credentials, nil
}

type userCredentials struct {
email string
password string
}
56 changes: 56 additions & 0 deletions cmd/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,59 @@ func TestTerminalAPIInitializer_InitializeWithExistingAPIUser(t *testing.T) {
assert.Equal(t, initialUser.HashedPassword, user.HashedPassword)
assert.Empty(t, user.SessionID)
}

func TestFileAPIInitializer_InitializeWithoutAPIUser(t *testing.T) {
tests := []struct {
name string
file string
wantError bool
}{
{"correct", "../internal/fixtures/apicredentials", false},
{"no file", "", true},
{"incorrect file", "/tmp/doesnotexist", true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
store, cleanup := cltest.NewStore()
defer cleanup()

tfi := cmd.NewFileAPIInitializer(test.file)
user, err := tfi.Initialize(store)
if test.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, cltest.APIEmail, user.Email)
persistedUser, err := store.FindUser()
assert.NoError(t, err)
assert.Equal(t, persistedUser.Email, user.Email)
}
})
}
}

func TestFileAPIInitializer_InitializeWithExistingAPIUser(t *testing.T) {
store, cleanup := cltest.NewStore()
defer cleanup()

initialUser := cltest.MustUser(cltest.APIEmail, cltest.Password)
require.NoError(t, store.Save(&initialUser))

tests := []struct {
name string
file string
wantError bool
}{
{"correct", "../internal/fixtures/apicredentials", false},
{"no file", "", true},
{"incorrect file", "/tmp/doesnotexist", true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tfi := cmd.NewFileAPIInitializer(test.file)
user, err := tfi.Initialize(store)
assert.NoError(t, err)
assert.Equal(t, initialUser.Email, user.Email)
})
}
}
4 changes: 4 additions & 0 deletions cmd/local_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ func (cli *Client) RunNode(c *clipkg.Context) error {
if err != nil {
return cli.errorOut(fmt.Errorf("error starting app: %+v", err))
}

var user models.User
if _, err = NewFileAPIInitializer(c.String("api")).Initialize(store); err != nil && err != errNoCredentialFile {
return cli.errorOut(fmt.Errorf("error starting app: %+v", err))
}
if user, err = cli.APIInitializer.Initialize(store); err != nil {
return cli.errorOut(fmt.Errorf("error starting app: %+v", err))
}
Expand Down
66 changes: 52 additions & 14 deletions cmd/local_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,13 @@ func TestClient_RunNodeShowsEnv(t *testing.T) {
app, cleanup := cltest.NewApplicationWithConfigAndKeyStore(config)
defer cleanup()

r := &cltest.RendererMock{}
auth := cltest.CallbackAuthenticator{Callback: func(*store.Store, string) error { return nil }}
client := cmd.Client{
Renderer: r,
Config: app.Store.Config,
AppFactory: cltest.InstanceAppFactory{App: app.ChainlinkApplication},
Auth: auth,
APIInitializer: &cltest.MockAPIInitializer{},
Runner: cltest.EmptyRunner{},
RemoteClient: cltest.NewMockAuthenticatedRemoteClient(app.Store.Config),
}

set := flag.NewFlagSet("test", 0)
Expand Down Expand Up @@ -76,12 +73,10 @@ func TestClient_RunNodeWithPasswords(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
app, _ := cltest.NewApplication() // cleanup invoked in client.RunNode
_, err := app.Store.KeyStore.NewAccount("password")
app, cleanup := cltest.NewApplication()
defer cleanup()
_, err := app.Store.KeyStore.NewAccount("password") // matches correct_password.txt
assert.NoError(t, err)
r := &cltest.RendererMock{}
eth := app.MockEthClient()
app.MustSeedUserSession()

var unlocked bool
callback := func(store *store.Store, phrase string) error {
Expand All @@ -91,31 +86,74 @@ func TestClient_RunNodeWithPasswords(t *testing.T) {
}

auth := cltest.CallbackAuthenticator{Callback: callback}
apiInitializer := &cltest.MockAPIInitializer{}
apiPrompt := &cltest.MockAPIInitializer{}
client := cmd.Client{
Renderer: r,
Config: app.Store.Config,
AppFactory: cltest.InstanceAppFactory{App: app},
Auth: auth,
APIInitializer: apiInitializer,
APIInitializer: apiPrompt,
Runner: cltest.EmptyRunner{},
RemoteClient: cltest.NewMockAuthenticatedRemoteClient(app.Store.Config),
}

set := flag.NewFlagSet("test", 0)
set.String("password", test.pwdfile, "")
c := cli.NewContext(nil, set, nil)

eth := app.MockEthClient()
eth.Register("eth_getTransactionCount", `0x1`)
if test.wantUnlocked {
assert.NoError(t, client.RunNode(c))
assert.True(t, unlocked)
assert.Equal(t, 1, apiInitializer.Count)
assert.Equal(t, 1, apiPrompt.Count)
} else {
assert.Error(t, client.RunNode(c))
assert.False(t, unlocked)
assert.Equal(t, 0, apiInitializer.Count)
assert.Equal(t, 0, apiPrompt.Count)
}
})
}
}

func TestClient_RunNodeWithAPICredentialsFile(t *testing.T) {
tests := []struct {
name string
apiFile string
wantPrompt bool
wantError bool
}{
{"correct", "../internal/fixtures/apicredentials", false, false},
{"no file", "", true, false},
{"wrong file", "doesntexist.txt", false, true},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
app, cleanup := cltest.NewApplicationWithKeyStore()
defer cleanup()

noauth := cltest.CallbackAuthenticator{Callback: func(*store.Store, string) error { return nil }}
apiPrompt := &cltest.MockAPIInitializer{}
client := cmd.Client{
Config: app.Config,
AppFactory: cltest.InstanceAppFactory{App: app},
Auth: noauth,
APIInitializer: apiPrompt,
Runner: cltest.EmptyRunner{},
}

set := flag.NewFlagSet("test", 0)
set.String("api", test.apiFile, "")
c := cli.NewContext(nil, set, nil)

eth := app.MockEthClient()
eth.Register("eth_getTransactionCount", `0x1`)

if test.wantError {
assert.Error(t, client.RunNode(c))
} else {
assert.NoError(t, client.RunNode(c))
}
assert.Equal(t, test.wantPrompt, apiPrompt.Count > 0)
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/bin/cldev
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ if [ "$#" == 0 ]; then
popd >/dev/null
go run -ldflags "$LDFLAGS" main.go node -d -p $ROOT/password.txt
elif [ "$1" == "node" ]; then
go run -ldflags "$LDFLAGS" main.go node -d -p $ROOT/password.txt
go run -ldflags "$LDFLAGS" main.go node -d -p $ROOT/password.txt ${@:2}
elif [ "$1" == "clean" ]; then
rm -f $ROOT/db.bolt $ROOT/log.jsonl
else
Expand Down
8 changes: 6 additions & 2 deletions internal/cltest/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,9 +565,13 @@ type MockAPIInitializer struct {
Count int
}

func (m *MockAPIInitializer) Initialize(*store.Store) (models.User, error) {
func (m *MockAPIInitializer) Initialize(store *store.Store) (models.User, error) {
if user, err := store.FindUser(); err == nil {
return user, err
}
m.Count += 1
return MustUser(APIEmail, Password), nil
user := MustUser(APIEmail, Password)
return user, store.Save(&user)
}

func NewMockAuthenticatedRemoteClient(cfg store.Config) cmd.RemoteClient {
Expand Down
2 changes: 2 additions & 0 deletions internal/fixtures/apicredentials
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
email@test.net
password
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ func Run(client *cmd.Client, args ...string) {
Name: "node",
Aliases: []string{"n"},
Flags: []cli.Flag{
cli.StringFlag{
Name: "api, a",
Usage: "text file holding the API email and password, each on a line",
},
cli.StringFlag{
Name: "password, p",
Usage: "text file holding the password for the node's account",
Expand Down

0 comments on commit 3d23112

Please sign in to comment.