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