github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 errNoModels = errors.New(` 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".`[1:]) 41 42 // NewRegisterCommand returns a command to allow the user to register a controller. 43 func NewRegisterCommand() cmd.Command { 44 cmd := ®isterCommand{} 45 cmd.apiOpen = cmd.APIOpen 46 cmd.listModelsFunc = cmd.listModels 47 cmd.store = jujuclient.NewFileClientStore() 48 return modelcmd.WrapBase(cmd) 49 } 50 51 // registerCommand logs in to a Juju controller and caches the connection 52 // information. 53 type registerCommand struct { 54 modelcmd.JujuCommandBase 55 apiOpen api.OpenFunc 56 listModelsFunc func(_ jujuclient.ClientStore, controller, user string) ([]base.UserModel, error) 57 store jujuclient.ClientStore 58 EncodedData string 59 } 60 61 var usageRegisterSummary = ` 62 Registers a Juju user to a controller.`[1:] 63 64 var usageRegisterDetails = ` 65 Connects to a controller and completes the user registration process that began 66 with the `[1:] + "`juju add-user`" + ` command. The latter prints out the 'string' that is 67 referred to in Usage. 68 69 The user will be prompted for a password, which, once set, causes the 70 registration string to be voided. In order to start using Juju the user can now 71 either add a model or wait for a model to be shared with them. Some machine 72 providers will require the user to be in possession of certain credentials in 73 order to add a model. 74 75 Examples: 76 77 juju register MFATA3JvZDAnExMxMDQuMTU0LjQyLjQ0OjE3MDcwExAxMC4xMjguMC4yOjE3MDcwBCBEFCaXerhNImkKKabuX5ULWf2Bp4AzPNJEbXVWgraLrAA= 78 79 See also: 80 add-user 81 change-user-password 82 unregister` 83 84 // Info implements Command.Info 85 // `register` may seem generic, but is seen as simple and without potential 86 // naming collisions in any current or planned features. 87 func (c *registerCommand) Info() *cmd.Info { 88 return &cmd.Info{ 89 Name: "register", 90 Args: "<string>", 91 Purpose: usageRegisterSummary, 92 Doc: usageRegisterDetails, 93 } 94 } 95 96 // SetFlags implements Command.Init. 97 func (c *registerCommand) Init(args []string) error { 98 if len(args) < 1 { 99 return errors.New("registration data missing") 100 } 101 c.EncodedData, args = args[0], args[1:] 102 if err := cmd.CheckEmpty(args); err != nil { 103 return err 104 } 105 return nil 106 } 107 108 func (c *registerCommand) Run(ctx *cmd.Context) error { 109 110 store := modelcmd.QualifyingClientStore{c.store} 111 registrationParams, err := c.getParameters(ctx, store) 112 if err != nil { 113 return errors.Trace(err) 114 } 115 _, err = store.ControllerByName(registrationParams.controllerName) 116 if err == nil { 117 return errors.AlreadyExistsf("controller %q", registrationParams.controllerName) 118 } else if !errors.IsNotFound(err) { 119 return errors.Trace(err) 120 } 121 122 // During registration we must set a new password. This has to be done 123 // atomically with the clearing of the secret key. 124 payloadBytes, err := json.Marshal(params.SecretKeyLoginRequestPayload{ 125 registrationParams.newPassword, 126 }) 127 if err != nil { 128 return errors.Trace(err) 129 } 130 131 // Make the registration call. If this is successful, the client's 132 // cookie jar will be populated with a macaroon that may be used 133 // to log in below without the user having to type in the password 134 // again. 135 req := params.SecretKeyLoginRequest{ 136 Nonce: registrationParams.nonce[:], 137 User: registrationParams.userTag.String(), 138 PayloadCiphertext: secretbox.Seal( 139 nil, payloadBytes, 140 ®istrationParams.nonce, 141 ®istrationParams.key, 142 ), 143 } 144 resp, err := c.secretKeyLogin(registrationParams.controllerAddrs, req) 145 if err != nil { 146 return errors.Trace(err) 147 } 148 149 // Decrypt the response to authenticate the controller and 150 // obtain its CA certificate. 151 if len(resp.Nonce) != len(registrationParams.nonce) { 152 return errors.NotValidf("response nonce") 153 } 154 var respNonce [24]byte 155 copy(respNonce[:], resp.Nonce) 156 payloadBytes, ok := secretbox.Open(nil, resp.PayloadCiphertext, &respNonce, ®istrationParams.key) 157 if !ok { 158 return errors.NotValidf("response payload") 159 } 160 var responsePayload params.SecretKeyLoginResponsePayload 161 if err := json.Unmarshal(payloadBytes, &responsePayload); err != nil { 162 return errors.Annotate(err, "unmarshalling response payload") 163 } 164 165 // Store the controller and account details. 166 controllerDetails := jujuclient.ControllerDetails{ 167 APIEndpoints: registrationParams.controllerAddrs, 168 ControllerUUID: responsePayload.ControllerUUID, 169 CACert: responsePayload.CACert, 170 } 171 if err := store.AddController(registrationParams.controllerName, controllerDetails); err != nil { 172 return errors.Trace(err) 173 } 174 accountDetails := jujuclient.AccountDetails{ 175 User: registrationParams.userTag.Canonical(), 176 LastKnownAccess: string(permission.LoginAccess), 177 } 178 if err := store.UpdateAccount(registrationParams.controllerName, accountDetails); err != nil { 179 return errors.Trace(err) 180 } 181 182 // Log into the controller to verify the credentials, and 183 // list the models available. 184 models, err := c.listModelsFunc(store, registrationParams.controllerName, accountDetails.User) 185 if err != nil { 186 return errors.Trace(err) 187 } 188 for _, model := range models { 189 owner := names.NewUserTag(model.Owner) 190 if err := store.UpdateModel( 191 registrationParams.controllerName, 192 jujuclient.JoinOwnerModelName(owner, model.Name), 193 jujuclient.ModelDetails{model.UUID}, 194 ); err != nil { 195 return errors.Annotate(err, "storing model details") 196 } 197 } 198 if err := store.SetCurrentController(registrationParams.controllerName); err != nil { 199 return errors.Trace(err) 200 } 201 202 fmt.Fprintf( 203 ctx.Stderr, "\nWelcome, %s. You are now logged into %q.\n", 204 registrationParams.userTag.Id(), registrationParams.controllerName, 205 ) 206 return c.maybeSetCurrentModel(ctx, store, registrationParams.controllerName, accountDetails.User, models) 207 } 208 209 func (c *registerCommand) listModels(store jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 210 api, err := c.NewAPIRoot(store, controllerName, "") 211 if err != nil { 212 return nil, errors.Trace(err) 213 } 214 defer api.Close() 215 mm := modelmanager.NewClient(api) 216 return mm.ListModels(userName) 217 } 218 219 func (c *registerCommand) maybeSetCurrentModel(ctx *cmd.Context, store jujuclient.ClientStore, controllerName, userName string, models []base.UserModel) error { 220 if len(models) == 0 { 221 fmt.Fprintf(ctx.Stderr, "\n%s\n\n", errNoModels.Error()) 222 return nil 223 } 224 225 // If we get to here, there is at least one model. 226 if len(models) == 1 { 227 // There is exactly one model shared, 228 // so set it as the current model. 229 model := models[0] 230 owner := names.NewUserTag(model.Owner) 231 modelName := jujuclient.JoinOwnerModelName(owner, model.Name) 232 err := store.SetCurrentModel(controllerName, modelName) 233 if err != nil { 234 return errors.Trace(err) 235 } 236 fmt.Fprintf(ctx.Stderr, "\nCurrent model set to %q.\n\n", modelName) 237 } else { 238 fmt.Fprintf(ctx.Stderr, ` 239 There are %d models available. Use "juju switch" to select 240 one of them: 241 `, len(models)) 242 user := names.NewUserTag(userName) 243 ownerModelNames := make(set.Strings) 244 otherModelNames := make(set.Strings) 245 for _, model := range models { 246 if model.Owner == userName { 247 ownerModelNames.Add(model.Name) 248 continue 249 } 250 owner := names.NewUserTag(model.Owner) 251 modelName := common.OwnerQualifiedModelName(model.Name, owner, user) 252 otherModelNames.Add(modelName) 253 } 254 for _, modelName := range ownerModelNames.SortedValues() { 255 fmt.Fprintf(ctx.Stderr, " - juju switch %s\n", modelName) 256 } 257 for _, modelName := range otherModelNames.SortedValues() { 258 fmt.Fprintf(ctx.Stderr, " - juju switch %s\n", modelName) 259 } 260 fmt.Fprintln(ctx.Stderr) 261 } 262 return nil 263 } 264 265 type registrationParams struct { 266 userTag names.UserTag 267 controllerName string 268 controllerAddrs []string 269 key [32]byte 270 nonce [24]byte 271 newPassword string 272 } 273 274 // getParameters gets all of the parameters required for registering, prompting 275 // the user as necessary. 276 func (c *registerCommand) getParameters(ctx *cmd.Context, store jujuclient.ClientStore) (*registrationParams, error) { 277 278 // Decode key, username, controller addresses from the string supplied 279 // on the command line. 280 decodedData, err := base64.URLEncoding.DecodeString(c.EncodedData) 281 if err != nil { 282 return nil, errors.Trace(err) 283 } 284 var info jujuclient.RegistrationInfo 285 if _, err := asn1.Unmarshal(decodedData, &info); err != nil { 286 return nil, errors.Trace(err) 287 } 288 289 params := registrationParams{ 290 controllerAddrs: info.Addrs, 291 userTag: names.NewUserTag(info.User), 292 } 293 if len(info.SecretKey) != len(params.key) { 294 return nil, errors.NotValidf("secret key") 295 } 296 copy(params.key[:], info.SecretKey) 297 298 // Prompt the user for the controller name. 299 controllerName, err := c.promptControllerName(store, info.ControllerName, ctx.Stderr, ctx.Stdin) 300 if err != nil { 301 return nil, errors.Trace(err) 302 } 303 params.controllerName = controllerName 304 305 // Prompt the user for the new password to set. 306 newPassword, err := c.promptNewPassword(ctx.Stderr, ctx.Stdin) 307 if err != nil { 308 return nil, errors.Trace(err) 309 } 310 params.newPassword = newPassword 311 312 // Generate a random nonce for encrypting the request. 313 if _, err := rand.Read(params.nonce[:]); err != nil { 314 return nil, errors.Trace(err) 315 } 316 317 return ¶ms, nil 318 } 319 320 func (c *registerCommand) secretKeyLogin(addrs []string, request params.SecretKeyLoginRequest) (*params.SecretKeyLoginResponse, error) { 321 apiContext, err := c.APIContext() 322 if err != nil { 323 return nil, errors.Annotate(err, "getting API context") 324 } 325 326 buf, err := json.Marshal(&request) 327 if err != nil { 328 return nil, errors.Annotate(err, "marshalling request") 329 } 330 r := bytes.NewReader(buf) 331 332 // Determine which address to use by attempting to open an API 333 // connection with each of the addresses. Note that we do not 334 // know the CA certificate yet, so we do not want to send any 335 // sensitive information. We make no attempt to log in until 336 // we can verify the server's identity. 337 opts := api.DefaultDialOpts() 338 opts.InsecureSkipVerify = true 339 conn, err := c.apiOpen(&api.Info{ 340 Addrs: addrs, 341 SkipLogin: true, 342 // NOTE(axw) CACert is required, but ignored if 343 // InsecureSkipVerify is set. We should try to 344 // bring together CACert and InsecureSkipVerify 345 // so they can be validated together. 346 CACert: "ignored", 347 }, opts) 348 if err != nil { 349 return nil, errors.Trace(err) 350 } 351 apiAddr := conn.Addr() 352 if err := conn.Close(); err != nil { 353 return nil, errors.Trace(err) 354 } 355 356 // Using the address we connected to above, perform the request. 357 // A success response will include a macaroon cookie that we can 358 // use to log in with. 359 urlString := fmt.Sprintf("https://%s/register", apiAddr) 360 httpReq, err := http.NewRequest("POST", urlString, r) 361 if err != nil { 362 return nil, errors.Trace(err) 363 } 364 httpReq.Header.Set("Content-Type", "application/json") 365 httpClient := utils.GetNonValidatingHTTPClient() 366 httpClient.Jar = apiContext.Jar 367 httpResp, err := httpClient.Do(httpReq) 368 if err != nil { 369 return nil, errors.Trace(err) 370 } 371 defer httpResp.Body.Close() 372 373 if httpResp.StatusCode != http.StatusOK { 374 var resp params.ErrorResult 375 if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { 376 return nil, errors.Trace(err) 377 } 378 return nil, resp.Error 379 } 380 381 var resp params.SecretKeyLoginResponse 382 if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { 383 return nil, errors.Trace(err) 384 } 385 return &resp, nil 386 } 387 388 func (c *registerCommand) promptNewPassword(stderr io.Writer, stdin io.Reader) (string, error) { 389 password, err := c.readPassword("Enter a new password: ", stderr, stdin) 390 if err != nil { 391 return "", errors.Trace(err) 392 } 393 if password == "" { 394 return "", errors.NewNotValid(nil, "you must specify a non-empty password") 395 } 396 passwordConfirmation, err := c.readPassword("Confirm password: ", stderr, stdin) 397 if err != nil { 398 return "", errors.Trace(err) 399 } 400 if password != passwordConfirmation { 401 return "", errors.Errorf("passwords do not match") 402 } 403 return password, nil 404 } 405 406 const errControllerConflicts = `WARNING: You already have a controller registered with the name %q. Please choose a different name for the new controller. 407 408 ` 409 410 func (c *registerCommand) promptControllerName(store jujuclient.ClientStore, suggestedName string, stderr io.Writer, stdin io.Reader) (string, error) { 411 _, err := store.ControllerByName(suggestedName) 412 if err == nil { 413 fmt.Fprintf(stderr, errControllerConflicts, suggestedName) 414 suggestedName = "" 415 } 416 var setMsg string 417 setMsg = "Enter a name for this controller: " 418 if suggestedName != "" { 419 setMsg = fmt.Sprintf("Enter a name for this controller [%s]: ", 420 suggestedName) 421 } 422 fmt.Fprintf(stderr, setMsg) 423 defer stderr.Write([]byte{'\n'}) 424 name, err := c.readLine(stdin) 425 if err != nil { 426 return "", errors.Trace(err) 427 } 428 name = strings.TrimSpace(name) 429 if name == "" && suggestedName == "" { 430 return "", errors.NewNotValid(nil, "you must specify a non-empty controller name") 431 } 432 if name == "" && suggestedName != "" { 433 return suggestedName, nil 434 } 435 return name, nil 436 } 437 438 func (c *registerCommand) readPassword(prompt string, stderr io.Writer, stdin io.Reader) (string, error) { 439 fmt.Fprintf(stderr, "%s", prompt) 440 defer stderr.Write([]byte{'\n'}) 441 if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) { 442 password, err := terminal.ReadPassword(int(f.Fd())) 443 if err != nil { 444 return "", errors.Trace(err) 445 } 446 return string(password), nil 447 } 448 return c.readLine(stdin) 449 } 450 451 func (c *registerCommand) readLine(stdin io.Reader) (string, error) { 452 // Read one byte at a time to avoid reading beyond the delimiter. 453 line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n') 454 if err != nil { 455 return "", errors.Trace(err) 456 } 457 return line[:len(line)-1], nil 458 } 459 460 type byteAtATimeReader struct { 461 io.Reader 462 } 463 464 func (r byteAtATimeReader) Read(out []byte) (int, error) { 465 return r.Reader.Read(out[:1]) 466 }