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