github.com/wanddynosios/cli/v8@v8.7.9-0.20240221182337-1a92e3a7017f/command/v7/login_command.go (about)

     1  package v7
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/url"
     7  	"strings"
     8  
     9  	"code.cloudfoundry.org/cli/api/uaa"
    10  	"code.cloudfoundry.org/cli/resources"
    11  	"code.cloudfoundry.org/cli/util/ui"
    12  	"code.cloudfoundry.org/clock"
    13  
    14  	"code.cloudfoundry.org/cli/actor/actionerror"
    15  	"code.cloudfoundry.org/cli/actor/v7action"
    16  	"code.cloudfoundry.org/cli/api/uaa/constant"
    17  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    18  	"code.cloudfoundry.org/cli/cf/errors"
    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 go run github.com/maxbrunsfeld/counterfeiter/v6 . 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  	err = cmd.validateTargetSpecificFlags()
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	versionWarning, err := shared.CheckCCAPIVersion(cmd.Config.APIVersion())
   107  	if err != nil {
   108  		cmd.UI.DisplayWarning("Warning: unable to determine whether targeted API's version meets minimum supported.")
   109  	}
   110  	if versionWarning != "" {
   111  		cmd.UI.DisplayWarning(versionWarning)
   112  	}
   113  
   114  	cmd.UI.DisplayNewline()
   115  
   116  	cmd.Actor, err = cmd.ActorReloader.Reload(cmd.Config, cmd.UI)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	defer cmd.showStatus()
   122  
   123  	var authErr error
   124  	if cmd.SSO || cmd.SSOPasscode != "" {
   125  		authErr = cmd.authenticateSSO()
   126  	} else {
   127  		authErr = cmd.authenticate()
   128  	}
   129  
   130  	if authErr != nil {
   131  		return errors.New("Unable to authenticate.")
   132  	}
   133  
   134  	err = cmd.Config.WriteConfig()
   135  	if err != nil {
   136  		return fmt.Errorf("Error writing config: %s", err.Error())
   137  	}
   138  
   139  	if cmd.Organization != "" {
   140  		org, warnings, err := cmd.Actor.GetOrganizationByName(cmd.Organization)
   141  		cmd.UI.DisplayWarnings(warnings)
   142  		if err != nil {
   143  			return err
   144  		}
   145  
   146  		cmd.Config.SetOrganizationInformation(org.GUID, org.Name)
   147  	} else {
   148  		orgs, warnings, err := cmd.Actor.GetOrganizations("")
   149  		cmd.UI.DisplayWarnings(warnings)
   150  		if err != nil {
   151  			return err
   152  		}
   153  
   154  		filteredOrgs, err := cmd.filterOrgsForSpace(orgs)
   155  		if err != nil {
   156  			return err
   157  		}
   158  
   159  		if len(filteredOrgs) == 1 {
   160  			cmd.Config.SetOrganizationInformation(filteredOrgs[0].GUID, filteredOrgs[0].Name)
   161  		} else if len(filteredOrgs) > 1 {
   162  			chosenOrg, err := cmd.promptChosenOrg(filteredOrgs)
   163  			if err != nil {
   164  				return err
   165  			}
   166  
   167  			if chosenOrg.GUID != "" {
   168  				cmd.Config.SetOrganizationInformation(chosenOrg.GUID, chosenOrg.Name)
   169  			}
   170  		}
   171  	}
   172  
   173  	targetedOrg := cmd.Config.TargetedOrganization()
   174  
   175  	if targetedOrg.GUID != "" {
   176  		cmd.UI.DisplayTextWithFlavor("Targeted org {{.Organization}}.", map[string]interface{}{
   177  			"Organization": cmd.Config.TargetedOrganizationName(),
   178  		})
   179  		cmd.UI.DisplayNewline()
   180  
   181  		if cmd.Space != "" {
   182  			space, warnings, err := cmd.Actor.GetSpaceByNameAndOrganization(cmd.Space, targetedOrg.GUID)
   183  			cmd.UI.DisplayWarnings(warnings)
   184  			if err != nil {
   185  				return err
   186  			}
   187  			cmd.targetSpace(space)
   188  		} else {
   189  			spaces, warnings, err := cmd.Actor.GetOrganizationSpaces(targetedOrg.GUID)
   190  			cmd.UI.DisplayWarnings(warnings)
   191  			if err != nil {
   192  				return err
   193  			}
   194  
   195  			if len(spaces) == 1 {
   196  				cmd.targetSpace(spaces[0])
   197  			} else if len(spaces) > 1 {
   198  				chosenSpace, err := cmd.promptChosenSpace(spaces)
   199  				if err != nil {
   200  					return err
   201  				}
   202  				if chosenSpace.Name != "" {
   203  					cmd.targetSpace(chosenSpace)
   204  				}
   205  			}
   206  		}
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func (cmd *LoginCommand) determineAPIEndpoint() (v7action.TargetSettings, error) {
   213  	endpoint := cmd.APIEndpoint
   214  	skipSSLValidation := cmd.SkipSSLValidation
   215  
   216  	configTarget := cmd.Config.Target()
   217  
   218  	if endpoint == "" && configTarget != "" {
   219  		endpoint = configTarget
   220  		skipSSLValidation = cmd.Config.SkipSSLValidation() || cmd.SkipSSLValidation
   221  	}
   222  
   223  	if len(endpoint) > 0 {
   224  		cmd.UI.DisplayTextWithFlavor("API endpoint: {{.APIEndpoint}}", map[string]interface{}{
   225  			"APIEndpoint": endpoint,
   226  		})
   227  	} else {
   228  		userInput, err := cmd.UI.DisplayTextPrompt("API endpoint")
   229  		if err != nil {
   230  			return v7action.TargetSettings{}, err
   231  		}
   232  		endpoint = userInput
   233  	}
   234  
   235  	strippedEndpoint := strings.TrimRight(endpoint, "/")
   236  	parsedURL, err := url.Parse(strippedEndpoint)
   237  	if err != nil {
   238  		return v7action.TargetSettings{}, err
   239  	}
   240  	if parsedURL.Scheme == "" {
   241  		parsedURL.Scheme = "https"
   242  	}
   243  
   244  	return v7action.TargetSettings{URL: parsedURL.String(), SkipSSLValidation: skipSSLValidation}, nil
   245  }
   246  
   247  func (cmd *LoginCommand) targetAPI(settings v7action.TargetSettings) error {
   248  	warnings, err := cmd.Actor.SetTarget(settings)
   249  	cmd.UI.DisplayWarnings(warnings)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	if strings.HasPrefix(settings.URL, "http:") {
   255  		cmd.UI.DisplayWarning("Warning: Insecure http API endpoint detected: secure https API endpoints are recommended")
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func (cmd *LoginCommand) authenticate() error {
   262  	var err error
   263  	credentials := make(map[string]string)
   264  
   265  	prompts, err := cmd.Actor.GetLoginPrompts()
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	nonSensitivePrompts, sensitivePrompts := cmd.groupPrompts(prompts)
   271  
   272  	if value, ok := prompts["username"]; ok {
   273  		credentials["username"], err = cmd.getFlagValOrPrompt(&cmd.Username, value, true)
   274  		if err != nil {
   275  			return err
   276  		}
   277  	}
   278  
   279  	for key, prompt := range nonSensitivePrompts {
   280  		credentials[key], err = cmd.UI.DisplayTextPrompt(prompt.DisplayName)
   281  		if err != nil {
   282  			return err
   283  		}
   284  	}
   285  
   286  	for i := 0; i < maxLoginTries; i++ {
   287  		// ensure that password gets prompted before other codes (eg. mfa code)
   288  		if prompt, ok := prompts["password"]; ok {
   289  			credentials["password"], err = cmd.getFlagValOrPrompt(&cmd.Password, prompt, false)
   290  			if err != nil {
   291  				return err
   292  			}
   293  		}
   294  
   295  		for key, prompt := range sensitivePrompts {
   296  			credentials[key], err = cmd.UI.DisplayPasswordPrompt(prompt.DisplayName)
   297  			if err != nil {
   298  				return err
   299  			}
   300  		}
   301  
   302  		cmd.UI.DisplayNewline()
   303  		cmd.UI.DisplayText("Authenticating...")
   304  
   305  		err = cmd.Actor.Authenticate(credentials, cmd.Origin, constant.GrantTypePassword)
   306  
   307  		if err == nil {
   308  			cmd.UI.DisplayOK()
   309  			break
   310  		}
   311  
   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  	return err
   321  }
   322  
   323  func (cmd *LoginCommand) authenticateSSO() error {
   324  	prompts, err := cmd.Actor.GetLoginPrompts()
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	for i := 0; i < maxLoginTries; i++ {
   330  		var passcode string
   331  
   332  		passcode, err = cmd.getFlagValOrPrompt(&cmd.SSOPasscode, prompts["passcode"], false)
   333  		if err != nil {
   334  			return err
   335  		}
   336  
   337  		credentials := map[string]string{"passcode": passcode}
   338  
   339  		cmd.UI.DisplayText("Authenticating...")
   340  		err = cmd.Actor.Authenticate(credentials, "", constant.GrantTypePassword)
   341  
   342  		if err != nil {
   343  			cmd.UI.DisplayWarning(translatableerror.ConvertToTranslatableError(err).Error())
   344  			cmd.UI.DisplayNewline()
   345  		} else {
   346  			cmd.UI.DisplayOK()
   347  			cmd.UI.DisplayNewline()
   348  			break
   349  		}
   350  	}
   351  	return err
   352  }
   353  
   354  func (cmd *LoginCommand) groupPrompts(prompts map[string]coreconfig.AuthPrompt) (map[string]coreconfig.AuthPrompt, map[string]coreconfig.AuthPrompt) {
   355  	var (
   356  		nonPasswordPrompts = make(map[string]coreconfig.AuthPrompt)
   357  		passwordPrompts    = make(map[string]coreconfig.AuthPrompt)
   358  	)
   359  
   360  	for key, prompt := range prompts {
   361  		if prompt.Type == coreconfig.AuthPromptTypePassword {
   362  			if key == "passcode" || key == "password" {
   363  				continue
   364  			}
   365  
   366  			passwordPrompts[key] = prompt
   367  		} else {
   368  			if key == "username" {
   369  				continue
   370  			}
   371  
   372  			nonPasswordPrompts[key] = prompt
   373  		}
   374  	}
   375  
   376  	return nonPasswordPrompts, passwordPrompts
   377  }
   378  
   379  func (cmd *LoginCommand) getFlagValOrPrompt(field *string, prompt coreconfig.AuthPrompt, isText bool) (string, error) {
   380  	if *field != "" {
   381  		value := *field
   382  		*field = ""
   383  		return value, nil
   384  	}
   385  
   386  	if prompt.Type == coreconfig.AuthPromptTypeMenu {
   387  		return cmd.UI.DisplayTextMenu(prompt.Entries, prompt.DisplayName)
   388  	}
   389  
   390  	if isText {
   391  		return cmd.UI.DisplayTextPrompt(prompt.DisplayName)
   392  	}
   393  	return cmd.UI.DisplayPasswordPrompt(prompt.DisplayName)
   394  }
   395  
   396  func (cmd *LoginCommand) showStatus() {
   397  	tableContent := [][]string{
   398  		{
   399  			cmd.UI.TranslateText("API endpoint:"),
   400  			strings.TrimRight(cmd.Config.Target(), "/"),
   401  		},
   402  		{
   403  			cmd.UI.TranslateText("API version:"),
   404  			cmd.Config.APIVersion(),
   405  		},
   406  	}
   407  
   408  	user, err := cmd.Actor.GetCurrentUser()
   409  	if user.Name == "" || err != nil {
   410  		cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   411  		command.DisplayNotLoggedInText(cmd.Config.BinaryName(), cmd.UI)
   412  		return
   413  	}
   414  	tableContent = append(tableContent, []string{cmd.UI.TranslateText("user:"), user.Name})
   415  
   416  	orgName := cmd.Config.TargetedOrganizationName()
   417  	if orgName == "" {
   418  		cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   419  		cmd.UI.DisplayText("No org or space targeted, use '{{.CFTargetCommand}} -o ORG -s SPACE'",
   420  			map[string]interface{}{
   421  				"CFTargetCommand": fmt.Sprintf("%s target", cmd.Config.BinaryName()),
   422  			},
   423  		)
   424  		return
   425  	}
   426  	tableContent = append(tableContent, []string{cmd.UI.TranslateText("org:"), orgName})
   427  
   428  	spaceContent := cmd.Config.TargetedSpace().Name
   429  	if spaceContent == "" {
   430  		spaceContent = cmd.UI.TranslateText("No space targeted, use '{{.Command}}'",
   431  			map[string]interface{}{
   432  				"Command": fmt.Sprintf("%s target -s SPACE", cmd.Config.BinaryName()),
   433  			},
   434  		)
   435  	}
   436  	tableContent = append(tableContent, []string{cmd.UI.TranslateText("space:"), spaceContent})
   437  
   438  	cmd.UI.DisplayKeyValueTable("", tableContent, 3)
   439  }
   440  
   441  func (cmd *LoginCommand) filterOrgsForSpace(allOrgs []resources.Organization) ([]resources.Organization, error) {
   442  	if cmd.Space == "" {
   443  		return allOrgs, nil
   444  	}
   445  
   446  	var filteredOrgs []resources.Organization
   447  	for _, org := range allOrgs {
   448  		_, warnings, err := cmd.Actor.GetSpaceByNameAndOrganization(cmd.Space, org.GUID)
   449  		cmd.UI.DisplayWarnings(warnings)
   450  		if err == nil {
   451  			filteredOrgs = append(filteredOrgs, org)
   452  			continue
   453  		}
   454  		if _, ok := err.(actionerror.SpaceNotFoundError); !ok {
   455  			return []resources.Organization{}, err
   456  		}
   457  	}
   458  
   459  	return filteredOrgs, nil
   460  }
   461  
   462  func (cmd *LoginCommand) promptChosenOrg(orgs []resources.Organization) (resources.Organization, error) {
   463  	orgNames := make([]string, len(orgs))
   464  	for i, org := range orgs {
   465  		orgNames[i] = org.Name
   466  	}
   467  
   468  	chosenOrgName, err := cmd.promptMenu(orgNames, "Select an org:", "Org")
   469  	if err != nil {
   470  		if invalidChoice, ok := err.(ui.InvalidChoiceError); ok {
   471  			if cmd.Space != "" {
   472  				return resources.Organization{}, translatableerror.OrganizationWithSpaceNotFoundError{Name: invalidChoice.Choice, SpaceName: cmd.Space}
   473  			}
   474  			return resources.Organization{}, translatableerror.OrganizationNotFoundError{Name: invalidChoice.Choice}
   475  		}
   476  
   477  		if err == io.EOF {
   478  			return resources.Organization{}, nil
   479  		}
   480  
   481  		return resources.Organization{}, err
   482  	}
   483  
   484  	for _, org := range orgs {
   485  		if org.Name == chosenOrgName {
   486  			return org, nil
   487  		}
   488  	}
   489  
   490  	return resources.Organization{}, nil
   491  }
   492  
   493  func (cmd *LoginCommand) promptChosenSpace(spaces []resources.Space) (resources.Space, error) {
   494  	spaceNames := make([]string, len(spaces))
   495  	for i, space := range spaces {
   496  		spaceNames[i] = space.Name
   497  	}
   498  
   499  	chosenSpaceName, err := cmd.promptMenu(spaceNames, "Select a space:", "Space")
   500  	if err != nil {
   501  		if invalidChoice, ok := err.(ui.InvalidChoiceError); ok {
   502  			return resources.Space{}, translatableerror.SpaceNotFoundError{Name: invalidChoice.Choice}
   503  		}
   504  
   505  		if err == io.EOF {
   506  			return resources.Space{}, nil
   507  		}
   508  
   509  		return resources.Space{}, err
   510  	}
   511  
   512  	for _, space := range spaces {
   513  		if space.Name == chosenSpaceName {
   514  			return space, nil
   515  		}
   516  	}
   517  	return resources.Space{}, nil
   518  }
   519  
   520  func (cmd *LoginCommand) promptMenu(choices []string, text string, prompt string) (string, error) {
   521  	var choice string
   522  	var err error
   523  
   524  	if len(choices) < 50 {
   525  		for {
   526  			cmd.UI.DisplayText(text)
   527  			choice, err = cmd.UI.DisplayTextMenu(choices, prompt)
   528  			if err != ui.ErrInvalidIndex {
   529  				break
   530  			}
   531  		}
   532  	} else {
   533  		cmd.UI.DisplayText(text)
   534  		cmd.UI.DisplayText("There are too many options to display; please type in the name.")
   535  		cmd.UI.DisplayNewline()
   536  		defaultChoice := "enter to skip"
   537  		choice, err = cmd.UI.DisplayOptionalTextPrompt(defaultChoice, prompt)
   538  
   539  		if choice == defaultChoice {
   540  			return "", nil
   541  		}
   542  		if !contains(choices, choice) {
   543  			return "", ui.InvalidChoiceError{Choice: choice}
   544  		}
   545  	}
   546  
   547  	return choice, err
   548  }
   549  
   550  func (cmd *LoginCommand) targetSpace(space resources.Space) {
   551  	cmd.Config.SetSpaceInformation(space.GUID, space.Name, true)
   552  
   553  	cmd.UI.DisplayTextWithFlavor("Targeted space {{.Space}}.", map[string]interface{}{
   554  		"Space": space.Name,
   555  	})
   556  	cmd.UI.DisplayNewline()
   557  }
   558  
   559  func (cmd *LoginCommand) validateFlags() error {
   560  	if cmd.Origin != "" && cmd.SSO {
   561  		return translatableerror.ArgumentCombinationError{
   562  			Args: []string{"--sso", "--origin"},
   563  		}
   564  	}
   565  
   566  	if cmd.Origin != "" && cmd.SSOPasscode != "" {
   567  		return translatableerror.ArgumentCombinationError{
   568  			Args: []string{"--sso-passcode", "--origin"},
   569  		}
   570  	}
   571  
   572  	if cmd.SSO && cmd.SSOPasscode != "" {
   573  		return translatableerror.ArgumentCombinationError{
   574  			Args: []string{"--sso-passcode", "--sso"},
   575  		}
   576  	}
   577  
   578  	return nil
   579  }
   580  
   581  func (cmd *LoginCommand) validateTargetSpecificFlags() error {
   582  	if !cmd.Config.IsCFOnK8s() {
   583  		return nil
   584  	}
   585  
   586  	if cmd.Password != "" {
   587  		cmd.UI.DisplayWarning("Warning: password is ignored when authenticating against Kubernetes.")
   588  	}
   589  
   590  	if cmd.SSO {
   591  		return translatableerror.NotSupportedOnKubernetesArgumentError{Arg: "--sso"}
   592  	}
   593  	if cmd.SSOPasscode != "" {
   594  		return translatableerror.NotSupportedOnKubernetesArgumentError{Arg: "--sso-passcode"}
   595  	}
   596  	if cmd.Origin != "" {
   597  		return translatableerror.NotSupportedOnKubernetesArgumentError{Arg: "--origin"}
   598  	}
   599  	return nil
   600  }
   601  
   602  func contains(s []string, v string) bool {
   603  	for _, x := range s {
   604  		if x == v {
   605  			return true
   606  		}
   607  	}
   608  	return false
   609  }