github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/controller/register_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package controller_test 5 6 import ( 7 "encoding/asn1" 8 "encoding/base64" 9 "encoding/json" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "net/http/httptest" 14 "net/url" 15 "strings" 16 17 "github.com/juju/cmd" 18 "github.com/juju/errors" 19 jc "github.com/juju/testing/checkers" 20 "golang.org/x/crypto/nacl/secretbox" 21 gc "gopkg.in/check.v1" 22 "gopkg.in/juju/names.v2" 23 24 "github.com/juju/juju/api" 25 "github.com/juju/juju/api/base" 26 "github.com/juju/juju/apiserver/params" 27 "github.com/juju/juju/cmd/juju/controller" 28 cmdtesting "github.com/juju/juju/cmd/testing" 29 "github.com/juju/juju/jujuclient" 30 "github.com/juju/juju/jujuclient/jujuclienttesting" 31 "github.com/juju/juju/testing" 32 ) 33 34 type RegisterSuite struct { 35 testing.FakeJujuXDGDataHomeSuite 36 apiConnection *mockAPIConnection 37 store *jujuclienttesting.MemStore 38 apiOpenError error 39 listModels func(jujuclient.ClientStore, string, string) ([]base.UserModel, error) 40 listModelsControllerName string 41 listModelsUserName string 42 server *httptest.Server 43 httpHandler http.Handler 44 } 45 46 const noModelsText = ` 47 There are no models available. You can add models with 48 "juju add-model", or you can ask an administrator or owner 49 of a model to grant access to that model with "juju grant". 50 ` 51 52 var _ = gc.Suite(&RegisterSuite{}) 53 54 func (s *RegisterSuite) SetUpTest(c *gc.C) { 55 s.FakeJujuXDGDataHomeSuite.SetUpTest(c) 56 57 s.apiOpenError = nil 58 s.httpHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 59 s.server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 60 s.httpHandler.ServeHTTP(w, r) 61 })) 62 63 serverURL, err := url.Parse(s.server.URL) 64 c.Assert(err, jc.ErrorIsNil) 65 s.apiConnection = &mockAPIConnection{ 66 controllerTag: names.NewControllerTag(mockControllerUUID), 67 addr: serverURL.Host, 68 } 69 s.listModelsControllerName = "" 70 s.listModelsUserName = "" 71 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 72 s.listModelsControllerName = controllerName 73 s.listModelsUserName = userName 74 return nil, nil 75 } 76 77 s.store = jujuclienttesting.NewMemStore() 78 } 79 80 func (s *RegisterSuite) TearDownTest(c *gc.C) { 81 s.server.Close() 82 s.FakeJujuXDGDataHomeSuite.TearDownTest(c) 83 } 84 85 func (s *RegisterSuite) TestInit(c *gc.C) { 86 registerCommand := controller.NewRegisterCommandForTest(nil, nil, nil) 87 88 err := testing.InitCommand(registerCommand, []string{}) 89 c.Assert(err, gc.ErrorMatches, "registration data missing") 90 91 err = testing.InitCommand(registerCommand, []string{"foo"}) 92 c.Assert(err, jc.ErrorIsNil) 93 c.Assert(registerCommand.Arg, gc.Equals, "foo") 94 95 err = testing.InitCommand(registerCommand, []string{"foo", "bar"}) 96 c.Assert(err, gc.ErrorMatches, `unrecognized args: \["bar"\]`) 97 } 98 99 func (s *RegisterSuite) TestRegister(c *gc.C) { 100 s.testRegisterSuccess(c, nil, "") 101 c.Assert(s.listModelsControllerName, gc.Equals, "controller-name") 102 c.Assert(s.listModelsUserName, gc.Equals, "bob") 103 } 104 105 func (s *RegisterSuite) TestRegisterOneModel(c *gc.C) { 106 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 107 return []base.UserModel{{ 108 Name: "theoneandonly", 109 Owner: "carol", 110 UUID: mockControllerUUID, 111 }}, nil 112 } 113 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 114 Enter a new password: »hunter2 115 116 Confirm password: »hunter2 117 118 Initial password successfully set for bob. 119 Enter a name for this controller \[controller-name\]: » 120 121 Welcome, bob. You are now logged into "controller-name". 122 123 Current model set to "carol/theoneandonly". 124 `[1:]) 125 s.testRegisterSuccess(c, prompter, "") 126 c.Assert( 127 s.store.Models["controller-name"].CurrentModel, 128 gc.Equals, "carol/theoneandonly", 129 ) 130 prompter.CheckDone() 131 } 132 133 func (s *RegisterSuite) TestRegisterMultipleModels(c *gc.C) { 134 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 135 return []base.UserModel{{ 136 Name: "model1", 137 Owner: "bob", 138 UUID: mockControllerUUID, 139 }, { 140 Name: "model2", 141 Owner: "bob", 142 UUID: "eeeeeeee-12e9-11e4-8a70-b2227cce2b55", 143 }}, nil 144 } 145 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 146 Enter a new password: »hunter2 147 148 Confirm password: »hunter2 149 150 Initial password successfully set for bob. 151 Enter a name for this controller \[controller-name\]: » 152 153 Welcome, bob. You are now logged into "controller-name". 154 155 There are 2 models available. Use "juju switch" to select 156 one of them: 157 - juju switch model1 158 - juju switch model2 159 `[1:]) 160 defer prompter.CheckDone() 161 s.testRegisterSuccess(c, prompter, "") 162 163 // When there are multiple models, no current model will be set. 164 // Instead, the command will output the list of models and inform 165 // the user how to set the current model. 166 _, err := s.store.CurrentModel("controller-name") 167 c.Assert(err, jc.Satisfies, errors.IsNotFound) 168 } 169 170 // testRegisterSuccess tests that the register command when the given 171 // stdio instance is used for input and output. If stdio is nil, a 172 // default prompter will be used. 173 // If controllerName is non-empty, that name will be expected 174 // to be the name of the registered controller. 175 func (s *RegisterSuite) testRegisterSuccess(c *gc.C, stdio io.ReadWriter, controllerName string) { 176 srv := s.mockServer(c) 177 s.httpHandler = srv 178 179 if controllerName == "" { 180 controllerName = "controller-name" 181 } 182 183 registrationData := s.encodeRegistrationData(c, jujuclient.RegistrationInfo{ 184 User: "bob", 185 SecretKey: mockSecretKey, 186 ControllerName: "controller-name", 187 }) 188 c.Logf("registration data: %q", registrationData) 189 if stdio == nil { 190 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 191 Enter a new password: »hunter2 192 193 Confirm password: »hunter2 194 195 Initial password successfully set for bob. 196 Enter a name for this controller \[controller-name\]: » 197 198 Welcome, bob. You are now logged into "controller-name". 199 `[1:]+noModelsText) 200 defer prompter.CheckDone() 201 stdio = prompter 202 } 203 err := s.run(c, stdio, registrationData) 204 c.Assert(err, jc.ErrorIsNil) 205 206 // There should have been one POST command to "/register". 207 c.Assert(srv.requests, gc.HasLen, 1) 208 c.Assert(srv.requests[0].Method, gc.Equals, "POST") 209 c.Assert(srv.requests[0].URL.Path, gc.Equals, "/register") 210 var request params.SecretKeyLoginRequest 211 err = json.Unmarshal(srv.requestBodies[0], &request) 212 c.Assert(err, jc.ErrorIsNil) 213 c.Assert(request.User, jc.DeepEquals, "user-bob") 214 c.Assert(request.Nonce, gc.HasLen, 24) 215 requestPayloadPlaintext, err := json.Marshal(params.SecretKeyLoginRequestPayload{ 216 "hunter2", 217 }) 218 c.Assert(err, jc.ErrorIsNil) 219 expectedCiphertext := s.seal(c, requestPayloadPlaintext, mockSecretKey, request.Nonce) 220 c.Assert(request.PayloadCiphertext, jc.DeepEquals, expectedCiphertext) 221 222 // The controller and account details should be recorded with 223 // the specified controller name and user 224 // name from the registration string. 225 226 controller, err := s.store.ControllerByName(controllerName) 227 c.Assert(err, jc.ErrorIsNil) 228 c.Assert(controller, jc.DeepEquals, &jujuclient.ControllerDetails{ 229 ControllerUUID: mockControllerUUID, 230 APIEndpoints: []string{s.apiConnection.addr}, 231 CACert: testing.CACert, 232 }) 233 account, err := s.store.AccountDetails(controllerName) 234 c.Assert(err, jc.ErrorIsNil) 235 c.Assert(account, jc.DeepEquals, &jujuclient.AccountDetails{ 236 User: "bob", 237 LastKnownAccess: "login", 238 }) 239 } 240 241 func (s *RegisterSuite) TestRegisterInvalidRegistrationData(c *gc.C) { 242 err := s.run(c, nil, "not base64") 243 c.Assert(err, gc.ErrorMatches, "illegal base64 data at input byte 3") 244 245 err = s.run(c, nil, "YXNuLjEK") 246 c.Assert(err, gc.ErrorMatches, "asn1: structure error: .*") 247 } 248 249 func (s *RegisterSuite) TestRegisterEmptyControllerName(c *gc.C) { 250 srv := s.mockServer(c) 251 s.httpHandler = srv 252 registrationData := s.encodeRegistrationData(c, jujuclient.RegistrationInfo{ 253 User: "bob", 254 SecretKey: mockSecretKey, 255 }) 256 // We check that it loops when an empty controller name 257 // is entered and that the loop terminates when the user 258 // types ^D. 259 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 260 Enter a new password: »hunter2 261 262 Confirm password: »hunter2 263 264 Initial password successfully set for bob. 265 Enter a name for this controller: » 266 You must specify a non-empty controller name. 267 Enter a name for this controller: » 268 You must specify a non-empty controller name. 269 Enter a name for this controller: »» 270 `[1:]) 271 err := s.run(c, prompter, registrationData) 272 c.Assert(err, gc.ErrorMatches, "EOF") 273 prompter.AssertDone() 274 } 275 276 func (s *RegisterSuite) TestRegisterControllerNameExists(c *gc.C) { 277 err := s.store.AddController("controller-name", jujuclient.ControllerDetails{ 278 ControllerUUID: "0d75314a-5266-4f4f-8523-415be76f92dc", 279 CACert: testing.CACert, 280 }) 281 c.Assert(err, jc.ErrorIsNil) 282 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 283 Enter a new password: »hunter2 284 285 Confirm password: »hunter2 286 287 Initial password successfully set for bob. 288 Enter a name for this controller: »controller-name 289 Controller "controller-name" already exists. 290 Enter a name for this controller: »other-name 291 292 Welcome, bob. You are now logged into "other-name". 293 `[1:]+noModelsText) 294 s.testRegisterSuccess(c, prompter, "other-name") 295 prompter.AssertDone() 296 } 297 298 func (s *RegisterSuite) TestControllerUUIDExists(c *gc.C) { 299 // Controller has the UUID from s.testRegister to mimic a user with 300 // this controller already registered (regardless of its name). 301 err := s.store.AddController("controller-name", jujuclient.ControllerDetails{ 302 ControllerUUID: mockControllerUUID, 303 CACert: testing.CACert, 304 }) 305 306 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 307 return []base.UserModel{{ 308 Name: "model-name", 309 Owner: "bob", 310 UUID: mockControllerUUID, 311 }}, nil 312 } 313 314 registrationData := s.encodeRegistrationData(c, jujuclient.RegistrationInfo{ 315 User: "bob", 316 SecretKey: mockSecretKey, 317 ControllerName: "controller-name", 318 }) 319 320 srv := s.mockServer(c) 321 s.httpHandler = srv 322 323 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 324 Enter a new password: »hunter2 325 326 Confirm password: »hunter2 327 328 Initial password successfully set for bob. 329 `[1:]) 330 err = s.run(c, prompter, registrationData) 331 c.Assert(err, gc.ErrorMatches, `controller is already registered as "controller-name"`, gc.Commentf("details: %v", errors.Details(err))) 332 prompter.CheckDone() 333 } 334 335 func (s *RegisterSuite) TestProposedControllerNameExists(c *gc.C) { 336 // Controller does not have the UUID from s.testRegister, thereby 337 // mimicing a user with an already registered 'foreign' controller. 338 err := s.store.AddController("controller-name", jujuclient.ControllerDetails{ 339 ControllerUUID: "0d75314a-5266-4f4f-8523-415be76f92dc", 340 CACert: testing.CACert, 341 }) 342 c.Assert(err, jc.ErrorIsNil) 343 344 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 345 return []base.UserModel{{ 346 Name: "model-name", 347 Owner: "bob", 348 UUID: mockControllerUUID, 349 }}, nil 350 } 351 352 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 353 Enter a new password: »hunter2 354 355 Confirm password: »hunter2 356 357 Initial password successfully set for bob. 358 Enter a name for this controller: »controller-name 359 Controller "controller-name" already exists. 360 Enter a name for this controller: »other-name 361 362 Welcome, bob. You are now logged into "other-name". 363 364 Current model set to "bob/model-name". 365 `[1:]) 366 defer prompter.CheckDone() 367 s.testRegisterSuccess(c, prompter, "other-name") 368 } 369 370 func (s *RegisterSuite) TestRegisterEmptyPassword(c *gc.C) { 371 registrationData := s.encodeRegistrationData(c, jujuclient.RegistrationInfo{ 372 User: "bob", 373 SecretKey: mockSecretKey, 374 }) 375 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 376 Enter a new password: » 377 378 `[1:]) 379 defer prompter.CheckDone() 380 err := s.run(c, prompter, registrationData) 381 c.Assert(err, gc.ErrorMatches, "you must specify a non-empty password") 382 } 383 384 func (s *RegisterSuite) TestRegisterPasswordMismatch(c *gc.C) { 385 registrationData := s.encodeRegistrationData(c, jujuclient.RegistrationInfo{ 386 User: "bob", 387 SecretKey: mockSecretKey, 388 }) 389 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 390 Enter a new password: »hunter2 391 392 Confirm password: »hunter3 393 394 `[1:]) 395 defer prompter.CheckDone() 396 err := s.run(c, prompter, registrationData) 397 c.Assert(err, gc.ErrorMatches, "passwords do not match") 398 } 399 400 func (s *RegisterSuite) TestAPIOpenError(c *gc.C) { 401 registrationData := s.encodeRegistrationData(c, jujuclient.RegistrationInfo{ 402 User: "bob", 403 SecretKey: mockSecretKey, 404 }) 405 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 406 Enter a new password: »hunter2 407 408 Confirm password: »hunter2 409 410 `[1:]) 411 defer prompter.CheckDone() 412 s.apiOpenError = errors.New("open failed") 413 err := s.run(c, prompter, registrationData) 414 c.Assert(err, gc.ErrorMatches, `open failed`) 415 } 416 417 func (s *RegisterSuite) TestRegisterServerError(c *gc.C) { 418 response, err := json.Marshal(params.ErrorResult{ 419 Error: ¶ms.Error{Message: "xyz", Code: "123"}, 420 }) 421 422 s.httpHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 423 w.WriteHeader(http.StatusInternalServerError) 424 _, err = w.Write(response) 425 c.Check(err, jc.ErrorIsNil) 426 }) 427 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 428 Enter a new password: »hunter2 429 430 Confirm password: »hunter2 431 432 `[1:]) 433 434 registrationData := s.encodeRegistrationData(c, jujuclient.RegistrationInfo{ 435 User: "bob", 436 SecretKey: mockSecretKey, 437 }) 438 err = s.run(c, prompter, registrationData) 439 c.Assert(err, gc.ErrorMatches, "xyz") 440 441 // Check that the controller hasn't been added. 442 _, err = s.store.ControllerByName("controller-name") 443 c.Assert(err, jc.Satisfies, errors.IsNotFound) 444 } 445 446 func (s *RegisterSuite) TestRegisterPublic(c *gc.C) { 447 s.apiConnection.authTag = names.NewUserTag("bob@external") 448 s.apiConnection.controllerAccess = "login" 449 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 450 Enter a name for this controller: »public-controller-name 451 452 Welcome, bob@external. You are now logged into "public-controller-name". 453 `[1:]+noModelsText) 454 defer prompter.CheckDone() 455 err := s.run(c, prompter, "0.1.2.3") 456 c.Assert(err, jc.ErrorIsNil) 457 458 // The controller and account details should be recorded with 459 // the specified controller name and user 460 // name from the auth tag. 461 462 controller, err := s.store.ControllerByName("public-controller-name") 463 c.Assert(err, jc.ErrorIsNil) 464 c.Assert(controller, jc.DeepEquals, &jujuclient.ControllerDetails{ 465 ControllerUUID: mockControllerUUID, 466 APIEndpoints: []string{"0.1.2.3:443"}, 467 }) 468 account, err := s.store.AccountDetails("public-controller-name") 469 c.Assert(err, jc.ErrorIsNil) 470 c.Assert(account, jc.DeepEquals, &jujuclient.AccountDetails{ 471 User: "bob@external", 472 LastKnownAccess: "login", 473 }) 474 } 475 476 func (s *RegisterSuite) TestRegisterPublicAPIOpenError(c *gc.C) { 477 s.apiOpenError = errors.New("open failed") 478 err := s.run(c, noPrompts(c), "0.1.2.3") 479 c.Assert(err, gc.ErrorMatches, `open failed`) 480 } 481 482 func (s *RegisterSuite) TestRegisterPublicWithPort(c *gc.C) { 483 s.apiConnection.authTag = names.NewUserTag("bob@external") 484 s.apiConnection.controllerAccess = "login" 485 prompter := cmdtesting.NewSeqPrompter(c, "»", ` 486 Enter a name for this controller: »public-controller-name 487 488 Welcome, bob@external. You are now logged into "public-controller-name". 489 `[1:]+noModelsText) 490 defer prompter.CheckDone() 491 err := s.run(c, prompter, "0.1.2.3:5678") 492 c.Assert(err, jc.ErrorIsNil) 493 494 // The controller and account details should be recorded with 495 // the specified controller name and user 496 // name from the auth tag. 497 498 controller, err := s.store.ControllerByName("public-controller-name") 499 c.Assert(err, jc.ErrorIsNil) 500 c.Assert(controller, jc.DeepEquals, &jujuclient.ControllerDetails{ 501 ControllerUUID: mockControllerUUID, 502 APIEndpoints: []string{"0.1.2.3:5678"}, 503 }) 504 } 505 506 type mockServer struct { 507 requests []*http.Request 508 requestBodies [][]byte 509 response []byte 510 } 511 512 const mockControllerUUID = "df136476-12e9-11e4-8a70-b2227cce2b54" 513 514 var mockSecretKey = []byte(strings.Repeat("X", 32)) 515 516 // mockServer returns a mock HTTP server that will always respond with a 517 // response encoded with mockSecretKey and a constant nonce, containing 518 // testing.CACert and mockControllerUUID. 519 // 520 // Each time a call is made, the requests and requestBodies fields in 521 // the returned mockServer instance are appended with the request details. 522 func (s *RegisterSuite) mockServer(c *gc.C) *mockServer { 523 respNonce := []byte(strings.Repeat("X", 24)) 524 525 responsePayloadPlaintext, err := json.Marshal(params.SecretKeyLoginResponsePayload{ 526 CACert: testing.CACert, 527 ControllerUUID: mockControllerUUID, 528 }) 529 c.Assert(err, jc.ErrorIsNil) 530 response, err := json.Marshal(params.SecretKeyLoginResponse{ 531 Nonce: respNonce, 532 PayloadCiphertext: s.seal(c, responsePayloadPlaintext, mockSecretKey, respNonce), 533 }) 534 c.Assert(err, jc.ErrorIsNil) 535 return &mockServer{ 536 response: response, 537 } 538 } 539 540 func (s *RegisterSuite) encodeRegistrationData(c *gc.C, info jujuclient.RegistrationInfo) string { 541 info.Addrs = []string{s.apiConnection.addr} 542 data, err := asn1.Marshal(info) 543 c.Assert(err, jc.ErrorIsNil) 544 // Append some junk to the end of the encoded data to 545 // ensure that, if we have to pad the data in add-user, 546 // register can still decode it. 547 data = append(data, 0, 0, 0) 548 return base64.URLEncoding.EncodeToString(data) 549 } 550 551 func (srv *mockServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 552 srv.requests = append(srv.requests, r) 553 requestBody, err := ioutil.ReadAll(r.Body) 554 if err != nil { 555 panic(err) 556 } 557 srv.requestBodies = append(srv.requestBodies, requestBody) 558 _, err = w.Write(srv.response) 559 if err != nil { 560 panic(err) 561 } 562 } 563 564 func (s *RegisterSuite) apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 565 if s.apiOpenError != nil { 566 return nil, s.apiOpenError 567 } 568 return s.apiConnection, nil 569 } 570 571 func (s *RegisterSuite) run(c *gc.C, stdio io.ReadWriter, args ...string) error { 572 if stdio == nil { 573 p := noPrompts(c) 574 stdio = p 575 defer p.CheckDone() 576 } 577 578 command := controller.NewRegisterCommandForTest(s.apiOpen, s.listModels, s.store) 579 err := testing.InitCommand(command, args) 580 c.Assert(err, jc.ErrorIsNil) 581 return command.Run(&cmd.Context{ 582 Dir: c.MkDir(), 583 Stdin: stdio, 584 Stdout: stdio, 585 Stderr: stdio, 586 }) 587 } 588 589 func noPrompts(c *gc.C) *cmdtesting.SeqPrompter { 590 return cmdtesting.NewSeqPrompter(c, "»", "") 591 } 592 593 func (s *RegisterSuite) seal(c *gc.C, message, key, nonce []byte) []byte { 594 var keyArray [32]byte 595 var nonceArray [24]byte 596 c.Assert(copy(keyArray[:], key), gc.Equals, len(keyArray)) 597 c.Assert(copy(nonceArray[:], nonce), gc.Equals, len(nonceArray)) 598 return secretbox.Seal(nil, message, &nonceArray, &keyArray) 599 }