Repository: Versent/saml2aws
Pull request #167 (2018-05-05 19:15:51)

refactor(Prompter) Simplify and decouple the prompter.
Working on providing alternative CLI interfaces.


Patch:
diff --git a/.gitignore b/.gitignore
index fc59a25..2406fb1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ vendor
 /glide
 /package
 /stage
+coverage.txt
diff --git a/cmd/saml2aws/commands/configure.go b/cmd/saml2aws/commands/configure.go
index 6f4b4f7..fdc46e3 100644
--- a/cmd/saml2aws/commands/configure.go
+++ b/cmd/saml2aws/commands/configure.go
@@ -59,16 +59,14 @@ func Configure(configFlags *flags.CommonFlags) error {
 
 func storeCredentials(configFlags *flags.CommonFlags, account *cfg.IDPAccount) error {
 
-	var prompt = prompter.NewCli()
-
 	if configFlags.Password != "" {
 		if err := credentials.SaveCredentials(account.URL, account.Username, configFlags.Password); err != nil {
 			return errors.Wrap(err, "error storing password in keychain")
 		}
 	} else {
-		password := prompt.Password("Password")
+		password := prompter.Password("Password")
 		if password != "" {
-			if confirmPassword := prompt.Password("Confirm"); confirmPassword == password {
+			if confirmPassword := prompter.Password("Confirm"); confirmPassword == password {
 				if err := credentials.SaveCredentials(account.URL, account.Username, password); err != nil {
 					return errors.Wrap(err, "error storing password in keychain")
 				}
diff --git a/input.go b/input.go
index 74034c1..f2d8c5d 100644
--- a/input.go
+++ b/input.go
@@ -12,13 +12,11 @@ import (
 // PromptForConfigurationDetails prompt the user to present their hostname, username and mfa
 func PromptForConfigurationDetails(idpAccount *cfg.IDPAccount) error {
 
-	var prompt = prompter.NewCli()
-
 	providers := MFAsByProvider.Names()
 
 	var err error
 
-	idpAccount.Provider, err = prompt.ChooseWithDefault("Please choose a provider:", idpAccount.Provider, providers)
+	idpAccount.Provider, err = prompter.ChooseWithDefault("Please choose a provider:", idpAccount.Provider, providers)
 	if err != nil {
 		return errors.Wrap(err, "error selecting provider file")
 	}
@@ -27,7 +25,7 @@ func PromptForConfigurationDetails(idpAccount *cfg.IDPAccount) error {
 
 	// only prompt for MFA if there is more than one option
 	if len(mfas) > 1 {
-		idpAccount.MFA, err = prompt.ChooseWithDefault("Please choose an MFA", idpAccount.MFA, mfas)
+		idpAccount.MFA, err = prompter.ChooseWithDefault("Please choose an MFA", idpAccount.MFA, mfas)
 		if err != nil {
 			return errors.Wrap(err, "error selecting provider file")
 		}
@@ -38,8 +36,8 @@ func PromptForConfigurationDetails(idpAccount *cfg.IDPAccount) error {
 
 	fmt.Println("")
 
-	idpAccount.URL = prompt.String("URL", idpAccount.URL)
-	idpAccount.Username = prompt.String("Username", idpAccount.Username)
+	idpAccount.URL = prompter.String("URL", idpAccount.URL)
+	idpAccount.Username = prompter.String("Username", idpAccount.Username)
 
 	fmt.Println("")
 
@@ -48,13 +46,12 @@ func PromptForConfigurationDetails(idpAccount *cfg.IDPAccount) error {
 
 // PromptForLoginDetails prompt the user to present their username, password
 func PromptForLoginDetails(loginDetails *creds.LoginDetails) error {
-	var prompt = prompter.NewCli()
 
 	fmt.Println("To use saved password just hit enter.")
 
-	loginDetails.Username = prompt.String("Username", loginDetails.Username)
+	loginDetails.Username = prompter.String("Username", loginDetails.Username)
 
-	if enteredPassword := prompt.Password("Password"); enteredPassword != "" {
+	if enteredPassword := prompter.Password("Password"); enteredPassword != "" {
 		loginDetails.Password = enteredPassword
 	}
 
@@ -82,7 +79,7 @@ func PromptForAWSRoleSelection(accounts []*AWSAccount) (*AWSRole, error) {
 		roleOptions = append(roleOptions, k)
 	}
 
-	selectedRole, err := prompter.NewCli().ChooseWithDefault("Please choose the role", "", roleOptions)
+	selectedRole, err := prompter.ChooseWithDefault("Please choose the role", "", roleOptions)
 	if err != nil {
 		return nil, errors.Wrap(err, "Role selection failed")
 	}
diff --git a/pkg/prompter/prompter.go b/pkg/prompter/prompter.go
index 885411c..a11a29d 100644
--- a/pkg/prompter/prompter.go
+++ b/pkg/prompter/prompter.go
@@ -1,11 +1,6 @@
 package prompter
 
-import (
-	"errors"
-	"fmt"
-
-	"github.com/AlecAivazis/survey"
-)
+var defaultPrompter Prompter = NewCli()
 
 // Prompter handles prompting user for input
 type Prompter interface {
@@ -17,89 +12,37 @@ type Prompter interface {
 	Password(string) string
 }
 
-// CliPrompter used to prompt for cli input
-type CliPrompter struct {
-}
-
-// NewCli builds a new cli prompter
-func NewCli() *CliPrompter {
-	return &CliPrompter{}
+// SetPrompter configure an aternate prompter to the default one
+func SetPrompter(prmpt Prompter) {
+	defaultPrompter = prmpt
 }
 
 // RequestSecurityCode request a security code to be entered by the user
-func (cli *CliPrompter) RequestSecurityCode(pattern string) string {
-	token := ""
-	prompt := &survey.Input{
-		Message: fmt.Sprintf("Security Token [%s]", pattern),
-	}
-	survey.AskOne(prompt, &token, survey.Required)
-	return token
+func RequestSecurityCode(pattern string) string {
+	return defaultPrompter.RequestSecurityCode(pattern)
 }
 
 // ChooseWithDefault given the choice return the option selected with a default
-func (cli *CliPrompter) ChooseWithDefault(pr string, defaultValue string, options []string) (string, error) {
-	selected := ""
-	prompt := &survey.Select{
-		Message: pr,
-		Options: options,
-		Default: defaultValue,
-	}
-	survey.AskOne(prompt, &selected, survey.Required)
-
-	// return the selected element index
-	for i, option := range options {
-		if selected == option {
-			return options[i], nil
-		}
-	}
-	return "", errors.New("bad input")
+func ChooseWithDefault(pr string, defaultValue string, options []string) (string, error) {
+	return defaultPrompter.ChooseWithDefault(pr, defaultValue, options)
 }
 
 // Choose given the choice return the option selected
-func (cli *CliPrompter) Choose(pr string, options []string) int {
-	selected := ""
-	prompt := &survey.Select{
-		Message: pr,
-		Options: options,
-	}
-	survey.AskOne(prompt, &selected, survey.Required)
-
-	// return the selected element index
-	for i, option := range options {
-		if selected == option {
-			return i
-		}
-	}
-	return 0
+func Choose(pr string, options []string) int {
+	return defaultPrompter.Choose(pr, options)
 }
 
 // StringRequired prompt for string which is required
-func (cli *CliPrompter) String(pr string, defaultValue string) string {
-	val := ""
-	prompt := &survey.Input{
-		Message: pr,
-		Default: defaultValue,
-	}
-	survey.AskOne(prompt, &val, nil)
-	return val
+func StringRequired(pr string) string {
+	return defaultPrompter.StringRequired(pr)
 }
 
-// StringRequired prompt for string which is required
-func (cli *CliPrompter) StringRequired(pr string) string {
-	val := ""
-	prompt := &survey.Input{
-		Message: pr,
-	}
-	survey.AskOne(prompt, &val, survey.Required)
-	return val
+// String prompt for string which is required
+func String(pr string, defaultValue string) string {
+	return defaultPrompter.String(pr, defaultValue)
 }
 
 // Password prompt for password which is required
-func (cli *CliPrompter) Password(pr string) string {
-	val := ""
-	prompt := &survey.Password{
-		Message: pr,
-	}
-	survey.AskOne(prompt, &val, nil)
-	return val
+func Password(pr string) string {
+	return defaultPrompter.Password(pr)
 }
diff --git a/pkg/prompter/survey.go b/pkg/prompter/survey.go
new file mode 100644
index 0000000..72ac4a8
--- /dev/null
+++ b/pkg/prompter/survey.go
@@ -0,0 +1,95 @@
+package prompter
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/AlecAivazis/survey"
+)
+
+// CliPrompter used to prompt for cli input
+type CliPrompter struct {
+}
+
+// NewCli builds a new cli prompter
+func NewCli() *CliPrompter {
+	return &CliPrompter{}
+}
+
+// RequestSecurityCode request a security code to be entered by the user
+func (cli *CliPrompter) RequestSecurityCode(pattern string) string {
+	token := ""
+	prompt := &survey.Input{
+		Message: fmt.Sprintf("Security Token [%s]", pattern),
+	}
+	survey.AskOne(prompt, &token, survey.Required)
+	return token
+}
+
+// ChooseWithDefault given the choice return the option selected with a default
+func (cli *CliPrompter) ChooseWithDefault(pr string, defaultValue string, options []string) (string, error) {
+	selected := ""
+	prompt := &survey.Select{
+		Message: pr,
+		Options: options,
+		Default: defaultValue,
+	}
+	survey.AskOne(prompt, &selected, survey.Required)
+
+	// return the selected element index
+	for i, option := range options {
+		if selected == option {
+			return options[i], nil
+		}
+	}
+	return "", errors.New("bad input")
+}
+
+// Choose given the choice return the option selected
+func (cli *CliPrompter) Choose(pr string, options []string) int {
+	selected := ""
+	prompt := &survey.Select{
+		Message: pr,
+		Options: options,
+	}
+	survey.AskOne(prompt, &selected, survey.Required)
+
+	// return the selected element index
+	for i, option := range options {
+		if selected == option {
+			return i
+		}
+	}
+	return 0
+}
+
+// StringRequired prompt for string which is required
+func (cli *CliPrompter) String(pr string, defaultValue string) string {
+	val := ""
+	prompt := &survey.Input{
+		Message: pr,
+		Default: defaultValue,
+	}
+	survey.AskOne(prompt, &val, nil)
+	return val
+}
+
+// StringRequired prompt for string which is required
+func (cli *CliPrompter) StringRequired(pr string) string {
+	val := ""
+	prompt := &survey.Input{
+		Message: pr,
+	}
+	survey.AskOne(prompt, &val, survey.Required)
+	return val
+}
+
+// Password prompt for password which is required
+func (cli *CliPrompter) Password(pr string) string {
+	val := ""
+	prompt := &survey.Password{
+		Message: pr,
+	}
+	survey.AskOne(prompt, &val, nil)
+	return val
+}
diff --git a/pkg/provider/adfs/adfs.go b/pkg/provider/adfs/adfs.go
index 5bcf4d2..65c6126 100644
--- a/pkg/provider/adfs/adfs.go
+++ b/pkg/provider/adfs/adfs.go
@@ -24,7 +24,6 @@ var logger = logrus.WithField("provider", "adfs")
 type Client struct {
 	client     *provider.HTTPClient
 	idpAccount *cfg.IDPAccount
-	prompter   prompter.Prompter
 }
 
 // New create a new ADFS client
@@ -43,7 +42,6 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 	return &Client{
 		client:     client,
 		idpAccount: idpAccount,
-		prompter:   prompter.NewCli(),
 	}, nil
 }
 
@@ -150,7 +148,7 @@ func (ac *Client) vipMFA(authSubmitURL string, res *http.Response) (*http.Respon
 		return res, nil // if we didn't find the MFA flag then just continue
 	}
 
-	var token = ac.prompter.RequestSecurityCode("000000")
+	var token = prompter.RequestSecurityCode("000000")
 
 	doc.Find("input").Each(func(i int, s *goquery.Selection) {
 		updateOTPFormData(otpForm, s, token)
diff --git a/pkg/provider/adfs2/rsa.go b/pkg/provider/adfs2/rsa.go
index 4dfdf1f..aad5e2c 100644
--- a/pkg/provider/adfs2/rsa.go
+++ b/pkg/provider/adfs2/rsa.go
@@ -18,8 +18,6 @@ import (
 // Authenticate authenticate the user using the supplied login details
 func (ac *Client) authenticateRsa(loginDetails *creds.LoginDetails) (string, error) {
 
-	var prompt = prompter.NewCli()
-
 	authSubmitURL, authForm, err := ac.getLoginForm(loginDetails)
 	if err != nil {
 		return "", errors.Wrap(err, "error retrieving login form from idp")
@@ -35,7 +33,7 @@ func (ac *Client) authenticateRsa(loginDetails *creds.LoginDetails) (string, err
 		return "", errors.Wrap(err, "error extracting mfa form data")
 	}
 
-	token := prompt.Password("Enter passcode")
+	token := prompter.Password("Enter passcode")
 
 	passcodeForm.Set("Passcode", token)
 	passcodeForm.Del("submit")
@@ -50,7 +48,7 @@ func (ac *Client) authenticateRsa(loginDetails *creds.LoginDetails) (string, err
 		return "", errors.Wrap(err, "error extracting rsa form data")
 	}
 
-	nextCode := prompt.Password("Enter nextCode")
+	nextCode := prompter.Password("Enter nextCode")
 
 	rsaForm.Set("NextCode", nextCode)
 	rsaForm.Del("submit")
diff --git a/pkg/provider/jumpcloud/jumpcloud.go b/pkg/provider/jumpcloud/jumpcloud.go
index 2c5599c..2d7acfe 100644
--- a/pkg/provider/jumpcloud/jumpcloud.go
+++ b/pkg/provider/jumpcloud/jumpcloud.go
@@ -39,7 +39,7 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 
 // Authenticate logs into JumpCloud and returns a SAML response
 func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
-	var prompt = prompter.NewCli()
+	//var prompt = prompter.NewCli()
 
 	var authSubmitURL string
 	var samlAssertion string
@@ -110,7 +110,7 @@ func (jc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 	jc.client.EnableFollowRedirect()
 
 	if mfaRequired {
-		token := prompt.StringRequired("MFA Token")
+		token := prompter.StringRequired("MFA Token")
 		authForm.Add("otp", token)
 
 		req, err = http.NewRequest("POST", authSubmitURL, strings.NewReader(authForm.Encode()))
diff --git a/pkg/provider/keycloak/keycloak.go b/pkg/provider/keycloak/keycloak.go
index 448793e..89e7ab9 100644
--- a/pkg/provider/keycloak/keycloak.go
+++ b/pkg/provider/keycloak/keycloak.go
@@ -24,8 +24,7 @@ var logger = logrus.WithField("provider", "keycloak")
 
 // Client wrapper around KeyCloak.
 type Client struct {
-	client   *provider.HTTPClient
-	prompter prompter.Prompter
+	client *provider.HTTPClient
 }
 
 // New create a new KeyCloakClient
@@ -39,8 +38,7 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 	}
 
 	return &Client{
-		client:   client,
-		prompter: prompter.NewCli(),
+		client: client,
 	}, nil
 }
 
@@ -152,7 +150,7 @@ func (kc *Client) postTotpForm(totpSubmitURL string, doc *goquery.Document) (*go
 
 	otpForm := url.Values{}
 
-	var token = kc.prompter.RequestSecurityCode("000000")
+	var token = prompter.RequestSecurityCode("000000")
 
 	doc.Find("input").Each(func(i int, s *goquery.Selection) {
 		updateOTPFormData(otpForm, s, token)
diff --git a/pkg/provider/okta/okta.go b/pkg/provider/okta/okta.go
index aa3c794..b6d69d9 100644
--- a/pkg/provider/okta/okta.go
+++ b/pkg/provider/okta/okta.go
@@ -46,8 +46,7 @@ var (
 
 // Client is a wrapper representing a Okta SAML client
 type Client struct {
-	client   *provider.HTTPClient
-	prompter prompter.Prompter
+	client *provider.HTTPClient
 }
 
 // AuthRequest represents an mfa okta request
@@ -73,8 +72,7 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 	}
 
 	return &Client{
-		client:   client,
-		prompter: prompter.NewCli(),
+		client: client,
 	}, nil
 }
 
@@ -175,8 +173,6 @@ func parseMfaIdentifer(json string, arrayPosition int) string {
 
 func verifyMfa(oc *Client, oktaOrgHost string, resp string) (string, error) {
 
-	var prompt = prompter.NewCli()
-
 	stateToken := gjson.Get(resp, "stateToken").String()
 
 	// choose an mfa option if there are multiple enabled
@@ -191,7 +187,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, resp string) (string, error) {
 		}
 	}
 	if len(mfaOptions) > 1 {
-		mfaOption = prompt.Choose("Select which MFA option to use", mfaOptions)
+		mfaOption = prompter.Choose("Select which MFA option to use", mfaOptions)
 	}
 
 	factorID := gjson.Get(resp, fmt.Sprintf("_embedded.factors.%d.id", mfaOption)).String()
@@ -235,7 +231,7 @@ func verifyMfa(oc *Client, oktaOrgHost string, resp string) (string, error) {
 
 	switch mfa := mfaIdentifer; mfa {
 	case IdentifierSmsMfa, IdentifierTotpMfa, IdentifierOktaTotpMfa:
-		verifyCode := prompt.StringRequired("Enter verification code")
+		verifyCode := prompter.StringRequired("Enter verification code")
 		tokenReq := VerifyRequest{StateToken: stateToken, PassCode: verifyCode}
 		tokenBody := new(bytes.Buffer)
 		json.NewEncoder(tokenBody).Encode(tokenReq)
@@ -365,11 +361,11 @@ func verifyMfa(oc *Client, oktaOrgHost string, resp string) (string, error) {
 			"Duo Push",
 		}
 
-		duoMfaOption := prompt.Choose("Select a DUO MFA Option", duoMfaOptions)
+		duoMfaOption := prompter.Choose("Select a DUO MFA Option", duoMfaOptions)
 
 		if duoMfaOptions[duoMfaOption] == "Passcode" {
 			//get users DUO MFA Token
-			token = prompt.StringRequired("Enter passcode")
+			token = prompter.StringRequired("Enter passcode")
 		}
 
 		// send mfa auth request
diff --git a/pkg/provider/pingfed/pingfed.go b/pkg/provider/pingfed/pingfed.go
index c1d63dd..2d50b34 100644
--- a/pkg/provider/pingfed/pingfed.go
+++ b/pkg/provider/pingfed/pingfed.go
@@ -50,8 +50,6 @@ func New(idpAccount *cfg.IDPAccount) (*Client, error) {
 // Authenticate Authenticate to PingFed and return the data from the body of the SAML assertion.
 func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
 
-	var prompt = prompter.NewCli()
-
 	authSubmitURL, authForm, err := ac.getLoginForm(loginDetails)
 	if err != nil {
 		return "", errors.Wrap(err, "error retrieving login form")
@@ -160,7 +158,7 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 		//if actionURL is OTP then prompt for token
 		//user has disabled swipe
 		if strings.Contains(actionURL, "/pingid/ppm/auth/otp") {
-			token := prompt.StringRequired("Enter passcode")
+			token := prompter.StringRequired("Enter passcode")
 
 			//build request
 			otpReq := url.Values{}
diff --git a/pkg/provider/pingone/pingone.go b/pkg/provider/pingone/pingone.go
index f53124e..11643ea 100644
--- a/pkg/provider/pingone/pingone.go
+++ b/pkg/provider/pingone/pingone.go
@@ -55,8 +55,6 @@ func (ac *Client) EnableFollowRedirect() {
 // Authenticate Authenticate to PingOne and return the data from the body of the SAML assertion.
 func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error) {
 
-	var prompt = prompter.NewCli()
-
 	// Access to PingOne
 	authSubmitURL, authForm, err := ac.getLoginForm(loginDetails)
 	if err != nil {
@@ -182,7 +180,7 @@ func (ac *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
 
 	//user has disabled swipe
 	if strings.Contains(actionURL, "/pingid/ppm/auth/otp") {
-		token := prompt.StringRequired("Enter passcode")
+		token := prompter.StringRequired("Enter passcode")
 
 		//build request
 		otpReq := url.Values{}


Test patch:
diff --git a/pkg/provider/keycloak/keycloak_test.go b/pkg/provider/keycloak/keycloak_test.go
index a0d8750..cec3de1 100644
--- a/pkg/provider/keycloak/keycloak_test.go
+++ b/pkg/provider/keycloak/keycloak_test.go
@@ -82,7 +82,7 @@ func TestClient_postTotpForm(t *testing.T) {
 
 	pr.Mock.On("RequestSecurityCode", "000000").Return("111222")
 
-	kc := Client{client: &provider.HTTPClient{Client: http.Client{}}, prompter: pr}
+	kc := Client{client: &provider.HTTPClient{Client: http.Client{}}}
 
 	kc.postTotpForm(ts.URL, doc)
 }