diff --git a/cmd/bytemark/config_test.go b/cmd/bytemark/config_test.go index 6a30754d..6776f645 100644 --- a/cmd/bytemark/config_test.go +++ b/cmd/bytemark/config_test.go @@ -97,7 +97,7 @@ func TestCommandConfigSet(t *testing.T) { config.When("GetV", "endpoint").Return(util.ConfigVar{"endpoint", "", ""}) config.When("GetV", "group").Return(util.ConfigVar{"group", "", ""}) config.When("GetV", "debug-level").Return(util.ConfigVar{"debug-level", "", ""}) - config.When("Get", "token").Return("test-token", nil) + config.When("GetIgnoreErr", "token").Return("test-token") config.When("GetIgnoreErr", "user").Return("old-test-user") config.When("GetIgnoreErr", "yubikey").Return("") config.When("GetIgnoreErr", "2fa-otp").Return("") diff --git a/cmd/bytemark/create_test.go b/cmd/bytemark/create_test.go index 07db0468..3aacba20 100644 --- a/cmd/bytemark/create_test.go +++ b/cmd/bytemark/create_test.go @@ -270,11 +270,8 @@ func TestCreateServer(t *testing.T) { func TestCreateBackup(t *testing.T) { is := is.New(t) - config, c := baseTestSetup(t, false) + config, c := baseTestAuthSetup(t, false) - config.When("Get", "account").Return("test-account") - config.When("Get", "token").Return("test-token") - config.When("GetIgnoreErr", "yubikey").Return("") config.When("GetVirtualMachine").Return(&defVM) vmname := lib.VirtualMachineName{ @@ -282,7 +279,6 @@ func TestCreateBackup(t *testing.T) { Group: "default", Account: "default-account", } - c.When("AuthWithToken", "test-token").Return(nil).Times(1) c.When("CreateBackup", vmname, "test-disc").Return(brain.Backup{}, nil).Times(1) diff --git a/cmd/bytemark/delete_test.go b/cmd/bytemark/delete_test.go index bfcf2b2a..3a5bbc71 100644 --- a/cmd/bytemark/delete_test.go +++ b/cmd/bytemark/delete_test.go @@ -93,12 +93,10 @@ func TestDeleteKey(t *testing.T) { if ok, vErr := c.Verify(); !ok { t.Fatal(vErr) } - c.Reset() - config.Reset() - config.When("Get", "token").Return("test-token") + + config, c = baseTestAuthSetup(t, false) + config.When("Force").Return(true) - config.When("GetIgnoreErr", "yubikey").Return("") - config.When("GetIgnoreErr", "2fa-otp").Return("") config.When("GetIgnoreErr", "user").Return("test-user") c.When("AuthWithToken", "test-token").Return(nil) @@ -117,7 +115,7 @@ func TestDeleteKey(t *testing.T) { func TestDeleteBackup(t *testing.T) { is := is.New(t) - config, c := baseTestSetup(t, false) + config, c := baseTestAuthSetup(t, false) vmname := lib.VirtualMachineName{ VirtualMachine: "test-server", @@ -125,11 +123,8 @@ func TestDeleteBackup(t *testing.T) { Account: "default-account", } - config.When("Get", "token").Return("test-token") - config.When("GetIgnoreErr", "yubikey").Return("") config.When("GetVirtualMachine").Return(&defVM) - c.When("AuthWithToken", "test-token").Return(nil).Times(1) c.When("DeleteBackup", vmname, "test-disc", "test-backup").Return(nil).Times(1) err := global.App.Run([]string{ diff --git a/cmd/bytemark/list_test.go b/cmd/bytemark/list_test.go index a69c218a..e96d8f11 100644 --- a/cmd/bytemark/list_test.go +++ b/cmd/bytemark/list_test.go @@ -100,7 +100,7 @@ func TestListServers(t *testing.T) { func TestListBackups(t *testing.T) { is := is.New(t) - config, c := baseTestSetup(t, false) + config, c := baseTestAuthSetup(t, false) vmname := lib.VirtualMachineName{ VirtualMachine: "test-server", @@ -108,11 +108,8 @@ func TestListBackups(t *testing.T) { Account: "default-account", } - config.When("Get", "token").Return("test-token") - config.When("GetIgnoreErr", "yubikey").Return("") config.When("GetVirtualMachine").Return(&defVM) - c.When("AuthWithToken", "test-token").Return(nil).Times(1) c.When("GetBackups", vmname, "test-disc").Return(nil).Times(1) err := global.App.Run([]string{ diff --git a/cmd/bytemark/main.go b/cmd/bytemark/main.go index f3d6f534..3b001263 100644 --- a/cmd/bytemark/main.go +++ b/cmd/bytemark/main.go @@ -128,11 +128,28 @@ func outputDebugInfo() { log.Debugf(log.LvlFlags, "invocation: %s\r\n\r\n", strings.Join(os.Args, " ")) } +func makeCredentials() (credents map[string]string, err error) { + err = PromptForCredentials() + if err != nil { + return + } + credents = map[string]string{ + "username": global.Config.GetIgnoreErr("user"), + "password": global.Config.GetIgnoreErr("pass"), + "validity": global.Config.GetIgnoreErr("session-validity"), + } + if useKey, _ := global.Config.GetBool("yubikey"); useKey { + credents["yubikey"] = global.Config.GetIgnoreErr("yubikey-otp") + } + return +} + // EnsureAuth authenticates with the Bytemark authentication server, prompting for credentials if necessary. +// TODO(telyn): This REALLY, REALLY needs breaking apart into more manageable chunks func EnsureAuth() error { - token, err := global.Config.Get("token") + token := global.Config.GetIgnoreErr("token") - err = global.Client.AuthWithToken(token) + err := global.Client.AuthWithToken(token) if err != nil { if aErr, ok := err.(*auth3.Error); ok { if _, ok := aErr.Err.(*url.Error); ok { @@ -145,18 +162,11 @@ func EnsureAuth() error { for err != nil { attempts-- - err = PromptForCredentials() + credents, err := makeCredentials() + if err != nil { return err } - credents := map[string]string{ - "username": global.Config.GetIgnoreErr("user"), - "password": global.Config.GetIgnoreErr("pass"), - } - if useKey, _ := global.Config.GetBool("yubikey"); useKey { - credents["yubikey"] = global.Config.GetIgnoreErr("yubikey-otp") - } - err = global.Client.AuthWithCredentials(credents) // Handle the special case here where we just need to prompt for 2FA and try again @@ -173,7 +183,7 @@ func EnsureAuth() error { if err == nil { // success! - // it doesn't _really_ matter if we can't write the token to the token place, right? + // TODO(telyn): warn on failure to write to token _ = global.Config.SetPersistent("token", global.Client.GetSessionToken(), "AUTH") // Check this here, as it is only relevant the initial login, @@ -374,6 +384,12 @@ func globalFlags() (flags []cli.Flag) { Name: "yubikey-otp", Usage: "one-time password from your yubikey to use to login", }, + cli.IntFlag{ + Name: "session-validity", + Usage: "seconds until your session is automatically invalidated (max 3600)", + Value: util.DefaultSessionValidity, + // TODO(telyn): add more defaults to these flags + }, } } diff --git a/cmd/bytemark/main_test.go b/cmd/bytemark/main_test.go index be433616..c9c9dc42 100644 --- a/cmd/bytemark/main_test.go +++ b/cmd/bytemark/main_test.go @@ -101,6 +101,7 @@ func TestEnsureAuth(t *testing.T) { credentials := auth3.Credentials{ "username": test.InputUsername, "password": test.InputPassword, + "validity": "1800", } c.When("AuthWithCredentials", credentials).Return(test.AuthWithCredentialsErrors[0]).Times(1) @@ -110,6 +111,7 @@ func TestEnsureAuth(t *testing.T) { credentials := auth3.Credentials{ "username": test.InputUsername, "password": test.InputPassword, + "validity": "1800", "2fa": test.Input2FA, } c.When("AuthWithCredentials", credentials).Return(test.AuthWithCredentialsErrors[1]).Times(1) // Returns nil means success @@ -170,8 +172,8 @@ func baseTestSetup(t *testing.T, admin bool) (config *mocks.Config, client *mock func baseTestAuthSetup(t *testing.T, admin bool) (config *mocks.Config, c *mocks.Client) { config, c = baseTestSetup(t, admin) - config.When("Get", "token").Return("test-token") config.When("Get", "account").Return("test-account") + config.When("GetIgnoreErr", "token").Return("test-token") config.When("GetIgnoreErr", "user").Return("test-user") config.When("GetIgnoreErr", "yubikey").Return("") config.When("GetIgnoreErr", "2fa-otp").Return("") diff --git a/cmd/bytemark/restore_test.go b/cmd/bytemark/restore_test.go index 8124dc08..a813509d 100644 --- a/cmd/bytemark/restore_test.go +++ b/cmd/bytemark/restore_test.go @@ -10,7 +10,7 @@ import ( func TestRestoreBackup(t *testing.T) { is := is.New(t) - config, c := baseTestSetup(t, false) + config, c := baseTestAuthSetup(t, false) vmname := lib.VirtualMachineName{ VirtualMachine: "test-server", @@ -18,11 +18,8 @@ func TestRestoreBackup(t *testing.T) { Account: "default-account", } - config.When("Get", "token").Return("test-token") - config.When("GetIgnoreErr", "yubikey").Return("") config.When("GetVirtualMachine").Return(&defVM) - c.When("AuthWithToken", "test-token").Return(nil).Times(1) c.When("RestoreBackup", vmname, "test-disc", "test-backup").Return(nil).Times(1) err := global.App.Run([]string{ diff --git a/cmd/bytemark/schedule_test.go b/cmd/bytemark/schedule_test.go index f5e5a645..15d2189b 100644 --- a/cmd/bytemark/schedule_test.go +++ b/cmd/bytemark/schedule_test.go @@ -4,15 +4,11 @@ import ( "fmt" "github.com/BytemarkHosting/bytemark-client/lib" "github.com/BytemarkHosting/bytemark-client/lib/brain" + "github.com/BytemarkHosting/bytemark-client/mocks" "testing" ) func TestScheduleBackups(t *testing.T) { - config, client := baseTestSetup(t, false) - config.When("Get", "token").Return("test-token") - config.When("GetIgnoreErr", "yubikey").Return("") - config.When("GetVirtualMachine").Return(&defVM) - type ScheduleTest struct { Args []string @@ -24,17 +20,20 @@ func TestScheduleBackups(t *testing.T) { ShouldErr bool ShouldCall bool CreateErr error + BaseTestFn func(*testing.T, bool) (*mocks.Config, *mocks.Client) } tests := []ScheduleTest{ { ShouldCall: false, ShouldErr: true, + BaseTestFn: baseTestSetup, }, { Args: []string{"vm-name"}, ShouldCall: false, ShouldErr: true, + BaseTestFn: baseTestSetup, }, { Args: []string{"vm-name", "disc-label"}, @@ -44,6 +43,7 @@ func TestScheduleBackups(t *testing.T) { Interval: 86400, ShouldCall: true, ShouldErr: false, + BaseTestFn: baseTestAuthSetup, }, { ShouldCall: true, @@ -52,6 +52,7 @@ func TestScheduleBackups(t *testing.T) { DiscLabel: "disc-label", Start: "00:00", Interval: 3600, + BaseTestFn: baseTestAuthSetup, }, { Args: []string{"--start", "thursday", "vm-name", "disc-label", "3235"}, @@ -62,6 +63,7 @@ func TestScheduleBackups(t *testing.T) { ShouldCall: true, ShouldErr: true, CreateErr: fmt.Errorf("intermittent failure"), + BaseTestFn: baseTestAuthSetup, }, } @@ -75,7 +77,9 @@ func TestScheduleBackups(t *testing.T) { for i, test = range tests { fmt.Println(i) // fmt.Println still works even when the test panics - unlike t.Log - client.When("AuthWithToken", "test-token").Return(nil) + + config, client := test.BaseTestFn(t, false) + config.When("GetVirtualMachine").Return(&defVM) retSched := brain.BackupSchedule{ StartDate: test.Start, diff --git a/cmd/bytemark/set_test.go b/cmd/bytemark/set_test.go index 250df694..9af62e49 100644 --- a/cmd/bytemark/set_test.go +++ b/cmd/bytemark/set_test.go @@ -72,15 +72,10 @@ func TestSetMemory(t *testing.T) { t.Fatal(vErr) } - config.Reset() - config.When("Get", "token").Return("test-token") - config.When("GetIgnoreErr", "yubikey").Return("") - config.When("GetIgnoreErr", "2fa-otp").Return("") + config, c = baseTestAuthSetup(t, false) config.When("GetVirtualMachine").Return(&defVM) - c.Reset() c.When("GetVirtualMachine", &vmname).Return(&vm) - c.When("AuthWithToken", "test-token").Return(nil).Times(1) c.When("SetVirtualMachineMemory", &vmname, 16384).Return(nil).Times(1) err = global.App.Run(strings.Split("bytemark set memory --force test-server 16384M", " ")) diff --git a/cmd/bytemark/unschedule_test.go b/cmd/bytemark/unschedule_test.go index 840d5e26..e2101078 100644 --- a/cmd/bytemark/unschedule_test.go +++ b/cmd/bytemark/unschedule_test.go @@ -3,14 +3,11 @@ package main import ( "fmt" "github.com/BytemarkHosting/bytemark-client/lib" + "github.com/BytemarkHosting/bytemark-client/mocks" "testing" ) func TestUnscheduleBackups(t *testing.T) { - config, client := baseTestSetup(t, false) - config.When("Get", "token").Return("test-token") - config.When("GetIgnoreErr", "yubikey").Return("") - config.When("GetVirtualMachine").Return(&defVM) tests := []struct { Args []string @@ -22,22 +19,26 @@ func TestUnscheduleBackups(t *testing.T) { ShouldErr bool ShouldCall bool CreateErr error + BaseTestFn func(*testing.T, bool) (*mocks.Config, *mocks.Client) }{ { ShouldCall: false, ShouldErr: true, + BaseTestFn: baseTestSetup, }, { Args: []string{"vm-name"}, Name: lib.VirtualMachineName{"vm-name", "default", "default-account"}, ShouldCall: false, ShouldErr: true, + BaseTestFn: baseTestSetup, }, { Args: []string{"vm-name", "disc-label"}, Name: lib.VirtualMachineName{"vm-name", "default", "default-account"}, ShouldCall: false, ShouldErr: true, + BaseTestFn: baseTestSetup, }, { ShouldCall: true, @@ -45,12 +46,14 @@ func TestUnscheduleBackups(t *testing.T) { Name: lib.VirtualMachineName{"vm-name", "default", "default-account"}, DiscLabel: "disc-label", ID: 324, + BaseTestFn: baseTestAuthSetup, }, } for i, test := range tests { + config, client := test.BaseTestFn(t, false) + config.When("GetVirtualMachine").Return(&defVM) fmt.Println(i) // fmt.Println still works even when the test panics - unlike t.Log - client.When("AuthWithToken", "test-token").Return(nil) if test.ShouldCall { client.When("DeleteBackupSchedule", test.Name, test.DiscLabel, test.ID).Return(test.CreateErr).Times(1) diff --git a/cmd/bytemark/util/config.go b/cmd/bytemark/util/config.go index c4e42a5d..a9324c25 100644 --- a/cmd/bytemark/util/config.go +++ b/cmd/bytemark/util/config.go @@ -14,6 +14,11 @@ import ( "strings" ) +// DefaultSessionValidity is the default for the --session-validity flag +const DefaultSessionValidity = 1800 + +// TODO(telyn): extract more config vars' defaults into consts + var configVars = [...]string{ "endpoint", "billing-endpoint", @@ -24,6 +29,7 @@ var configVars = [...]string{ "account", "group", "output-format", + "session-validity", "token", "debug-level", "yubikey", @@ -82,6 +88,7 @@ type ConfigManager interface { GetIgnoreErr(string) string GetBool(string) (bool, error) GetV(string) (ConfigVar, error) + GetSessionValidity() (int, error) GetVirtualMachine() *lib.VirtualMachineName GetGroup() *lib.GroupName GetAll() ([]ConfigVar, error) @@ -279,6 +286,28 @@ func (config *Config) Get(name string) (string, error) { return v.Value, err } +// GetSessionValidity returns the configured session validity or the default, if the configured one is not a valid int between 0 and infinity +func (config *Config) GetSessionValidity() (validity int, err error) { + validity = DefaultSessionValidity + v, err := config.Get("session-validity") + if err != nil { + return + } + n, err := strconv.Atoi(v) + if err != nil { + return + } + // if the configured session validity is a negative number, return default without error + // the brain will happily clamp the validity to whatever the maximum is if it's more than that, so we don't need to worry on that score + // TODO(telyn): print a warning to cmd/bytemark.global.App.Writer + if n < 0 { + return + } + validity = n + return + +} + // GetIgnoreErr returns the value of a ConfigVar or an empty string , if it was unable to read it for whatever reason. func (config *Config) GetIgnoreErr(name string) string { s, _ := config.Get(name) @@ -416,6 +445,8 @@ func (config *Config) GetDefault(name string) ConfigVar { return ConfigVar{"force", "false", "CODE"} case "output-format": return ConfigVar{"output-format", "human", "CODE"} + case "session-validity": + return ConfigVar{"session-validity", fmt.Sprintf("%d", DefaultSessionValidity), "CODE"} } return ConfigVar{name, "", "UNSET"} } diff --git a/doc/bytemark.asciidoc b/doc/bytemark.asciidoc index 67459094..6a965906 100644 --- a/doc/bytemark.asciidoc +++ b/doc/bytemark.asciidoc @@ -49,6 +49,10 @@ Global options apply to any subsequent command. Use this option if you want enter the Yubikey one-time password on the command-line rather than be prompted for it. +*--session-validity* 'num':: + Specifies the length of time, in seconds, that your login session will be + valid for without running another command. + *--2fa-otp* 'string':: Use this option if you want enter the 2 Factor Authentication one-time password on the command-line rather than be prompted for it. diff --git a/doc/changelog b/doc/changelog index 6855b8fc..692d5eb1 100644 --- a/doc/changelog +++ b/doc/changelog @@ -3,9 +3,12 @@ bytemark-client (2.4) UNRELEASED; urgency=low New features: * Support for backups via the `create backup`, `delete backup`, `schedule backups`, `unschedule backups`, `show disc` and `list backups` commands. - + * Support for longer session validities than the old default of 5 minutes. + Use the --session-validity global flag & config variable to set your + session validity. Otherwise, it will default to the new default of 30 + minutes. - -- telyn Fri, 16 Jun 2017 16:56:28 +0100 + -- telyn Fri, 27 Jun 2017 16:56:28 +0100 bytemark-client (2.3) UNRELEASED; urgency=low diff --git a/mocks/config.go b/mocks/config.go index a4fa03d8..77c566a1 100644 --- a/mocks/config.go +++ b/mocks/config.go @@ -40,6 +40,11 @@ func (c *Config) GetBool(name string) (bool, error) { return ret.Bool(0), ret.Error(1) } +func (c *Config) GetSessionValidity() (int, error) { + ret := c.Called() + return ret.Int(0), ret.Error(1) +} + func (c *Config) GetV(name string) (util.ConfigVar, error) { ret := c.Called(name) return ret.Get(0).(util.ConfigVar), ret.Error(1)