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