github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/authentication/user_test.go (about)

     1  // Copyright 2014 Canonical Ltd. All rights reserved.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package authentication_test
     5  
     6  import (
     7  	"context"
     8  	"time"
     9  
    10  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery"
    11  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers"
    12  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker"
    13  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/bakerytest"
    14  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
    15  	"github.com/juju/clock/testclock"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/names/v5"
    18  	"github.com/juju/testing"
    19  	jc "github.com/juju/testing/checkers"
    20  	"github.com/juju/utils/v3"
    21  	gc "gopkg.in/check.v1"
    22  	"gopkg.in/macaroon.v2"
    23  
    24  	"github.com/juju/juju/apiserver/authentication"
    25  	apiservererrors "github.com/juju/juju/apiserver/errors"
    26  	jujutesting "github.com/juju/juju/juju/testing"
    27  	"github.com/juju/juju/state"
    28  	"github.com/juju/juju/testing/factory"
    29  )
    30  
    31  type userAuthenticatorSuite struct {
    32  	jujutesting.JujuConnSuite
    33  }
    34  
    35  var _ = gc.Suite(&userAuthenticatorSuite{})
    36  
    37  func (s *userAuthenticatorSuite) TestMachineLoginFails(c *gc.C) {
    38  	// add machine for testing machine agent authentication
    39  	machine, err := s.State.AddMachine(state.UbuntuBase("12.10"), state.JobHostUnits)
    40  	c.Assert(err, jc.ErrorIsNil)
    41  	nonce, err := utils.RandomPassword()
    42  	c.Assert(err, jc.ErrorIsNil)
    43  	err = machine.SetProvisioned("foo", "", nonce, nil)
    44  	c.Assert(err, jc.ErrorIsNil)
    45  	password, err := utils.RandomPassword()
    46  	c.Assert(err, jc.ErrorIsNil)
    47  	err = machine.SetPassword(password)
    48  	c.Assert(err, jc.ErrorIsNil)
    49  	machinePassword := password
    50  
    51  	// attempt machine login
    52  	authenticator := &authentication.LocalUserAuthenticator{}
    53  	_, err = authenticator.Authenticate(context.TODO(), nil, authentication.AuthParams{
    54  		AuthTag:     machine.Tag(),
    55  		Credentials: machinePassword,
    56  		Nonce:       nonce,
    57  	})
    58  	c.Assert(err, gc.ErrorMatches, "invalid request")
    59  }
    60  
    61  func (s *userAuthenticatorSuite) TestUnitLoginFails(c *gc.C) {
    62  	// add a unit for testing unit agent authentication
    63  	wordpress := s.AddTestingApplication(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
    64  	unit, err := wordpress.AddUnit(state.AddUnitParams{})
    65  	c.Assert(err, jc.ErrorIsNil)
    66  	password, err := utils.RandomPassword()
    67  	c.Assert(err, jc.ErrorIsNil)
    68  	err = unit.SetPassword(password)
    69  	c.Assert(err, jc.ErrorIsNil)
    70  	unitPassword := password
    71  
    72  	// Attempt unit login
    73  	authenticator := &authentication.LocalUserAuthenticator{}
    74  	_, err = authenticator.Authenticate(context.TODO(), nil, authentication.AuthParams{
    75  		AuthTag:     unit.UnitTag(),
    76  		Credentials: unitPassword,
    77  	})
    78  	c.Assert(err, gc.ErrorMatches, "invalid request")
    79  }
    80  
    81  func (s *userAuthenticatorSuite) TestValidUserLogin(c *gc.C) {
    82  	user := s.Factory.MakeUser(c, &factory.UserParams{
    83  		Name:        "bobbrown",
    84  		DisplayName: "Bob Brown",
    85  		Password:    "password",
    86  	})
    87  
    88  	// User login
    89  	authenticator := &authentication.LocalUserAuthenticator{}
    90  	_, err := authenticator.Authenticate(context.TODO(), s.State, authentication.AuthParams{
    91  		AuthTag:     user.Tag(),
    92  		Credentials: "password",
    93  	})
    94  	c.Assert(err, jc.ErrorIsNil)
    95  }
    96  
    97  func (s *userAuthenticatorSuite) TestUserLoginWrongPassword(c *gc.C) {
    98  	user := s.Factory.MakeUser(c, &factory.UserParams{
    99  		Name:        "bobbrown",
   100  		DisplayName: "Bob Brown",
   101  		Password:    "password",
   102  	})
   103  
   104  	// User login
   105  	authenticator := &authentication.LocalUserAuthenticator{}
   106  	_, err := authenticator.Authenticate(context.TODO(), s.State, authentication.AuthParams{
   107  		AuthTag:     user.Tag(),
   108  		Credentials: "wrongpassword",
   109  	})
   110  	c.Assert(err, gc.ErrorMatches, "invalid entity name or password")
   111  
   112  }
   113  
   114  func (s *userAuthenticatorSuite) TestInvalidRelationLogin(c *gc.C) {
   115  
   116  	// add relation
   117  	wordpress := s.AddTestingApplication(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
   118  	wordpressEP, err := wordpress.Endpoint("db")
   119  	c.Assert(err, jc.ErrorIsNil)
   120  	mysql := s.AddTestingApplication(c, "mysql", s.AddTestingCharm(c, "mysql"))
   121  	mysqlEP, err := mysql.Endpoint("server")
   122  	c.Assert(err, jc.ErrorIsNil)
   123  	relation, err := s.State.AddRelation(wordpressEP, mysqlEP)
   124  	c.Assert(err, jc.ErrorIsNil)
   125  
   126  	// Attempt relation login
   127  	authenticator := &authentication.LocalUserAuthenticator{}
   128  	_, err = authenticator.Authenticate(context.TODO(), nil, authentication.AuthParams{
   129  		AuthTag:     relation.Tag(),
   130  		Credentials: "dummy-secret",
   131  	})
   132  	c.Assert(err, gc.ErrorMatches, "invalid request")
   133  }
   134  
   135  func (s *userAuthenticatorSuite) TestValidMacaroonUserLogin(c *gc.C) {
   136  	user := s.Factory.MakeUser(c, &factory.UserParams{
   137  		Name: "bob",
   138  	})
   139  	mac, err := macaroon.New(nil, nil, "", macaroon.LatestVersion)
   140  	c.Assert(err, jc.ErrorIsNil)
   141  	err = mac.AddFirstPartyCaveat([]byte("declared username bob"))
   142  	c.Assert(err, jc.ErrorIsNil)
   143  	macaroons := []macaroon.Slice{{mac}}
   144  	service := mockBakeryService{}
   145  
   146  	// User login
   147  	authenticator := &authentication.LocalUserAuthenticator{Bakery: &service, Clock: testclock.NewClock(time.Time{})}
   148  	_, err = authenticator.Authenticate(context.TODO(), s.State, authentication.AuthParams{
   149  		AuthTag:   user.Tag(),
   150  		Macaroons: macaroons,
   151  	})
   152  	c.Assert(err, jc.ErrorIsNil)
   153  
   154  	service.CheckCallNames(c, "Auth")
   155  	call := service.Calls()[0]
   156  	c.Assert(call.Args, gc.HasLen, 1)
   157  	c.Assert(call.Args[0], jc.DeepEquals, macaroons)
   158  }
   159  
   160  func (s *userAuthenticatorSuite) TestCreateLocalLoginMacaroon(c *gc.C) {
   161  	service := mockBakeryService{}
   162  	clock := testclock.NewClock(time.Time{})
   163  	_, err := authentication.CreateLocalLoginMacaroon(
   164  		context.TODO(),
   165  		names.NewUserTag("bobbrown"), &service, clock, bakery.LatestVersion,
   166  	)
   167  	c.Assert(err, jc.ErrorIsNil)
   168  	service.CheckCallNames(c, "NewMacaroon")
   169  	service.CheckCall(c, 0, "NewMacaroon", []checkers.Caveat{
   170  		{Condition: "is-authenticated-user bobbrown"},
   171  		{Condition: "time-before 0001-01-01T00:02:00Z", Namespace: "std"},
   172  	})
   173  }
   174  
   175  func (s *userAuthenticatorSuite) TestAuthenticateLocalLoginMacaroon(c *gc.C) {
   176  	service := mockBakeryService{}
   177  	clock := testclock.NewClock(time.Time{})
   178  	authenticator := &authentication.LocalUserAuthenticator{
   179  		Bakery:                    &service,
   180  		Clock:                     clock,
   181  		LocalUserIdentityLocation: "https://testing.invalid:1234/auth",
   182  	}
   183  
   184  	service.SetErrors(nil, &bakery.VerificationError{})
   185  	_, err := authenticator.Authenticate(
   186  		context.TODO(),
   187  		authentication.EntityFinder(nil),
   188  		authentication.AuthParams{
   189  			AuthTag: names.NewUserTag("bobbrown"),
   190  		},
   191  	)
   192  	c.Assert(err, gc.FitsTypeOf, &apiservererrors.DischargeRequiredError{})
   193  
   194  	service.CheckCallNames(c, "Auth", "ExpireStorageAfter", "NewMacaroon")
   195  	calls := service.Calls()
   196  	c.Assert(calls[1].Args, jc.DeepEquals, []interface{}{24 * time.Hour})
   197  	c.Assert(calls[2].Args, jc.DeepEquals, []interface{}{
   198  		[]checkers.Caveat{
   199  			{Condition: "time-before 0001-01-02T00:00:00Z", Namespace: "std"},
   200  			checkers.NeedDeclaredCaveat(
   201  				checkers.Caveat{
   202  					Location:  "https://testing.invalid:1234/auth",
   203  					Condition: "is-authenticated-user bobbrown",
   204  					Namespace: "std",
   205  				},
   206  				"username",
   207  			),
   208  		},
   209  	})
   210  }
   211  
   212  type mockBakeryService struct {
   213  	testing.Stub
   214  }
   215  
   216  func (s *mockBakeryService) Auth(mss ...macaroon.Slice) *bakery.AuthChecker {
   217  	s.MethodCall(s, "Auth", mss)
   218  	checker := bakery.NewChecker(bakery.CheckerParams{
   219  		OpsAuthorizer:    mockAuthorizer{},
   220  		MacaroonVerifier: mockVerifier{},
   221  	})
   222  	return checker.Auth(mss...)
   223  }
   224  
   225  func (s *mockBakeryService) NewMacaroon(ctx context.Context, version bakery.Version, caveats []checkers.Caveat, ops ...bakery.Op) (*bakery.Macaroon, error) {
   226  	s.MethodCall(s, "NewMacaroon", caveats)
   227  	mac, err := macaroon.New(nil, nil, "", macaroon.LatestVersion)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	return bakery.NewLegacyMacaroon(mac)
   232  }
   233  
   234  func (s *mockBakeryService) ExpireStorageAfter(t time.Duration) (authentication.ExpirableStorageBakery, error) {
   235  	s.MethodCall(s, "ExpireStorageAfter", t)
   236  	return s, s.NextErr()
   237  }
   238  
   239  type mockAuthorizer struct{}
   240  
   241  func (mockAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) {
   242  	allowed := make([]bool, len(queryOps))
   243  	for i := range allowed {
   244  		allowed[i] = queryOps[i] == identchecker.LoginOp
   245  	}
   246  	return allowed, nil, nil
   247  }
   248  
   249  type mockVerifier struct{}
   250  
   251  func (mockVerifier) VerifyMacaroon(ctx context.Context, ms macaroon.Slice) ([]bakery.Op, []string, error) {
   252  	return []bakery.Op{identchecker.LoginOp}, []string{"declared username bob"}, nil
   253  }
   254  
   255  type macaroonAuthenticatorSuite struct {
   256  	jujutesting.JujuConnSuite
   257  	// username holds the username that will be
   258  	// declared in the discharger's caveats.
   259  	username string
   260  }
   261  
   262  var _ = gc.Suite(&macaroonAuthenticatorSuite{})
   263  
   264  var authenticateSuccessTests = []struct {
   265  	about              string
   266  	dischargedUsername string
   267  	finder             authentication.EntityFinder
   268  	expectTag          string
   269  	expectError        string
   270  }{{
   271  	about:              "user that can be found",
   272  	dischargedUsername: "bobbrown@somewhere",
   273  	expectTag:          "user-bobbrown@somewhere",
   274  	finder:             simpleEntityFinder{},
   275  }, {
   276  	about:              "user with no @ domain",
   277  	dischargedUsername: "bobbrown",
   278  	finder: simpleEntityFinder{
   279  		"user-bobbrown@external": true,
   280  	},
   281  	expectTag: "user-bobbrown@external",
   282  }, {
   283  	about:              "invalid user name",
   284  	dischargedUsername: "--",
   285  	finder:             simpleEntityFinder{},
   286  	expectError:        `"--" is an invalid user name`,
   287  }, {
   288  	about:              "ostensibly local name",
   289  	dischargedUsername: "cheat@local",
   290  	finder: simpleEntityFinder{
   291  		"cheat@local": true,
   292  	},
   293  	expectError: `external identity provider has provided ostensibly local name "cheat@local"`,
   294  }}
   295  
   296  type alwaysIdent struct {
   297  	IdentityLocation string
   298  	username         string
   299  }
   300  
   301  // IdentityFromContext implements IdentityClient.IdentityFromContext.
   302  func (m *alwaysIdent) IdentityFromContext(ctx context.Context) (identchecker.Identity, []checkers.Caveat, error) {
   303  	return nil, []checkers.Caveat{checkers.DeclaredCaveat("username", m.username)}, nil
   304  }
   305  
   306  func (alwaysIdent) DeclaredIdentity(ctx context.Context, declared map[string]string) (identchecker.Identity, error) {
   307  	user := declared["username"]
   308  	return identchecker.SimpleIdentity(user), nil
   309  }
   310  
   311  func (s *macaroonAuthenticatorSuite) TestMacaroonAuthentication(c *gc.C) {
   312  	discharger := bakerytest.NewDischarger(nil)
   313  	defer discharger.Close()
   314  	for i, test := range authenticateSuccessTests {
   315  		c.Logf("\ntest %d; %s", i, test.about)
   316  		s.username = test.dischargedUsername
   317  
   318  		bakery := identchecker.NewBakery(identchecker.BakeryParams{
   319  			Locator:        discharger,
   320  			IdentityClient: &alwaysIdent{username: s.username},
   321  		})
   322  		authenticator := &authentication.ExternalMacaroonAuthenticator{
   323  			Bakery:           bakery,
   324  			IdentityLocation: discharger.Location(),
   325  			Clock:            testclock.NewClock(time.Time{}),
   326  		}
   327  
   328  		// Authenticate once to obtain the macaroon to be discharged.
   329  		_, err := authenticator.Authenticate(context.TODO(), test.finder, authentication.AuthParams{})
   330  
   331  		// Discharge the macaroon.
   332  		dischargeErr := errors.Cause(err).(*apiservererrors.DischargeRequiredError)
   333  		client := httpbakery.NewClient()
   334  		ms, err := client.DischargeAll(context.Background(), dischargeErr.Macaroon)
   335  		c.Assert(err, jc.ErrorIsNil)
   336  
   337  		// Authenticate again with the discharged macaroon.
   338  		entity, err := authenticator.Authenticate(context.TODO(), test.finder, authentication.AuthParams{
   339  			Macaroons: []macaroon.Slice{ms},
   340  		})
   341  		if test.expectError != "" {
   342  			c.Assert(err, gc.ErrorMatches, test.expectError)
   343  			c.Assert(entity, gc.Equals, nil)
   344  		} else {
   345  			c.Assert(err, jc.ErrorIsNil)
   346  			c.Assert(entity.Tag().String(), gc.Equals, test.expectTag)
   347  		}
   348  	}
   349  }
   350  
   351  type simpleEntityFinder map[string]bool
   352  
   353  func (f simpleEntityFinder) FindEntity(tag names.Tag) (state.Entity, error) {
   354  	if utag, ok := tag.(names.UserTag); ok {
   355  		// It's a user tag which we need to be in canonical form
   356  		// so we can look it up unambiguously.
   357  		tag = names.NewUserTag(utag.Id())
   358  	}
   359  	if f[tag.String()] {
   360  		return &simpleEntity{tag}, nil
   361  	}
   362  	return nil, errors.NotFoundf("entity %q", tag)
   363  }
   364  
   365  type simpleEntity struct {
   366  	tag names.Tag
   367  }
   368  
   369  func (e *simpleEntity) Tag() names.Tag {
   370  	return e.tag
   371  }