github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/controller/register.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package controller 5 6 import ( 7 "bufio" 8 "bytes" 9 "crypto/rand" 10 "encoding/asn1" 11 "encoding/base64" 12 "encoding/json" 13 "fmt" 14 "io" 15 "net/http" 16 "os" 17 "strings" 18 19 "github.com/juju/cmd" 20 "github.com/juju/errors" 21 "github.com/juju/utils" 22 "github.com/juju/utils/set" 23 "golang.org/x/crypto/nacl/secretbox" 24 "golang.org/x/crypto/ssh/terminal" 25 "gopkg.in/juju/names.v2" 26 27 "github.com/juju/juju/api" 28 "github.com/juju/juju/api/base" 29 "github.com/juju/juju/api/modelmanager" 30 "github.com/juju/juju/apiserver/params" 31 "github.com/juju/juju/cmd/juju/common" 32 "github.com/juju/juju/cmd/modelcmd" 33 "github.com/juju/juju/jujuclient" 34 "github.com/juju/juju/permission" 35 ) 36 37 var noModelsMessage = ` 38 There are no models available. You can add models with 39 "juju add-model", or you can ask an administrator or owner 40 of a model to grant access to that model with "juju grant". 41 ` 42 43 // NewRegisterCommand returns a command to allow the user to register a controller. 44 func NewRegisterCommand() cmd.Command { 45 c := ®isterCommand{} 46 c.apiOpen = c.APIOpen 47 c.listModelsFunc = c.listModels 48 c.store = jujuclient.NewFileClientStore() 49 return modelcmd.WrapBase(c) 50 } 51 52 // registerCommand logs in to a Juju controller and caches the connection 53 // information. 54 type registerCommand struct { 55 modelcmd.JujuCommandBase 56 apiOpen api.OpenFunc 57 listModelsFunc func(_ jujuclient.ClientStore, controller, user string) ([]base.UserModel, error) 58 store jujuclient.ClientStore 59 Arg string 60 61 // onRunError is executed if non-nil if there is an error at the end 62 // of the Run method. 63 onRunError func() 64 } 65 66 var usageRegisterSummary = ` 67 Registers a controller.`[1:] 68 69 var usageRegisterDetails = ` 70 The register command adds details of a controller to the local system. 71 This is done either by completing the user registration process that 72 began with the 'juju add-user' command, or by providing the DNS host 73 name of a public controller. 74 75 To complete the user registration process, you should have been provided 76 with a base64-encoded blob of data (the output of 'juju add-user') 77 which can be copied and pasted as the <string> argument to 'register'. 78 You will be prompted for a password, which, once set, causes the 79 registration string to be voided. In order to start using Juju the user 80 can now either add a model or wait for a model to be shared with them. 81 Some machine providers will require the user to be in possession of 82 certain credentials in order to add a model. 83 84 When adding a controller at a public address, authentication via some 85 external third party (for example Ubuntu SSO) will be required, usually 86 by using a web browser. 87 88 Examples: 89 90 juju register MFATA3JvZDAnExMxMDQuMTU0LjQyLjQ0OjE3MDcwExAxMC4xMjguMC4yOjE3MDcwBCBEFCaXerhNImkKKabuX5ULWf2Bp4AzPNJEbXVWgraLrAA= 91 92 juju register public-controller.example.com 93 94 See also: 95 add-user 96 change-user-password 97 unregister` 98 99 // Info implements Command.Info 100 // `register` may seem generic, but is seen as simple and without potential 101 // naming collisions in any current or planned features. 102 func (c *registerCommand) Info() *cmd.Info { 103 return &cmd.Info{ 104 Name: "register", 105 Args: "<registration string>|<controller host name>", 106 Purpose: usageRegisterSummary, 107 Doc: usageRegisterDetails, 108 } 109 } 110 111 // SetFlags implements Command.Init. 112 func (c *registerCommand) Init(args []string) error { 113 if len(args) < 1 { 114 return errors.New("registration data missing") 115 } 116 c.Arg, args = args[0], args[1:] 117 if err := cmd.CheckEmpty(args); err != nil { 118 return errors.Trace(err) 119 } 120 return nil 121 } 122 123 // Run implements Command.Run. 124 func (c *registerCommand) Run(ctx *cmd.Context) error { 125 err := c.run(ctx) 126 if err != nil && c.onRunError != nil { 127 c.onRunError() 128 } 129 return err 130 } 131 132 func (c *registerCommand) run(ctx *cmd.Context) error { 133 store := modelcmd.QualifyingClientStore{c.store} 134 registrationParams, err := c.getParameters(ctx, store) 135 if err != nil { 136 return errors.Trace(err) 137 } 138 controllerDetails, accountDetails, err := c.controllerDetails(ctx, registrationParams) 139 if err != nil { 140 return errors.Trace(err) 141 } 142 controllerName, err := c.updateController( 143 ctx, 144 store, 145 registrationParams.defaultControllerName, 146 controllerDetails, 147 accountDetails, 148 ) 149 if err != nil { 150 return errors.Trace(err) 151 } 152 // Log into the controller to verify the credentials, and 153 // list the models available. 154 models, err := c.listModelsFunc(store, controllerName, accountDetails.User) 155 if err != nil { 156 return errors.Trace(err) 157 } 158 for _, model := range models { 159 owner := names.NewUserTag(model.Owner) 160 if err := store.UpdateModel( 161 controllerName, 162 jujuclient.JoinOwnerModelName(owner, model.Name), 163 jujuclient.ModelDetails{model.UUID}, 164 ); err != nil { 165 return errors.Annotate(err, "storing model details") 166 } 167 } 168 if err := store.SetCurrentController(controllerName); err != nil { 169 return errors.Trace(err) 170 } 171 172 fmt.Fprintf( 173 ctx.Stderr, "\nWelcome, %s. You are now logged into %q.\n", 174 friendlyUserName(accountDetails.User), controllerName, 175 ) 176 return c.maybeSetCurrentModel(ctx, store, controllerName, accountDetails.User, models) 177 } 178 179 func friendlyUserName(user string) string { 180 u := names.NewUserTag(user) 181 if u.IsLocal() { 182 return u.Name() 183 } 184 return u.Id() 185 } 186 187 // controllerDetails returns controller and account details to be registered for the 188 // given registration parameters. 189 func (c *registerCommand) controllerDetails(ctx *cmd.Context, p *registrationParams) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) { 190 if p.publicHost != "" { 191 return c.publicControllerDetails(p.publicHost) 192 } 193 return c.nonPublicControllerDetails(ctx, p) 194 } 195 196 // publicControllerDetails returns controller and account details to be registered 197 // for the given public controller host name. 198 func (c *registerCommand) publicControllerDetails(host string) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) { 199 errRet := func(err error) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) { 200 return jujuclient.ControllerDetails{}, jujuclient.AccountDetails{}, err 201 } 202 apiAddr := host 203 if !strings.Contains(apiAddr, ":") { 204 apiAddr += ":443" 205 } 206 // Make a direct API connection because we don't yet know the 207 // controller UUID so can't store the thus-incomplete controller 208 // details to make a conventional connection. 209 // 210 // Unfortunately this means we'll connect twice to the controller 211 // but it's probably best to go through the conventional path the 212 // second time. 213 bclient, err := c.BakeryClient() 214 if err != nil { 215 return errRet(errors.Trace(err)) 216 } 217 dialOpts := api.DefaultDialOpts() 218 dialOpts.BakeryClient = bclient 219 conn, err := c.apiOpen(&api.Info{ 220 Addrs: []string{apiAddr}, 221 }, dialOpts) 222 if err != nil { 223 return errRet(errors.Trace(err)) 224 } 225 defer conn.Close() 226 user, ok := conn.AuthTag().(names.UserTag) 227 if !ok { 228 return errRet(errors.Errorf("logged in as %v, not a user", conn.AuthTag())) 229 } 230 // If we get to here, then we have a cached macaroon for the registered 231 // user. If we encounter an error after here, we need to clear it. 232 c.onRunError = func() { 233 if err := c.ClearControllerMacaroons([]string{apiAddr}); err != nil { 234 logger.Errorf("failed to clear macaroon: %v", err) 235 } 236 } 237 return jujuclient.ControllerDetails{ 238 APIEndpoints: []string{apiAddr}, 239 ControllerUUID: conn.ControllerTag().Id(), 240 }, jujuclient.AccountDetails{ 241 User: user.Id(), 242 LastKnownAccess: conn.ControllerAccess(), 243 }, nil 244 } 245 246 // nonPublicControllerDetails returns controller and account details to be registered with 247 // respect to the given registration parameters. 248 func (c *registerCommand) nonPublicControllerDetails(ctx *cmd.Context, registrationParams *registrationParams) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) { 249 errRet := func(err error) (jujuclient.ControllerDetails, jujuclient.AccountDetails, error) { 250 return jujuclient.ControllerDetails{}, jujuclient.AccountDetails{}, err 251 } 252 // During registration we must set a new password. This has to be done 253 // atomically with the clearing of the secret key. 254 payloadBytes, err := json.Marshal(params.SecretKeyLoginRequestPayload{ 255 registrationParams.newPassword, 256 }) 257 if err != nil { 258 return errRet(errors.Trace(err)) 259 } 260 261 // Make the registration call. If this is successful, the client's 262 // cookie jar will be populated with a macaroon that may be used 263 // to log in below without the user having to type in the password 264 // again. 265 req := params.SecretKeyLoginRequest{ 266 Nonce: registrationParams.nonce[:], 267 User: registrationParams.userTag.String(), 268 PayloadCiphertext: secretbox.Seal( 269 nil, payloadBytes, 270 ®istrationParams.nonce, 271 ®istrationParams.key, 272 ), 273 } 274 resp, err := c.secretKeyLogin(registrationParams.controllerAddrs, req) 275 if err != nil { 276 return errRet(errors.Trace(err)) 277 } 278 279 // Decrypt the response to authenticate the controller and 280 // obtain its CA certificate. 281 if len(resp.Nonce) != len(registrationParams.nonce) { 282 return errRet(errors.NotValidf("response nonce")) 283 } 284 var respNonce [24]byte 285 copy(respNonce[:], resp.Nonce) 286 payloadBytes, ok := secretbox.Open(nil, resp.PayloadCiphertext, &respNonce, ®istrationParams.key) 287 if !ok { 288 return errRet(errors.NotValidf("response payload")) 289 } 290 var responsePayload params.SecretKeyLoginResponsePayload 291 if err := json.Unmarshal(payloadBytes, &responsePayload); err != nil { 292 return errRet(errors.Annotate(err, "unmarshalling response payload")) 293 } 294 user := registrationParams.userTag.Id() 295 ctx.Infof("Initial password successfully set for %s.", friendlyUserName(user)) 296 // If we get to here, then we have a cached macaroon for the registered 297 // user. If we encounter an error after here, we need to clear it. 298 c.onRunError = func() { 299 if err := c.ClearControllerMacaroons(registrationParams.controllerAddrs); err != nil { 300 logger.Errorf("failed to clear macaroon: %v", err) 301 } 302 } 303 return jujuclient.ControllerDetails{ 304 APIEndpoints: registrationParams.controllerAddrs, 305 ControllerUUID: responsePayload.ControllerUUID, 306 CACert: responsePayload.CACert, 307 }, jujuclient.AccountDetails{ 308 User: user, 309 LastKnownAccess: string(permission.LoginAccess), 310 }, nil 311 } 312 313 // updateController prompts for a controller name and updates the 314 // controller and account details in the given client store. 315 // It returns the name of the updated controller. 316 func (c *registerCommand) updateController( 317 ctx *cmd.Context, 318 store jujuclient.ClientStore, 319 defaultControllerName string, 320 controllerDetails jujuclient.ControllerDetails, 321 accountDetails jujuclient.AccountDetails, 322 ) (string, error) { 323 // Check that the same controller isn't already stored, so that we 324 // can avoid needlessly asking for a controller name in that case. 325 all, err := store.AllControllers() 326 if err != nil { 327 return "", errors.Trace(err) 328 } 329 for name, ctl := range all { 330 if ctl.ControllerUUID == controllerDetails.ControllerUUID { 331 // TODO(rogpeppe) lp#1614010 Succeed but override the account details in this case? 332 return "", errors.Errorf("controller is already registered as %q", name) 333 } 334 } 335 controllerName, err := c.promptControllerName(store, defaultControllerName, ctx.Stderr, ctx.Stdin) 336 if err != nil { 337 return "", errors.Trace(err) 338 } 339 340 if err := store.AddController(controllerName, controllerDetails); err != nil { 341 return "", errors.Trace(err) 342 } 343 if err := store.UpdateAccount(controllerName, accountDetails); err != nil { 344 return "", errors.Annotatef(err, "cannot update account information: %v", err) 345 } 346 return controllerName, nil 347 } 348 349 func (c *registerCommand) listModels(store jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 350 api, err := c.NewAPIRoot(store, controllerName, "") 351 if err != nil { 352 return nil, errors.Trace(err) 353 } 354 defer api.Close() 355 mm := modelmanager.NewClient(api) 356 return mm.ListModels(userName) 357 } 358 359 func (c *registerCommand) maybeSetCurrentModel(ctx *cmd.Context, store jujuclient.ClientStore, controllerName, userName string, models []base.UserModel) error { 360 if len(models) == 0 { 361 fmt.Fprint(ctx.Stderr, noModelsMessage) 362 return nil 363 } 364 365 // If we get to here, there is at least one model. 366 if len(models) == 1 { 367 // There is exactly one model shared, 368 // so set it as the current model. 369 model := models[0] 370 owner := names.NewUserTag(model.Owner) 371 modelName := jujuclient.JoinOwnerModelName(owner, model.Name) 372 err := store.SetCurrentModel(controllerName, modelName) 373 if err != nil { 374 return errors.Trace(err) 375 } 376 fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q.\n", modelName) 377 return nil 378 } 379 fmt.Fprintf(ctx.Stderr, ` 380 There are %d models available. Use "juju switch" to select 381 one of them: 382 `, len(models)) 383 user := names.NewUserTag(userName) 384 ownerModelNames := make(set.Strings) 385 otherModelNames := make(set.Strings) 386 for _, model := range models { 387 if model.Owner == userName { 388 ownerModelNames.Add(model.Name) 389 continue 390 } 391 owner := names.NewUserTag(model.Owner) 392 modelName := common.OwnerQualifiedModelName(model.Name, owner, user) 393 otherModelNames.Add(modelName) 394 } 395 for _, modelName := range ownerModelNames.SortedValues() { 396 fmt.Fprintf(ctx.Stderr, " - juju switch %s\n", modelName) 397 } 398 for _, modelName := range otherModelNames.SortedValues() { 399 fmt.Fprintf(ctx.Stderr, " - juju switch %s\n", modelName) 400 } 401 return nil 402 } 403 404 type registrationParams struct { 405 // publicHost holds the host name of a public controller. 406 // If this is set, all other fields will be empty. 407 publicHost string 408 409 defaultControllerName string 410 userTag names.UserTag 411 controllerAddrs []string 412 key [32]byte 413 nonce [24]byte 414 newPassword string 415 } 416 417 // getParameters gets all of the parameters required for registering, prompting 418 // the user as necessary. 419 func (c *registerCommand) getParameters(ctx *cmd.Context, store jujuclient.ClientStore) (*registrationParams, error) { 420 var params registrationParams 421 if strings.Contains(c.Arg, ".") || c.Arg == "localhost" { 422 // Looks like a host name - no URL-encoded base64 string should 423 // contain a dot and every public controller name should. 424 // Allow localhost for development purposes. 425 params.publicHost = c.Arg 426 // No need for password shenanigans if we're using a public controller. 427 return ¶ms, nil 428 } 429 // Decode key, username, controller addresses from the string supplied 430 // on the command line. 431 decodedData, err := base64.URLEncoding.DecodeString(c.Arg) 432 if err != nil { 433 return nil, errors.Trace(err) 434 } 435 var info jujuclient.RegistrationInfo 436 if _, err := asn1.Unmarshal(decodedData, &info); err != nil { 437 return nil, errors.Trace(err) 438 } 439 440 params.controllerAddrs = info.Addrs 441 params.userTag = names.NewUserTag(info.User) 442 if len(info.SecretKey) != len(params.key) { 443 return nil, errors.NotValidf("secret key") 444 } 445 copy(params.key[:], info.SecretKey) 446 params.defaultControllerName = info.ControllerName 447 448 // Prompt the user for the new password to set. 449 newPassword, err := c.promptNewPassword(ctx.Stderr, ctx.Stdin) 450 if err != nil { 451 return nil, errors.Trace(err) 452 } 453 params.newPassword = newPassword 454 455 // Generate a random nonce for encrypting the request. 456 if _, err := rand.Read(params.nonce[:]); err != nil { 457 return nil, errors.Trace(err) 458 } 459 460 return ¶ms, nil 461 } 462 463 func (c *registerCommand) secretKeyLogin(addrs []string, request params.SecretKeyLoginRequest) (*params.SecretKeyLoginResponse, error) { 464 apiContext, err := c.APIContext() 465 if err != nil { 466 return nil, errors.Annotate(err, "getting API context") 467 } 468 469 buf, err := json.Marshal(&request) 470 if err != nil { 471 return nil, errors.Annotate(err, "marshalling request") 472 } 473 r := bytes.NewReader(buf) 474 475 // Determine which address to use by attempting to open an API 476 // connection with each of the addresses. Note that we do not 477 // know the CA certificate yet, so we do not want to send any 478 // sensitive information. We make no attempt to log in until 479 // we can verify the server's identity. 480 opts := api.DefaultDialOpts() 481 opts.InsecureSkipVerify = true 482 conn, err := c.apiOpen(&api.Info{ 483 Addrs: addrs, 484 SkipLogin: true, 485 // NOTE(axw) CACert is required, but ignored if 486 // InsecureSkipVerify is set. We should try to 487 // bring together CACert and InsecureSkipVerify 488 // so they can be validated together. 489 CACert: "ignored", 490 }, opts) 491 if err != nil { 492 return nil, errors.Trace(err) 493 } 494 apiAddr := conn.Addr() 495 if err := conn.Close(); err != nil { 496 return nil, errors.Trace(err) 497 } 498 499 // Using the address we connected to above, perform the request. 500 // A success response will include a macaroon cookie that we can 501 // use to log in with. 502 urlString := fmt.Sprintf("https://%s/register", apiAddr) 503 httpReq, err := http.NewRequest("POST", urlString, r) 504 if err != nil { 505 return nil, errors.Trace(err) 506 } 507 httpReq.Header.Set("Content-Type", "application/json") 508 httpClient := utils.GetNonValidatingHTTPClient() 509 httpClient.Jar = apiContext.Jar 510 httpResp, err := httpClient.Do(httpReq) 511 if err != nil { 512 return nil, errors.Trace(err) 513 } 514 defer httpResp.Body.Close() 515 516 if httpResp.StatusCode != http.StatusOK { 517 var resp params.ErrorResult 518 if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { 519 return nil, errors.Trace(err) 520 } 521 return nil, resp.Error 522 } 523 524 var resp params.SecretKeyLoginResponse 525 if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { 526 return nil, errors.Annotatef(err, "cannot decode login response") 527 } 528 return &resp, nil 529 } 530 531 func (c *registerCommand) promptNewPassword(stderr io.Writer, stdin io.Reader) (string, error) { 532 password, err := c.readPassword("Enter a new password: ", stderr, stdin) 533 if err != nil { 534 return "", errors.Annotatef(err, "cannot read password") 535 } 536 if password == "" { 537 return "", errors.NewNotValid(nil, "you must specify a non-empty password") 538 } 539 passwordConfirmation, err := c.readPassword("Confirm password: ", stderr, stdin) 540 if err != nil { 541 return "", errors.Trace(err) 542 } 543 if password != passwordConfirmation { 544 return "", errors.Errorf("passwords do not match") 545 } 546 return password, nil 547 } 548 549 func (c *registerCommand) promptControllerName(store jujuclient.ClientStore, suggestedName string, stderr io.Writer, stdin io.Reader) (string, error) { 550 if suggestedName != "" { 551 if _, err := store.ControllerByName(suggestedName); err == nil { 552 suggestedName = "" 553 } 554 } 555 for { 556 var setMsg string 557 setMsg = "Enter a name for this controller: " 558 if suggestedName != "" { 559 setMsg = fmt.Sprintf("Enter a name for this controller [%s]: ", suggestedName) 560 } 561 fmt.Fprintf(stderr, setMsg) 562 name, err := c.readLine(stdin) 563 if err != nil { 564 return "", errors.Trace(err) 565 } 566 name = strings.TrimSpace(name) 567 if name == "" { 568 if suggestedName == "" { 569 fmt.Fprintln(stderr, "You must specify a non-empty controller name.") 570 continue 571 } 572 name = suggestedName 573 } 574 _, err = store.ControllerByName(name) 575 if err == nil { 576 fmt.Fprintf(stderr, "Controller %q already exists.\n", name) 577 continue 578 } 579 return name, nil 580 } 581 } 582 583 func (c *registerCommand) readPassword(prompt string, stderr io.Writer, stdin io.Reader) (string, error) { 584 fmt.Fprintf(stderr, "%s", prompt) 585 defer stderr.Write([]byte{'\n'}) 586 if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) { 587 password, err := terminal.ReadPassword(int(f.Fd())) 588 if err != nil { 589 return "", errors.Trace(err) 590 } 591 return string(password), nil 592 } 593 return c.readLine(stdin) 594 } 595 596 func (c *registerCommand) readLine(stdin io.Reader) (string, error) { 597 // Read one byte at a time to avoid reading beyond the delimiter. 598 line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n') 599 if err != nil { 600 return "", errors.Trace(err) 601 } 602 return line[:len(line)-1], nil 603 } 604 605 type byteAtATimeReader struct { 606 io.Reader 607 } 608 609 func (r byteAtATimeReader) Read(out []byte) (int, error) { 610 return r.Reader.Read(out[:1]) 611 }