github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 "bytes" 8 "encoding/asn1" 9 "encoding/base64" 10 "encoding/json" 11 "io" 12 "io/ioutil" 13 "net/http" 14 "net/http/httptest" 15 "net/url" 16 "strings" 17 18 "github.com/juju/cmd" 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 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 "github.com/juju/juju/jujuclient" 29 "github.com/juju/juju/jujuclient/jujuclienttesting" 30 "github.com/juju/juju/testing" 31 ) 32 33 type RegisterSuite struct { 34 testing.FakeJujuXDGDataHomeSuite 35 apiConnection *mockAPIConnection 36 store *jujuclienttesting.MemStore 37 apiOpenError error 38 listModels func(jujuclient.ClientStore, string, string) ([]base.UserModel, error) 39 listModelsControllerName string 40 listModelsUserName string 41 server *httptest.Server 42 httpHandler http.Handler 43 } 44 45 var _ = gc.Suite(&RegisterSuite{}) 46 47 func (s *RegisterSuite) SetUpTest(c *gc.C) { 48 s.FakeJujuXDGDataHomeSuite.SetUpTest(c) 49 50 s.apiOpenError = nil 51 s.httpHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 52 s.server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 s.httpHandler.ServeHTTP(w, r) 54 })) 55 56 serverURL, err := url.Parse(s.server.URL) 57 c.Assert(err, jc.ErrorIsNil) 58 s.apiConnection = &mockAPIConnection{ 59 controllerTag: testing.ControllerTag, 60 addr: serverURL.Host, 61 } 62 s.listModelsControllerName = "" 63 s.listModelsUserName = "" 64 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 65 s.listModelsControllerName = controllerName 66 s.listModelsUserName = userName 67 return nil, nil 68 } 69 70 s.store = jujuclienttesting.NewMemStore() 71 } 72 73 func (s *RegisterSuite) TearDownTest(c *gc.C) { 74 s.server.Close() 75 s.FakeJujuXDGDataHomeSuite.TearDownTest(c) 76 } 77 78 func (s *RegisterSuite) apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 79 if s.apiOpenError != nil { 80 return nil, s.apiOpenError 81 } 82 s.apiConnection.info = info 83 s.apiConnection.opts = opts 84 return s.apiConnection, nil 85 } 86 87 func (s *RegisterSuite) run(c *gc.C, stdin io.Reader, args ...string) (*cmd.Context, error) { 88 command := controller.NewRegisterCommandForTest(s.apiOpen, s.listModels, s.store) 89 err := testing.InitCommand(command, args) 90 c.Assert(err, jc.ErrorIsNil) 91 ctx := testing.Context(c) 92 ctx.Stdin = stdin 93 return ctx, command.Run(ctx) 94 } 95 96 func (s *RegisterSuite) encodeRegistrationData(c *gc.C, user string, secretKey []byte) string { 97 data, err := asn1.Marshal(jujuclient.RegistrationInfo{ 98 User: user, 99 Addrs: []string{s.apiConnection.addr}, 100 SecretKey: secretKey, 101 }) 102 c.Assert(err, jc.ErrorIsNil) 103 // Append some junk to the end of the encoded data to 104 // ensure that, if we have to pad the data in add-user, 105 // register can still decode it. 106 data = append(data, 0, 0, 0) 107 return base64.URLEncoding.EncodeToString(data) 108 } 109 110 func (s *RegisterSuite) encodeRegistrationDataWithControllerName(c *gc.C, user string, secretKey []byte, controller string) string { 111 data, err := asn1.Marshal(jujuclient.RegistrationInfo{ 112 User: user, 113 Addrs: []string{s.apiConnection.addr}, 114 SecretKey: secretKey, 115 ControllerName: controller, 116 }) 117 c.Assert(err, jc.ErrorIsNil) 118 // Append some junk to the end of the encoded data to 119 // ensure that, if we have to pad the data in add-user, 120 // register can still decode it. 121 data = append(data, 0, 0, 0) 122 return base64.URLEncoding.EncodeToString(data) 123 } 124 125 func (s *RegisterSuite) seal(c *gc.C, message, key, nonce []byte) []byte { 126 var keyArray [32]byte 127 var nonceArray [24]byte 128 c.Assert(copy(keyArray[:], key), gc.Equals, len(keyArray)) 129 c.Assert(copy(nonceArray[:], nonce), gc.Equals, len(nonceArray)) 130 return secretbox.Seal(nil, message, &nonceArray, &keyArray) 131 } 132 133 func (s *RegisterSuite) TestInit(c *gc.C) { 134 registerCommand := controller.NewRegisterCommandForTest(nil, nil, nil) 135 136 err := testing.InitCommand(registerCommand, []string{}) 137 c.Assert(err, gc.ErrorMatches, "registration data missing") 138 139 err = testing.InitCommand(registerCommand, []string{"foo"}) 140 c.Assert(err, jc.ErrorIsNil) 141 c.Assert(registerCommand.EncodedData, gc.Equals, "foo") 142 143 err = testing.InitCommand(registerCommand, []string{"foo", "bar"}) 144 c.Assert(err, gc.ErrorMatches, `unrecognized args: \["bar"\]`) 145 } 146 147 func (s *RegisterSuite) TestRegister(c *gc.C) { 148 ctx := s.testRegister(c, "") 149 c.Assert(s.listModelsControllerName, gc.Equals, "controller-name") 150 c.Assert(s.listModelsUserName, gc.Equals, "bob@local") 151 stderr := testing.Stderr(ctx) 152 c.Assert(stderr, gc.Equals, ` 153 Enter a name for this controller [controller-name]: 154 Enter a new password: 155 Confirm password: 156 157 Welcome, bob. You are now logged into "controller-name". 158 159 There are no models available. You can add models with 160 "juju add-model", or you can ask an administrator or owner 161 of a model to grant access to that model with "juju grant". 162 163 `[1:]) 164 } 165 166 func (s *RegisterSuite) TestRegisterOneModel(c *gc.C) { 167 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 168 return []base.UserModel{{ 169 Name: "theoneandonly", 170 Owner: "carol@local", 171 UUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 172 }}, nil 173 } 174 s.testRegister(c, "") 175 c.Assert( 176 s.store.Models["controller-name"].CurrentModel, 177 gc.Equals, "carol@local/theoneandonly", 178 ) 179 } 180 181 func (s *RegisterSuite) TestRegisterMultipleModels(c *gc.C) { 182 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 183 return []base.UserModel{{ 184 Name: "model1", 185 Owner: "bob@local", 186 UUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 187 }, { 188 Name: "model2", 189 Owner: "bob@local", 190 UUID: "df136476-12e9-11e4-8a70-b2227cce2b55", 191 }}, nil 192 } 193 ctx := s.testRegister(c, "") 194 195 // When there are multiple models, no current model will be set. 196 // Instead, the command will output the list of models and inform 197 // the user how to set the current model. 198 _, err := s.store.CurrentModel("controller-name") 199 c.Assert(err, jc.Satisfies, errors.IsNotFound) 200 201 stderr := testing.Stderr(ctx) 202 c.Assert(stderr, gc.Equals, ` 203 Enter a name for this controller [controller-name]: 204 Enter a new password: 205 Confirm password: 206 207 Welcome, bob. You are now logged into "controller-name". 208 209 There are 2 models available. Use "juju switch" to select 210 one of them: 211 - juju switch model1 212 - juju switch model2 213 214 `[1:]) 215 } 216 217 func (s *RegisterSuite) testRegister(c *gc.C, expectedError string) *cmd.Context { 218 secretKey := []byte(strings.Repeat("X", 32)) 219 respNonce := []byte(strings.Repeat("X", 24)) 220 221 var requests []*http.Request 222 var requestBodies [][]byte 223 const controllerUUID = "df136476-12e9-11e4-8a70-b2227cce2b54" 224 responsePayloadPlaintext, err := json.Marshal(params.SecretKeyLoginResponsePayload{ 225 CACert: testing.CACert, 226 ControllerUUID: controllerUUID, 227 }) 228 c.Assert(err, jc.ErrorIsNil) 229 response, err := json.Marshal(params.SecretKeyLoginResponse{ 230 Nonce: respNonce, 231 PayloadCiphertext: s.seal(c, responsePayloadPlaintext, secretKey, respNonce), 232 }) 233 c.Assert(err, jc.ErrorIsNil) 234 s.httpHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 235 requests = append(requests, r) 236 requestBody, err := ioutil.ReadAll(requests[0].Body) 237 c.Check(err, jc.ErrorIsNil) 238 requestBodies = append(requestBodies, requestBody) 239 _, err = w.Write(response) 240 c.Check(err, jc.ErrorIsNil) 241 }) 242 243 registrationData := s.encodeRegistrationDataWithControllerName(c, "bob", secretKey, "controller-name") 244 stdin := strings.NewReader("\nhunter2\nhunter2\n") 245 ctx, err := s.run(c, stdin, registrationData) 246 if expectedError != "" { 247 c.Assert(err, gc.ErrorMatches, expectedError) 248 return ctx 249 } 250 c.Assert(err, jc.ErrorIsNil) 251 252 // There should have been one POST command to "/register". 253 c.Assert(requests, gc.HasLen, 1) 254 c.Assert(requests[0].Method, gc.Equals, "POST") 255 c.Assert(requests[0].URL.Path, gc.Equals, "/register") 256 var request params.SecretKeyLoginRequest 257 err = json.Unmarshal(requestBodies[0], &request) 258 c.Assert(err, jc.ErrorIsNil) 259 c.Assert(request.User, jc.DeepEquals, "user-bob") 260 c.Assert(request.Nonce, gc.HasLen, 24) 261 requestPayloadPlaintext, err := json.Marshal(params.SecretKeyLoginRequestPayload{ 262 "hunter2", 263 }) 264 c.Assert(err, jc.ErrorIsNil) 265 expectedCiphertext := s.seal(c, requestPayloadPlaintext, secretKey, request.Nonce) 266 c.Assert(request.PayloadCiphertext, jc.DeepEquals, expectedCiphertext) 267 268 // The controller and account details should be recorded with 269 // the specified controller name ("controller-name") and user 270 // name from the registration string. 271 272 controller, err := s.store.ControllerByName("controller-name") 273 c.Assert(err, jc.ErrorIsNil) 274 c.Assert(controller, jc.DeepEquals, &jujuclient.ControllerDetails{ 275 ControllerUUID: controllerUUID, 276 APIEndpoints: []string{s.apiConnection.addr}, 277 CACert: testing.CACert, 278 }) 279 account, err := s.store.AccountDetails("controller-name") 280 c.Assert(err, jc.ErrorIsNil) 281 c.Assert(account, jc.DeepEquals, &jujuclient.AccountDetails{ 282 User: "bob@local", 283 LastKnownAccess: "login", 284 }) 285 return ctx 286 } 287 288 func (s *RegisterSuite) TestRegisterInvalidRegistrationData(c *gc.C) { 289 _, err := s.run(c, bytes.NewReader(nil), "not base64") 290 c.Assert(err, gc.ErrorMatches, "illegal base64 data at input byte 3") 291 292 _, err = s.run(c, bytes.NewReader(nil), "YXNuLjEK") 293 c.Assert(err, gc.ErrorMatches, "asn1: structure error: .*") 294 } 295 296 func (s *RegisterSuite) TestRegisterEmptyControllerName(c *gc.C) { 297 secretKey := []byte(strings.Repeat("X", 32)) 298 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 299 stdin := strings.NewReader("\n") 300 _, err := s.run(c, stdin, registrationData) 301 c.Assert(err, gc.ErrorMatches, "you must specify a non-empty controller name") 302 } 303 304 func (s *RegisterSuite) TestRegisterControllerNameExists(c *gc.C) { 305 err := s.store.AddController("controller-name", jujuclient.ControllerDetails{ 306 ControllerUUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 307 CACert: testing.CACert, 308 }) 309 310 secretKey := []byte(strings.Repeat("X", 32)) 311 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 312 stdin := strings.NewReader("controller-name\nhunter2\nhunter2\n") 313 _, err = s.run(c, stdin, registrationData) 314 c.Assert(err, gc.ErrorMatches, `controller "controller-name" already exists`) 315 } 316 317 func (s *RegisterSuite) TestControllerUUIDExists(c *gc.C) { 318 // Controller has the UUID from s.testRegister to mimic a user with 319 // this controller already registered (regardless of its name). 320 err := s.store.AddController("controller-name", jujuclient.ControllerDetails{ 321 ControllerUUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 322 CACert: testing.CACert, 323 }) 324 325 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 326 return []base.UserModel{{ 327 Name: "model-name", 328 Owner: "bob@local", 329 UUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 330 }}, nil 331 } 332 333 s.testRegister(c, "you must specify a non-empty controller name") 334 335 secretKey := []byte(strings.Repeat("X", 32)) 336 registrationData := s.encodeRegistrationDataWithControllerName(c, "bob", secretKey, "controller-name") 337 338 stdin := strings.NewReader("another-controller-name\nhunter2\nhunter2\n") 339 _, err = s.run(c, stdin, registrationData) 340 c.Assert(err, gc.ErrorMatches, "controller with UUID.*already exists") 341 } 342 343 func (s *RegisterSuite) TestProposedControllerNameExists(c *gc.C) { 344 // Controller does not have the UUID from s.testRegister, thereby 345 // mimicing a user with an already registered 'foreign' controller. 346 err := s.store.AddController("controller-name", jujuclient.ControllerDetails{ 347 ControllerUUID: "0d75314a-5266-4f4f-8523-415be76f92dc", 348 CACert: testing.CACert, 349 }) 350 351 s.listModels = func(_ jujuclient.ClientStore, controllerName, userName string) ([]base.UserModel, error) { 352 return []base.UserModel{{ 353 Name: "model-name", 354 Owner: "bob@local", 355 UUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 356 }}, nil 357 } 358 359 ctx := s.testRegister(c, "you must specify a non-empty controller name") 360 361 secretKey := []byte(strings.Repeat("X", 32)) 362 registrationData := s.encodeRegistrationDataWithControllerName(c, "bob", secretKey, "controller-name") 363 364 stdin := strings.NewReader("another-controller-name\nhunter2\nhunter2\n") 365 _, err = s.run(c, stdin, registrationData) 366 c.Assert(err, jc.ErrorIsNil) 367 stderr := testing.Stderr(ctx) 368 c.Assert(stderr, gc.Equals, ` 369 WARNING: You already have a controller registered with the name "controller-name". Please choose a different name for the new controller. 370 371 Enter a name for this controller: 372 `[1:]) 373 374 } 375 376 func (s *RegisterSuite) TestRegisterEmptyPassword(c *gc.C) { 377 secretKey := []byte(strings.Repeat("X", 32)) 378 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 379 stdin := strings.NewReader("controller-name\n\n") 380 _, err := s.run(c, stdin, 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 secretKey := []byte(strings.Repeat("X", 32)) 386 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 387 stdin := strings.NewReader("controller-name\nhunter2\nhunter3\n") 388 _, err := s.run(c, stdin, registrationData) 389 c.Assert(err, gc.ErrorMatches, "passwords do not match") 390 } 391 392 func (s *RegisterSuite) TestAPIOpenError(c *gc.C) { 393 secretKey := []byte(strings.Repeat("X", 32)) 394 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 395 stdin := strings.NewReader("controller-name\nhunter2\nhunter2\n") 396 s.apiOpenError = errors.New("open failed") 397 _, err := s.run(c, stdin, registrationData) 398 c.Assert(err, gc.ErrorMatches, `open failed`) 399 } 400 401 func (s *RegisterSuite) TestRegisterServerError(c *gc.C) { 402 secretKey := []byte(strings.Repeat("X", 32)) 403 response, err := json.Marshal(params.ErrorResult{ 404 Error: ¶ms.Error{Message: "xyz", Code: "123"}, 405 }) 406 407 s.httpHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 408 w.WriteHeader(http.StatusInternalServerError) 409 _, err = w.Write(response) 410 c.Check(err, jc.ErrorIsNil) 411 }) 412 413 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 414 stdin := strings.NewReader("controller-name\nhunter2\nhunter2\n") 415 _, err = s.run(c, stdin, registrationData) 416 c.Assert(err, gc.ErrorMatches, "xyz") 417 418 _, err = s.store.ControllerByName("controller-name") 419 c.Assert(err, jc.Satisfies, errors.IsNotFound) 420 }