github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/user/login.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package user 5 6 import ( 7 "fmt" 8 "net/http" 9 "os" 10 "strings" 11 12 "github.com/juju/cmd" 13 "github.com/juju/collections/set" 14 "github.com/juju/errors" 15 "github.com/juju/gnuflag" 16 "github.com/juju/httprequest" 17 "gopkg.in/juju/names.v2" 18 19 "github.com/juju/juju/api" 20 apibase "github.com/juju/juju/api/base" 21 "github.com/juju/juju/api/modelmanager" 22 "github.com/juju/juju/apiserver/params" 23 jujucmd "github.com/juju/juju/cmd" 24 "github.com/juju/juju/cmd/juju/common" 25 "github.com/juju/juju/cmd/modelcmd" 26 "github.com/juju/juju/juju" 27 "github.com/juju/juju/jujuclient" 28 ) 29 30 const loginDoc = ` 31 By default, the juju login command logs the user into a controller. 32 The argument to the command can be a public controller 33 host name or alias (see Aliases below). 34 35 If no argument is provided, the controller specified with 36 the -c argument will be used, or the current controller 37 if that's not provided. 38 39 On success, the current controller is switched to the logged-in 40 controller. 41 42 If the user is already logged in, the juju login command does nothing 43 except verify that fact. 44 45 If the -u option is provided, the juju login command will attempt to log 46 into the controller as that user. 47 48 After login, a token ("macaroon") will become active. It has an expiration 49 time of 24 hours. Upon expiration, no further Juju commands can be issued 50 and the user will be prompted to log in again. 51 52 Aliases 53 ------- 54 55 Public controller aliases are provided by a directory service 56 that is queried to find the host name for a given alias. 57 The URL for the directory service may be configured 58 by setting the environment variable JUJU_DIRECTORY. 59 60 Examples: 61 62 juju login somepubliccontroller 63 juju login jimm.jujucharms.com 64 juju login -u bob 65 66 See also: 67 disable-user 68 enable-user 69 logout 70 register 71 unregister 72 ` 73 74 // Functions defined as variables so they can be overridden in tests. 75 var ( 76 apiOpen = (*modelcmd.CommandBase).APIOpen 77 newAPIConnection = juju.NewAPIConnection 78 listModels = func(c api.Connection, userName string) ([]apibase.UserModel, error) { 79 return modelmanager.NewClient(c).ListModels(userName) 80 } 81 // loginClientStore is used as the client store. When it is nil, 82 // the default client store will be used. 83 loginClientStore jujuclient.ClientStore 84 ) 85 86 // NewLoginCommand returns a new cmd.Command to handle "juju login". 87 func NewLoginCommand() cmd.Command { 88 var c loginCommand 89 c.SetClientStore(loginClientStore) 90 c.CanClearCurrentModel = true 91 return modelcmd.WrapController(&c, modelcmd.WrapControllerSkipControllerFlags) 92 } 93 94 // loginCommand changes the password for a user. 95 type loginCommand struct { 96 modelcmd.ControllerCommandBase 97 domain string 98 username string 99 100 // controllerName holds the name of the current controller. 101 // We define this and the --controller flag here because 102 // the controller does not necessarily exist when the command 103 // is executed. 104 controllerName string 105 106 // onRunError is executed if non-nil if there is an error at the end 107 // of the Run method. 108 onRunError func() 109 } 110 111 // Info implements Command.Info. 112 func (c *loginCommand) Info() *cmd.Info { 113 return jujucmd.Info(&cmd.Info{ 114 Name: "login", 115 Args: "[controller host name or alias]", 116 Purpose: "Logs a user in to a controller.", 117 Doc: loginDoc, 118 }) 119 } 120 121 func (c *loginCommand) SetFlags(fset *gnuflag.FlagSet) { 122 c.ControllerCommandBase.SetFlags(fset) 123 fset.StringVar(&c.controllerName, "c", "", "Controller to operate in") 124 fset.StringVar(&c.controllerName, "controller", "", "") 125 fset.StringVar(&c.username, "u", "", "log in as this local user") 126 fset.StringVar(&c.username, "user", "", "") 127 } 128 129 // Init implements Command.Init. 130 func (c *loginCommand) Init(args []string) error { 131 domain, err := cmd.ZeroOrOneArgs(args) 132 if err != nil { 133 return errors.Trace(err) 134 } 135 c.domain = domain 136 return nil 137 } 138 139 // Run implements Command.Run. 140 func (c *loginCommand) Run(ctx *cmd.Context) error { 141 err := c.run(ctx) 142 if err != nil && c.onRunError != nil { 143 c.onRunError() 144 } 145 return err 146 } 147 148 func (c *loginCommand) run(ctx *cmd.Context) error { 149 store := c.ClientStore() 150 switch { 151 case c.controllerName == "" && c.domain == "": 152 current, err := store.CurrentController() 153 if err != nil && !errors.IsNotFound(err) { 154 return errors.Annotatef(err, "cannot get current controller") 155 } 156 c.controllerName = current 157 case c.controllerName == "": 158 c.controllerName = c.domain 159 } 160 if strings.Contains(c.controllerName, ":") { 161 return errors.Errorf("cannot use %q as a controller name - use -c option to choose a different one", c.controllerName) 162 } 163 164 // Find out details on the specified controller if there is one. 165 var controllerDetails *jujuclient.ControllerDetails 166 if c.controllerName != "" { 167 d, err := store.ControllerByName(c.controllerName) 168 if err != nil && !errors.IsNotFound(err) { 169 return errors.Trace(err) 170 } 171 controllerDetails = d 172 } 173 174 // Find out details of the controller domain if it's specified. 175 var ( 176 conn api.Connection 177 publicControllerDetails *jujuclient.ControllerDetails 178 accountDetails *jujuclient.AccountDetails 179 oldAccountDetails *jujuclient.AccountDetails 180 err error 181 ) 182 if controllerDetails != nil { 183 // Fetch current details for the specified controller name so we 184 // can tell if the logged in user has changed. 185 d, err := store.AccountDetails(c.controllerName) 186 if err != nil && !errors.IsNotFound(err) { 187 return errors.Trace(err) 188 } 189 oldAccountDetails = d 190 } 191 switch { 192 case c.domain != "": 193 // Note: the controller name is guaranteed to be non-empty 194 // in this case via the test at the start of this function. 195 conn, publicControllerDetails, accountDetails, err = c.publicControllerLogin(ctx, c.domain, c.controllerName, oldAccountDetails) 196 if err != nil { 197 return errors.Annotatef(err, "cannot log into %q", c.domain) 198 } 199 case controllerDetails == nil && c.controllerName != "": 200 // No controller found and no domain specified - we 201 // have no idea where we should be logging in. 202 return errors.Errorf("controller %q does not exist", c.controllerName) 203 case controllerDetails == nil: 204 return errors.Errorf("no current controller") 205 default: 206 conn, accountDetails, err = c.existingControllerLogin(ctx, store, c.controllerName, oldAccountDetails) 207 if err != nil { 208 return errors.Annotatef(err, "cannot log into controller %q", c.controllerName) 209 } 210 } 211 defer conn.Close() 212 if controllerDetails != nil && publicControllerDetails != nil && controllerDetails.ControllerUUID != publicControllerDetails.ControllerUUID { 213 // The domain we're trying to log into doesn't match the 214 // existing controller. 215 return errors.Errorf(` 216 controller at %q does not match existing controller. 217 Please choose a different controller name with the -c option, or 218 use "juju unregister %s" to remove the existing controller.`[1:], c.domain, c.controllerName) 219 } 220 if controllerDetails == nil { 221 // The controller did not exist previously, so create it. 222 // Note that the "controllerDetails == nil" 223 // test above means that we will always have a valid publicControllerDetails 224 // value here. 225 if err := store.AddController(c.controllerName, *publicControllerDetails); err != nil { 226 return errors.Trace(err) 227 } 228 } 229 accountDetails.LastKnownAccess = conn.ControllerAccess() 230 if err := store.UpdateAccount(c.controllerName, *accountDetails); err != nil { 231 return errors.Annotatef(err, "cannot update account information: %v", err) 232 } 233 if err := store.SetCurrentController(c.controllerName); err != nil { 234 return errors.Annotatef(err, "cannot switch") 235 } 236 if controllerDetails != nil && oldAccountDetails != nil && oldAccountDetails.User == accountDetails.User { 237 // We're still using the same controller and the same user name, 238 // so no need to list models or set the current controller 239 return nil 240 } 241 // Now list the models available so we can show them and store their 242 // details locally. 243 models, err := listModels(conn, accountDetails.User) 244 if err != nil { 245 return errors.Trace(err) 246 } 247 if err := c.SetControllerModels(store, c.controllerName, models); err != nil { 248 return errors.Annotate(err, "storing model details") 249 } 250 fmt.Fprintf( 251 ctx.Stderr, "Welcome, %s. You are now logged into %q.\n", 252 friendlyUserName(accountDetails.User), c.controllerName, 253 ) 254 return c.maybeSetCurrentModel(ctx, store, c.controllerName, accountDetails.User, models) 255 } 256 257 func (c *loginCommand) existingControllerLogin(ctx *cmd.Context, store jujuclient.ClientStore, controllerName string, currentAccountDetails *jujuclient.AccountDetails) (api.Connection, *jujuclient.AccountDetails, error) { 258 dial := func(accountDetails *jujuclient.AccountDetails) (api.Connection, error) { 259 args, err := c.NewAPIConnectionParams(store, controllerName, "", accountDetails) 260 if err != nil { 261 return nil, errors.Trace(err) 262 } 263 return newAPIConnection(args) 264 } 265 return c.login(ctx, currentAccountDetails, dial) 266 } 267 268 // publicControllerLogin logs into the public controller at the given 269 // host. The currentAccountDetails parameter holds existing account 270 // information about the controller account. 271 func (c *loginCommand) publicControllerLogin( 272 ctx *cmd.Context, 273 host string, 274 controllerName string, 275 currentAccountDetails *jujuclient.AccountDetails, 276 ) (api.Connection, *jujuclient.ControllerDetails, *jujuclient.AccountDetails, error) { 277 fail := func(err error) (api.Connection, *jujuclient.ControllerDetails, *jujuclient.AccountDetails, error) { 278 return nil, nil, nil, err 279 } 280 if !strings.ContainsAny(host, ".:") { 281 host1, err := c.getKnownControllerDomain(host, controllerName) 282 if errors.IsNotFound(err) { 283 return fail(errors.Errorf("%q is not a known public controller", host)) 284 } 285 if err != nil { 286 return fail(errors.Annotatef(err, "could not determine controller host name")) 287 } 288 host = host1 289 } else if !strings.Contains(host, ":") { 290 host += ":443" 291 } 292 293 // Make a direct API connection because we don't yet know the 294 // controller UUID so can't store the thus-incomplete controller 295 // details to make a conventional connection. 296 // 297 // Unfortunately this means we'll connect twice to the controller 298 // but it's probably best to go through the conventional path the 299 // second time. 300 bclient, err := c.CommandBase.BakeryClient(c.ClientStore(), controllerName) 301 if err != nil { 302 return fail(errors.Trace(err)) 303 } 304 dialOpts := api.DefaultDialOpts() 305 dialOpts.BakeryClient = bclient 306 307 dial := func(d *jujuclient.AccountDetails) (api.Connection, error) { 308 var tag names.Tag 309 if d.User != "" { 310 tag = names.NewUserTag(d.User) 311 } 312 return apiOpen(&c.CommandBase, &api.Info{ 313 Tag: tag, 314 Password: d.Password, 315 Addrs: []string{host}, 316 }, dialOpts) 317 } 318 conn, accountDetails, err := c.login(ctx, currentAccountDetails, dial) 319 if err != nil { 320 return fail(errors.Trace(err)) 321 } 322 // If we get to here, then we have a cached macaroon for the registered 323 // user. If we encounter an error after here, we need to clear it. 324 c.onRunError = func() { 325 if err := c.ClearControllerMacaroons(c.ClientStore(), controllerName); err != nil { 326 logger.Errorf("failed to clear macaroon: %v", err) 327 } 328 } 329 return conn, 330 &jujuclient.ControllerDetails{ 331 APIEndpoints: []string{host}, 332 ControllerUUID: conn.ControllerTag().Id(), 333 }, accountDetails, nil 334 } 335 336 // login logs into a controller using the given account details by 337 // default, but falling back to prompting for a username and password if 338 // necessary. The details of making an API connection are abstracted out 339 // into the dial function because we need to dial differently depending 340 // on whether we have some existing local controller information or not. 341 // 342 // The dial function should make API connection using the account 343 // details that it is passed. 344 func (c *loginCommand) login( 345 ctx *cmd.Context, 346 accountDetails *jujuclient.AccountDetails, 347 dial func(*jujuclient.AccountDetails) (api.Connection, error), 348 ) (api.Connection, *jujuclient.AccountDetails, error) { 349 username := c.username 350 if c.username != "" && accountDetails != nil && accountDetails.User != c.username { 351 // The user has specified a different username than the 352 // user we've found in the controller's account details. 353 return nil, nil, errors.Errorf(`already logged in as %s. 354 355 Run "juju logout" first before attempting to log in as a different user.`, 356 accountDetails.User) 357 } 358 359 if accountDetails != nil && accountDetails.Password != "" { 360 // We've been provided some account details that 361 // contain a password, so try that first. 362 conn, err := dial(accountDetails) 363 if err == nil { 364 return conn, accountDetails, nil 365 } 366 if !errors.IsUnauthorized(err) { 367 return nil, nil, errors.Trace(err) 368 } 369 } 370 if c.username == "" { 371 // No username specified, so try external-user login first. 372 conn, err := dial(&jujuclient.AccountDetails{}) 373 if err == nil { 374 user, ok := conn.AuthTag().(names.UserTag) 375 if !ok { 376 conn.Close() 377 return nil, nil, errors.Errorf("logged in as %v, not a user", conn.AuthTag()) 378 } 379 return conn, &jujuclient.AccountDetails{ 380 User: user.Id(), 381 }, nil 382 } 383 if !params.IsCodeNoCreds(err) { 384 return nil, nil, errors.Trace(err) 385 } 386 // CodeNoCreds was returned, which means that external 387 // users are not supported. Fall back to prompting the 388 // user for their username and password. 389 390 fmt.Fprint(ctx.Stderr, "username: ") 391 u, err := readLine(ctx.Stdin) 392 if err != nil { 393 return nil, nil, errors.Trace(err) 394 } 395 if u == "" { 396 return nil, nil, errors.Errorf("you must specify a username") 397 } 398 username = u 399 } 400 // Log in without specifying a password in the account details. This 401 // will trigger macaroon-based authentication, which will prompt the 402 // user for their password. 403 accountDetails = &jujuclient.AccountDetails{ 404 User: username, 405 } 406 conn, err := dial(accountDetails) 407 return conn, accountDetails, errors.Trace(err) 408 } 409 410 const noModelsMessage = ` 411 There are no models available. You can add models with 412 "juju add-model", or you can ask an administrator or owner 413 of a model to grant access to that model with "juju grant". 414 ` 415 416 func (c *loginCommand) maybeSetCurrentModel(ctx *cmd.Context, store jujuclient.ClientStore, controllerName, userName string, models []apibase.UserModel) error { 417 if len(models) == 0 { 418 fmt.Fprint(ctx.Stderr, noModelsMessage) 419 return nil 420 } 421 422 // If we get to here, there is at least one model. 423 if len(models) == 1 { 424 // There is exactly one model shared, 425 // so set it as the current model. 426 model := models[0] 427 owner := names.NewUserTag(model.Owner) 428 modelName := jujuclient.JoinOwnerModelName(owner, model.Name) 429 err := store.SetCurrentModel(controllerName, modelName) 430 if err != nil { 431 return errors.Trace(err) 432 } 433 fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q.\n", modelName) 434 return nil 435 } 436 fmt.Fprintf(ctx.Stderr, ` 437 There are %d models available. Use "juju switch" to select 438 one of them: 439 `, len(models)) 440 user := names.NewUserTag(userName) 441 ownerModelNames := make(set.Strings) 442 otherModelNames := make(set.Strings) 443 for _, model := range models { 444 if model.Owner == userName { 445 ownerModelNames.Add(model.Name) 446 continue 447 } 448 owner := names.NewUserTag(model.Owner) 449 modelName := common.OwnerQualifiedModelName(model.Name, owner, user) 450 otherModelNames.Add(modelName) 451 } 452 for _, modelName := range ownerModelNames.SortedValues() { 453 fmt.Fprintf(ctx.Stderr, " - juju switch %s\n", modelName) 454 } 455 for _, modelName := range otherModelNames.SortedValues() { 456 fmt.Fprintf(ctx.Stderr, " - juju switch %s\n", modelName) 457 } 458 return nil 459 } 460 461 type controllerDomainResponse struct { 462 Host string `json:"host"` 463 } 464 465 const defaultJujuDirectory = "https://api.jujucharms.com/directory" 466 467 // getKnownControllerDomain returns the list of known 468 // controller domain aliases. 469 func (c *loginCommand) getKnownControllerDomain(name, controllerName string) (string, error) { 470 if strings.Contains(name, ".") || strings.Contains(name, ":") { 471 return "", errors.NotFoundf("controller %q", name) 472 } 473 baseURL := defaultJujuDirectory 474 if u := os.Getenv("JUJU_DIRECTORY"); u != "" { 475 baseURL = u 476 } 477 client, err := c.CommandBase.BakeryClient(c.ClientStore(), controllerName) 478 if err != nil { 479 return "", errors.Trace(err) 480 } 481 req, err := http.NewRequest("GET", baseURL+"/v1/controller/"+name, nil) 482 if err != nil { 483 return "", errors.Trace(err) 484 } 485 httpResp, err := client.Do(req) 486 if err != nil { 487 return "", errors.Trace(err) 488 } 489 defer httpResp.Body.Close() 490 if httpResp.StatusCode != http.StatusOK { 491 if httpResp.StatusCode == http.StatusNotFound { 492 return "", errors.NotFoundf("controller %q", name) 493 } 494 return "", errors.Errorf("unexpected HTTP response %q", httpResp.Status) 495 } 496 var resp controllerDomainResponse 497 if err := httprequest.UnmarshalJSONResponse(httpResp, &resp); err != nil { 498 return "", errors.Trace(err) 499 } 500 if resp.Host == "" { 501 return "", errors.Errorf("no host field found in response") 502 } 503 return resp.Host, nil 504 } 505 506 func friendlyUserName(user string) string { 507 u := names.NewUserTag(user) 508 if u.IsLocal() { 509 return u.Name() 510 } 511 return u.Id() 512 }