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