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