github.com/sleungcy/cli@v7.1.0+incompatible/command/v7/login_command.go (about)

     1  package v7
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net/url"
     8  	"strings"
     9  
    10  	"code.cloudfoundry.org/cli/api/uaa"
    11  	"code.cloudfoundry.org/cli/resources"
    12  	"code.cloudfoundry.org/cli/util/ui"
    13  	"code.cloudfoundry.org/clock"
    14  
    15  	"code.cloudfoundry.org/cli/actor/actionerror"
    16  	"code.cloudfoundry.org/cli/actor/v7action"
    17  	"code.cloudfoundry.org/cli/api/uaa/constant"
    18  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    19  	"code.cloudfoundry.org/cli/command"
    20  	"code.cloudfoundry.org/cli/command/translatableerror"
    21  	"code.cloudfoundry.org/cli/command/v7/shared"
    22  )
    23  
    24  //go:generate counterfeiter . ActorReloader
    25  
    26  type ActorReloader interface {
    27  	Reload(command.Config, command.UI) (Actor, error)
    28  }
    29  
    30  type ActualActorReloader struct{}
    31  
    32  func (a ActualActorReloader) Reload(config command.Config, ui command.UI) (Actor, error) {
    33  	ccClient, uaaClient, routingClient, err := shared.GetNewClientsAndConnectToCF(config, ui, "")
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  
    38  	return v7action.NewActor(ccClient, config, nil, uaaClient, routingClient, clock.NewClock()), nil
    39  }
    40  
    41  const maxLoginTries = 3
    42  
    43  type LoginCommand struct {
    44  	UI            command.UI
    45  	Actor         Actor
    46  	Config        command.Config
    47  	ActorReloader ActorReloader
    48  
    49  	APIEndpoint       string      `short:"a" description:"API endpoint (e.g. https://api.example.com)"`
    50  	Organization      string      `short:"o" description:"Org"`
    51  	Password          string      `short:"p" description:"Password"`
    52  	Space             string      `short:"s" description:"Space"`
    53  	SkipSSLValidation bool        `long:"skip-ssl-validation" description:"Skip verification of the API endpoint. Not recommended!"`
    54  	SSO               bool        `long:"sso" description:"Prompt for a one-time passcode to login"`
    55  	SSOPasscode       string      `long:"sso-passcode" description:"One-time passcode"`
    56  	Username          string      `short:"u" description:"Username"`
    57  	Origin            string      `long:"origin" description:"Indicates the identity provider to be used for login"`
    58  	usage             interface{} `usage:"CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE] [--sso | --sso-passcode PASSCODE] [--origin ORIGIN]\n\nWARNING:\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\n\nEXAMPLES:\n   CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)\n   CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)\n   CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)\n   CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)\n   CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time passcode to login)\n   CF_NAME login --origin ldap"`
    59  	relatedCommands   interface{} `related_commands:"api, auth, target"`
    60  }
    61  
    62  func (cmd *LoginCommand) Setup(config command.Config, ui command.UI) error {
    63  	ccClient, _ := shared.NewWrappedCloudControllerClient(config, ui)
    64  	cmd.Actor = v7action.NewActor(ccClient, config, nil, nil, nil, clock.NewClock())
    65  	cmd.ActorReloader = ActualActorReloader{}
    66  
    67  	cmd.UI = ui
    68  	cmd.Config = config
    69  	return nil
    70  }
    71  
    72  func (cmd *LoginCommand) Execute(args []string) error {
    73  	if cmd.Config.UAAGrantType() == string(constant.GrantTypeClientCredentials) {
    74  		return translatableerror.PasswordGrantTypeLogoutRequiredError{}
    75  	}
    76  
    77  	if cmd.Config.UAAOAuthClient() != "cf" || cmd.Config.UAAOAuthClientSecret() != "" {
    78  		return translatableerror.ManualClientCredentialsError{}
    79  	}
    80  
    81  	err := cmd.validateFlags()
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	endpoint, err := cmd.determineAPIEndpoint()
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	err = cmd.targetAPI(endpoint)
    92  	if err != nil {
    93  		translatedErr := translatableerror.ConvertToTranslatableError(err)
    94  		if invalidSSLErr, ok := translatedErr.(translatableerror.InvalidSSLCertError); ok {
    95  			invalidSSLErr.SuggestedCommand = "login"
    96  			return invalidSSLErr
    97  		}
    98  		return err
    99  	}
   100  
   101  	versionWarning, err := shared.CheckCCAPIVersion(cmd.Config.APIVersion())
   102  	if err != nil {
   103  		cmd.UI.DisplayWarning("Warning: unable to determine whether targeted API's version meets minimum supported.")
   104  	}
   105  	if versionWarning != "" {
   106  		cmd.UI.DisplayWarning(versionWarning)
   107  	}
   108  
   109  	cmd.UI.DisplayNewline()
   110  
   111  	cmd.Actor, err = cmd.ActorReloader.Reload(cmd.Config, cmd.UI)
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	defer cmd.showStatus()
   117  
   118  	var authErr error
   119  	if cmd.SSO || cmd.SSOPasscode != "" {
   120  		authErr = cmd.authenticateSSO()
   121  	} else {
   122  		authErr = cmd.authenticate()
   123  	}
   124  
   125  	if authErr != nil {
   126  		return errors.New("Unable to authenticate.")
   127  	}
   128  
   129  	err = cmd.Config.WriteConfig()
   130  	if err != nil {
   131  		return fmt.Errorf("Error writing config: %s", err.Error())
   132  	}
   133  
   134  	if cmd.Organization != "" {
   135  		org, warnings, err := cmd.Actor.GetOrganizationByName(cmd.Organization)
   136  		cmd.UI.DisplayWarnings(warnings)
   137  		if err != nil {
   138  			return err
   139  		}
   140  
   141  		cmd.Config.SetOrganizationInformation(org.GUID, org.Name)
   142  	} else {
   143  		orgs, warnings, err := cmd.Actor.GetOrganizations("")
   144  		cmd.UI.DisplayWarnings(warnings)
   145  		if err != nil {
   146  			return err
   147  		}
   148  
   149  		filteredOrgs, err := cmd.filterOrgsForSpace(orgs)
   150  		if err != nil {
   151  			return err
   152  		}
   153  
   154  		if len(filteredOrgs) == 1 {
   155  			cmd.Config.SetOrganizationInformation(filteredOrgs[0].GUID, filteredOrgs[0].Name)
   156  		} else if len(filteredOrgs) > 1 {
   157  			chosenOrg, err := cmd.promptChosenOrg(filteredOrgs)
   158  			if err != nil {
   159  				return err
   160  			}
   161  
   162  			if chosenOrg.GUID != "" {
   163  				cmd.Config.SetOrganizationInformation(chosenOrg.GUID, chosenOrg.Name)
   164  			}
   165  		}
   166  	}
   167  
   168  	targetedOrg := cmd.Config.TargetedOrganization()
   169  
   170  	if targetedOrg.GUID != "" {
   171  		cmd.UI.DisplayTextWithFlavor("Targeted org {{.Organization}}.", map[string]interface{}{
   172  			"Organization": cmd.Config.TargetedOrganizationName(),
   173  		})
   174  		cmd.UI.DisplayNewline()
   175  
   176  		if cmd.Space != "" {
   177  			space, warnings, err := cmd.Actor.GetSpaceByNameAndOrganization(cmd.Space, targetedOrg.GUID)
   178  			cmd.UI.DisplayWarnings(warnings)
   179  			if err != nil {
   180  				return err
   181  			}
   182  			cmd.targetSpace(space)
   183  		} else {
   184  			spaces, warnings, err := cmd.Actor.GetOrganizationSpaces(targetedOrg.GUID)
   185  			cmd.UI.DisplayWarnings(warnings)
   186  			if err != nil {
   187  				return err
   188  			}
   189  
   190  			if len(spaces) == 1 {
   191  				cmd.targetSpace(spaces[0])
   192  			} else if len(spaces) > 1 {
   193  				chosenSpace, err := cmd.promptChosenSpace(spaces)
   194  				if err != nil {
   195  					return err
   196  				}
   197  				if chosenSpace.Name != "" {
   198  					cmd.targetSpace(chosenSpace)
   199  				}
   200  			}
   201  		}
   202  	}
   203  
   204  	return nil
   205  }
   206  
   207  func (cmd *LoginCommand) determineAPIEndpoint() (v7action.TargetSettings, error) {
   208  	endpoint := cmd.APIEndpoint
   209  	skipSSLValidation := cmd.SkipSSLValidation
   210  
   211  	var configTarget = cmd.Config.Target()
   212  
   213  	if endpoint == "" && configTarget != "" {
   214  		endpoint = configTarget
   215  		skipSSLValidation = cmd.Config.SkipSSLValidation() || cmd.SkipSSLValidation
   216  	}
   217  
   218  	if len(endpoint) > 0 {
   219  		cmd.UI.DisplayTextWithFlavor("API endpoint: {{.APIEndpoint}}", map[string]interface{}{
   220  			"APIEndpoint": endpoint,
   221  		})
   222  	} else {
   223  		userInput, err := cmd.UI.DisplayTextPrompt("API endpoint")
   224  		if err != nil {
   225  			return v7action.TargetSettings{}, err
   226  		}
   227  		endpoint = userInput
   228  	}
   229  
   230  	strippedEndpoint := strings.TrimRight(endpoint, "/")
   231  	parsedURL, err := url.Parse(strippedEndpoint)
   232  	if err != nil {
   233  		return v7action.TargetSettings{}, err
   234  	}
   235  	if parsedURL.Scheme == "" {
   236  		parsedURL.Scheme = "https"
   237  	}
   238  
   239  	return v7action.TargetSettings{URL: parsedURL.String(), SkipSSLValidation: skipSSLValidation}, nil
   240  }
   241  
   242  func (cmd *LoginCommand) targetAPI(settings v7action.TargetSettings) error {
   243  	warnings, err := cmd.Actor.SetTarget(settings)
   244  	cmd.UI.DisplayWarnings(warnings)
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	if strings.HasPrefix(settings.URL, "http:") {
   250  		cmd.UI.DisplayWarning("Warning: Insecure http API endpoint detected: secure https API endpoints are recommended")
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  func (cmd *LoginCommand) authenticate() error {
   257  	var err error
   258  	var credentials = make(map[string]string)
   259  
   260  	prompts := cmd.Actor.GetLoginPrompts()
   261  	nonPasswordPrompts, passwordPrompts := cmd.groupPrompts(prompts)
   262  
   263  	if value, ok := prompts["username"]; ok {
   264  		credentials["username"], err = cmd.getFlagValOrPrompt(&cmd.Username, value, true)
   265  		if err != nil {
   266  			return err
   267  		}
   268  	}
   269  
   270  	for key, prompt := range nonPasswordPrompts {
   271  		credentials[key], err = cmd.UI.DisplayTextPrompt(prompt.DisplayName)
   272  		if err != nil {
   273  			return err
   274  		}
   275  	}
   276  
   277  	for i := 0; i < maxLoginTries; i++ {
   278  		// ensure that password gets prompted before other codes (eg. mfa code)
   279  		if prompt, ok := prompts["password"]; ok {
   280  			credentials["password"], err = cmd.getFlagValOrPrompt(&cmd.Password, prompt, false)
   281  			if err != nil {
   282  				return err
   283  			}
   284  		}
   285  
   286  		for key, prompt := range passwordPrompts {
   287  			credentials[key], err = cmd.UI.DisplayPasswordPrompt(prompt.DisplayName)
   288  			if err != nil {
   289  				return err
   290  			}
   291  		}
   292  
   293  		cmd.UI.DisplayNewline()
   294  		cmd.UI.DisplayText("Authenticating...")
   295  
   296  		err = cmd.Actor.Authenticate(credentials, cmd.Origin, constant.GrantTypePassword)
   297  
   298  		if err != nil {
   299  			cmd.UI.DisplayWarning(translatableerror.ConvertToTranslatableError(err).Error())
   300  			cmd.UI.DisplayNewline()
   301  
   302  			if _, ok := err.(uaa.AccountLockedError); ok {
   303  				break
   304  			}
   305  		}
   306  
   307  		if err == nil {
   308  			cmd.UI.DisplayOK()
   309  			break
   310  		}
   311  	}
   312  
   313  	return err
   314  }
   315  
   316  func (cmd *LoginCommand) authenticateSSO() error {
   317  	prompts := cmd.Actor.GetLoginPrompts()
   318  
   319  	var err error
   320  	for i := 0; i < maxLoginTries; i++ {
   321  		var passcode string
   322  
   323  		passcode, err = cmd.getFlagValOrPrompt(&cmd.SSOPasscode, prompts["passcode"], false)
   324  		if err != nil {
   325  			return err
   326  		}
   327  
   328  		credentials := map[string]string{"passcode": passcode}
   329  
   330  		cmd.UI.DisplayText("Authenticating...")
   331  		err = cmd.Actor.Authenticate(credentials, "", constant.GrantTypePassword)
   332  
   333  		if err != nil {
   334  			cmd.UI.DisplayWarning(translatableerror.ConvertToTranslatableError(err).Error())
   335  			cmd.UI.DisplayNewline()
   336  		} else {
   337  			cmd.UI.DisplayOK()
   338  			cmd.UI.DisplayNewline()
   339  			break
   340  		}
   341  	}
   342  	return err
   343  }
   344  
   345  func (cmd *LoginCommand) groupPrompts(prompts map[string]coreconfig.AuthPrompt) (map[string]coreconfig.AuthPrompt, map[string]coreconfig.AuthPrompt) {
   346  	var (
   347  		nonPasswordPrompts = make(map[string]coreconfig.AuthPrompt)
   348  		passwordPrompts    = make(map[string]coreconfig.AuthPrompt)
   349  	)
   350  
   351  	for key, prompt := range prompts {
   352  		if prompt.Type == coreconfig.AuthPromptTypePassword {
   353  			if key == "passcode" || key == "password" {
   354  				continue
   355  			}
   356  
   357  			passwordPrompts[key] = prompt
   358  		} else {
   359  			if key == "username" {
   360  				continue
   361  			}
   362  
   363  			nonPasswordPrompts[key] = prompt
   364  		}
   365  	}
   366  
   367  	return nonPasswordPrompts, passwordPrompts
   368  }
   369  
   370  func (cmd *LoginCommand) getFlagValOrPrompt(field *string, prompt coreconfig.AuthPrompt, isText bool) (string, error) {
   371  	if *field != "" {
   372  		value := *field
   373  		*field = ""
   374  		return value, nil
   375  	} else {
   376  		if isText {
   377  			return cmd.UI.DisplayTextPrompt(prompt.DisplayName)
   378  		}
   379  		return cmd.UI.DisplayPasswordPrompt(prompt.DisplayName)
   380  	}
   381  }
   382  
   383  func (cmd *LoginCommand) showStatus() {
   384  	tableContent := [][]string{
   385  		{
   386  			cmd.UI.TranslateText("API endpoint:"),
   387  			strings.TrimRight(cmd.Config.Target(), "/"),
   388  		},
   389  		{
   390  			cmd.UI.TranslateText("API version:"),
   391  			cmd.Config.APIVersion(),
   392  		},
   393  	}
   394  
   395  	user, err := cmd.Config.CurrentUserName()
   396  	if user == "" || err != nil {
   397  		cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   398  		command.DisplayNotLoggedInText(cmd.Config.BinaryName(), cmd.UI)
   399  		return
   400  	}
   401  	tableContent = append(tableContent, []string{cmd.UI.TranslateText("user:"), user})
   402  
   403  	orgName := cmd.Config.TargetedOrganizationName()
   404  	if orgName == "" {
   405  		cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   406  		cmd.UI.DisplayText("No org or space targeted, use '{{.CFTargetCommand}} -o ORG -s SPACE'",
   407  			map[string]interface{}{
   408  				"CFTargetCommand": fmt.Sprintf("%s target", cmd.Config.BinaryName()),
   409  			},
   410  		)
   411  		return
   412  	}
   413  	tableContent = append(tableContent, []string{cmd.UI.TranslateText("org:"), orgName})
   414  
   415  	spaceContent := cmd.Config.TargetedSpace().Name
   416  	if spaceContent == "" {
   417  		spaceContent = cmd.UI.TranslateText("No space targeted, use '{{.Command}}'",
   418  			map[string]interface{}{
   419  				"Command": fmt.Sprintf("%s target -s SPACE", cmd.Config.BinaryName()),
   420  			},
   421  		)
   422  	}
   423  	tableContent = append(tableContent, []string{cmd.UI.TranslateText("space:"), spaceContent})
   424  
   425  	cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   426  }
   427  
   428  func (cmd *LoginCommand) filterOrgsForSpace(allOrgs []resources.Organization) ([]resources.Organization, error) {
   429  	if cmd.Space == "" {
   430  		return allOrgs, nil
   431  	}
   432  
   433  	var filteredOrgs []resources.Organization
   434  	for _, org := range allOrgs {
   435  		_, warnings, err := cmd.Actor.GetSpaceByNameAndOrganization(cmd.Space, org.GUID)
   436  		cmd.UI.DisplayWarnings(warnings)
   437  		if err == nil {
   438  			filteredOrgs = append(filteredOrgs, org)
   439  			continue
   440  		}
   441  		if _, ok := err.(actionerror.SpaceNotFoundError); !ok {
   442  			return []resources.Organization{}, err
   443  		}
   444  	}
   445  
   446  	return filteredOrgs, nil
   447  }
   448  
   449  func (cmd *LoginCommand) promptChosenOrg(orgs []resources.Organization) (resources.Organization, error) {
   450  	orgNames := make([]string, len(orgs))
   451  	for i, org := range orgs {
   452  		orgNames[i] = org.Name
   453  	}
   454  
   455  	chosenOrgName, err := cmd.promptMenu(orgNames, "Select an org:", "Org")
   456  
   457  	if err != nil {
   458  		if invalidChoice, ok := err.(ui.InvalidChoiceError); ok {
   459  			if cmd.Space != "" {
   460  				return resources.Organization{}, translatableerror.OrganizationWithSpaceNotFoundError{Name: invalidChoice.Choice, SpaceName: cmd.Space}
   461  			}
   462  			return resources.Organization{}, translatableerror.OrganizationNotFoundError{Name: invalidChoice.Choice}
   463  		}
   464  
   465  		if err == io.EOF {
   466  			return resources.Organization{}, nil
   467  		}
   468  
   469  		return resources.Organization{}, err
   470  	}
   471  
   472  	for _, org := range orgs {
   473  		if org.Name == chosenOrgName {
   474  			return org, nil
   475  		}
   476  	}
   477  
   478  	return resources.Organization{}, nil
   479  }
   480  
   481  func (cmd *LoginCommand) promptChosenSpace(spaces []resources.Space) (resources.Space, error) {
   482  	spaceNames := make([]string, len(spaces))
   483  	for i, space := range spaces {
   484  		spaceNames[i] = space.Name
   485  	}
   486  
   487  	chosenSpaceName, err := cmd.promptMenu(spaceNames, "Select a space:", "Space")
   488  	if err != nil {
   489  		if invalidChoice, ok := err.(ui.InvalidChoiceError); ok {
   490  			return resources.Space{}, translatableerror.SpaceNotFoundError{Name: invalidChoice.Choice}
   491  		}
   492  
   493  		if err == io.EOF {
   494  			return resources.Space{}, nil
   495  		}
   496  
   497  		return resources.Space{}, err
   498  	}
   499  
   500  	for _, space := range spaces {
   501  		if space.Name == chosenSpaceName {
   502  			return space, nil
   503  		}
   504  	}
   505  	return resources.Space{}, nil
   506  }
   507  
   508  func (cmd *LoginCommand) promptMenu(choices []string, text string, prompt string) (string, error) {
   509  	var choice string
   510  	var err error
   511  
   512  	if len(choices) < 50 {
   513  		for {
   514  			cmd.UI.DisplayText(text)
   515  			choice, err = cmd.UI.DisplayTextMenu(choices, prompt)
   516  			if err != ui.ErrInvalidIndex {
   517  				break
   518  			}
   519  		}
   520  	} else {
   521  		cmd.UI.DisplayText(text)
   522  		cmd.UI.DisplayText("There are too many options to display; please type in the name.")
   523  		cmd.UI.DisplayNewline()
   524  		defaultChoice := "enter to skip"
   525  		choice, err = cmd.UI.DisplayOptionalTextPrompt(defaultChoice, prompt)
   526  
   527  		if choice == defaultChoice {
   528  			return "", nil
   529  		}
   530  		if !contains(choices, choice) {
   531  			return "", ui.InvalidChoiceError{Choice: choice}
   532  		}
   533  	}
   534  
   535  	return choice, err
   536  }
   537  
   538  func (cmd *LoginCommand) targetSpace(space resources.Space) {
   539  	cmd.Config.SetSpaceInformation(space.GUID, space.Name, true)
   540  
   541  	cmd.UI.DisplayTextWithFlavor("Targeted space {{.Space}}.", map[string]interface{}{
   542  		"Space": space.Name,
   543  	})
   544  	cmd.UI.DisplayNewline()
   545  }
   546  
   547  func (cmd *LoginCommand) validateFlags() error {
   548  	if cmd.Origin != "" && cmd.SSO {
   549  		return translatableerror.ArgumentCombinationError{
   550  			Args: []string{"--sso", "--origin"},
   551  		}
   552  	}
   553  
   554  	if cmd.Origin != "" && cmd.SSOPasscode != "" {
   555  		return translatableerror.ArgumentCombinationError{
   556  			Args: []string{"--sso-passcode", "--origin"},
   557  		}
   558  	}
   559  
   560  	if cmd.SSO && cmd.SSOPasscode != "" {
   561  		return translatableerror.ArgumentCombinationError{
   562  			Args: []string{"--sso-passcode", "--sso"},
   563  		}
   564  	}
   565  
   566  	return nil
   567  }
   568  
   569  func contains(s []string, v string) bool {
   570  	for _, x := range s {
   571  		if x == v {
   572  			return true
   573  		}
   574  	}
   575  	return false
   576  }