github.com/swisscom/cloudfoundry-cli@v7.1.0+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.MinSupportedV2ClientVersion)
   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  	if passcode.DisplayName == "" {
   180  		passcode = coreconfig.AuthPrompt{
   181  			Type: coreconfig.AuthPromptTypePassword,
   182  			DisplayName: T("Temporary Authentication Code ( Get one at {{.AuthenticationEndpoint}}/passcode )",
   183  				map[string]interface{}{
   184  					"AuthenticationEndpoint": cmd.config.AuthenticationEndpoint(),
   185  				}),
   186  		}
   187  	}
   188  
   189  	for i := 0; i < maxLoginTries; i++ {
   190  		if c.IsSet("sso-passcode") && i == 0 {
   191  			credentials["passcode"] = c.String("sso-passcode")
   192  		} else {
   193  			credentials["passcode"] = cmd.ui.AskForPassword(passcode.DisplayName)
   194  		}
   195  
   196  		cmd.ui.Say(T("Authenticating..."))
   197  		err = cmd.authenticator.Authenticate(credentials)
   198  
   199  		if err == nil {
   200  			cmd.ui.Ok()
   201  			cmd.ui.Say("")
   202  			break
   203  		}
   204  
   205  		cmd.ui.Say(err.Error())
   206  	}
   207  
   208  	if err != nil {
   209  		return errors.New(T("Unable to authenticate."))
   210  	}
   211  	return nil
   212  }
   213  
   214  func (cmd Login) authenticate(c flags.FlagContext) error {
   215  	if cmd.config.UAAGrantType() == "client_credentials" {
   216  		return errors.New(T("Service account currently logged in. Use 'cf logout' to log out service account and try again."))
   217  	}
   218  
   219  	usernameFlagValue := c.String("u")
   220  	passwordFlagValue := c.String("p")
   221  
   222  	prompts, err := cmd.authenticator.GetLoginPromptsAndSaveUAAServerURL()
   223  	if err != nil {
   224  		return err
   225  	}
   226  	passwordKeys := []string{}
   227  	credentials := make(map[string]string)
   228  
   229  	if value, ok := prompts["username"]; ok {
   230  		if prompts["username"].Type == coreconfig.AuthPromptTypeText && usernameFlagValue != "" {
   231  			credentials["username"] = usernameFlagValue
   232  		} else {
   233  			credentials["username"] = cmd.ui.Ask(T(value.DisplayName))
   234  		}
   235  	}
   236  
   237  	for key, prompt := range prompts {
   238  		if prompt.Type == coreconfig.AuthPromptTypePassword {
   239  			if key == "passcode" || key == "password" {
   240  				continue
   241  			}
   242  
   243  			passwordKeys = append(passwordKeys, key)
   244  		} else if key == "username" {
   245  			continue
   246  		} else {
   247  			credentials[key] = cmd.ui.Ask(T(prompt.DisplayName))
   248  		}
   249  	}
   250  
   251  	for i := 0; i < maxLoginTries; i++ {
   252  
   253  		// ensure that password gets prompted before other codes (eg. mfa code)
   254  		if passPrompt, ok := prompts["password"]; ok {
   255  			if passwordFlagValue != "" {
   256  				credentials["password"] = passwordFlagValue
   257  				passwordFlagValue = ""
   258  			} else {
   259  				credentials["password"] = cmd.ui.AskForPassword(T(passPrompt.DisplayName))
   260  			}
   261  		}
   262  
   263  		for _, key := range passwordKeys {
   264  			credentials[key] = cmd.ui.AskForPassword(T(prompts[key].DisplayName))
   265  		}
   266  
   267  		credentialsCopy := make(map[string]string, len(credentials))
   268  		for k, v := range credentials {
   269  			credentialsCopy[k] = v
   270  		}
   271  
   272  		cmd.ui.Say(T("Authenticating..."))
   273  		err = cmd.authenticator.Authenticate(credentialsCopy)
   274  
   275  		if err == nil {
   276  			cmd.ui.Ok()
   277  			cmd.ui.Say("")
   278  			break
   279  		}
   280  
   281  		cmd.ui.Say(err.Error())
   282  	}
   283  
   284  	if err != nil {
   285  		return errors.New(T("Unable to authenticate."))
   286  	}
   287  	return nil
   288  }
   289  
   290  func (cmd Login) setOrganization(c flags.FlagContext) (bool, error) {
   291  	orgName := c.String("o")
   292  
   293  	if orgName == "" {
   294  		orgs, err := cmd.orgRepo.ListOrgs(maxChoices)
   295  		if err != nil {
   296  			return false, errors.New(T("Error finding available orgs\n{{.APIErr}}",
   297  				map[string]interface{}{"APIErr": err.Error()}))
   298  		}
   299  
   300  		switch len(orgs) {
   301  		case 0:
   302  			return false, nil
   303  		case 1:
   304  			cmd.targetOrganization(orgs[0])
   305  			return true, nil
   306  		default:
   307  			orgName = cmd.promptForOrgName(orgs)
   308  			if orgName == "" {
   309  				cmd.ui.Say("")
   310  				return false, nil
   311  			}
   312  		}
   313  	}
   314  
   315  	org, err := cmd.orgRepo.FindByName(orgName)
   316  	if err != nil {
   317  		return false, errors.New(T("Error finding org {{.OrgName}}\n{{.Err}}",
   318  			map[string]interface{}{"OrgName": terminal.EntityNameColor(orgName), "Err": err.Error()}))
   319  	}
   320  
   321  	cmd.targetOrganization(org)
   322  	return true, nil
   323  }
   324  
   325  func (cmd Login) promptForOrgName(orgs []models.Organization) string {
   326  	orgNames := []string{}
   327  	for _, org := range orgs {
   328  		orgNames = append(orgNames, org.Name)
   329  	}
   330  
   331  	return cmd.promptForName(orgNames, T("Select an org (or press enter to skip):"), "Org")
   332  }
   333  
   334  func (cmd Login) targetOrganization(org models.Organization) {
   335  	cmd.config.SetOrganizationFields(org.OrganizationFields)
   336  	cmd.ui.Say(T("Targeted org {{.OrgName}}\n",
   337  		map[string]interface{}{"OrgName": terminal.EntityNameColor(org.Name)}))
   338  }
   339  
   340  func (cmd Login) setSpace(c flags.FlagContext) error {
   341  	spaceName := c.String("s")
   342  
   343  	if spaceName == "" {
   344  		var availableSpaces []models.Space
   345  		err := cmd.spaceRepo.ListSpaces(func(space models.Space) bool {
   346  			availableSpaces = append(availableSpaces, space)
   347  			return (len(availableSpaces) < maxChoices)
   348  		})
   349  		if err != nil {
   350  			return errors.New(T("Error finding available spaces\n{{.Err}}",
   351  				map[string]interface{}{"Err": err.Error()}))
   352  		}
   353  
   354  		if len(availableSpaces) == 0 {
   355  			return nil
   356  		} else if len(availableSpaces) == 1 {
   357  			cmd.targetSpace(availableSpaces[0])
   358  			return nil
   359  		} else {
   360  			spaceName = cmd.promptForSpaceName(availableSpaces)
   361  			if spaceName == "" {
   362  				cmd.ui.Say("")
   363  				return nil
   364  			}
   365  		}
   366  	}
   367  
   368  	space, err := cmd.spaceRepo.FindByName(spaceName)
   369  	if err != nil {
   370  		return errors.New(T("Error finding space {{.SpaceName}}\n{{.Err}}",
   371  			map[string]interface{}{"SpaceName": terminal.EntityNameColor(spaceName), "Err": err.Error()}))
   372  	}
   373  
   374  	cmd.targetSpace(space)
   375  	return nil
   376  }
   377  
   378  func (cmd Login) promptForSpaceName(spaces []models.Space) string {
   379  	spaceNames := []string{}
   380  	for _, space := range spaces {
   381  		spaceNames = append(spaceNames, space.Name)
   382  	}
   383  
   384  	return cmd.promptForName(spaceNames, T("Select a space (or press enter to skip):"), "Space")
   385  }
   386  
   387  func (cmd Login) targetSpace(space models.Space) {
   388  	cmd.config.SetSpaceFields(space.SpaceFields)
   389  	cmd.ui.Say(T("Targeted space {{.SpaceName}}\n",
   390  		map[string]interface{}{"SpaceName": terminal.EntityNameColor(space.Name)}))
   391  }
   392  
   393  func (cmd Login) promptForName(names []string, listPrompt, itemPrompt string) string {
   394  	nameIndex := 0
   395  	var nameString string
   396  	for nameIndex < 1 || nameIndex > len(names) {
   397  		var err error
   398  
   399  		// list header
   400  		cmd.ui.Say(listPrompt)
   401  
   402  		// only display list if it is shorter than maxChoices
   403  		if len(names) < maxChoices {
   404  			for i, name := range names {
   405  				cmd.ui.Say("%d. %s", i+1, name)
   406  			}
   407  		} else {
   408  			cmd.ui.Say(T("There are too many options to display, please type in the name."))
   409  		}
   410  
   411  		nameString = cmd.ui.Ask(itemPrompt)
   412  		if nameString == "" {
   413  			return ""
   414  		}
   415  
   416  		nameIndex, err = strconv.Atoi(nameString)
   417  
   418  		if err != nil {
   419  			nameIndex = 1
   420  			return nameString
   421  		}
   422  	}
   423  
   424  	return names[nameIndex-1]
   425  }