github.com/LukasHeimann/cloudfoundrycli/v8@v8.4.4/command/v7/login_command.go (about) 1 package v7 2 3 import ( 4 "fmt" 5 "io" 6 "net/url" 7 "strings" 8 9 "github.com/LukasHeimann/cloudfoundrycli/v8/api/uaa" 10 "github.com/LukasHeimann/cloudfoundrycli/v8/resources" 11 "github.com/LukasHeimann/cloudfoundrycli/v8/util/ui" 12 "code.cloudfoundry.org/clock" 13 14 "github.com/LukasHeimann/cloudfoundrycli/v8/actor/actionerror" 15 "github.com/LukasHeimann/cloudfoundrycli/v8/actor/v7action" 16 "github.com/LukasHeimann/cloudfoundrycli/v8/api/uaa/constant" 17 "github.com/LukasHeimann/cloudfoundrycli/v8/cf/configuration/coreconfig" 18 "github.com/LukasHeimann/cloudfoundrycli/v8/cf/errors" 19 "github.com/LukasHeimann/cloudfoundrycli/v8/command" 20 "github.com/LukasHeimann/cloudfoundrycli/v8/command/translatableerror" 21 "github.com/LukasHeimann/cloudfoundrycli/v8/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 nonPasswordPrompts, passwordPrompts := 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 nonPasswordPrompts { 280 if prompt.Type == coreconfig.AuthPromptTypeMenu { 281 credentials[key], err = cmd.UI.DisplayTextMenu(prompt.Entries, prompt.DisplayName) 282 } else { 283 credentials[key], err = cmd.UI.DisplayTextPrompt(prompt.DisplayName) 284 } 285 if err != nil { 286 return err 287 } 288 } 289 290 for i := 0; i < maxLoginTries; i++ { 291 // ensure that password gets prompted before other codes (eg. mfa code) 292 if prompt, ok := prompts["password"]; ok { 293 credentials["password"], err = cmd.getFlagValOrPrompt(&cmd.Password, prompt, false) 294 if err != nil { 295 return err 296 } 297 } 298 299 for key, prompt := range passwordPrompts { 300 credentials[key], err = cmd.UI.DisplayPasswordPrompt(prompt.DisplayName) 301 if err != nil { 302 return err 303 } 304 } 305 306 cmd.UI.DisplayNewline() 307 cmd.UI.DisplayText("Authenticating...") 308 309 err = cmd.Actor.Authenticate(credentials, cmd.Origin, 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 326 return err 327 } 328 329 func (cmd *LoginCommand) authenticateSSO() error { 330 prompts, err := cmd.Actor.GetLoginPrompts() 331 if err != nil { 332 return err 333 } 334 335 for i := 0; i < maxLoginTries; i++ { 336 var passcode string 337 338 passcode, err = cmd.getFlagValOrPrompt(&cmd.SSOPasscode, prompts["passcode"], false) 339 if err != nil { 340 return err 341 } 342 343 credentials := map[string]string{"passcode": passcode} 344 345 cmd.UI.DisplayText("Authenticating...") 346 err = cmd.Actor.Authenticate(credentials, "", constant.GrantTypePassword) 347 348 if err != nil { 349 cmd.UI.DisplayWarning(translatableerror.ConvertToTranslatableError(err).Error()) 350 cmd.UI.DisplayNewline() 351 } else { 352 cmd.UI.DisplayOK() 353 cmd.UI.DisplayNewline() 354 break 355 } 356 } 357 return err 358 } 359 360 func (cmd *LoginCommand) groupPrompts(prompts map[string]coreconfig.AuthPrompt) (map[string]coreconfig.AuthPrompt, map[string]coreconfig.AuthPrompt) { 361 var ( 362 nonPasswordPrompts = make(map[string]coreconfig.AuthPrompt) 363 passwordPrompts = make(map[string]coreconfig.AuthPrompt) 364 ) 365 366 for key, prompt := range prompts { 367 if prompt.Type == coreconfig.AuthPromptTypePassword { 368 if key == "passcode" || key == "password" { 369 continue 370 } 371 372 passwordPrompts[key] = prompt 373 } else { 374 if key == "username" { 375 continue 376 } 377 378 nonPasswordPrompts[key] = prompt 379 } 380 } 381 382 return nonPasswordPrompts, passwordPrompts 383 } 384 385 func (cmd *LoginCommand) getFlagValOrPrompt(field *string, prompt coreconfig.AuthPrompt, isText bool) (string, error) { 386 if *field != "" { 387 value := *field 388 *field = "" 389 return value, nil 390 } else { 391 if isText { 392 return cmd.UI.DisplayTextPrompt(prompt.DisplayName) 393 } 394 return cmd.UI.DisplayPasswordPrompt(prompt.DisplayName) 395 } 396 } 397 398 func (cmd *LoginCommand) showStatus() { 399 tableContent := [][]string{ 400 { 401 cmd.UI.TranslateText("API endpoint:"), 402 strings.TrimRight(cmd.Config.Target(), "/"), 403 }, 404 { 405 cmd.UI.TranslateText("API version:"), 406 cmd.Config.APIVersion(), 407 }, 408 } 409 410 user, err := cmd.Actor.GetCurrentUser() 411 if user.Name == "" || err != nil { 412 cmd.UI.DisplayKeyValueTable("", tableContent, 3) 413 command.DisplayNotLoggedInText(cmd.Config.BinaryName(), cmd.UI) 414 return 415 } 416 tableContent = append(tableContent, []string{cmd.UI.TranslateText("user:"), user.Name}) 417 418 orgName := cmd.Config.TargetedOrganizationName() 419 if orgName == "" { 420 cmd.UI.DisplayKeyValueTable("", tableContent, 3) 421 cmd.UI.DisplayText("No org or space targeted, use '{{.CFTargetCommand}} -o ORG -s SPACE'", 422 map[string]interface{}{ 423 "CFTargetCommand": fmt.Sprintf("%s target", cmd.Config.BinaryName()), 424 }, 425 ) 426 return 427 } 428 tableContent = append(tableContent, []string{cmd.UI.TranslateText("org:"), orgName}) 429 430 spaceContent := cmd.Config.TargetedSpace().Name 431 if spaceContent == "" { 432 spaceContent = cmd.UI.TranslateText("No space targeted, use '{{.Command}}'", 433 map[string]interface{}{ 434 "Command": fmt.Sprintf("%s target -s SPACE", cmd.Config.BinaryName()), 435 }, 436 ) 437 } 438 tableContent = append(tableContent, []string{cmd.UI.TranslateText("space:"), spaceContent}) 439 440 cmd.UI.DisplayKeyValueTable("", tableContent, 3) 441 } 442 443 func (cmd *LoginCommand) filterOrgsForSpace(allOrgs []resources.Organization) ([]resources.Organization, error) { 444 if cmd.Space == "" { 445 return allOrgs, nil 446 } 447 448 var filteredOrgs []resources.Organization 449 for _, org := range allOrgs { 450 _, warnings, err := cmd.Actor.GetSpaceByNameAndOrganization(cmd.Space, org.GUID) 451 cmd.UI.DisplayWarnings(warnings) 452 if err == nil { 453 filteredOrgs = append(filteredOrgs, org) 454 continue 455 } 456 if _, ok := err.(actionerror.SpaceNotFoundError); !ok { 457 return []resources.Organization{}, err 458 } 459 } 460 461 return filteredOrgs, nil 462 } 463 464 func (cmd *LoginCommand) promptChosenOrg(orgs []resources.Organization) (resources.Organization, error) { 465 orgNames := make([]string, len(orgs)) 466 for i, org := range orgs { 467 orgNames[i] = org.Name 468 } 469 470 chosenOrgName, err := cmd.promptMenu(orgNames, "Select an org:", "Org") 471 if err != nil { 472 if invalidChoice, ok := err.(ui.InvalidChoiceError); ok { 473 if cmd.Space != "" { 474 return resources.Organization{}, translatableerror.OrganizationWithSpaceNotFoundError{Name: invalidChoice.Choice, SpaceName: cmd.Space} 475 } 476 return resources.Organization{}, translatableerror.OrganizationNotFoundError{Name: invalidChoice.Choice} 477 } 478 479 if err == io.EOF { 480 return resources.Organization{}, nil 481 } 482 483 return resources.Organization{}, err 484 } 485 486 for _, org := range orgs { 487 if org.Name == chosenOrgName { 488 return org, nil 489 } 490 } 491 492 return resources.Organization{}, nil 493 } 494 495 func (cmd *LoginCommand) promptChosenSpace(spaces []resources.Space) (resources.Space, error) { 496 spaceNames := make([]string, len(spaces)) 497 for i, space := range spaces { 498 spaceNames[i] = space.Name 499 } 500 501 chosenSpaceName, err := cmd.promptMenu(spaceNames, "Select a space:", "Space") 502 if err != nil { 503 if invalidChoice, ok := err.(ui.InvalidChoiceError); ok { 504 return resources.Space{}, translatableerror.SpaceNotFoundError{Name: invalidChoice.Choice} 505 } 506 507 if err == io.EOF { 508 return resources.Space{}, nil 509 } 510 511 return resources.Space{}, err 512 } 513 514 for _, space := range spaces { 515 if space.Name == chosenSpaceName { 516 return space, nil 517 } 518 } 519 return resources.Space{}, nil 520 } 521 522 func (cmd *LoginCommand) promptMenu(choices []string, text string, prompt string) (string, error) { 523 var choice string 524 var err error 525 526 if len(choices) < 50 { 527 for { 528 cmd.UI.DisplayText(text) 529 choice, err = cmd.UI.DisplayTextMenu(choices, prompt) 530 if err != ui.ErrInvalidIndex { 531 break 532 } 533 } 534 } else { 535 cmd.UI.DisplayText(text) 536 cmd.UI.DisplayText("There are too many options to display; please type in the name.") 537 cmd.UI.DisplayNewline() 538 defaultChoice := "enter to skip" 539 choice, err = cmd.UI.DisplayOptionalTextPrompt(defaultChoice, prompt) 540 541 if choice == defaultChoice { 542 return "", nil 543 } 544 if !contains(choices, choice) { 545 return "", ui.InvalidChoiceError{Choice: choice} 546 } 547 } 548 549 return choice, err 550 } 551 552 func (cmd *LoginCommand) targetSpace(space resources.Space) { 553 cmd.Config.SetSpaceInformation(space.GUID, space.Name, true) 554 555 cmd.UI.DisplayTextWithFlavor("Targeted space {{.Space}}.", map[string]interface{}{ 556 "Space": space.Name, 557 }) 558 cmd.UI.DisplayNewline() 559 } 560 561 func (cmd *LoginCommand) validateFlags() error { 562 if cmd.Origin != "" && cmd.SSO { 563 return translatableerror.ArgumentCombinationError{ 564 Args: []string{"--sso", "--origin"}, 565 } 566 } 567 568 if cmd.Origin != "" && cmd.SSOPasscode != "" { 569 return translatableerror.ArgumentCombinationError{ 570 Args: []string{"--sso-passcode", "--origin"}, 571 } 572 } 573 574 if cmd.SSO && cmd.SSOPasscode != "" { 575 return translatableerror.ArgumentCombinationError{ 576 Args: []string{"--sso-passcode", "--sso"}, 577 } 578 } 579 580 return nil 581 } 582 583 func (cmd *LoginCommand) validateTargetSpecificFlags() error { 584 if !cmd.Config.IsCFOnK8s() { 585 return nil 586 } 587 588 if cmd.Password != "" { 589 return translatableerror.NotSupportedOnKubernetesArgumentError{Arg: "-p"} 590 } 591 if cmd.SSO { 592 return translatableerror.NotSupportedOnKubernetesArgumentError{Arg: "--sso"} 593 } 594 if cmd.SSOPasscode != "" { 595 return translatableerror.NotSupportedOnKubernetesArgumentError{Arg: "--sso-passcode"} 596 } 597 if cmd.Username != "" { 598 return translatableerror.NotSupportedOnKubernetesArgumentError{Arg: "-u"} 599 } 600 if cmd.Origin != "" { 601 return translatableerror.NotSupportedOnKubernetesArgumentError{Arg: "--origin"} 602 } 603 return nil 604 } 605 606 func contains(s []string, v string) bool { 607 for _, x := range s { 608 if x == v { 609 return true 610 } 611 } 612 return false 613 }