github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 "gopkg.in/macaroon.v1" 24 25 "github.com/juju/juju/api" 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 refreshModels func(jujuclient.ClientStore, string, string) error 39 refreshModelsControllerName string 40 refreshModelsAccountName 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.ModelTag, 60 addr: serverURL.Host, 61 } 62 s.refreshModelsControllerName = "" 63 s.refreshModelsAccountName = "" 64 s.refreshModels = func(store jujuclient.ClientStore, controllerName, accountName string) error { 65 s.refreshModelsControllerName = controllerName 66 s.refreshModelsAccountName = accountName 67 return 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.refreshModels, 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) seal(c *gc.C, message, key, nonce []byte) []byte { 111 var keyArray [32]byte 112 var nonceArray [24]byte 113 c.Assert(copy(keyArray[:], key), gc.Equals, len(keyArray)) 114 c.Assert(copy(nonceArray[:], nonce), gc.Equals, len(nonceArray)) 115 return secretbox.Seal(nil, message, &nonceArray, &keyArray) 116 } 117 118 func (s *RegisterSuite) TestInit(c *gc.C) { 119 registerCommand := controller.NewRegisterCommandForTest(nil, nil, nil) 120 121 err := testing.InitCommand(registerCommand, []string{}) 122 c.Assert(err, gc.ErrorMatches, "registration data missing") 123 124 err = testing.InitCommand(registerCommand, []string{"foo"}) 125 c.Assert(err, jc.ErrorIsNil) 126 c.Assert(registerCommand.EncodedData, gc.Equals, "foo") 127 128 err = testing.InitCommand(registerCommand, []string{"foo", "bar"}) 129 c.Assert(err, gc.ErrorMatches, `unrecognized args: \["bar"\]`) 130 } 131 132 func (s *RegisterSuite) TestRegister(c *gc.C) { 133 ctx := s.testRegister(c) 134 c.Assert(s.refreshModelsControllerName, gc.Equals, "controller-name") 135 c.Assert(s.refreshModelsAccountName, gc.Equals, "bob@local") 136 stderr := testing.Stderr(ctx) 137 c.Assert(stderr, gc.Equals, ` 138 Please set a name for this controller: 139 Enter password: 140 Confirm password: 141 142 Welcome, bob. You are now logged into "controller-name". 143 144 There are no models available. You can create models with 145 "juju create-model", or you can ask an administrator or owner 146 of a model to grant access to that model with "juju grant". 147 148 `[1:]) 149 } 150 151 func (s *RegisterSuite) TestRegisterOneModel(c *gc.C) { 152 s.refreshModels = func(store jujuclient.ClientStore, controller, account string) error { 153 err := store.UpdateModel(controller, account, "theoneandonly", jujuclient.ModelDetails{ 154 ModelUUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 155 }) 156 c.Assert(err, jc.ErrorIsNil) 157 return nil 158 } 159 s.testRegister(c) 160 c.Assert( 161 s.store.Models["controller-name"].AccountModels["bob@local"].CurrentModel, 162 gc.Equals, "theoneandonly", 163 ) 164 } 165 166 func (s *RegisterSuite) TestRegisterMultipleModels(c *gc.C) { 167 s.refreshModels = func(store jujuclient.ClientStore, controller, account string) error { 168 for _, name := range [...]string{"model1", "model2"} { 169 err := store.UpdateModel(controller, account, name, jujuclient.ModelDetails{ 170 ModelUUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 171 }) 172 c.Assert(err, jc.ErrorIsNil) 173 } 174 return nil 175 } 176 ctx := s.testRegister(c) 177 178 // When there are multiple models, no current model will be set. 179 // Instead, the command will output the list of models and inform 180 // the user how to set the current model. 181 _, err := s.store.CurrentModel("controller-name", "bob@local") 182 c.Assert(err, jc.Satisfies, errors.IsNotFound) 183 184 stderr := testing.Stderr(ctx) 185 c.Assert(stderr, gc.Equals, ` 186 Please set a name for this controller: 187 Enter password: 188 Confirm password: 189 190 Welcome, bob. You are now logged into "controller-name". 191 192 There are 2 models available. Use "juju switch" to select 193 one of them: 194 - juju switch model1 195 - juju switch model2 196 197 `[1:]) 198 } 199 200 func (s *RegisterSuite) testRegister(c *gc.C) *cmd.Context { 201 secretKey := []byte(strings.Repeat("X", 32)) 202 respNonce := []byte(strings.Repeat("X", 24)) 203 204 macaroon, err := macaroon.New(nil, "mymacaroon", "tone") 205 c.Assert(err, jc.ErrorIsNil) 206 macaroonJSON, err := macaroon.MarshalJSON() 207 c.Assert(err, jc.ErrorIsNil) 208 209 var requests []*http.Request 210 var requestBodies [][]byte 211 const controllerUUID = "df136476-12e9-11e4-8a70-b2227cce2b54" 212 responsePayloadPlaintext, err := json.Marshal(params.SecretKeyLoginResponsePayload{ 213 CACert: testing.CACert, 214 ControllerUUID: controllerUUID, 215 Macaroon: macaroon, 216 }) 217 c.Assert(err, jc.ErrorIsNil) 218 response, err := json.Marshal(params.SecretKeyLoginResponse{ 219 Nonce: respNonce, 220 PayloadCiphertext: s.seal(c, responsePayloadPlaintext, secretKey, respNonce), 221 }) 222 c.Assert(err, jc.ErrorIsNil) 223 s.httpHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 224 requests = append(requests, r) 225 requestBody, err := ioutil.ReadAll(requests[0].Body) 226 c.Check(err, jc.ErrorIsNil) 227 requestBodies = append(requestBodies, requestBody) 228 _, err = w.Write(response) 229 c.Check(err, jc.ErrorIsNil) 230 }) 231 232 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 233 stdin := strings.NewReader("controller-name\nhunter2\nhunter2\n") 234 ctx, err := s.run(c, stdin, registrationData) 235 c.Assert(err, jc.ErrorIsNil) 236 237 // There should have been one POST command to "/register". 238 c.Assert(requests, gc.HasLen, 1) 239 c.Assert(requests[0].Method, gc.Equals, "POST") 240 c.Assert(requests[0].URL.Path, gc.Equals, "/register") 241 var request params.SecretKeyLoginRequest 242 err = json.Unmarshal(requestBodies[0], &request) 243 c.Assert(err, jc.ErrorIsNil) 244 c.Assert(request.User, jc.DeepEquals, "user-bob") 245 c.Assert(request.Nonce, gc.HasLen, 24) 246 requestPayloadPlaintext, err := json.Marshal(params.SecretKeyLoginRequestPayload{ 247 "hunter2", 248 }) 249 c.Assert(err, jc.ErrorIsNil) 250 expectedCiphertext := s.seal(c, requestPayloadPlaintext, secretKey, request.Nonce) 251 c.Assert(request.PayloadCiphertext, jc.DeepEquals, expectedCiphertext) 252 253 // The controller and account details should be recorded with 254 // the specified controller name ("controller-name") and user 255 // name from the registration string. 256 257 controller, err := s.store.ControllerByName("controller-name") 258 c.Assert(err, jc.ErrorIsNil) 259 c.Assert(controller, jc.DeepEquals, &jujuclient.ControllerDetails{ 260 ControllerUUID: controllerUUID, 261 APIEndpoints: []string{s.apiConnection.addr}, 262 CACert: testing.CACert, 263 }) 264 account, err := s.store.AccountByName("controller-name", "bob@local") 265 c.Assert(err, jc.ErrorIsNil) 266 c.Assert(account, jc.DeepEquals, &jujuclient.AccountDetails{ 267 User: "bob@local", 268 Macaroon: string(macaroonJSON), 269 }) 270 return ctx 271 } 272 273 func (s *RegisterSuite) TestRegisterInvalidRegistrationData(c *gc.C) { 274 _, err := s.run(c, bytes.NewReader(nil), "not base64") 275 c.Assert(err, gc.ErrorMatches, "illegal base64 data at input byte 3") 276 277 _, err = s.run(c, bytes.NewReader(nil), "YXNuLjEK") 278 c.Assert(err, gc.ErrorMatches, "asn1: structure error: .*") 279 } 280 281 func (s *RegisterSuite) TestRegisterEmptyControllerName(c *gc.C) { 282 secretKey := []byte(strings.Repeat("X", 32)) 283 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 284 stdin := strings.NewReader("\n") 285 _, err := s.run(c, stdin, registrationData) 286 c.Assert(err, gc.ErrorMatches, "you must specify a non-empty controller name") 287 } 288 289 func (s *RegisterSuite) TestRegisterControllerNameExists(c *gc.C) { 290 err := s.store.UpdateController("controller-name", jujuclient.ControllerDetails{ 291 ControllerUUID: "df136476-12e9-11e4-8a70-b2227cce2b54", 292 CACert: testing.CACert, 293 }) 294 295 secretKey := []byte(strings.Repeat("X", 32)) 296 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 297 stdin := strings.NewReader("controller-name\nhunter2\nhunter2\n") 298 _, err = s.run(c, stdin, registrationData) 299 c.Assert(err, gc.ErrorMatches, `controller "controller-name" already exists`) 300 } 301 302 func (s *RegisterSuite) TestRegisterEmptyPassword(c *gc.C) { 303 secretKey := []byte(strings.Repeat("X", 32)) 304 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 305 stdin := strings.NewReader("controller-name\n\n") 306 _, err := s.run(c, stdin, registrationData) 307 c.Assert(err, gc.ErrorMatches, "you must specify a non-empty password") 308 } 309 310 func (s *RegisterSuite) TestRegisterPasswordMismatch(c *gc.C) { 311 secretKey := []byte(strings.Repeat("X", 32)) 312 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 313 stdin := strings.NewReader("controller-name\nhunter2\nhunter3\n") 314 _, err := s.run(c, stdin, registrationData) 315 c.Assert(err, gc.ErrorMatches, "passwords do not match") 316 } 317 318 func (s *RegisterSuite) TestAPIOpenError(c *gc.C) { 319 secretKey := []byte(strings.Repeat("X", 32)) 320 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 321 stdin := strings.NewReader("controller-name\nhunter2\nhunter2\n") 322 s.apiOpenError = errors.New("open failed") 323 _, err := s.run(c, stdin, registrationData) 324 c.Assert(err, gc.ErrorMatches, `open failed`) 325 } 326 327 func (s *RegisterSuite) TestRegisterServerError(c *gc.C) { 328 secretKey := []byte(strings.Repeat("X", 32)) 329 response, err := json.Marshal(params.ErrorResult{ 330 Error: ¶ms.Error{Message: "xyz", Code: "123"}, 331 }) 332 333 s.httpHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 334 w.WriteHeader(http.StatusInternalServerError) 335 _, err = w.Write(response) 336 c.Check(err, jc.ErrorIsNil) 337 }) 338 339 registrationData := s.encodeRegistrationData(c, "bob", secretKey) 340 stdin := strings.NewReader("controller-name\nhunter2\nhunter2\n") 341 _, err = s.run(c, stdin, registrationData) 342 c.Assert(err, gc.ErrorMatches, "xyz") 343 344 _, err = s.store.ControllerByName("controller-name") 345 c.Assert(err, jc.Satisfies, errors.IsNotFound) 346 }