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 }