github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/common/crossmodel/auth.go (about)

     1  // Copyright 2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package crossmodel
     5  
     6  import (
     7  	"time"
     8  
     9  	"github.com/juju/clock"
    10  	"github.com/juju/errors"
    11  	"gopkg.in/errgo.v1"
    12  	"gopkg.in/juju/names.v2"
    13  	"gopkg.in/macaroon-bakery.v2-unstable/bakery"
    14  	"gopkg.in/macaroon-bakery.v2-unstable/bakery/checkers"
    15  	"gopkg.in/macaroon.v2-unstable"
    16  	"gopkg.in/yaml.v2"
    17  
    18  	"github.com/juju/juju/apiserver/authentication"
    19  	"github.com/juju/juju/apiserver/common"
    20  	"github.com/juju/juju/apiserver/params"
    21  	"github.com/juju/juju/permission"
    22  )
    23  
    24  const (
    25  	usernameKey    = "username"
    26  	offeruuidKey   = "offer-uuid"
    27  	sourcemodelKey = "source-model-uuid"
    28  	relationKey    = "relation-key"
    29  
    30  	offerPermissionCaveat = "has-offer-permission"
    31  
    32  	// localOfferPermissionExpiryTime is used to expire offer macaroons.
    33  	// It should be long enough to allow machines hosting workloads to
    34  	// be provisioned so that the macaroon is still valid when the macaroon
    35  	// is next used. If a machine takes longer, that's ok, a new discharge
    36  	// will be obtained.
    37  	localOfferPermissionExpiryTime = 3 * time.Minute
    38  )
    39  
    40  // AuthContext is used to validate macaroons used to access
    41  // application offers.
    42  type AuthContext struct {
    43  	pool StatePool
    44  
    45  	clock                             clock.Clock
    46  	localOfferThirdPartyBakeryService authentication.BakeryService
    47  	localOfferBakeryService           authentication.ExpirableStorageBakeryService
    48  
    49  	offerAccessEndpoint string
    50  }
    51  
    52  // NewAuthContext creates a new authentication context for checking
    53  // macaroons used with application offer requests.
    54  func NewAuthContext(
    55  	pool StatePool,
    56  	localOfferThirdPartyBakeryService authentication.BakeryService,
    57  	localOfferBakeryService authentication.ExpirableStorageBakeryService,
    58  ) (*AuthContext, error) {
    59  	ctxt := &AuthContext{
    60  		pool:                              pool,
    61  		clock:                             clock.WallClock,
    62  		localOfferBakeryService:           localOfferBakeryService,
    63  		localOfferThirdPartyBakeryService: localOfferThirdPartyBakeryService,
    64  	}
    65  	return ctxt, nil
    66  }
    67  
    68  // WithClock creates a new authentication context
    69  // using the specified clock.
    70  func (a *AuthContext) WithClock(clock clock.Clock) *AuthContext {
    71  	ctxtCopy := *a
    72  	ctxtCopy.clock = clock
    73  	return &ctxtCopy
    74  }
    75  
    76  // WithDischargeURL create an auth context based on this context and used
    77  // to perform third party discharges at the specified URL.
    78  func (a *AuthContext) WithDischargeURL(offerAccessEndpoint string) *AuthContext {
    79  	ctxtCopy := *a
    80  	ctxtCopy.offerAccessEndpoint = offerAccessEndpoint
    81  	return &ctxtCopy
    82  }
    83  
    84  // ThirdPartyBakeryService returns the third party bakery service.
    85  func (a *AuthContext) ThirdPartyBakeryService() authentication.BakeryService {
    86  	return a.localOfferThirdPartyBakeryService
    87  }
    88  
    89  // CheckOfferAccessCaveat checks that the specified caveat required to be satisfied
    90  // to gain access to an offer is valid, and returns the attributes return to check
    91  // that the caveat is satisfied.
    92  func (a *AuthContext) CheckOfferAccessCaveat(caveat string) (*offerPermissionCheck, error) {
    93  	op, rest, err := checkers.ParseCaveat(caveat)
    94  	if err != nil {
    95  		return nil, errors.Annotatef(err, "cannot parse caveat %q", caveat)
    96  	}
    97  	if op != offerPermissionCaveat {
    98  		return nil, checkers.ErrCaveatNotRecognized
    99  	}
   100  	var details offerPermissionCheck
   101  	err = yaml.Unmarshal([]byte(rest), &details)
   102  	if err != nil {
   103  		return nil, errors.Trace(err)
   104  	}
   105  	logger.Debugf("offer access caveat details: %+v", details)
   106  	if !names.IsValidModel(details.SourceModelUUID) {
   107  		return nil, errors.NotValidf("source-model-uuid %q", details.SourceModelUUID)
   108  	}
   109  	if !names.IsValidUser(details.User) {
   110  		return nil, errors.NotValidf("username %q", details.User)
   111  	}
   112  	if err := permission.ValidateOfferAccess(permission.Access(details.Permission)); err != nil {
   113  		return nil, errors.NotValidf("permission %q", details.Permission)
   114  	}
   115  	return &details, nil
   116  }
   117  
   118  // CheckLocalAccessRequest checks that the user in the specified permission
   119  // check details has consume access to the offer in the details.
   120  // It returns an error with a *bakery.VerificationError cause if the macaroon
   121  // verification failed. If the macaroon is valid, CheckLocalAccessRequest
   122  // returns a list of caveats to add to the discharge macaroon.
   123  func (a *AuthContext) CheckLocalAccessRequest(details *offerPermissionCheck) ([]checkers.Caveat, error) {
   124  	logger.Debugf("authenticate local offer access: %+v", details)
   125  	st, releaser, err := a.pool.Get(details.SourceModelUUID)
   126  	if err != nil {
   127  		return nil, errors.Trace(err)
   128  	}
   129  	defer releaser()
   130  	if err := a.checkOfferAccess(st, details.User, details.OfferUUID); err != nil {
   131  		return nil, errors.Trace(err)
   132  	}
   133  
   134  	firstPartyCaveats := []checkers.Caveat{
   135  		checkers.DeclaredCaveat(sourcemodelKey, details.SourceModelUUID),
   136  		checkers.DeclaredCaveat(offeruuidKey, details.OfferUUID),
   137  		checkers.DeclaredCaveat(usernameKey, details.User),
   138  		checkers.TimeBeforeCaveat(a.clock.Now().Add(localOfferPermissionExpiryTime)),
   139  	}
   140  	if details.Relation != "" {
   141  		firstPartyCaveats = append(firstPartyCaveats, checkers.DeclaredCaveat(relationKey, details.Relation))
   142  	}
   143  	return firstPartyCaveats, nil
   144  }
   145  
   146  func (a *AuthContext) checkOfferAccess(st Backend, username, offerUUID string) error {
   147  	userTag := names.NewUserTag(username)
   148  	isAdmin, err := a.hasControllerAdminAccess(st, userTag)
   149  	if err != nil {
   150  		return common.ErrPerm
   151  	}
   152  	if isAdmin {
   153  		return nil
   154  	}
   155  	isAdmin, err = a.hasModelAdminAccess(st, userTag)
   156  	if err != nil {
   157  		return common.ErrPerm
   158  	}
   159  	if isAdmin {
   160  		return nil
   161  	}
   162  	access, err := st.GetOfferAccess(offerUUID, userTag)
   163  	if err != nil && !errors.IsNotFound(err) {
   164  		return common.ErrPerm
   165  	}
   166  	if !access.EqualOrGreaterOfferAccessThan(permission.ConsumeAccess) {
   167  		return common.ErrPerm
   168  	}
   169  	return nil
   170  }
   171  
   172  func (api *AuthContext) hasControllerAdminAccess(st Backend, userTag names.UserTag) (bool, error) {
   173  	isAdmin, err := common.HasPermission(st.UserPermission, userTag, permission.SuperuserAccess, st.ControllerTag())
   174  	if errors.IsNotFound(err) {
   175  		return false, nil
   176  	}
   177  	return isAdmin, err
   178  }
   179  
   180  func (api *AuthContext) hasModelAdminAccess(st Backend, userTag names.UserTag) (bool, error) {
   181  	isAdmin, err := common.HasPermission(st.UserPermission, userTag, permission.AdminAccess, st.ModelTag())
   182  	if errors.IsNotFound(err) {
   183  		return false, nil
   184  	}
   185  	return isAdmin, err
   186  }
   187  
   188  func (a *AuthContext) offerPermissionYaml(sourceModelUUID, username, offerURL, relationKey string, permission permission.Access) (string, error) {
   189  	out, err := yaml.Marshal(offerPermissionCheck{
   190  		SourceModelUUID: sourceModelUUID,
   191  		User:            username,
   192  		OfferUUID:       offerURL,
   193  		Relation:        relationKey,
   194  		Permission:      string(permission),
   195  	})
   196  	if err != nil {
   197  		return "", err
   198  	}
   199  	return string(out), nil
   200  }
   201  
   202  // CreateConsumeOfferMacaroon creates a macaroon that authorises access to the specified offer.
   203  func (a *AuthContext) CreateConsumeOfferMacaroon(offer *params.ApplicationOfferDetails, username string) (*macaroon.Macaroon, error) {
   204  	sourceModelTag, err := names.ParseModelTag(offer.SourceModelTag)
   205  	if err != nil {
   206  		return nil, errors.Trace(err)
   207  	}
   208  	expiryTime := a.clock.Now().Add(localOfferPermissionExpiryTime)
   209  	bakery, err := a.localOfferBakeryService.ExpireStorageAfter(localOfferPermissionExpiryTime)
   210  	if err != nil {
   211  		return nil, errors.Trace(err)
   212  	}
   213  
   214  	return bakery.NewMacaroon(
   215  		[]checkers.Caveat{
   216  			checkers.TimeBeforeCaveat(expiryTime),
   217  			checkers.DeclaredCaveat(sourcemodelKey, sourceModelTag.Id()),
   218  			checkers.DeclaredCaveat(offeruuidKey, offer.OfferUUID),
   219  			checkers.DeclaredCaveat(usernameKey, username),
   220  		})
   221  
   222  }
   223  
   224  // CreateRemoteRelationMacaroon creates a macaroon that authorises access to the specified relation.
   225  func (a *AuthContext) CreateRemoteRelationMacaroon(sourceModelUUID, offerUUID string, username string, rel names.Tag) (*macaroon.Macaroon, error) {
   226  	expiryTime := a.clock.Now().Add(localOfferPermissionExpiryTime)
   227  	bakery, err := a.localOfferBakeryService.ExpireStorageAfter(localOfferPermissionExpiryTime)
   228  	if err != nil {
   229  		return nil, errors.Trace(err)
   230  	}
   231  
   232  	offerMacaroon, err := bakery.NewMacaroon(
   233  		[]checkers.Caveat{
   234  			checkers.TimeBeforeCaveat(expiryTime),
   235  			checkers.DeclaredCaveat(sourcemodelKey, sourceModelUUID),
   236  			checkers.DeclaredCaveat(offeruuidKey, offerUUID),
   237  			checkers.DeclaredCaveat(usernameKey, username),
   238  			checkers.DeclaredCaveat(relationKey, rel.Id()),
   239  		})
   240  
   241  	return offerMacaroon, err
   242  }
   243  
   244  type offerPermissionCheck struct {
   245  	SourceModelUUID string `yaml:"source-model-uuid"`
   246  	User            string `yaml:"username"`
   247  	OfferUUID       string `yaml:"offer-uuid"`
   248  	Relation        string `yaml:"relation-key"`
   249  	Permission      string `yaml:"permission"`
   250  }
   251  
   252  type authenticator struct {
   253  	clock  clock.Clock
   254  	bakery authentication.ExpirableStorageBakeryService
   255  	ctxt   *AuthContext
   256  
   257  	sourceModelUUID string
   258  	offerUUID       string
   259  
   260  	// offerAccessEndpoint holds the URL of the trusted third party
   261  	// that is used to address the has-offer-permission third party caveat.
   262  	offerAccessEndpoint string
   263  }
   264  
   265  // Authenticator returns an instance used to authenticate macaroons used to
   266  // access the specified offer.
   267  func (a *AuthContext) Authenticator(sourceModelUUID, offerUUID string) *authenticator {
   268  	auth := &authenticator{
   269  		clock:               a.clock,
   270  		bakery:              a.localOfferBakeryService,
   271  		ctxt:                a,
   272  		sourceModelUUID:     sourceModelUUID,
   273  		offerUUID:           offerUUID,
   274  		offerAccessEndpoint: a.offerAccessEndpoint,
   275  	}
   276  	return auth
   277  }
   278  
   279  func (a *authenticator) checkMacaroons(mac macaroon.Slice, requiredValues map[string]string) (map[string]string, error) {
   280  	logger.Debugf("check %d macaroons with required attrs: %v", len(mac), requiredValues)
   281  	for _, m := range mac {
   282  		if m == nil {
   283  			logger.Warningf("unexpected nil cross model macaroon")
   284  			continue
   285  		}
   286  		logger.Debugf("- mac %s", m.Id())
   287  	}
   288  	declared := checkers.InferDeclared(mac)
   289  	logger.Debugf("check macaroons with declared attrs: %v", declared)
   290  	username, ok := declared[usernameKey]
   291  	if !ok {
   292  		return nil, common.ErrPerm
   293  	}
   294  	relation := declared[relationKey]
   295  	attrs, err := a.bakery.CheckAny([]macaroon.Slice{mac}, requiredValues, checkers.TimeBefore)
   296  	if err == nil {
   297  		logger.Debugf("macaroon check ok, attr: %v", attrs)
   298  		return attrs, nil
   299  	}
   300  
   301  	if _, ok := errgo.Cause(err).(*bakery.VerificationError); !ok {
   302  		logger.Debugf("macaroon verification failed: %+v", err)
   303  		return nil, common.ErrPerm
   304  	}
   305  
   306  	logger.Debugf("generating discharge macaroon because: %v", err)
   307  	cause := err
   308  	authYaml, err := a.ctxt.offerPermissionYaml(a.sourceModelUUID, username, a.offerUUID, relation, permission.ConsumeAccess)
   309  	if err != nil {
   310  		return nil, errors.Trace(err)
   311  	}
   312  	bakery, err := a.bakery.ExpireStorageAfter(localOfferPermissionExpiryTime)
   313  	if err != nil {
   314  		return nil, errors.Trace(err)
   315  	}
   316  	keys := []string{usernameKey}
   317  	for k := range requiredValues {
   318  		keys = append(keys, k)
   319  	}
   320  	m, err := bakery.NewMacaroon([]checkers.Caveat{
   321  		checkers.NeedDeclaredCaveat(
   322  			checkers.Caveat{
   323  				Location:  a.offerAccessEndpoint,
   324  				Condition: offerPermissionCaveat + " " + authYaml,
   325  			},
   326  			keys...,
   327  		),
   328  		checkers.TimeBeforeCaveat(a.clock.Now().Add(localOfferPermissionExpiryTime)),
   329  	})
   330  
   331  	if err != nil {
   332  		return nil, errors.Annotate(err, "cannot create macaroon")
   333  	}
   334  	return nil, &common.DischargeRequiredError{
   335  		Cause:    cause,
   336  		Macaroon: m,
   337  	}
   338  }
   339  
   340  // CheckOfferMacaroons verifies that the specified macaroons allow access to the offer.
   341  func (a *authenticator) CheckOfferMacaroons(offerUUID string, mac macaroon.Slice) (map[string]string, error) {
   342  	requiredValues := map[string]string{
   343  		sourcemodelKey: a.sourceModelUUID,
   344  		offeruuidKey:   offerUUID,
   345  	}
   346  	return a.checkMacaroons(mac, requiredValues)
   347  }
   348  
   349  // CheckRelationMacaroons verifies that the specified macaroons allow access to the relation.
   350  func (a *authenticator) CheckRelationMacaroons(relationTag names.Tag, mac macaroon.Slice) error {
   351  	requiredValues := map[string]string{
   352  		sourcemodelKey: a.sourceModelUUID,
   353  		relationKey:    relationTag.Id(),
   354  	}
   355  	_, err := a.checkMacaroons(mac, requiredValues)
   356  	return err
   357  }