This is an automated email from the ASF dual-hosted git repository. dahn pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/cloudstack-cloudmonkey.git
The following commit(s) were added to refs/heads/main by this push: new 939ce65 login: allow 2fa code input if mandated (#175) 939ce65 is described below commit 939ce6542422bca63a3a9eb1a6209c47242f21cb Author: Abhishek Kumar <abhishek.mr...@gmail.com> AuthorDate: Mon Aug 11 15:03:10 2025 +0530 login: allow 2fa code input if mandated (#175) Signed-off-by: Abhishek Kumar <abhishek.mr...@gmail.com> --- cmd/network.go | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ config/config.go | 22 +++++++------- config/spinner.go | 27 +++++++++++++++++ 3 files changed, 127 insertions(+), 10 deletions(-) diff --git a/cmd/network.go b/cmd/network.go index c6d4d3e..69cf6ce 100644 --- a/cmd/network.go +++ b/cmd/network.go @@ -49,6 +49,84 @@ func findSessionCookie(cookies []*http.Cookie) *http.Cookie { return nil } +func getLoginResponse(responseBody []byte) (map[string]interface{}, error) { + var responseMap map[string]interface{} + err := json.Unmarshal(responseBody, &responseMap) + if err != nil { + return nil, errors.New("failed to parse login response: " + err.Error()) + } + loginRespRaw, ok := responseMap["loginresponse"] + if !ok { + return nil, errors.New("failed to parse login response, expected 'loginresponse' key not found") + } + loginResponse, ok := loginRespRaw.(map[string]interface{}) + if !ok { + return nil, errors.New("failed to parse login response, expected 'loginresponse' to be a map") + } + return loginResponse, nil +} + +func getResponseBooleanValue(response map[string]interface{}, key string) (bool, bool) { + v, found := response[key] + if !found { + return false, false + } + switch value := v.(type) { + case bool: + return true, value + case string: + return true, strings.ToLower(value) == "true" + case float64: + return true, value != 0 + default: + return true, false + } +} + +func checkLogin2FAPromptAndValidate(r *Request, response map[string]interface{}, sessionKey string) error { + if !r.Config.HasShell { + return nil + } + config.Debug("Checking if 2FA is enabled and verified for the user ", response) + found, is2faEnabled := getResponseBooleanValue(response, "is2faenabled") + if !found || !is2faEnabled { + config.Debug("2FA is not enabled for the user, skipping 2FA validation") + return nil + } + found, is2faVerified := getResponseBooleanValue(response, "is2faverified") + if !found || is2faVerified { + config.Debug("2FA is already verified for the user, skipping 2FA validation") + return nil + } + activeSpinners := r.Config.PauseActiveSpinners() + fmt.Print("Enter 2FA code: ") + var code string + fmt.Scanln(&code) + if activeSpinners > 0 { + r.Config.ResumePausedSpinners() + } + params := make(url.Values) + params.Add("command", "validateUserTwoFactorAuthenticationCode") + params.Add("codefor2fa", code) + params.Add("sessionkey", sessionKey) + + msURL, _ := url.Parse(r.Config.ActiveProfile.URL) + + config.Debug("Validating 2FA with POST URL:", msURL, params) + spinner := r.Config.StartSpinner("trying to validate 2FA...") + resp, err := r.Client().PostForm(msURL.String(), params) + r.Config.StopSpinner(spinner) + if err != nil { + return errors.New("failed to failed to validate 2FA code: " + err.Error()) + } + config.Debug("ValidateUserTwoFactorAuthenticationCode POST response status code:", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + r.Client().Jar, _ = cookiejar.New(nil) + return errors.New("failed to validate 2FA code, please check the code. Invalidating session") + } + return nil +} + // Login logs in a user based on provided request and returns http client and session key func Login(r *Request) (string, error) { params := make(url.Values) @@ -81,6 +159,13 @@ func Login(r *Request) (string, error) { return "", e } + body, _ := ioutil.ReadAll(resp.Body) + config.Debug("Login response body:", string(body)) + loginResponse, err := getLoginResponse(body) + if err != nil { + return "", err + } + var sessionKey string curTime := time.Now() expiryDuration := 15 * time.Minute @@ -98,6 +183,9 @@ func Login(r *Request) (string, error) { }() config.Debug("Login sessionkey:", sessionKey) + if err := checkLogin2FAPromptAndValidate(r, loginResponse, sessionKey); err != nil { + return "", err + } return sessionKey, nil } diff --git a/config/config.go b/config/config.go index 91632aa..1619363 100644 --- a/config/config.go +++ b/config/config.go @@ -30,6 +30,7 @@ import ( "strconv" "time" + "github.com/briandowns/spinner" "github.com/gofrs/flock" homedir "github.com/mitchellh/go-homedir" ini "gopkg.in/ini.v1" @@ -73,16 +74,17 @@ type Core struct { // Config describes CLI config file and default options type Config struct { - Dir string - ConfigFile string - HistoryFile string - LogFile string - HasShell bool - Core *Core - ActiveProfile *ServerProfile - Context *context.Context - Cancel context.CancelFunc - C chan bool + Dir string + ConfigFile string + HistoryFile string + LogFile string + HasShell bool + Core *Core + ActiveProfile *ServerProfile + Context *context.Context + Cancel context.CancelFunc + C chan bool + activeSpinners []*spinner.Spinner } // GetOutputFormats returns the supported output formats. diff --git a/config/spinner.go b/config/spinner.go index 1b0b7f9..4f58209 100644 --- a/config/spinner.go +++ b/config/spinner.go @@ -40,6 +40,7 @@ func (c *Config) StartSpinner(suffix string) *spinner.Spinner { waiter := spinner.New(cursor, 200*time.Millisecond) waiter.Suffix = " " + suffix waiter.Start() + c.activeSpinners = append(c.activeSpinners, waiter) return waiter } @@ -47,5 +48,31 @@ func (c *Config) StartSpinner(suffix string) *spinner.Spinner { func (c *Config) StopSpinner(waiter *spinner.Spinner) { if waiter != nil { waiter.Stop() + for i, s := range c.activeSpinners { + if s == waiter { + c.activeSpinners = append(c.activeSpinners[:i], c.activeSpinners[i+1:]...) + break + } + } + } +} + +// PauseActiveSpinners stops the spinners without removing them from the acive spinners list, allowing resume. +func (c *Config) PauseActiveSpinners() int { + count := len(c.activeSpinners) + for _, s := range c.activeSpinners { + if s != nil && s.Active() { + s.Stop() + } + } + return count +} + +// ResumePausedSpinners restarts the spinners from the active spinners list if they are not already running. +func (c *Config) ResumePausedSpinners() { + for _, s := range c.activeSpinners { + if s != nil && !s.Active() { + s.Start() + } } }