github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/state/apiserver/login_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "net" 8 "strconv" 9 "sync" 10 "time" 11 12 "github.com/juju/loggo" 13 jc "github.com/juju/testing/checkers" 14 "github.com/juju/utils" 15 gc "launchpad.net/gocheck" 16 17 "github.com/juju/juju/instance" 18 jujutesting "github.com/juju/juju/juju/testing" 19 "github.com/juju/juju/state" 20 "github.com/juju/juju/state/api" 21 "github.com/juju/juju/state/api/params" 22 "github.com/juju/juju/state/apiserver" 23 coretesting "github.com/juju/juju/testing" 24 ) 25 26 type loginSuite struct { 27 jujutesting.JujuConnSuite 28 } 29 30 var _ = gc.Suite(&loginSuite{}) 31 32 var badLoginTests = []struct { 33 tag string 34 password string 35 err string 36 code string 37 }{{ 38 tag: "user-admin", 39 password: "wrong password", 40 err: "invalid entity name or password", 41 code: params.CodeUnauthorized, 42 }, { 43 tag: "user-foo", 44 password: "password", 45 err: "invalid entity name or password", 46 code: params.CodeUnauthorized, 47 }, { 48 tag: "bar", 49 password: "password", 50 err: `"bar" is not a valid tag`, 51 }} 52 53 func (s *loginSuite) setupServer(c *gc.C) (*api.Info, func()) { 54 srv, err := apiserver.NewServer( 55 s.State, 56 "localhost:0", 57 []byte(coretesting.ServerCert), 58 []byte(coretesting.ServerKey), 59 "", "", 60 ) 61 c.Assert(err, gc.IsNil) 62 env, err := s.State.Environment() 63 c.Assert(err, gc.IsNil) 64 info := &api.Info{ 65 Tag: "", 66 Password: "", 67 EnvironTag: env.Tag(), 68 Addrs: []string{srv.Addr()}, 69 CACert: coretesting.CACert, 70 } 71 return info, func() { 72 err := srv.Stop() 73 c.Assert(err, gc.IsNil) 74 } 75 } 76 77 func (s *loginSuite) setupMachineAndServer(c *gc.C) (*api.Info, func()) { 78 machine, err := s.State.AddMachine("quantal", state.JobHostUnits) 79 c.Assert(err, gc.IsNil) 80 err = machine.SetProvisioned("foo", "fake_nonce", nil) 81 c.Assert(err, gc.IsNil) 82 password, err := utils.RandomPassword() 83 c.Assert(err, gc.IsNil) 84 err = machine.SetPassword(password) 85 c.Assert(err, gc.IsNil) 86 info, cleanup := s.setupServer(c) 87 info.Tag = machine.Tag() 88 info.Password = password 89 info.Nonce = "fake_nonce" 90 return info, cleanup 91 } 92 93 func (s *loginSuite) TestBadLogin(c *gc.C) { 94 // Start our own server so we can control when the first login 95 // happens. Otherwise in JujuConnSuite.SetUpTest api.Open is 96 // called with user-admin permissions automatically. 97 info, cleanup := s.setupServer(c) 98 defer cleanup() 99 100 for i, t := range badLoginTests { 101 c.Logf("test %d; entity %q; password %q", i, t.tag, t.password) 102 // Note that Open does not log in if the tag and password 103 // are empty. This allows us to test operations on the connection 104 // before calling Login, which we could not do if Open 105 // always logged in. 106 info.Tag = "" 107 info.Password = "" 108 func() { 109 st, err := api.Open(info, fastDialOpts) 110 c.Assert(err, gc.IsNil) 111 defer st.Close() 112 113 _, err = st.Machiner().Machine("0") 114 c.Assert(err, gc.ErrorMatches, `unknown object type "Machiner"`) 115 116 // Since these are user login tests, the nonce is empty. 117 err = st.Login(t.tag, t.password, "") 118 c.Assert(err, gc.ErrorMatches, t.err) 119 c.Assert(params.ErrCode(err), gc.Equals, t.code) 120 121 _, err = st.Machiner().Machine("0") 122 c.Assert(err, gc.ErrorMatches, `unknown object type "Machiner"`) 123 }() 124 } 125 } 126 127 func (s *loginSuite) TestLoginAsDeactivatedUser(c *gc.C) { 128 info, cleanup := s.setupServer(c) 129 defer cleanup() 130 131 info.Tag = "" 132 info.Password = "" 133 st, err := api.Open(info, fastDialOpts) 134 c.Assert(err, gc.IsNil) 135 defer st.Close() 136 u := s.AddUser(c, "inactive") 137 err = u.Deactivate() 138 c.Assert(err, gc.IsNil) 139 140 _, err = st.Client().Status([]string{}) 141 c.Assert(err, gc.ErrorMatches, `unknown object type "Client"`) 142 143 // Since these are user login tests, the nonce is empty. 144 err = st.Login("user-inactive", "password", "") 145 c.Assert(err, gc.ErrorMatches, "invalid entity name or password") 146 147 _, err = st.Client().Status([]string{}) 148 c.Assert(err, gc.ErrorMatches, `unknown object type "Client"`) 149 } 150 151 func (s *loginSuite) TestLoginSetsLogIdentifier(c *gc.C) { 152 info, cleanup := s.setupServer(c) 153 defer cleanup() 154 155 machineInState, err := s.State.AddMachine("quantal", state.JobHostUnits) 156 c.Assert(err, gc.IsNil) 157 err = machineInState.SetProvisioned("foo", "fake_nonce", nil) 158 c.Assert(err, gc.IsNil) 159 password, err := utils.RandomPassword() 160 c.Assert(err, gc.IsNil) 161 err = machineInState.SetPassword(password) 162 c.Assert(err, gc.IsNil) 163 c.Assert(machineInState.Tag(), gc.Equals, "machine-0") 164 165 tw := &loggo.TestWriter{} 166 c.Assert(loggo.RegisterWriter("login-tester", tw, loggo.DEBUG), gc.IsNil) 167 defer loggo.RemoveWriter("login-tester") 168 169 info.Tag = machineInState.Tag() 170 info.Password = password 171 info.Nonce = "fake_nonce" 172 173 apiConn, err := api.Open(info, fastDialOpts) 174 c.Assert(err, gc.IsNil) 175 apiMachine, err := apiConn.Machiner().Machine(machineInState.Tag()) 176 c.Assert(err, gc.IsNil) 177 c.Assert(apiMachine.Tag(), gc.Equals, machineInState.Tag()) 178 apiConn.Close() 179 180 c.Assert(tw.Log, jc.LogMatches, []string{ 181 `<- \[[0-9A-F]+\] <unknown> {"RequestId":1,"Type":"Admin","Request":"Login","Params":` + 182 `{"AuthTag":"machine-0","Password":"[^"]*","Nonce":"fake_nonce"}` + 183 `}`, 184 // Now that we are logged in, we see the entity's tag 185 // [0-9.umns] is to handle timestamps that are ns, us, ms, or s 186 // long, though we expect it to be in the 'ms' range. 187 `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":.*} Admin\[""\].Login`, 188 `<- \[[0-9A-F]+\] machine-0 {"RequestId":2,"Type":"Machiner","Request":"Life","Params":{"Entities":\[{"Tag":"machine-0"}\]}}`, 189 `-> \[[0-9A-F]+\] machine-0 [0-9.umns]+ {"RequestId":2,"Response":{"Results":\[{"Life":"alive","Error":null}\]}} Machiner\[""\]\.Life`, 190 }) 191 } 192 193 func (s *loginSuite) TestLoginAddrs(c *gc.C) { 194 info, cleanup := s.setupMachineAndServer(c) 195 defer cleanup() 196 197 // Initially just the address we connect with is returned, 198 // despite there being no APIHostPorts in state. 199 connectedAddr, hostPorts := s.loginHostPorts(c, info) 200 connectedAddrHost, connectedAddrPortString, err := net.SplitHostPort(connectedAddr) 201 c.Assert(err, gc.IsNil) 202 connectedAddrPort, err := strconv.Atoi(connectedAddrPortString) 203 c.Assert(err, gc.IsNil) 204 connectedAddrHostPorts := [][]instance.HostPort{ 205 []instance.HostPort{{ 206 instance.NewAddress(connectedAddrHost, instance.NetworkUnknown), 207 connectedAddrPort, 208 }}, 209 } 210 c.Assert(hostPorts, gc.DeepEquals, connectedAddrHostPorts) 211 212 // After storing APIHostPorts in state, Login should store 213 // all of them and the address we connected with. 214 server1Addresses := []instance.Address{{ 215 Value: "server-1", 216 Type: instance.HostName, 217 NetworkScope: instance.NetworkPublic, 218 }, { 219 Value: "10.0.0.1", 220 Type: instance.Ipv4Address, 221 NetworkName: "internal", 222 NetworkScope: instance.NetworkCloudLocal, 223 }} 224 server2Addresses := []instance.Address{{ 225 Value: "::1", 226 Type: instance.Ipv6Address, 227 NetworkName: "loopback", 228 NetworkScope: instance.NetworkMachineLocal, 229 }} 230 stateAPIHostPorts := [][]instance.HostPort{ 231 instance.AddressesWithPort(server1Addresses, 123), 232 instance.AddressesWithPort(server2Addresses, 456), 233 } 234 err = s.State.SetAPIHostPorts(stateAPIHostPorts) 235 c.Assert(err, gc.IsNil) 236 connectedAddr, hostPorts = s.loginHostPorts(c, info) 237 // Now that we connected, we add the other stateAPIHostPorts. However, 238 // the one we connected to comes first. 239 stateAPIHostPorts = append(connectedAddrHostPorts, stateAPIHostPorts...) 240 c.Assert(hostPorts, gc.DeepEquals, stateAPIHostPorts) 241 } 242 243 func (s *loginSuite) loginHostPorts(c *gc.C, info *api.Info) (connectedAddr string, hostPorts [][]instance.HostPort) { 244 st, err := api.Open(info, fastDialOpts) 245 c.Assert(err, gc.IsNil) 246 defer st.Close() 247 return st.Addr(), st.APIHostPorts() 248 } 249 250 func startNLogins(c *gc.C, n int, info *api.Info) (chan error, *sync.WaitGroup) { 251 errResults := make(chan error, 100) 252 var doneWG sync.WaitGroup 253 var startedWG sync.WaitGroup 254 c.Logf("starting %d concurrent logins to %v", n, info.Addrs) 255 for i := 0; i < n; i++ { 256 i := i 257 c.Logf("starting login request %d", i) 258 startedWG.Add(1) 259 doneWG.Add(1) 260 go func() { 261 c.Logf("started login %d", i) 262 startedWG.Done() 263 st, err := api.Open(info, fastDialOpts) 264 errResults <- err 265 if err == nil { 266 st.Close() 267 } 268 doneWG.Done() 269 c.Logf("finished login %d: %v", i, err) 270 }() 271 } 272 startedWG.Wait() 273 return errResults, &doneWG 274 } 275 276 func (s *loginSuite) TestDelayLogins(c *gc.C) { 277 info, cleanup := s.setupMachineAndServer(c) 278 defer cleanup() 279 delayChan, cleanup := apiserver.DelayLogins() 280 defer cleanup() 281 282 // numConcurrentLogins is how many logins will fire off simultaneously. 283 // It doesn't really matter, as long as it is less than LoginRateLimit 284 const numConcurrentLogins = 5 285 c.Assert(numConcurrentLogins, jc.LessThan, apiserver.LoginRateLimit) 286 // Trigger a bunch of login requests 287 errResults, wg := startNLogins(c, numConcurrentLogins, info) 288 select { 289 case err := <-errResults: 290 c.Fatalf("we should not have gotten any logins yet: %v", err) 291 case <-time.After(coretesting.ShortWait): 292 } 293 // Allow one login to proceed 294 c.Logf("letting one login through") 295 select { 296 case delayChan <- struct{}{}: 297 default: 298 c.Fatalf("we should have been able to unblock a login") 299 } 300 select { 301 case err := <-errResults: 302 c.Check(err, gc.IsNil) 303 case <-time.After(coretesting.LongWait): 304 c.Fatalf("timed out while waiting for Login to finish") 305 } 306 c.Logf("checking no other logins succeeded") 307 // It should have only let 1 login through 308 select { 309 case err := <-errResults: 310 c.Fatalf("we should not have gotten more logins: %v", err) 311 case <-time.After(coretesting.ShortWait): 312 } 313 // Now allow the rest of the logins to proceed 314 c.Logf("letting %d logins through", numConcurrentLogins-1) 315 for i := 0; i < numConcurrentLogins-1; i++ { 316 delayChan <- struct{}{} 317 } 318 c.Logf("waiting for Logins to finish") 319 wg.Wait() 320 close(errResults) 321 successCount := 0 322 for err := range errResults { 323 c.Check(err, gc.IsNil) 324 if err == nil { 325 successCount += 1 326 } 327 } 328 // All the logins should succeed, they were just delayed after 329 // connecting. 330 c.Check(successCount, gc.Equals, numConcurrentLogins-1) 331 c.Logf("done") 332 } 333 334 func (s *loginSuite) TestLoginRateLimited(c *gc.C) { 335 info, cleanup := s.setupMachineAndServer(c) 336 defer cleanup() 337 delayChan, cleanup := apiserver.DelayLogins() 338 defer cleanup() 339 340 // Start enough concurrent Login requests so that we max out our 341 // LoginRateLimit 342 errResults, wg := startNLogins(c, apiserver.LoginRateLimit, info) 343 // All of them should have started, none of them should have succeeded 344 // (or failed) yet 345 select { 346 case err := <-errResults: 347 c.Fatalf("we should not have gotten any logins yet: %v", err) 348 case <-time.After(coretesting.ShortWait): 349 } 350 // We now have a bunch of pending Login requests, the next login 351 // request should be immediately bounced 352 _, err := api.Open(info, fastDialOpts) 353 c.Check(err, gc.ErrorMatches, "try again") 354 c.Check(err, jc.Satisfies, params.IsCodeTryAgain) 355 // Let one request through, we should see that it succeeds without 356 // error, and then be able to start a new request, but it will block 357 delayChan <- struct{}{} 358 select { 359 case err := <-errResults: 360 c.Check(err, gc.IsNil) 361 case <-time.After(coretesting.LongWait): 362 c.Fatalf("timed out expecting one login to succeed") 363 } 364 chOne := make(chan error, 1) 365 wg.Add(1) 366 go func() { 367 st, err := api.Open(info, fastDialOpts) 368 chOne <- err 369 if err == nil { 370 st.Close() 371 } 372 wg.Done() 373 }() 374 select { 375 case err := <-chOne: 376 c.Fatalf("the open request should not have completed: %v", err) 377 case <-time.After(coretesting.ShortWait): 378 } 379 // Let all the logins finish. We started with LoginRateLimit, let one 380 // proceed, but the issued another one, so there should be 381 // LoginRateLimit logins pending. 382 for i := 0; i < apiserver.LoginRateLimit; i++ { 383 delayChan <- struct{}{} 384 } 385 wg.Wait() 386 close(errResults) 387 for err := range errResults { 388 c.Check(err, gc.IsNil) 389 } 390 } 391 392 func (s *loginSuite) TestUsersLoginWhileRateLimited(c *gc.C) { 393 info, cleanup := s.setupMachineAndServer(c) 394 defer cleanup() 395 delayChan, cleanup := apiserver.DelayLogins() 396 defer cleanup() 397 398 // Start enough concurrent Login requests so that we max out our 399 // LoginRateLimit. Do one extra so we know we are in overload 400 machineResults, machineWG := startNLogins(c, apiserver.LoginRateLimit+1, info) 401 select { 402 case err := <-machineResults: 403 c.Check(err, jc.Satisfies, params.IsCodeTryAgain) 404 case <-time.After(coretesting.LongWait): 405 c.Fatalf("timed out waiting for login to get rejected.") 406 } 407 408 userInfo := *info 409 userInfo.Tag = "user-admin" 410 userInfo.Password = "dummy-secret" 411 userResults, userWG := startNLogins(c, apiserver.LoginRateLimit+1, &userInfo) 412 // all of them should have started, and none of them in TryAgain state 413 select { 414 case err := <-userResults: 415 c.Fatalf("we should not have gotten any logins yet: %v", err) 416 case <-time.After(coretesting.ShortWait): 417 } 418 totalLogins := apiserver.LoginRateLimit*2 + 1 419 for i := 0; i < totalLogins; i++ { 420 delayChan <- struct{}{} 421 } 422 machineWG.Wait() 423 close(machineResults) 424 userWG.Wait() 425 close(userResults) 426 machineCount := 0 427 for err := range machineResults { 428 machineCount += 1 429 c.Check(err, gc.IsNil) 430 } 431 c.Check(machineCount, gc.Equals, apiserver.LoginRateLimit) 432 userCount := 0 433 for err := range userResults { 434 userCount += 1 435 c.Check(err, gc.IsNil) 436 } 437 c.Check(userCount, gc.Equals, apiserver.LoginRateLimit+1) 438 } 439 440 func (s *loginSuite) TestUsersAreNotRateLimited(c *gc.C) { 441 info, cleanup := s.setupServer(c) 442 info.Tag = "user-admin" 443 info.Password = "dummy-secret" 444 defer cleanup() 445 delayChan, cleanup := apiserver.DelayLogins() 446 defer cleanup() 447 // We can login more than LoginRateLimit users 448 nLogins := apiserver.LoginRateLimit * 2 449 errResults, wg := startNLogins(c, nLogins, info) 450 select { 451 case err := <-errResults: 452 c.Fatalf("we should not have gotten any logins yet: %v", err) 453 case <-time.After(coretesting.ShortWait): 454 } 455 c.Logf("letting %d logins complete", nLogins) 456 for i := 0; i < nLogins; i++ { 457 delayChan <- struct{}{} 458 } 459 c.Logf("waiting for original requests to finish") 460 wg.Wait() 461 close(errResults) 462 for err := range errResults { 463 c.Check(err, gc.IsNil) 464 } 465 } 466 467 func (s *loginSuite) TestLoginReportsEnvironTag(c *gc.C) { 468 info, cleanup := s.setupServer(c) 469 defer cleanup() 470 // If we call api.Open without giving a username and password, then it 471 // won't call Login, so we can call it ourselves. 472 // We Login without passing an EnvironTag, to show that it still lets 473 // us in, and that we can find out the real EnvironTag from the 474 // response. 475 st, err := api.Open(info, fastDialOpts) 476 c.Assert(err, gc.IsNil) 477 var result params.LoginResult 478 creds := ¶ms.Creds{ 479 AuthTag: "user-admin", 480 Password: "dummy-secret", 481 } 482 err = st.Call("Admin", "", "Login", creds, &result) 483 c.Assert(err, gc.IsNil) 484 env, err := s.State.Environment() 485 c.Assert(err, gc.IsNil) 486 c.Assert(result.EnvironTag, gc.Equals, env.Tag()) 487 }