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 <[email protected]>
AuthorDate: Mon Aug 11 15:03:10 2025 +0530
login: allow 2fa code input if mandated (#175)
Signed-off-by: Abhishek Kumar <[email protected]>
---
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()
+ }
}
}