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 := &params.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  }