github.com/willmadison/cli@v6.40.1-0.20181018160101-29d5937903ff+incompatible/cf/commands/login.go (about)

     1  package commands
     2  
     3  import (
     4  	"errors"
     5  	"strconv"
     6  
     7  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccversion"
     8  	"code.cloudfoundry.org/cli/cf/commandregistry"
     9  	"code.cloudfoundry.org/cli/cf/flags"
    10  	. "code.cloudfoundry.org/cli/cf/i18n"
    11  	"code.cloudfoundry.org/cli/command"
    12  	"code.cloudfoundry.org/cli/command/translatableerror"
    13  
    14  	"code.cloudfoundry.org/cli/cf/api/authentication"
    15  	"code.cloudfoundry.org/cli/cf/api/organizations"
    16  	"code.cloudfoundry.org/cli/cf/api/spaces"
    17  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    18  	"code.cloudfoundry.org/cli/cf/models"
    19  	"code.cloudfoundry.org/cli/cf/requirements"
    20  	"code.cloudfoundry.org/cli/cf/terminal"
    21  )
    22  
    23  const maxLoginTries = 3
    24  const maxChoices = 50
    25  
    26  type Login struct {
    27  	ui            terminal.UI
    28  	config        coreconfig.ReadWriter
    29  	authenticator authentication.Repository
    30  	endpointRepo  coreconfig.EndpointRepository
    31  	orgRepo       organizations.OrganizationRepository
    32  	spaceRepo     spaces.SpaceRepository
    33  }
    34  
    35  func init() {
    36  	commandregistry.Register(&Login{})
    37  }
    38  
    39  func (cmd *Login) MetaData() commandregistry.CommandMetadata {
    40  	fs := make(map[string]flags.FlagSet)
    41  	fs["a"] = &flags.StringFlag{ShortName: "a", Usage: T("API endpoint (e.g. https://api.example.com)")}
    42  	fs["u"] = &flags.StringFlag{ShortName: "u", Usage: T("Username")}
    43  	fs["p"] = &flags.StringFlag{ShortName: "p", Usage: T("Password")}
    44  	fs["o"] = &flags.StringFlag{ShortName: "o", Usage: T("Org")}
    45  	fs["s"] = &flags.StringFlag{ShortName: "s", Usage: T("Space")}
    46  	fs["sso"] = &flags.BoolFlag{Name: "sso", Usage: T("Prompt for a one-time passcode to login")}
    47  	fs["sso-passcode"] = &flags.StringFlag{Name: "sso-passcode", Usage: T("One-time passcode")}
    48  	fs["skip-ssl-validation"] = &flags.BoolFlag{Name: "skip-ssl-validation", Usage: T("Skip verification of the API endpoint. Not recommended!")}
    49  
    50  	return commandregistry.CommandMetadata{
    51  		Name:        "login",
    52  		ShortName:   "l",
    53  		Description: T("Log user in"),
    54  		Usage: []string{
    55  			T("CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE] [--sso | --sso-passcode PASSCODE]\n\n"),
    56  			terminal.WarningColor(T("WARNING:\n   Providing your password as a command line option is highly discouraged\n   Your password may be visible to others and may be recorded in your shell history")),
    57  		},
    58  		Examples: []string{
    59  			T("CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)"),
    60  			T("CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)"),
    61  			T("CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)"),
    62  			T("CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)"),
    63  			T("CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time passcode to login)"),
    64  		},
    65  		Flags: fs,
    66  	}
    67  }
    68  
    69  func (cmd *Login) Requirements(requirementsFactory requirements.Factory, fc flags.FlagContext) ([]requirements.Requirement, error) {
    70  	reqs := []requirements.Requirement{}
    71  	return reqs, nil
    72  }
    73  
    74  func (cmd *Login) SetDependency(deps commandregistry.Dependency, pluginCall bool) commandregistry.Command {
    75  	cmd.ui = deps.UI
    76  	cmd.config = deps.Config
    77  	cmd.authenticator = deps.RepoLocator.GetAuthenticationRepository()
    78  	cmd.endpointRepo = deps.RepoLocator.GetEndpointRepository()
    79  	cmd.orgRepo = deps.RepoLocator.GetOrganizationRepository()
    80  	cmd.spaceRepo = deps.RepoLocator.GetSpaceRepository()
    81  	return cmd
    82  }
    83  
    84  func (cmd *Login) Execute(c flags.FlagContext) error {
    85  	cmd.config.ClearSession()
    86  
    87  	endpoint, skipSSL := cmd.decideEndpoint(c)
    88  
    89  	api := API{
    90  		ui:           cmd.ui,
    91  		config:       cmd.config,
    92  		endpointRepo: cmd.endpointRepo,
    93  	}
    94  	err := api.setAPIEndpoint(endpoint, skipSSL, cmd.MetaData().Name)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	err = command.MinimumCCAPIVersionCheck(cmd.config.APIVersion(), ccversion.MinV2ClientVersion)
   100  	if err != nil {
   101  		if _, ok := err.(translatableerror.MinimumCFAPIVersionNotMetError); ok {
   102  			cmd.ui.Warn("Your API version is no longer supported. Upgrade to a newer version of the API.")
   103  		} else {
   104  			return err
   105  		}
   106  	}
   107  
   108  	defer func() {
   109  		cmd.ui.Say("")
   110  		cmd.ui.ShowConfiguration(cmd.config)
   111  	}()
   112  
   113  	// We thought we would never need to explicitly branch in this code
   114  	// for anything as simple as authentication, but it turns out that our
   115  	// assumptions did not match reality.
   116  
   117  	// When SAML is enabled (but not configured) then the UAA/Login server
   118  	// will always returns password prompts that includes the Passcode field.
   119  	// Users can authenticate with:
   120  	//   EITHER   username and password
   121  	//   OR       a one-time passcode
   122  
   123  	switch {
   124  	case c.Bool("sso") && c.IsSet("sso-passcode"):
   125  		return errors.New(T("Incorrect usage: --sso-passcode flag cannot be used with --sso"))
   126  	case c.Bool("sso") || c.IsSet("sso-passcode"):
   127  		err = cmd.authenticateSSO(c)
   128  		if err != nil {
   129  			return err
   130  		}
   131  	default:
   132  		err = cmd.authenticate(c)
   133  		if err != nil {
   134  			return err
   135  		}
   136  	}
   137  
   138  	orgIsSet, err := cmd.setOrganization(c)
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	if orgIsSet {
   144  		err = cmd.setSpace(c)
   145  		if err != nil {
   146  			return err
   147  		}
   148  	}
   149  	cmd.ui.NotifyUpdateIfNeeded(cmd.config)
   150  	return nil
   151  }
   152  
   153  func (cmd Login) decideEndpoint(c flags.FlagContext) (string, bool) {
   154  	endpoint := c.String("a")
   155  	skipSSL := c.Bool("skip-ssl-validation")
   156  	if endpoint == "" {
   157  		endpoint = cmd.config.APIEndpoint()
   158  		skipSSL = cmd.config.IsSSLDisabled() || skipSSL
   159  	}
   160  
   161  	if endpoint == "" {
   162  		endpoint = cmd.ui.Ask(T("API endpoint"))
   163  	} else {
   164  		cmd.ui.Say(T("API endpoint: {{.Endpoint}}", map[string]interface{}{"Endpoint": terminal.EntityNameColor(endpoint)}))
   165  	}
   166  
   167  	return endpoint, skipSSL
   168  }
   169  
   170  func (cmd Login) authenticateSSO(c flags.FlagContext) error {
   171  	prompts, err := cmd.authenticator.GetLoginPromptsAndSaveUAAServerURL()
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	credentials := make(map[string]string)
   177  	passcode := prompts["passcode"]
   178  
   179  	for i := 0; i < maxLoginTries; i++ {
   180  		if c.IsSet("sso-passcode") && i == 0 {
   181  			credentials["passcode"] = c.String("sso-passcode")
   182  		} else {
   183  			credentials["passcode"] = cmd.ui.AskForPassword(passcode.DisplayName)
   184  		}
   185  
   186  		cmd.ui.Say(T("Authenticating..."))
   187  		err = cmd.authenticator.Authenticate(credentials)
   188  
   189  		if err == nil {
   190  			cmd.ui.Ok()
   191  			cmd.ui.Say("")
   192  			break
   193  		}
   194  
   195  		cmd.ui.Say(err.Error())
   196  	}
   197  
   198  	if err != nil {
   199  		return errors.New(T("Unable to authenticate."))
   200  	}
   201  	return nil
   202  }
   203  
   204  func (cmd Login) authenticate(c flags.FlagContext) error {
   205  	if cmd.config.UAAGrantType() == "client_credentials" {
   206  		return errors.New(T("Service account currently logged in. Use 'cf logout' to log out service account and try again."))
   207  	}
   208  
   209  	usernameFlagValue := c.String("u")
   210  	passwordFlagValue := c.String("p")
   211  
   212  	prompts, err := cmd.authenticator.GetLoginPromptsAndSaveUAAServerURL()
   213  	if err != nil {
   214  		return err
   215  	}
   216  	passwordKeys := []string{}
   217  	credentials := make(map[string]string)
   218  
   219  	if value, ok := prompts["username"]; ok {
   220  		if prompts["username"].Type == coreconfig.AuthPromptTypeText && usernameFlagValue != "" {
   221  			credentials["username"] = usernameFlagValue
   222  		} else {
   223  			credentials["username"] = cmd.ui.Ask(value.DisplayName)
   224  		}
   225  	}
   226  
   227  	for key, prompt := range prompts {
   228  		if prompt.Type == coreconfig.AuthPromptTypePassword {
   229  			if key == "passcode" || key == "password" {
   230  				continue
   231  			}
   232  
   233  			passwordKeys = append(passwordKeys, key)
   234  		} else if key == "username" {
   235  			continue
   236  		} else {
   237  			credentials[key] = cmd.ui.Ask(prompt.DisplayName)
   238  		}
   239  	}
   240  
   241  	for i := 0; i < maxLoginTries; i++ {
   242  
   243  		// ensure that password gets prompted before other codes (eg. mfa code)
   244  		if passPrompt, ok := prompts["password"]; ok {
   245  			if passwordFlagValue != "" {
   246  				credentials["password"] = passwordFlagValue
   247  				passwordFlagValue = ""
   248  			} else {
   249  				credentials["password"] = cmd.ui.AskForPassword(passPrompt.DisplayName)
   250  			}
   251  		}
   252  
   253  		for _, key := range passwordKeys {
   254  			credentials[key] = cmd.ui.AskForPassword(prompts[key].DisplayName)
   255  		}
   256  
   257  		credentialsCopy := make(map[string]string, len(credentials))
   258  		for k, v := range credentials {
   259  			credentialsCopy[k] = v
   260  		}
   261  
   262  		cmd.ui.Say(T("Authenticating..."))
   263  		err = cmd.authenticator.Authenticate(credentialsCopy)
   264  
   265  		if err == nil {
   266  			cmd.ui.Ok()
   267  			cmd.ui.Say("")
   268  			break
   269  		}
   270  
   271  		cmd.ui.Say(err.Error())
   272  	}
   273  
   274  	if err != nil {
   275  		return errors.New(T("Unable to authenticate."))
   276  	}
   277  	return nil
   278  }
   279  
   280  func (cmd Login) setOrganization(c flags.FlagContext) (bool, error) {
   281  	orgName := c.String("o")
   282  
   283  	if orgName == "" {
   284  		orgs, err := cmd.orgRepo.ListOrgs(maxChoices)
   285  		if err != nil {
   286  			return false, errors.New(T("Error finding available orgs\n{{.APIErr}}",
   287  				map[string]interface{}{"APIErr": err.Error()}))
   288  		}
   289  
   290  		switch len(orgs) {
   291  		case 0:
   292  			return false, nil
   293  		case 1:
   294  			cmd.targetOrganization(orgs[0])
   295  			return true, nil
   296  		default:
   297  			orgName = cmd.promptForOrgName(orgs)
   298  			if orgName == "" {
   299  				cmd.ui.Say("")
   300  				return false, nil
   301  			}
   302  		}
   303  	}
   304  
   305  	org, err := cmd.orgRepo.FindByName(orgName)
   306  	if err != nil {
   307  		return false, errors.New(T("Error finding org {{.OrgName}}\n{{.Err}}",
   308  			map[string]interface{}{"OrgName": terminal.EntityNameColor(orgName), "Err": err.Error()}))
   309  	}
   310  
   311  	cmd.targetOrganization(org)
   312  	return true, nil
   313  }
   314  
   315  func (cmd Login) promptForOrgName(orgs []models.Organization) string {
   316  	orgNames := []string{}
   317  	for _, org := range orgs {
   318  		orgNames = append(orgNames, org.Name)
   319  	}
   320  
   321  	return cmd.promptForName(orgNames, T("Select an org (or press enter to skip):"), "Org")
   322  }
   323  
   324  func (cmd Login) targetOrganization(org models.Organization) {
   325  	cmd.config.SetOrganizationFields(org.OrganizationFields)
   326  	cmd.ui.Say(T("Targeted org {{.OrgName}}\n",
   327  		map[string]interface{}{"OrgName": terminal.EntityNameColor(org.Name)}))
   328  }
   329  
   330  func (cmd Login) setSpace(c flags.FlagContext) error {
   331  	spaceName := c.String("s")
   332  
   333  	if spaceName == "" {
   334  		var availableSpaces []models.Space
   335  		err := cmd.spaceRepo.ListSpaces(func(space models.Space) bool {
   336  			availableSpaces = append(availableSpaces, space)
   337  			return (len(availableSpaces) < maxChoices)
   338  		})
   339  		if err != nil {
   340  			return errors.New(T("Error finding available spaces\n{{.Err}}",
   341  				map[string]interface{}{"Err": err.Error()}))
   342  		}
   343  
   344  		if len(availableSpaces) == 0 {
   345  			return nil
   346  		} else if len(availableSpaces) == 1 {
   347  			cmd.targetSpace(availableSpaces[0])
   348  			return nil
   349  		} else {
   350  			spaceName = cmd.promptForSpaceName(availableSpaces)
   351  			if spaceName == "" {
   352  				cmd.ui.Say("")
   353  				return nil
   354  			}
   355  		}
   356  	}
   357  
   358  	space, err := cmd.spaceRepo.FindByName(spaceName)
   359  	if err != nil {
   360  		return errors.New(T("Error finding space {{.SpaceName}}\n{{.Err}}",
   361  			map[string]interface{}{"SpaceName": terminal.EntityNameColor(spaceName), "Err": err.Error()}))
   362  	}
   363  
   364  	cmd.targetSpace(space)
   365  	return nil
   366  }
   367  
   368  func (cmd Login) promptForSpaceName(spaces []models.Space) string {
   369  	spaceNames := []string{}
   370  	for _, space := range spaces {
   371  		spaceNames = append(spaceNames, space.Name)
   372  	}
   373  
   374  	return cmd.promptForName(spaceNames, T("Select a space (or press enter to skip):"), "Space")
   375  }
   376  
   377  func (cmd Login) targetSpace(space models.Space) {
   378  	cmd.config.SetSpaceFields(space.SpaceFields)
   379  	cmd.ui.Say(T("Targeted space {{.SpaceName}}\n",
   380  		map[string]interface{}{"SpaceName": terminal.EntityNameColor(space.Name)}))
   381  }
   382  
   383  func (cmd Login) promptForName(names []string, listPrompt, itemPrompt string) string {
   384  	nameIndex := 0
   385  	var nameString string
   386  	for nameIndex < 1 || nameIndex > len(names) {
   387  		var err error
   388  
   389  		// list header
   390  		cmd.ui.Say(listPrompt)
   391  
   392  		// only display list if it is shorter than maxChoices
   393  		if len(names) < maxChoices {
   394  			for i, name := range names {
   395  				cmd.ui.Say("%d. %s", i+1, name)
   396  			}
   397  		} else {
   398  			cmd.ui.Say(T("There are too many options to display, please type in the name."))
   399  		}
   400  
   401  		nameString = cmd.ui.Ask(itemPrompt)
   402  		if nameString == "" {
   403  			return ""
   404  		}
   405  
   406  		nameIndex, err = strconv.Atoi(nameString)
   407  
   408  		if err != nil {
   409  			nameIndex = 1
   410  			return nameString
   411  		}
   412  	}
   413  
   414  	return names[nameIndex-1]
   415  }