github.com/cloudfoundry-attic/cli-with-i18n@v6.32.1-0.20171002233121-7401370d3b85+incompatible/cf/commands/login.go (about)

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