github.com/willmadison/cli@v6.40.1-0.20181018160101-29d5937903ff+incompatible/cf/commands/login.go (about) 1 package commands 2 3 import ( 4 "errors" 5 "strconv" 6 7 "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" 8 "code.cloudfoundry.org/cli/cf/commandregistry" 9 "code.cloudfoundry.org/cli/cf/flags" 10 . "code.cloudfoundry.org/cli/cf/i18n" 11 "code.cloudfoundry.org/cli/command" 12 "code.cloudfoundry.org/cli/command/translatableerror" 13 14 "code.cloudfoundry.org/cli/cf/api/authentication" 15 "code.cloudfoundry.org/cli/cf/api/organizations" 16 "code.cloudfoundry.org/cli/cf/api/spaces" 17 "code.cloudfoundry.org/cli/cf/configuration/coreconfig" 18 "code.cloudfoundry.org/cli/cf/models" 19 "code.cloudfoundry.org/cli/cf/requirements" 20 "code.cloudfoundry.org/cli/cf/terminal" 21 ) 22 23 const maxLoginTries = 3 24 const maxChoices = 50 25 26 type Login struct { 27 ui terminal.UI 28 config coreconfig.ReadWriter 29 authenticator authentication.Repository 30 endpointRepo coreconfig.EndpointRepository 31 orgRepo organizations.OrganizationRepository 32 spaceRepo spaces.SpaceRepository 33 } 34 35 func init() { 36 commandregistry.Register(&Login{}) 37 } 38 39 func (cmd *Login) MetaData() commandregistry.CommandMetadata { 40 fs := make(map[string]flags.FlagSet) 41 fs["a"] = &flags.StringFlag{ShortName: "a", Usage: T("API endpoint (e.g. https://api.example.com)")} 42 fs["u"] = &flags.StringFlag{ShortName: "u", Usage: T("Username")} 43 fs["p"] = &flags.StringFlag{ShortName: "p", Usage: T("Password")} 44 fs["o"] = &flags.StringFlag{ShortName: "o", Usage: T("Org")} 45 fs["s"] = &flags.StringFlag{ShortName: "s", Usage: T("Space")} 46 fs["sso"] = &flags.BoolFlag{Name: "sso", Usage: T("Prompt for a one-time passcode to login")} 47 fs["sso-passcode"] = &flags.StringFlag{Name: "sso-passcode", Usage: T("One-time passcode")} 48 fs["skip-ssl-validation"] = &flags.BoolFlag{Name: "skip-ssl-validation", Usage: T("Skip verification of the API endpoint. Not recommended!")} 49 50 return commandregistry.CommandMetadata{ 51 Name: "login", 52 ShortName: "l", 53 Description: T("Log user in"), 54 Usage: []string{ 55 T("CF_NAME login [-a API_URL] [-u USERNAME] [-p PASSWORD] [-o ORG] [-s SPACE] [--sso | --sso-passcode PASSCODE]\n\n"), 56 terminal.WarningColor(T("WARNING:\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")), 57 }, 58 Examples: []string{ 59 T("CF_NAME login (omit username and password to login interactively -- CF_NAME will prompt for both)"), 60 T("CF_NAME login -u name@example.com -p pa55woRD (specify username and password as arguments)"), 61 T("CF_NAME login -u name@example.com -p \"my password\" (use quotes for passwords with a space)"), 62 T("CF_NAME login -u name@example.com -p \"\\\"password\\\"\" (escape quotes if used in password)"), 63 T("CF_NAME login --sso (CF_NAME will provide a url to obtain a one-time passcode to login)"), 64 }, 65 Flags: fs, 66 } 67 } 68 69 func (cmd *Login) Requirements(requirementsFactory requirements.Factory, fc flags.FlagContext) ([]requirements.Requirement, error) { 70 reqs := []requirements.Requirement{} 71 return reqs, nil 72 } 73 74 func (cmd *Login) SetDependency(deps commandregistry.Dependency, pluginCall bool) commandregistry.Command { 75 cmd.ui = deps.UI 76 cmd.config = deps.Config 77 cmd.authenticator = deps.RepoLocator.GetAuthenticationRepository() 78 cmd.endpointRepo = deps.RepoLocator.GetEndpointRepository() 79 cmd.orgRepo = deps.RepoLocator.GetOrganizationRepository() 80 cmd.spaceRepo = deps.RepoLocator.GetSpaceRepository() 81 return cmd 82 } 83 84 func (cmd *Login) Execute(c flags.FlagContext) error { 85 cmd.config.ClearSession() 86 87 endpoint, skipSSL := cmd.decideEndpoint(c) 88 89 api := API{ 90 ui: cmd.ui, 91 config: cmd.config, 92 endpointRepo: cmd.endpointRepo, 93 } 94 err := api.setAPIEndpoint(endpoint, skipSSL, cmd.MetaData().Name) 95 if err != nil { 96 return err 97 } 98 99 err = command.MinimumCCAPIVersionCheck(cmd.config.APIVersion(), ccversion.MinV2ClientVersion) 100 if err != nil { 101 if _, ok := err.(translatableerror.MinimumCFAPIVersionNotMetError); ok { 102 cmd.ui.Warn("Your API version is no longer supported. Upgrade to a newer version of the API.") 103 } else { 104 return err 105 } 106 } 107 108 defer func() { 109 cmd.ui.Say("") 110 cmd.ui.ShowConfiguration(cmd.config) 111 }() 112 113 // We thought we would never need to explicitly branch in this code 114 // for anything as simple as authentication, but it turns out that our 115 // assumptions did not match reality. 116 117 // When SAML is enabled (but not configured) then the UAA/Login server 118 // will always returns password prompts that includes the Passcode field. 119 // Users can authenticate with: 120 // EITHER username and password 121 // OR a one-time passcode 122 123 switch { 124 case c.Bool("sso") && c.IsSet("sso-passcode"): 125 return errors.New(T("Incorrect usage: --sso-passcode flag cannot be used with --sso")) 126 case c.Bool("sso") || c.IsSet("sso-passcode"): 127 err = cmd.authenticateSSO(c) 128 if err != nil { 129 return err 130 } 131 default: 132 err = cmd.authenticate(c) 133 if err != nil { 134 return err 135 } 136 } 137 138 orgIsSet, err := cmd.setOrganization(c) 139 if err != nil { 140 return err 141 } 142 143 if orgIsSet { 144 err = cmd.setSpace(c) 145 if err != nil { 146 return err 147 } 148 } 149 cmd.ui.NotifyUpdateIfNeeded(cmd.config) 150 return nil 151 } 152 153 func (cmd Login) decideEndpoint(c flags.FlagContext) (string, bool) { 154 endpoint := c.String("a") 155 skipSSL := c.Bool("skip-ssl-validation") 156 if endpoint == "" { 157 endpoint = cmd.config.APIEndpoint() 158 skipSSL = cmd.config.IsSSLDisabled() || skipSSL 159 } 160 161 if endpoint == "" { 162 endpoint = cmd.ui.Ask(T("API endpoint")) 163 } else { 164 cmd.ui.Say(T("API endpoint: {{.Endpoint}}", map[string]interface{}{"Endpoint": terminal.EntityNameColor(endpoint)})) 165 } 166 167 return endpoint, skipSSL 168 } 169 170 func (cmd Login) authenticateSSO(c flags.FlagContext) error { 171 prompts, err := cmd.authenticator.GetLoginPromptsAndSaveUAAServerURL() 172 if err != nil { 173 return err 174 } 175 176 credentials := make(map[string]string) 177 passcode := prompts["passcode"] 178 179 for i := 0; i < maxLoginTries; i++ { 180 if c.IsSet("sso-passcode") && i == 0 { 181 credentials["passcode"] = c.String("sso-passcode") 182 } else { 183 credentials["passcode"] = cmd.ui.AskForPassword(passcode.DisplayName) 184 } 185 186 cmd.ui.Say(T("Authenticating...")) 187 err = cmd.authenticator.Authenticate(credentials) 188 189 if err == nil { 190 cmd.ui.Ok() 191 cmd.ui.Say("") 192 break 193 } 194 195 cmd.ui.Say(err.Error()) 196 } 197 198 if err != nil { 199 return errors.New(T("Unable to authenticate.")) 200 } 201 return nil 202 } 203 204 func (cmd Login) authenticate(c flags.FlagContext) error { 205 if cmd.config.UAAGrantType() == "client_credentials" { 206 return errors.New(T("Service account currently logged in. Use 'cf logout' to log out service account and try again.")) 207 } 208 209 usernameFlagValue := c.String("u") 210 passwordFlagValue := c.String("p") 211 212 prompts, err := cmd.authenticator.GetLoginPromptsAndSaveUAAServerURL() 213 if err != nil { 214 return err 215 } 216 passwordKeys := []string{} 217 credentials := make(map[string]string) 218 219 if value, ok := prompts["username"]; ok { 220 if prompts["username"].Type == coreconfig.AuthPromptTypeText && usernameFlagValue != "" { 221 credentials["username"] = usernameFlagValue 222 } else { 223 credentials["username"] = cmd.ui.Ask(value.DisplayName) 224 } 225 } 226 227 for key, prompt := range prompts { 228 if prompt.Type == coreconfig.AuthPromptTypePassword { 229 if key == "passcode" || key == "password" { 230 continue 231 } 232 233 passwordKeys = append(passwordKeys, key) 234 } else if key == "username" { 235 continue 236 } else { 237 credentials[key] = cmd.ui.Ask(prompt.DisplayName) 238 } 239 } 240 241 for i := 0; i < maxLoginTries; i++ { 242 243 // ensure that password gets prompted before other codes (eg. mfa code) 244 if passPrompt, ok := prompts["password"]; ok { 245 if passwordFlagValue != "" { 246 credentials["password"] = passwordFlagValue 247 passwordFlagValue = "" 248 } else { 249 credentials["password"] = cmd.ui.AskForPassword(passPrompt.DisplayName) 250 } 251 } 252 253 for _, key := range passwordKeys { 254 credentials[key] = cmd.ui.AskForPassword(prompts[key].DisplayName) 255 } 256 257 credentialsCopy := make(map[string]string, len(credentials)) 258 for k, v := range credentials { 259 credentialsCopy[k] = v 260 } 261 262 cmd.ui.Say(T("Authenticating...")) 263 err = cmd.authenticator.Authenticate(credentialsCopy) 264 265 if err == nil { 266 cmd.ui.Ok() 267 cmd.ui.Say("") 268 break 269 } 270 271 cmd.ui.Say(err.Error()) 272 } 273 274 if err != nil { 275 return errors.New(T("Unable to authenticate.")) 276 } 277 return nil 278 } 279 280 func (cmd Login) setOrganization(c flags.FlagContext) (bool, error) { 281 orgName := c.String("o") 282 283 if orgName == "" { 284 orgs, err := cmd.orgRepo.ListOrgs(maxChoices) 285 if err != nil { 286 return false, errors.New(T("Error finding available orgs\n{{.APIErr}}", 287 map[string]interface{}{"APIErr": err.Error()})) 288 } 289 290 switch len(orgs) { 291 case 0: 292 return false, nil 293 case 1: 294 cmd.targetOrganization(orgs[0]) 295 return true, nil 296 default: 297 orgName = cmd.promptForOrgName(orgs) 298 if orgName == "" { 299 cmd.ui.Say("") 300 return false, nil 301 } 302 } 303 } 304 305 org, err := cmd.orgRepo.FindByName(orgName) 306 if err != nil { 307 return false, errors.New(T("Error finding org {{.OrgName}}\n{{.Err}}", 308 map[string]interface{}{"OrgName": terminal.EntityNameColor(orgName), "Err": err.Error()})) 309 } 310 311 cmd.targetOrganization(org) 312 return true, nil 313 } 314 315 func (cmd Login) promptForOrgName(orgs []models.Organization) string { 316 orgNames := []string{} 317 for _, org := range orgs { 318 orgNames = append(orgNames, org.Name) 319 } 320 321 return cmd.promptForName(orgNames, T("Select an org (or press enter to skip):"), "Org") 322 } 323 324 func (cmd Login) targetOrganization(org models.Organization) { 325 cmd.config.SetOrganizationFields(org.OrganizationFields) 326 cmd.ui.Say(T("Targeted org {{.OrgName}}\n", 327 map[string]interface{}{"OrgName": terminal.EntityNameColor(org.Name)})) 328 } 329 330 func (cmd Login) setSpace(c flags.FlagContext) error { 331 spaceName := c.String("s") 332 333 if spaceName == "" { 334 var availableSpaces []models.Space 335 err := cmd.spaceRepo.ListSpaces(func(space models.Space) bool { 336 availableSpaces = append(availableSpaces, space) 337 return (len(availableSpaces) < maxChoices) 338 }) 339 if err != nil { 340 return errors.New(T("Error finding available spaces\n{{.Err}}", 341 map[string]interface{}{"Err": err.Error()})) 342 } 343 344 if len(availableSpaces) == 0 { 345 return nil 346 } else if len(availableSpaces) == 1 { 347 cmd.targetSpace(availableSpaces[0]) 348 return nil 349 } else { 350 spaceName = cmd.promptForSpaceName(availableSpaces) 351 if spaceName == "" { 352 cmd.ui.Say("") 353 return nil 354 } 355 } 356 } 357 358 space, err := cmd.spaceRepo.FindByName(spaceName) 359 if err != nil { 360 return errors.New(T("Error finding space {{.SpaceName}}\n{{.Err}}", 361 map[string]interface{}{"SpaceName": terminal.EntityNameColor(spaceName), "Err": err.Error()})) 362 } 363 364 cmd.targetSpace(space) 365 return nil 366 } 367 368 func (cmd Login) promptForSpaceName(spaces []models.Space) string { 369 spaceNames := []string{} 370 for _, space := range spaces { 371 spaceNames = append(spaceNames, space.Name) 372 } 373 374 return cmd.promptForName(spaceNames, T("Select a space (or press enter to skip):"), "Space") 375 } 376 377 func (cmd Login) targetSpace(space models.Space) { 378 cmd.config.SetSpaceFields(space.SpaceFields) 379 cmd.ui.Say(T("Targeted space {{.SpaceName}}\n", 380 map[string]interface{}{"SpaceName": terminal.EntityNameColor(space.Name)})) 381 } 382 383 func (cmd Login) promptForName(names []string, listPrompt, itemPrompt string) string { 384 nameIndex := 0 385 var nameString string 386 for nameIndex < 1 || nameIndex > len(names) { 387 var err error 388 389 // list header 390 cmd.ui.Say(listPrompt) 391 392 // only display list if it is shorter than maxChoices 393 if len(names) < maxChoices { 394 for i, name := range names { 395 cmd.ui.Say("%d. %s", i+1, name) 396 } 397 } else { 398 cmd.ui.Say(T("There are too many options to display, please type in the name.")) 399 } 400 401 nameString = cmd.ui.Ask(itemPrompt) 402 if nameString == "" { 403 return "" 404 } 405 406 nameIndex, err = strconv.Atoi(nameString) 407 408 if err != nil { 409 nameIndex = 1 410 return nameString 411 } 412 } 413 414 return names[nameIndex-1] 415 }