github.com/cloudfoundry-community/cloudfoundry-cli@v6.44.1-0.20240130060226-cda5ed8e89a5+incompatible/command/v6/login_command.go (about)

     1  package v6
     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/util/ui"
    12  
    13  	"code.cloudfoundry.org/cli/actor/v2action"
    14  	"code.cloudfoundry.org/cli/actor/v3action"
    15  	"code.cloudfoundry.org/cli/api/uaa/constant"
    16  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    17  	"code.cloudfoundry.org/cli/command"
    18  	"code.cloudfoundry.org/cli/command/translatableerror"
    19  	"code.cloudfoundry.org/cli/command/v6/shared"
    20  )
    21  
    22  //go:generate counterfeiter . LoginActor
    23  
    24  const maxLoginTries = 3
    25  
    26  type LoginActor interface {
    27  	Authenticate(credentials map[string]string, origin string, grantType constant.GrantType) error
    28  	GetLoginPrompts() map[string]coreconfig.AuthPrompt
    29  	GetOrganizationByName(orgName string) (v3action.Organization, v3action.Warnings, error)
    30  	GetSpaceByNameAndOrganization(spaceName string, orgGUID string) (v3action.Space, v3action.Warnings, error)
    31  	GetOrganizations() ([]v3action.Organization, v3action.Warnings, error)
    32  	SetTarget(settings v3action.TargetSettings) (v3action.Warnings, error)
    33  }
    34  
    35  //go:generate counterfeiter . VersionChecker
    36  
    37  type VersionChecker interface {
    38  	MinCLIVersion() string
    39  	CloudControllerAPIVersion() string
    40  }
    41  
    42  //go:generate counterfeiter . ActorMaker
    43  
    44  type ActorMaker interface {
    45  	NewActor(command.Config, command.UI, bool) (LoginActor, error)
    46  }
    47  
    48  //go:generate counterfeiter . CheckerMaker
    49  
    50  type CheckerMaker interface {
    51  	NewVersionChecker(command.Config, command.UI, bool) (VersionChecker, error)
    52  }
    53  
    54  type ActorMakerFunc func(command.Config, command.UI, bool) (LoginActor, error)
    55  type CheckerMakerFunc func(command.Config, command.UI, bool) (VersionChecker, error)
    56  
    57  func (a ActorMakerFunc) NewActor(config command.Config, ui command.UI, targetCF bool) (LoginActor, error) {
    58  	return a(config, ui, targetCF)
    59  }
    60  
    61  func (c CheckerMakerFunc) NewVersionChecker(config command.Config, ui command.UI, targetCF bool) (VersionChecker, error) {
    62  	return c(config, ui, targetCF)
    63  }
    64  
    65  var actorMaker ActorMakerFunc = func(config command.Config, ui command.UI, targetCF bool) (LoginActor, error) {
    66  	client, uaa, err := shared.NewV3BasedClients(config, ui, targetCF, "")
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	v3Actor := v3action.NewActor(client, config, nil, uaa)
    72  	return v3Actor, nil
    73  }
    74  
    75  var checkerMaker CheckerMakerFunc = func(config command.Config, ui command.UI, targetCF bool) (VersionChecker, error) {
    76  	client, uaa, err := shared.NewClients(config, ui, targetCF)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	v2Actor := v2action.NewActor(client, uaa, config)
    82  	return v2Actor, nil
    83  }
    84  
    85  type LoginCommand struct {
    86  	APIEndpoint       string      `short:"a" description:"API endpoint (e.g. https://api.example.com)"`
    87  	Organization      string      `short:"o" description:"Org"`
    88  	Password          string      `short:"p" description:"Password"`
    89  	Space             string      `short:"s" description:"Space"`
    90  	SkipSSLValidation bool        `long:"skip-ssl-validation" description:"Skip verification of the API endpoint. Not recommended!"`
    91  	SSO               bool        `long:"sso" description:"Prompt for a one-time passcode to login"`
    92  	SSOPasscode       string      `long:"sso-passcode" description:"One-time passcode"`
    93  	Username          string      `short:"u" description:"Username"`
    94  	usage             interface{} `usage:"CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE] [--sso | --sso-passcode PASSCODE]\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)"`
    95  	relatedCommands   interface{} `related_commands:"api, auth, target"`
    96  
    97  	UI           command.UI
    98  	Actor        LoginActor
    99  	ActorMaker   ActorMaker
   100  	Checker      VersionChecker
   101  	CheckerMaker CheckerMaker
   102  	Config       command.Config
   103  }
   104  
   105  func (cmd *LoginCommand) Setup(config command.Config, ui command.UI) error {
   106  	cmd.ActorMaker = actorMaker
   107  	actor, err := cmd.ActorMaker.NewActor(config, ui, false)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	cmd.CheckerMaker = checkerMaker
   112  	cmd.Actor = actor
   113  	cmd.UI = ui
   114  	cmd.Config = config
   115  	return nil
   116  }
   117  
   118  func (cmd *LoginCommand) Execute(args []string) error {
   119  	if !cmd.Config.ExperimentalLogin() {
   120  		return translatableerror.UnrefactoredCommandError{}
   121  	}
   122  	cmd.UI.DisplayWarning("Using experimental login command, some behavior may be different")
   123  	var err error
   124  
   125  	err = cmd.getAPI()
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	cmd.UI.DisplayNewline()
   131  
   132  	err = cmd.retargetAPI()
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	defer cmd.showStatus()
   138  
   139  	if cmd.Config.UAAGrantType() == "client_credentials" {
   140  		return errors.New("Service account currently logged in. Use 'cf logout' to log out service account and try again.")
   141  	}
   142  
   143  	var authErr error
   144  	if cmd.SSO == true || cmd.SSOPasscode != "" {
   145  		if cmd.SSO && cmd.SSOPasscode != "" {
   146  			return translatableerror.ArgumentCombinationError{Args: []string{"--sso-passcode", "--sso"}}
   147  		}
   148  		authErr = cmd.authenticateSSO()
   149  	} else {
   150  		authErr = cmd.authenticate()
   151  	}
   152  
   153  	if authErr != nil {
   154  		return errors.New("Unable to authenticate.")
   155  	}
   156  
   157  	if cmd.Organization != "" {
   158  		org, warnings, err := cmd.Actor.GetOrganizationByName(cmd.Organization)
   159  		cmd.UI.DisplayWarnings(warnings)
   160  		if err != nil {
   161  			return err
   162  		}
   163  		cmd.Config.SetOrganizationInformation(org.GUID, org.Name)
   164  	} else {
   165  		orgs, warnings, err := cmd.Actor.GetOrganizations()
   166  		cmd.UI.DisplayWarnings(warnings)
   167  		if err != nil {
   168  			return err
   169  		}
   170  		switch {
   171  		case len(orgs) == 1:
   172  			cmd.Config.SetOrganizationInformation(orgs[0].GUID, orgs[0].Name)
   173  		case len(orgs) > 1:
   174  			var emptyOrg v3action.Organization
   175  			chosenOrg, err := cmd.promptChosenOrg(orgs)
   176  			if err != nil {
   177  				return err
   178  			}
   179  			if chosenOrg != emptyOrg {
   180  				cmd.Config.SetOrganizationInformation(chosenOrg.GUID, chosenOrg.Name)
   181  			}
   182  		}
   183  	}
   184  
   185  	targetedOrg := cmd.Config.TargetedOrganization()
   186  
   187  	if targetedOrg.GUID != "" {
   188  
   189  		cmd.UI.DisplayTextWithFlavor("Targeted org: {{.Organization}}", map[string]interface{}{
   190  			"Organization": cmd.Config.TargetedOrganizationName(),
   191  		})
   192  
   193  		if cmd.Space != "" {
   194  			space, warnings, err := cmd.Actor.GetSpaceByNameAndOrganization(cmd.Space, targetedOrg.GUID)
   195  			cmd.UI.DisplayWarnings(warnings)
   196  			if err != nil {
   197  				return err
   198  			}
   199  			// the "AllowSSH" field is not returned by v3, and is never read from the config.
   200  			// persist `true` to maintain compatibility in the config file.
   201  			// TODO: this field should be removed entirely in v7
   202  			cmd.Config.SetSpaceInformation(space.GUID, space.Name, true)
   203  
   204  			cmd.UI.DisplayNewline()
   205  			cmd.UI.DisplayTextWithFlavor("Targeted space: {{.Space}}", map[string]interface{}{
   206  				"Space": space.Name,
   207  			})
   208  		}
   209  		cmd.UI.DisplayNewline()
   210  	}
   211  
   212  	err = cmd.checkMinCLIVersion()
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	cmd.UI.DisplayNewline()
   218  	cmd.UI.DisplayNewline()
   219  
   220  	return nil
   221  }
   222  
   223  func (cmd *LoginCommand) getAPI() error {
   224  	if cmd.APIEndpoint != "" {
   225  		cmd.UI.DisplayTextWithFlavor("API endpoint: {{.APIEndpoint}}", map[string]interface{}{
   226  			"APIEndpoint": cmd.APIEndpoint,
   227  		})
   228  	} else if cmd.Config.Target() != "" {
   229  		cmd.APIEndpoint = cmd.Config.Target()
   230  		cmd.UI.DisplayTextWithFlavor("API endpoint: {{.APIEndpoint}}", map[string]interface{}{
   231  			"APIEndpoint": cmd.APIEndpoint,
   232  		})
   233  	} else {
   234  		apiEndpoint, err := cmd.UI.DisplayTextPrompt("API endpoint")
   235  		if err != nil {
   236  			return err
   237  		}
   238  		cmd.APIEndpoint = apiEndpoint
   239  	}
   240  	return nil
   241  }
   242  
   243  func (cmd *LoginCommand) retargetAPI() error {
   244  	strippedEndpoint := strings.TrimRight(cmd.APIEndpoint, "/")
   245  	endpoint, _ := url.Parse(strippedEndpoint)
   246  	if endpoint.Scheme == "" {
   247  		endpoint.Scheme = "https"
   248  	}
   249  
   250  	settings := v3action.TargetSettings{
   251  		URL:               endpoint.String(),
   252  		SkipSSLValidation: cmd.Config.SkipSSLValidation() || cmd.SkipSSLValidation,
   253  	}
   254  	_, err := cmd.Actor.SetTarget(settings)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	return cmd.reloadActor()
   260  }
   261  
   262  func (cmd *LoginCommand) authenticate() error {
   263  	prompts := cmd.Actor.GetLoginPrompts()
   264  	credentials := make(map[string]string)
   265  
   266  	if value, ok := prompts["username"]; ok {
   267  		if prompts["username"].Type == coreconfig.AuthPromptTypeText && cmd.Username != "" {
   268  			credentials["username"] = cmd.Username
   269  		} else {
   270  			var err error
   271  			credentials["username"], err = cmd.UI.DisplayTextPrompt(value.DisplayName)
   272  			if err != nil {
   273  				return err
   274  			}
   275  			cmd.UI.DisplayNewline()
   276  		}
   277  	}
   278  
   279  	passwordKeys := []string{}
   280  	for key, prompt := range prompts {
   281  		if prompt.Type == coreconfig.AuthPromptTypePassword {
   282  			if key == "passcode" || key == "password" {
   283  				continue
   284  			}
   285  
   286  			passwordKeys = append(passwordKeys, key)
   287  		} else if key == "username" {
   288  			continue
   289  		} else {
   290  			var err error
   291  			credentials[key], err = cmd.UI.DisplayTextPrompt(prompt.DisplayName)
   292  			if err != nil {
   293  				return err
   294  			}
   295  			cmd.UI.DisplayNewline()
   296  		}
   297  	}
   298  
   299  	var err error
   300  	for i := 0; i < maxLoginTries; i++ {
   301  		var promptedCredentials map[string]string
   302  		promptedCredentials, err = cmd.passwordPrompts(prompts, credentials, passwordKeys)
   303  		if err != nil {
   304  			return err
   305  		}
   306  
   307  		cmd.UI.DisplayText("Authenticating...")
   308  
   309  		err = cmd.Actor.Authenticate(promptedCredentials, "", constant.GrantTypePassword)
   310  
   311  		if err != nil {
   312  			cmd.UI.DisplayWarning(translatableerror.ConvertToTranslatableError(err).Error())
   313  			cmd.UI.DisplayNewline()
   314  
   315  			if _, ok := err.(uaa.AccountLockedError); ok {
   316  				break
   317  			}
   318  		}
   319  
   320  		if err == nil {
   321  			cmd.UI.DisplayOK()
   322  			break
   323  		}
   324  	}
   325  	if err != nil {
   326  		return err
   327  	}
   328  	return nil
   329  }
   330  
   331  func (cmd *LoginCommand) authenticateSSO() error {
   332  	prompts := cmd.Actor.GetLoginPrompts()
   333  	credentials := make(map[string]string)
   334  
   335  	var err error
   336  	for i := 0; i < maxLoginTries; i++ {
   337  		if len(cmd.SSOPasscode) > 0 {
   338  			credentials["passcode"] = cmd.SSOPasscode
   339  			cmd.SSOPasscode = ""
   340  		} else {
   341  			credentials["passcode"], err = cmd.UI.DisplayPasswordPrompt(prompts["passcode"].DisplayName)
   342  			if err != nil {
   343  				return err
   344  			}
   345  		}
   346  
   347  		credentialsCopy := make(map[string]string, len(credentials))
   348  		for k, v := range credentials {
   349  			credentialsCopy[k] = v
   350  		}
   351  
   352  		cmd.UI.DisplayText("Authenticating...")
   353  		err = cmd.Actor.Authenticate(credentialsCopy, "", constant.GrantTypePassword)
   354  
   355  		if err != nil {
   356  			cmd.UI.DisplayWarning(translatableerror.ConvertToTranslatableError(err).Error())
   357  			cmd.UI.DisplayNewline()
   358  		}
   359  
   360  		if err == nil {
   361  			cmd.UI.DisplayOK()
   362  			cmd.UI.DisplayNewline()
   363  			break
   364  		}
   365  	}
   366  	if err != nil {
   367  		return err
   368  	}
   369  	return nil
   370  }
   371  
   372  func (cmd *LoginCommand) checkMinCLIVersion() error {
   373  	newChecker, err := cmd.CheckerMaker.NewVersionChecker(cmd.Config, cmd.UI, true)
   374  	if err != nil {
   375  		return err
   376  	}
   377  
   378  	cmd.Checker = newChecker
   379  	cmd.Config.SetMinCLIVersion(cmd.Checker.MinCLIVersion())
   380  	return command.WarnIfCLIVersionBelowAPIDefinedMinimum(cmd.Config, cmd.Checker.CloudControllerAPIVersion(), cmd.UI)
   381  }
   382  
   383  func (cmd *LoginCommand) passwordPrompts(prompts map[string]coreconfig.AuthPrompt, credentials map[string]string, passwordKeys []string) (map[string]string, error) {
   384  	// ensure that password gets prompted before other codes (eg. mfa code)
   385  	var err error
   386  	if passPrompt, ok := prompts["password"]; ok {
   387  		if cmd.Password != "" {
   388  			credentials["password"] = cmd.Password
   389  			cmd.Password = ""
   390  		} else {
   391  			credentials["password"], err = cmd.UI.DisplayPasswordPrompt(passPrompt.DisplayName)
   392  			if err != nil {
   393  				return nil, err
   394  			}
   395  		}
   396  	}
   397  
   398  	for _, key := range passwordKeys {
   399  		cmd.UI.DisplayNewline()
   400  		credentials[key], err = cmd.UI.DisplayPasswordPrompt(prompts[key].DisplayName)
   401  		if err != nil {
   402  			return nil, err
   403  		}
   404  	}
   405  
   406  	credentialsCopy := make(map[string]string, len(credentials))
   407  	for k, v := range credentials {
   408  		credentialsCopy[k] = v
   409  	}
   410  
   411  	return credentialsCopy, nil
   412  }
   413  
   414  func (cmd *LoginCommand) reloadActor() error {
   415  	newActor, err := cmd.ActorMaker.NewActor(cmd.Config, cmd.UI, true)
   416  	if err != nil {
   417  		return err
   418  	}
   419  
   420  	cmd.Actor = newActor
   421  
   422  	return nil
   423  }
   424  
   425  func (cmd *LoginCommand) showStatus() {
   426  	tableContent := [][]string{
   427  		{
   428  			cmd.UI.TranslateText("API endpoint:"),
   429  			cmd.UI.TranslateText("{{.APIEndpoint}} (API version: {{.APIVersion}})",
   430  				map[string]interface{}{
   431  					"APIEndpoint": strings.TrimRight(cmd.APIEndpoint, "/"),
   432  					"APIVersion":  cmd.Config.APIVersion(),
   433  				}),
   434  		},
   435  	}
   436  
   437  	user, err := cmd.Config.CurrentUserName()
   438  	if user == "" || err != nil {
   439  		cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   440  		cmd.displayNotLoggedIn()
   441  		return
   442  	}
   443  	tableContent = append(tableContent, []string{cmd.UI.TranslateText("User:"), user})
   444  
   445  	orgName := cmd.Config.TargetedOrganizationName()
   446  	if orgName == "" {
   447  		cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   448  		cmd.displayNotTargetted()
   449  		return
   450  	}
   451  	tableContent = append(tableContent, []string{cmd.UI.TranslateText("Org:"), orgName})
   452  
   453  	spaceName := cmd.Config.TargetedSpace().Name
   454  	if spaceName == "" {
   455  		tableContent = append(tableContent, []string{cmd.UI.TranslateText("Space:"),
   456  			cmd.UI.TranslateText("No space targeted, use '{{.Command}}'", map[string]interface{}{
   457  				"Command": fmt.Sprintf("%s target -s SPACE", cmd.Config.BinaryName()),
   458  			})})
   459  	} else {
   460  		tableContent = append(tableContent, []string{cmd.UI.TranslateText("Space:"), spaceName})
   461  	}
   462  
   463  	cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   464  }
   465  
   466  func (cmd *LoginCommand) displayNotLoggedIn() {
   467  	cmd.UI.DisplayText(
   468  		"Not logged in. Use '{{.CFLoginCommand}}' to log in.",
   469  		map[string]interface{}{
   470  			"CFLoginCommand": fmt.Sprintf("%s login", cmd.Config.BinaryName()),
   471  		},
   472  	)
   473  }
   474  
   475  func (cmd *LoginCommand) displayNotTargetted() {
   476  	cmd.UI.DisplayText("No org or space targeted, use '{{.CFTargetCommand}} -o ORG -s SPACE'",
   477  		map[string]interface{}{
   478  			"CFTargetCommand": fmt.Sprintf("%s target", cmd.Config.BinaryName()),
   479  		},
   480  	)
   481  }
   482  
   483  func (cmd *LoginCommand) promptChosenOrg(orgs []v3action.Organization) (v3action.Organization, error) {
   484  
   485  	var (
   486  		chosenOrgName string
   487  		err           error
   488  	)
   489  
   490  	if len(orgs) < 50 {
   491  		orgNames := make([]string, len(orgs))
   492  		for i, org := range orgs {
   493  			orgNames[i] = org.Name
   494  		}
   495  
   496  		for {
   497  			cmd.UI.DisplayText("Select an org:")
   498  			chosenOrgName, err = cmd.UI.DisplayTextMenu(orgNames, "Org")
   499  			if err != ui.ErrInvalidIndex {
   500  				break
   501  			}
   502  		}
   503  
   504  		if err != nil {
   505  			if invalidChoice, ok := err.(ui.InvalidChoiceError); ok {
   506  				return v3action.Organization{}, translatableerror.OrganizationNotFoundError{Name: invalidChoice.Choice}
   507  			} else if err == io.EOF {
   508  				return v3action.Organization{}, nil
   509  			}
   510  
   511  			return v3action.Organization{}, err
   512  		}
   513  
   514  		if chosenOrgName == "" {
   515  			return v3action.Organization{}, nil
   516  		}
   517  
   518  	} else {
   519  		cmd.UI.DisplayText("Select an org:")
   520  		cmd.UI.DisplayText("There are too many options to display; please type in the name.")
   521  		defaultChoice := "enter to skip"
   522  		chosenOrgName, err = cmd.UI.DisplayOptionalTextPrompt(defaultChoice, "Org")
   523  		if chosenOrgName == defaultChoice {
   524  			return v3action.Organization{}, nil
   525  		}
   526  	}
   527  
   528  	if err != nil {
   529  		return v3action.Organization{}, err
   530  	}
   531  
   532  	for _, org := range orgs {
   533  		if org.Name == chosenOrgName {
   534  			return org, nil
   535  		}
   536  	}
   537  
   538  	return v3action.Organization{}, translatableerror.OrganizationNotFoundError{Name: chosenOrgName}
   539  }