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 }