github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/common/crossmodel/auth_test.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package crossmodel_test 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "strings" 11 "time" 12 13 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 14 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery/checkers" 15 "github.com/juju/clock" 16 "github.com/juju/clock/testclock" 17 "github.com/juju/errors" 18 "github.com/juju/names/v5" 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 "github.com/juju/juju/apiserver/common/crossmodel" 26 apiservererrors "github.com/juju/juju/apiserver/errors" 27 "github.com/juju/juju/core/permission" 28 "github.com/juju/juju/rpc/params" 29 coretesting "github.com/juju/juju/testing" 30 ) 31 32 var _ = gc.Suite(&authSuite{}) 33 34 type authSuite struct { 35 coretesting.BaseSuite 36 37 bakery authentication.ExpirableStorageBakery 38 offerBakery *crossmodel.OfferBakery 39 bakeryKey *bakery.KeyPair 40 } 41 42 type testLocator struct { 43 PublicKey bakery.PublicKey 44 } 45 46 func (b testLocator) ThirdPartyInfo(ctx context.Context, loc string) (bakery.ThirdPartyInfo, error) { 47 if loc != "http://thirdparty" { 48 return bakery.ThirdPartyInfo{}, errors.NotFoundf("location %v", loc) 49 } 50 return bakery.ThirdPartyInfo{ 51 PublicKey: b.PublicKey, 52 Version: bakery.LatestVersion, 53 }, nil 54 } 55 56 func (s *authSuite) SetUpTest(c *gc.C) { 57 s.BaseSuite.SetUpTest(c) 58 59 key, err := bakery.GenerateKey() 60 c.Assert(err, jc.ErrorIsNil) 61 locator := testLocator{key.Public} 62 bakery := bakery.New(bakery.BakeryParams{ 63 Locator: locator, 64 Key: bakery.MustGenerateKey(), 65 OpsAuthorizer: crossmodel.CrossModelAuthorizer{}, 66 }) 67 s.bakery = &mockBakery{bakery} 68 s.offerBakery = crossmodel.NewOfferBakeryForTest(s.bakery, clock.WallClock) 69 } 70 71 func (s *authSuite) TestCheckValidCaveat(c *gc.C) { 72 uuid := utils.MustNewUUID() 73 permCheckDetails := fmt.Sprintf(` 74 source-model-uuid: %v 75 username: mary 76 offer-uuid: mysql-uuid 77 relation-key: mediawiki:db mysql:server 78 permission: consume 79 `[1:], uuid) 80 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 81 c.Assert(err, jc.ErrorIsNil) 82 opc, err := authContext.CheckOfferAccessCaveat("has-offer-permission " + permCheckDetails) 83 c.Assert(err, jc.ErrorIsNil) 84 c.Assert(opc.SourceModelUUID, gc.Equals, uuid.String()) 85 c.Assert(opc.User, gc.Equals, "mary") 86 c.Assert(opc.OfferUUID, gc.Equals, "mysql-uuid") 87 c.Assert(opc.Relation, gc.Equals, "mediawiki:db mysql:server") 88 c.Assert(opc.Permission, gc.Equals, "consume") 89 } 90 91 func (s *authSuite) TestCheckInvalidCaveatId(c *gc.C) { 92 uuid := utils.MustNewUUID() 93 permCheckDetails := fmt.Sprintf(` 94 source-model-uuid: %v 95 username: mary 96 offer-uuid: mysql-uuid 97 relation-key: mediawiki:db mysql:server 98 permission: consume 99 `[1:], uuid) 100 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 101 c.Assert(err, jc.ErrorIsNil) 102 _, err = authContext.CheckOfferAccessCaveat("different-caveat " + permCheckDetails) 103 c.Assert(err, gc.ErrorMatches, ".*caveat not recognized.*") 104 } 105 106 func (s *authSuite) TestCheckInvalidCaveatContents(c *gc.C) { 107 permCheckDetails := ` 108 source-model-uuid: invalid 109 username: mary 110 offer-uuid: mysql-uuid 111 relation-key: mediawiki:db mysql:server 112 permission: consume 113 `[1:] 114 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 115 c.Assert(err, jc.ErrorIsNil) 116 _, err = authContext.CheckOfferAccessCaveat("has-offer-permission " + permCheckDetails) 117 c.Assert(err, gc.ErrorMatches, `source-model-uuid "invalid" not valid`) 118 } 119 120 func (s *authSuite) TestCheckLocalAccessRequest(c *gc.C) { 121 uuid := utils.MustNewUUID() 122 st := &mockState{ 123 tag: names.NewModelTag(uuid.String()), 124 permissions: map[string]permission.Access{ 125 "mysql-uuid:mary": permission.ConsumeAccess, 126 }, 127 } 128 authContext, err := crossmodel.NewAuthContext(st, s.bakeryKey, s.offerBakery) 129 c.Assert(err, jc.ErrorIsNil) 130 permCheckDetails := fmt.Sprintf(` 131 source-model-uuid: %v 132 username: mary 133 offer-uuid: mysql-uuid 134 relation-key: mediawiki:db mysql:server 135 permission: consume 136 `[1:], uuid) 137 opc, err := authContext.CheckOfferAccessCaveat("has-offer-permission " + permCheckDetails) 138 c.Assert(err, jc.ErrorIsNil) 139 cav, err := authContext.CheckLocalAccessRequest(opc) 140 c.Assert(err, jc.ErrorIsNil) 141 c.Assert(cav, gc.HasLen, 5) 142 c.Assert(cav[0].Condition, gc.Equals, "declared source-model-uuid "+uuid.String()) 143 c.Assert(cav[1].Condition, gc.Equals, "declared offer-uuid mysql-uuid") 144 c.Assert(cav[2].Condition, gc.Equals, "declared username mary") 145 c.Assert(strings.HasPrefix(cav[3].Condition, "time-before"), jc.IsTrue) 146 c.Assert(cav[4].Condition, gc.Equals, "declared relation-key mediawiki:db mysql:server") 147 } 148 149 func (s *authSuite) TestCheckLocalAccessRequestControllerAdmin(c *gc.C) { 150 uuid := utils.MustNewUUID() 151 st := &mockState{ 152 tag: names.NewModelTag(uuid.String()), 153 permissions: map[string]permission.Access{ 154 coretesting.ControllerTag.Id() + ":mary": permission.SuperuserAccess, 155 }, 156 } 157 authContext, err := crossmodel.NewAuthContext(st, s.bakeryKey, s.offerBakery) 158 c.Assert(err, jc.ErrorIsNil) 159 permCheckDetails := fmt.Sprintf(` 160 source-model-uuid: %v 161 username: mary 162 offer-uuid: mysql-uuid 163 relation-key: mediawiki:db mysql:server 164 permission: consume 165 `[1:], uuid) 166 opc, err := authContext.CheckOfferAccessCaveat("has-offer-permission " + permCheckDetails) 167 c.Assert(err, jc.ErrorIsNil) 168 _, err = authContext.CheckLocalAccessRequest(opc) 169 c.Assert(err, jc.ErrorIsNil) 170 } 171 172 func (s *authSuite) TestCheckLocalAccessRequestModelAdmin(c *gc.C) { 173 uuid := utils.MustNewUUID() 174 st := &mockState{ 175 tag: names.NewModelTag(uuid.String()), 176 permissions: map[string]permission.Access{ 177 uuid.String() + ":mary": permission.AdminAccess, 178 }, 179 } 180 authContext, err := crossmodel.NewAuthContext(st, s.bakeryKey, s.offerBakery) 181 c.Assert(err, jc.ErrorIsNil) 182 permCheckDetails := fmt.Sprintf(` 183 source-model-uuid: %v 184 username: mary 185 offer-uuid: mysql-uuid 186 relation-key: mediawiki:db mysql:server 187 permission: consume 188 `[1:], uuid) 189 opc, err := authContext.CheckOfferAccessCaveat("has-offer-permission " + permCheckDetails) 190 c.Assert(err, jc.ErrorIsNil) 191 _, err = authContext.CheckLocalAccessRequest(opc) 192 c.Assert(err, jc.ErrorIsNil) 193 } 194 195 func (s *authSuite) TestCheckLocalAccessRequestNoPermission(c *gc.C) { 196 uuid := utils.MustNewUUID() 197 st := &mockState{ 198 tag: names.NewModelTag(uuid.String()), 199 permissions: make(map[string]permission.Access), 200 } 201 authContext, err := crossmodel.NewAuthContext(st, s.bakeryKey, s.offerBakery) 202 c.Assert(err, jc.ErrorIsNil) 203 permCheckDetails := fmt.Sprintf(` 204 source-model-uuid: %v 205 username: mary 206 offer-uuid: mysql-uuid 207 relation-key: mediawiki:db mysql:server 208 permission: consume 209 `[1:], uuid) 210 opc, err := authContext.CheckOfferAccessCaveat("has-offer-permission " + permCheckDetails) 211 c.Assert(err, jc.ErrorIsNil) 212 _, err = authContext.CheckLocalAccessRequest(opc) 213 c.Assert(err, gc.ErrorMatches, "permission denied") 214 } 215 216 func (s *authSuite) TestCreateConsumeOfferMacaroon(c *gc.C) { 217 offer := ¶ms.ApplicationOfferDetailsV5{ 218 SourceModelTag: coretesting.ModelTag.String(), 219 OfferUUID: "mysql-uuid", 220 } 221 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 222 c.Assert(err, jc.ErrorIsNil) 223 mac, err := authContext.CreateConsumeOfferMacaroon(context.Background(), offer, "mary", bakery.LatestVersion) 224 c.Assert(err, jc.ErrorIsNil) 225 cav := mac.M().Caveats() 226 c.Assert(cav, gc.HasLen, 4) 227 c.Assert(bytes.HasPrefix(cav[0].Id, []byte("time-before")), jc.IsTrue) 228 c.Assert(cav[1].Id, jc.DeepEquals, []byte("declared source-model-uuid "+coretesting.ModelTag.Id())) 229 c.Assert(cav[2].Id, jc.DeepEquals, []byte("declared username mary")) 230 c.Assert(cav[3].Id, jc.DeepEquals, []byte("declared offer-uuid mysql-uuid")) 231 } 232 233 func (s *authSuite) TestCreateRemoteRelationMacaroon(c *gc.C) { 234 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 235 c.Assert(err, jc.ErrorIsNil) 236 mac, err := authContext.CreateRemoteRelationMacaroon( 237 context.Background(), 238 coretesting.ModelTag.Id(), "mysql-uuid", "mary", names.NewRelationTag("mediawiki:db mysql:server"), bakery.LatestVersion) 239 c.Assert(err, jc.ErrorIsNil) 240 cav := mac.M().Caveats() 241 c.Assert(cav, gc.HasLen, 5) 242 c.Assert(bytes.HasPrefix(cav[0].Id, []byte("time-before")), jc.IsTrue) 243 c.Assert(cav[1].Id, jc.DeepEquals, []byte("declared source-model-uuid "+coretesting.ModelTag.Id())) 244 c.Assert(cav[2].Id, jc.DeepEquals, []byte("declared offer-uuid mysql-uuid")) 245 c.Assert(cav[3].Id, jc.DeepEquals, []byte("declared username mary")) 246 c.Assert(cav[4].Id, jc.DeepEquals, []byte("declared relation-key mediawiki:db mysql:server")) 247 } 248 249 func (s *authSuite) TestCheckOfferMacaroons(c *gc.C) { 250 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 251 c.Assert(err, jc.ErrorIsNil) 252 mac, err := s.bakery.NewMacaroon( 253 context.Background(), 254 bakery.LatestVersion, 255 []checkers.Caveat{ 256 checkers.DeclaredCaveat("username", "mary"), 257 checkers.DeclaredCaveat("offer-uuid", "mysql-uuid"), 258 checkers.DeclaredCaveat("source-model-uuid", coretesting.ModelTag.Id()), 259 }, bakery.Op{"consume", "mysql-uuid"}) 260 261 c.Assert(err, jc.ErrorIsNil) 262 attr, err := authContext.Authenticator().CheckOfferMacaroons( 263 context.Background(), 264 coretesting.ModelTag.Id(), 265 "mysql-uuid", 266 macaroon.Slice{mac.M()}, 267 bakery.LatestVersion, 268 ) 269 c.Assert(err, jc.ErrorIsNil) 270 c.Assert(attr, gc.HasLen, 3) 271 c.Assert(attr, jc.DeepEquals, map[string]string{ 272 "username": "mary", 273 "offer-uuid": "mysql-uuid", 274 "source-model-uuid": coretesting.ModelTag.Id(), 275 }) 276 } 277 278 func (s *authSuite) TestCheckOfferMacaroonsWrongOffer(c *gc.C) { 279 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 280 c.Assert(err, jc.ErrorIsNil) 281 mac, err := s.bakery.NewMacaroon( 282 context.Background(), 283 bakery.LatestVersion, 284 []checkers.Caveat{ 285 checkers.DeclaredCaveat("username", "mary"), 286 checkers.DeclaredCaveat("offer-uuid", "mysql-uuid"), 287 checkers.DeclaredCaveat("source-model-uuid", coretesting.ModelTag.Id()), 288 }, bakery.Op{"consume", "mysql-uuid"}) 289 290 c.Assert(err, jc.ErrorIsNil) 291 _, err = authContext.Authenticator().CheckOfferMacaroons( 292 context.Background(), 293 coretesting.ModelTag.Id(), 294 "prod.another", 295 macaroon.Slice{mac.M()}, 296 bakery.LatestVersion, 297 ) 298 c.Assert( 299 err, 300 gc.ErrorMatches, 301 "permission denied") 302 } 303 304 func (s *authSuite) TestCheckOfferMacaroonsNoUser(c *gc.C) { 305 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 306 c.Assert(err, jc.ErrorIsNil) 307 mac, err := s.bakery.NewMacaroon( 308 context.Background(), 309 bakery.LatestVersion, 310 []checkers.Caveat{ 311 checkers.DeclaredCaveat("offer-uuid", "mysql-uuid"), 312 checkers.DeclaredCaveat("source-model-uuid", coretesting.ModelTag.Id()), 313 }, bakery.Op{"consume", "mysql-uuid"}) 314 315 c.Assert(err, jc.ErrorIsNil) 316 _, err = authContext.Authenticator().CheckOfferMacaroons( 317 context.Background(), 318 coretesting.ModelTag.Id(), 319 "mysql-uuid", 320 macaroon.Slice{mac.M()}, 321 bakery.LatestVersion, 322 ) 323 c.Assert(err, gc.ErrorMatches, "permission denied") 324 } 325 326 func (s *authSuite) TestCheckOfferMacaroonsDischargeRequired(c *gc.C) { 327 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 328 c.Assert(err, jc.ErrorIsNil) 329 clock := testclock.NewClock(time.Now().Add(-10 * time.Minute)) 330 authContext.SetClock(clock) 331 authContext, err = authContext.WithDischargeURL("http://thirdparty") 332 c.Assert(err, jc.ErrorIsNil) 333 offer := ¶ms.ApplicationOfferDetailsV5{ 334 SourceModelTag: coretesting.ModelTag.String(), 335 OfferUUID: "mysql-uuid", 336 } 337 mac, err := authContext.CreateConsumeOfferMacaroon(context.Background(), offer, "mary", bakery.LatestVersion) 338 c.Assert(err, jc.ErrorIsNil) 339 340 _, err = authContext.Authenticator().CheckOfferMacaroons( 341 context.Background(), 342 coretesting.ModelTag.Id(), 343 "mysql-uuid", 344 macaroon.Slice{mac.M()}, 345 bakery.LatestVersion, 346 ) 347 dischargeErr, ok := err.(*apiservererrors.DischargeRequiredError) 348 c.Assert(ok, jc.IsTrue) 349 cav := dischargeErr.LegacyMacaroon.Caveats() 350 c.Assert(cav, gc.HasLen, 2) 351 c.Assert(cav[0].Location, gc.Equals, "http://thirdparty") 352 } 353 354 func (s *authSuite) TestCheckRelationMacaroons(c *gc.C) { 355 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 356 c.Assert(err, jc.ErrorIsNil) 357 relationTag := names.NewRelationTag("mediawiki:db mysql:server") 358 mac, err := s.bakery.NewMacaroon( 359 context.Background(), 360 bakery.LatestVersion, 361 []checkers.Caveat{ 362 checkers.DeclaredCaveat("username", "mary"), 363 checkers.DeclaredCaveat("relation-key", relationTag.Id()), 364 checkers.DeclaredCaveat("source-model-uuid", coretesting.ModelTag.Id()), 365 }, bakery.Op{"relate", relationTag.Id()}) 366 367 c.Assert(err, jc.ErrorIsNil) 368 err = authContext.Authenticator().CheckRelationMacaroons( 369 context.Background(), 370 coretesting.ModelTag.Id(), 371 "mysql-uuid", 372 relationTag, 373 macaroon.Slice{mac.M()}, 374 bakery.LatestVersion, 375 ) 376 c.Assert(err, jc.ErrorIsNil) 377 } 378 379 func (s *authSuite) TestCheckRelationMacaroonsWrongRelation(c *gc.C) { 380 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 381 c.Assert(err, jc.ErrorIsNil) 382 relationTag := names.NewRelationTag("mediawiki:db mysql:server") 383 mac, err := s.bakery.NewMacaroon( 384 context.Background(), 385 bakery.LatestVersion, 386 []checkers.Caveat{ 387 checkers.DeclaredCaveat("username", "mary"), 388 checkers.DeclaredCaveat("relation-key", relationTag.Id()), 389 checkers.DeclaredCaveat("source-model-uuid", coretesting.ModelTag.Id()), 390 }, bakery.Op{"relate", relationTag.Id()}) 391 392 c.Assert(err, jc.ErrorIsNil) 393 err = authContext.Authenticator().CheckRelationMacaroons( 394 context.Background(), 395 coretesting.ModelTag.Id(), 396 "mysql-uuid", 397 names.NewRelationTag("app:db offer:db"), 398 macaroon.Slice{mac.M()}, 399 bakery.LatestVersion, 400 ) 401 c.Assert( 402 err, 403 gc.ErrorMatches, 404 "permission denied") 405 } 406 407 func (s *authSuite) TestCheckRelationMacaroonsNoUser(c *gc.C) { 408 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 409 c.Assert(err, jc.ErrorIsNil) 410 relationTag := names.NewRelationTag("mediawiki:db mysql:server") 411 mac, err := s.bakery.NewMacaroon( 412 context.Background(), 413 bakery.LatestVersion, 414 []checkers.Caveat{ 415 checkers.DeclaredCaveat("relation-key", relationTag.Id()), 416 checkers.DeclaredCaveat("source-model-uuid", coretesting.ModelTag.Id()), 417 }, bakery.Op{"relate", relationTag.Id()}) 418 419 c.Assert(err, jc.ErrorIsNil) 420 err = authContext.Authenticator().CheckRelationMacaroons( 421 context.Background(), 422 coretesting.ModelTag.Id(), 423 "mysql-uuid", 424 relationTag, 425 macaroon.Slice{mac.M()}, 426 bakery.LatestVersion, 427 ) 428 c.Assert(err, gc.ErrorMatches, "permission denied") 429 } 430 431 func (s *authSuite) TestCheckRelationMacaroonsDischargeRequired(c *gc.C) { 432 authContext, err := crossmodel.NewAuthContext(nil, s.bakeryKey, s.offerBakery) 433 c.Assert(err, jc.ErrorIsNil) 434 clock := testclock.NewClock(time.Now().Add(-10 * time.Minute)) 435 authContext.SetClock(clock) 436 authContext, err = authContext.WithDischargeURL("http://thirdparty") 437 c.Assert(err, jc.ErrorIsNil) 438 relationTag := names.NewRelationTag("mediawiki:db mysql:server") 439 mac, err := authContext.CreateRemoteRelationMacaroon( 440 context.Background(), 441 coretesting.ModelTag.Id(), "mysql-uuid", "mary", relationTag, bakery.LatestVersion) 442 c.Assert(err, jc.ErrorIsNil) 443 444 err = authContext.Authenticator().CheckRelationMacaroons( 445 context.Background(), 446 coretesting.ModelTag.Id(), 447 "mysql-uuid", 448 relationTag, 449 macaroon.Slice{mac.M()}, 450 bakery.LatestVersion, 451 ) 452 dischargeErr, ok := err.(*apiservererrors.DischargeRequiredError) 453 c.Assert(ok, jc.IsTrue) 454 cav := dischargeErr.LegacyMacaroon.Caveats() 455 c.Assert(cav, gc.HasLen, 2) 456 c.Assert(cav[0].Location, gc.Equals, "http://thirdparty") 457 }