github.com/mook-as/cf-cli@v7.0.0-beta.28.0.20200120190804-b91c115fae48+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 GetOrganizationSpaces(orgName string) ([]v3action.Space, v3action.Warnings, error) 31 GetSpaceByNameAndOrganization(spaceName string, orgGUID string) (v3action.Space, v3action.Warnings, error) 32 GetOrganizations() ([]v3action.Organization, v3action.Warnings, error) 33 SetTarget(settings v3action.TargetSettings) (v3action.Warnings, error) 34 } 35 36 //go:generate counterfeiter . V2LoginActor 37 38 type V2LoginActor interface { 39 MinCLIVersion() string 40 CloudControllerAPIVersion() string 41 AuthorizationEndpoint() string 42 } 43 44 //go:generate counterfeiter . ActorMaker 45 46 type ActorMaker interface { 47 NewActor(command.Config, command.UI, bool, string) (LoginActor, error) 48 } 49 50 //go:generate counterfeiter . V2ActorMaker 51 52 type V2ActorMaker interface { 53 NewV2Actor(command.Config, command.UI, bool) (V2LoginActor, error) 54 } 55 56 type ActorMakerFunc func(command.Config, command.UI, bool, string) (LoginActor, error) 57 type V2ActorMakerFunc func(command.Config, command.UI, bool) (V2LoginActor, error) 58 59 func (a ActorMakerFunc) NewActor(config command.Config, ui command.UI, targetCF bool, authorizationEndpoint string) (LoginActor, error) { 60 return a(config, ui, targetCF, authorizationEndpoint) 61 } 62 63 func (a V2ActorMakerFunc) NewV2Actor(config command.Config, ui command.UI, targetCF bool) (V2LoginActor, error) { 64 return a(config, ui, targetCF) 65 } 66 67 var actorMaker ActorMakerFunc = func(config command.Config, ui command.UI, targetCF bool, authorizationEndpoint string) (LoginActor, error) { 68 client, uaa, err := shared.NewV3BasedClientsWithAuthorizationEndpoint(config, ui, targetCF, authorizationEndpoint) 69 if err != nil { 70 return nil, err 71 } 72 73 v3Actor := v3action.NewActor(client, config, nil, uaa) 74 return v3Actor, nil 75 } 76 77 var v2ActorMaker V2ActorMakerFunc = func(config command.Config, ui command.UI, targetCF bool) (V2LoginActor, error) { 78 client, uaa, err := shared.GetNewClientsAndConnectToCF(config, ui) 79 if err != nil { 80 return nil, err 81 } 82 83 v2Actor := v2action.NewActor(client, uaa, config) 84 return v2Actor, nil 85 } 86 87 type LoginCommand struct { 88 APIEndpoint string `short:"a" description:"API endpoint (e.g. https://api.example.com)"` 89 Organization string `short:"o" description:"Org"` 90 Password string `short:"p" description:"Password"` 91 Space string `short:"s" description:"Space"` 92 SkipSSLValidation bool `long:"skip-ssl-validation" description:"Skip verification of the API endpoint. Not recommended!"` 93 SSO bool `long:"sso" description:"Prompt for a one-time passcode to login"` 94 SSOPasscode string `long:"sso-passcode" description:"One-time passcode"` 95 Username string `short:"u" description:"Username"` 96 Origin string `long:"origin" description:"Indicates the identity provider to be used for login"` 97 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"` 98 relatedCommands interface{} `related_commands:"api, auth, target"` 99 100 UI command.UI 101 Actor LoginActor 102 ActorMaker ActorMaker 103 V2Actor V2LoginActor 104 V2ActorMaker V2ActorMaker 105 Config command.Config 106 } 107 108 func (cmd *LoginCommand) Setup(config command.Config, ui command.UI) error { 109 cmd.ActorMaker = actorMaker 110 actor, err := cmd.ActorMaker.NewActor(config, ui, false, "") 111 if err != nil { 112 return err 113 } 114 cmd.V2ActorMaker = v2ActorMaker 115 cmd.Actor = actor 116 cmd.UI = ui 117 cmd.Config = config 118 return nil 119 } 120 121 func (cmd *LoginCommand) Execute(args []string) error { 122 err := cmd.validateFlags() 123 if err != nil { 124 return err 125 } 126 127 endpoint, err := cmd.determineAPIEndpoint() 128 if err != nil { 129 return err 130 } 131 132 cmd.UI.DisplayNewline() 133 134 err = cmd.targetAPI(endpoint) 135 if err != nil { 136 te := translatableerror.ConvertToTranslatableError(err) 137 if ise, ok := te.(translatableerror.InvalidSSLCertError); ok { 138 ise.SuggestedCommand = "login" 139 return ise 140 } 141 return err 142 } 143 144 err = cmd.checkMinCLIVersion() 145 if err != nil { 146 return err 147 } 148 149 defer cmd.showStatus() 150 151 if cmd.Config.UAAGrantType() == string(constant.GrantTypeClientCredentials) { 152 return translatableerror.PasswordGrantTypeLogoutRequiredError{} 153 } else if cmd.Config.UAAOAuthClient() != "cf" || cmd.Config.UAAOAuthClientSecret() != "" { 154 cmd.UI.DisplayWarning("Deprecation warning: Manually writing your client credentials to the config.json is deprecated and will be removed in the future. For similar functionality, please use the `cf auth --client-credentials` command instead.") 155 } 156 157 var authErr error 158 if cmd.SSO || cmd.SSOPasscode != "" { 159 authErr = cmd.authenticateSSO() 160 } else { 161 authErr = cmd.authenticate() 162 } 163 164 if authErr != nil { 165 return errors.New("Unable to authenticate.") 166 } 167 168 err = cmd.Config.WriteConfig() 169 if err != nil { 170 return fmt.Errorf("Error writing config: %s", err.Error()) 171 } 172 173 if cmd.Organization != "" { 174 org, warnings, err := cmd.Actor.GetOrganizationByName(cmd.Organization) 175 cmd.UI.DisplayWarnings(warnings) 176 if err != nil { 177 return err 178 } 179 cmd.Config.SetOrganizationInformation(org.GUID, org.Name) 180 } else { 181 orgs, warnings, err := cmd.Actor.GetOrganizations() 182 cmd.UI.DisplayWarnings(warnings) 183 if err != nil { 184 return err 185 } 186 if len(orgs) == 1 { 187 cmd.Config.SetOrganizationInformation(orgs[0].GUID, orgs[0].Name) 188 } else if len(orgs) > 1 { 189 var emptyOrg v3action.Organization 190 chosenOrg, err := cmd.promptChosenOrg(orgs) 191 if err != nil { 192 return err 193 } 194 if chosenOrg != emptyOrg { 195 cmd.Config.SetOrganizationInformation(chosenOrg.GUID, chosenOrg.Name) 196 } 197 } 198 } 199 200 targetedOrg := cmd.Config.TargetedOrganization() 201 202 if targetedOrg.GUID != "" { 203 cmd.UI.DisplayTextWithFlavor("Targeted org {{.Organization}}", map[string]interface{}{ 204 "Organization": cmd.Config.TargetedOrganizationName(), 205 }) 206 cmd.UI.DisplayNewline() 207 208 if cmd.Space != "" { 209 space, warnings, err := cmd.Actor.GetSpaceByNameAndOrganization(cmd.Space, targetedOrg.GUID) 210 cmd.UI.DisplayWarnings(warnings) 211 if err != nil { 212 return err 213 } 214 cmd.targetSpace(space) 215 } else { 216 spaces, warnings, err := cmd.Actor.GetOrganizationSpaces(targetedOrg.GUID) 217 cmd.UI.DisplayWarnings(warnings) 218 if err != nil { 219 return err 220 } 221 222 if len(spaces) == 1 { 223 cmd.targetSpace(spaces[0]) 224 } else if len(spaces) > 1 { 225 var emptySpace v3action.Space 226 chosenSpace, err := cmd.promptChosenSpace(spaces) 227 if err != nil { 228 return err 229 } 230 if chosenSpace != emptySpace { 231 cmd.targetSpace(chosenSpace) 232 } 233 } 234 } 235 } 236 237 cmd.UI.DisplayNewline() 238 cmd.UI.DisplayNewline() 239 cmd.UI.DisplayNewline() 240 241 return nil 242 } 243 244 func (cmd *LoginCommand) targetSpace(space v3action.Space) { 245 cmd.Config.SetSpaceInformation(space.GUID, space.Name, true) 246 247 cmd.UI.DisplayTextWithFlavor("Targeted space {{.Space}}", map[string]interface{}{ 248 "Space": space.Name, 249 }) 250 } 251 252 func (cmd *LoginCommand) determineAPIEndpoint() (v3action.TargetSettings, error) { 253 var ( 254 endpoint string 255 skipSSLValidation bool 256 ) 257 258 if cmd.APIEndpoint != "" { 259 endpoint = cmd.APIEndpoint 260 skipSSLValidation = cmd.SkipSSLValidation 261 } else if cmd.Config.Target() != "" { 262 endpoint = cmd.Config.Target() 263 skipSSLValidation = cmd.Config.SkipSSLValidation() || cmd.SkipSSLValidation 264 } else { 265 endpoint = "" 266 skipSSLValidation = cmd.SkipSSLValidation 267 } 268 269 if len(endpoint) > 0 { 270 cmd.UI.DisplayTextWithFlavor("API endpoint: {{.APIEndpoint}}", map[string]interface{}{ 271 "APIEndpoint": endpoint, 272 }) 273 } else { 274 var err error 275 endpoint, err = cmd.UI.DisplayTextPrompt("API endpoint") 276 if err != nil { 277 return v3action.TargetSettings{}, err 278 } 279 } 280 281 strippedEndpoint := strings.TrimRight(endpoint, "/") 282 u, _ := url.Parse(strippedEndpoint) 283 if u.Scheme == "" { 284 u.Scheme = "https" 285 } 286 return v3action.TargetSettings{ 287 URL: u.String(), 288 SkipSSLValidation: skipSSLValidation, 289 }, nil 290 } 291 292 func (cmd *LoginCommand) targetAPI(settings v3action.TargetSettings) error { 293 warnings, err := cmd.Actor.SetTarget(settings) 294 cmd.UI.DisplayWarnings(warnings) 295 if err != nil { 296 return err 297 } 298 if strings.HasPrefix(settings.URL, "http:") { 299 cmd.UI.DisplayWarning("Warning: Insecure http API endpoint detected: secure https API endpoints are recommended") 300 } 301 302 return cmd.reloadActors() 303 } 304 305 func (cmd *LoginCommand) authenticate() error { 306 prompts := cmd.Actor.GetLoginPrompts() 307 credentials := make(map[string]string) 308 309 if value, ok := prompts["username"]; ok { 310 if prompts["username"].Type == coreconfig.AuthPromptTypeText && cmd.Username != "" { 311 credentials["username"] = cmd.Username 312 } else { 313 var err error 314 credentials["username"], err = cmd.UI.DisplayTextPrompt(value.DisplayName) 315 if err != nil { 316 return err 317 } 318 cmd.UI.DisplayNewline() 319 } 320 } 321 322 passwordKeys := []string{} 323 for key, prompt := range prompts { 324 if prompt.Type == coreconfig.AuthPromptTypePassword { 325 if key == "passcode" || key == "password" { 326 continue 327 } 328 329 passwordKeys = append(passwordKeys, key) 330 } else if key == "username" { 331 continue 332 } else { 333 var err error 334 credentials[key], err = cmd.UI.DisplayTextPrompt(prompt.DisplayName) 335 if err != nil { 336 return err 337 } 338 cmd.UI.DisplayNewline() 339 } 340 } 341 342 var err error 343 for i := 0; i < maxLoginTries; i++ { 344 var promptedCredentials map[string]string 345 promptedCredentials, err = cmd.passwordPrompts(prompts, credentials, passwordKeys) 346 if err != nil { 347 return err 348 } 349 350 cmd.UI.DisplayText("Authenticating...") 351 352 err = cmd.Actor.Authenticate(promptedCredentials, cmd.Origin, constant.GrantTypePassword) 353 354 if err != nil { 355 cmd.UI.DisplayWarning(translatableerror.ConvertToTranslatableError(err).Error()) 356 cmd.UI.DisplayNewline() 357 358 if _, ok := err.(uaa.AccountLockedError); ok { 359 break 360 } 361 } 362 363 if err == nil { 364 cmd.UI.DisplayOK() 365 break 366 } 367 } 368 if err != nil { 369 return err 370 } 371 return nil 372 } 373 374 func (cmd *LoginCommand) authenticateSSO() error { 375 prompts := cmd.Actor.GetLoginPrompts() 376 credentials := make(map[string]string) 377 378 var err error 379 for i := 0; i < maxLoginTries; i++ { 380 if len(cmd.SSOPasscode) > 0 { 381 credentials["passcode"] = cmd.SSOPasscode 382 cmd.SSOPasscode = "" 383 } else { 384 credentials["passcode"], err = cmd.UI.DisplayPasswordPrompt(prompts["passcode"].DisplayName) 385 if err != nil { 386 return err 387 } 388 } 389 390 credentialsCopy := make(map[string]string, len(credentials)) 391 for k, v := range credentials { 392 credentialsCopy[k] = v 393 } 394 395 cmd.UI.DisplayText("Authenticating...") 396 err = cmd.Actor.Authenticate(credentialsCopy, "", constant.GrantTypePassword) 397 398 if err != nil { 399 cmd.UI.DisplayWarning(translatableerror.ConvertToTranslatableError(err).Error()) 400 cmd.UI.DisplayNewline() 401 } 402 403 if err == nil { 404 cmd.UI.DisplayOK() 405 cmd.UI.DisplayNewline() 406 break 407 } 408 } 409 if err != nil { 410 return err 411 } 412 return nil 413 } 414 415 func (cmd *LoginCommand) checkMinCLIVersion() error { 416 cmd.Config.SetMinCLIVersion(cmd.V2Actor.MinCLIVersion()) 417 return command.WarnIfCLIVersionBelowAPIDefinedMinimum(cmd.Config, cmd.V2Actor.CloudControllerAPIVersion(), cmd.UI) 418 } 419 420 func (cmd *LoginCommand) passwordPrompts(prompts map[string]coreconfig.AuthPrompt, credentials map[string]string, passwordKeys []string) (map[string]string, error) { 421 // ensure that password gets prompted before other codes (eg. mfa code) 422 var err error 423 if passPrompt, ok := prompts["password"]; ok { 424 if cmd.Password != "" { 425 credentials["password"] = cmd.Password 426 cmd.Password = "" 427 } else { 428 credentials["password"], err = cmd.UI.DisplayPasswordPrompt(passPrompt.DisplayName) 429 if err != nil { 430 return nil, err 431 } 432 } 433 } 434 435 for _, key := range passwordKeys { 436 cmd.UI.DisplayNewline() 437 credentials[key], err = cmd.UI.DisplayPasswordPrompt(prompts[key].DisplayName) 438 if err != nil { 439 return nil, err 440 } 441 } 442 443 credentialsCopy := make(map[string]string, len(credentials)) 444 for k, v := range credentials { 445 credentialsCopy[k] = v 446 } 447 448 return credentialsCopy, nil 449 } 450 451 func (cmd *LoginCommand) reloadActors() error { 452 newV2Actor, err := cmd.V2ActorMaker.NewV2Actor(cmd.Config, cmd.UI, true) 453 if err != nil { 454 return err 455 } 456 457 cmd.V2Actor = newV2Actor 458 459 newActor, err := cmd.ActorMaker.NewActor(cmd.Config, cmd.UI, true, cmd.V2Actor.AuthorizationEndpoint()) 460 if err != nil { 461 return err 462 } 463 464 cmd.Actor = newActor 465 466 return nil 467 } 468 469 func (cmd *LoginCommand) showStatus() { 470 tableContent := [][]string{ 471 { 472 cmd.UI.TranslateText("API endpoint:"), 473 cmd.UI.TranslateText("{{.APIEndpoint}} (API version: {{.APIVersion}})", 474 map[string]interface{}{ 475 "APIEndpoint": strings.TrimRight(cmd.Config.Target(), "/"), 476 "APIVersion": cmd.Config.APIVersion(), 477 }), 478 }, 479 } 480 481 user, err := cmd.Config.CurrentUserName() 482 if user == "" || err != nil { 483 cmd.UI.DisplayKeyValueTable("", tableContent, 3) 484 command.DisplayNotLoggedInText(cmd.Config.BinaryName(), cmd.UI) 485 return 486 } 487 tableContent = append(tableContent, []string{cmd.UI.TranslateText("User:"), user}) 488 489 orgName := cmd.Config.TargetedOrganizationName() 490 if orgName == "" { 491 cmd.UI.DisplayKeyValueTable("", tableContent, 3) 492 cmd.displayNotTargetted() 493 return 494 } 495 tableContent = append(tableContent, []string{cmd.UI.TranslateText("Org:"), orgName}) 496 497 spaceName := cmd.Config.TargetedSpace().Name 498 if spaceName == "" { 499 tableContent = append(tableContent, []string{cmd.UI.TranslateText("Space:"), 500 cmd.UI.TranslateText("No space targeted, use '{{.Command}}'", map[string]interface{}{ 501 "Command": fmt.Sprintf("%s target -s SPACE", cmd.Config.BinaryName()), 502 })}) 503 } else { 504 tableContent = append(tableContent, []string{cmd.UI.TranslateText("Space:"), spaceName}) 505 } 506 507 cmd.UI.DisplayKeyValueTable("", tableContent, 3) 508 } 509 510 func (cmd *LoginCommand) displayNotTargetted() { 511 cmd.UI.DisplayText("No org or space targeted, use '{{.CFTargetCommand}} -o ORG -s SPACE'", 512 map[string]interface{}{ 513 "CFTargetCommand": fmt.Sprintf("%s target", cmd.Config.BinaryName()), 514 }, 515 ) 516 } 517 518 func (cmd *LoginCommand) promptChosenOrg(orgs []v3action.Organization) (v3action.Organization, error) { 519 orgNames := make([]string, len(orgs)) 520 for i, org := range orgs { 521 orgNames[i] = org.Name 522 } 523 524 chosenOrgName, err := cmd.promptMenu(orgNames, "Select an org:", "Org") 525 526 if err != nil { 527 if invalidChoice, ok := err.(ui.InvalidChoiceError); ok { 528 return v3action.Organization{}, translatableerror.OrganizationNotFoundError{Name: invalidChoice.Choice} 529 } else if err == io.EOF { 530 return v3action.Organization{}, nil 531 } else { 532 return v3action.Organization{}, err 533 } 534 } 535 536 for _, org := range orgs { 537 if org.Name == chosenOrgName { 538 return org, nil 539 } 540 } 541 542 return v3action.Organization{}, nil 543 } 544 545 func (cmd *LoginCommand) promptChosenSpace(spaces []v3action.Space) (v3action.Space, error) { 546 spaceNames := make([]string, len(spaces)) 547 for i, space := range spaces { 548 spaceNames[i] = space.Name 549 } 550 551 chosenSpaceName, err := cmd.promptMenu(spaceNames, "Select a space:", "Space") 552 if err != nil { 553 if invalidChoice, ok := err.(ui.InvalidChoiceError); ok { 554 return v3action.Space{}, translatableerror.SpaceNotFoundError{Name: invalidChoice.Choice} 555 } else if err == io.EOF { 556 return v3action.Space{}, nil 557 } else { 558 return v3action.Space{}, err 559 } 560 } 561 562 for _, space := range spaces { 563 if space.Name == chosenSpaceName { 564 return space, nil 565 } 566 } 567 return v3action.Space{}, nil 568 } 569 570 func (cmd *LoginCommand) promptMenu(choices []string, text string, prompt string) (string, error) { 571 var ( 572 choice string 573 err error 574 ) 575 576 if len(choices) < 50 { 577 for { 578 cmd.UI.DisplayText(text) 579 choice, err = cmd.UI.DisplayTextMenu(choices, prompt) 580 if err == ui.ErrInvalidIndex { 581 continue 582 } else { 583 break 584 } 585 } 586 } else { 587 cmd.UI.DisplayText(text) 588 cmd.UI.DisplayText("There are too many options to display; please type in the name.") 589 cmd.UI.DisplayNewline() 590 defaultChoice := "enter to skip" 591 choice, err = cmd.UI.DisplayOptionalTextPrompt(defaultChoice, prompt) 592 593 switch { 594 case choice == defaultChoice: 595 return "", nil 596 case !contains(choices, choice): 597 return "", ui.InvalidChoiceError{Choice: choice} 598 } 599 } 600 601 return choice, err 602 } 603 604 func (cmd *LoginCommand) validateFlags() error { 605 if cmd.Origin != "" { 606 if cmd.SSO { 607 return translatableerror.ArgumentCombinationError{ 608 Args: []string{"--sso", "--origin"}, 609 } 610 } 611 if cmd.SSOPasscode != "" { 612 return translatableerror.ArgumentCombinationError{ 613 Args: []string{"--sso-passcode", "--origin"}, 614 } 615 } 616 } 617 if cmd.SSO && cmd.SSOPasscode != "" { 618 return translatableerror.ArgumentCombinationError{ 619 Args: []string{"--sso-passcode", "--sso"}, 620 } 621 } 622 return nil 623 } 624 625 func contains(s []string, v string) bool { 626 for _, x := range s { 627 if x == v { 628 return true 629 } 630 } 631 return false 632 }