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 }