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