github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/stateauthenticator/context.go (about)

     1  // Copyright 2015-2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package stateauthenticator
     5  
     6  import (
     7  	"context"
     8  	"net/http"
     9  	"net/url"
    10  	"sync"
    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/go-macaroon-bakery/macaroon-bakery/v3/bakery/identchecker"
    16  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
    17  	"github.com/juju/clock"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/names/v5"
    20  
    21  	"github.com/juju/juju/apiserver/authentication"
    22  	"github.com/juju/juju/apiserver/bakeryutil"
    23  	apiservererrors "github.com/juju/juju/apiserver/errors"
    24  	"github.com/juju/juju/core/macaroon"
    25  	"github.com/juju/juju/state"
    26  )
    27  
    28  const (
    29  	localUserIdentityLocationPath = "/auth"
    30  )
    31  
    32  // authContext holds authentication context shared
    33  // between all API endpoints.
    34  type authContext struct {
    35  	st *state.State
    36  
    37  	clock     clock.Clock
    38  	agentAuth authentication.AgentAuthenticator
    39  
    40  	// localUserBakery is the bakery.Bakery used by the controller
    41  	// for authenticating local users. In time, we may want to use this for
    42  	// both local and external users. Note that this service does not
    43  	// discharge the third-party caveats.
    44  	localUserBakery *bakeryutil.ExpirableStorageBakery
    45  
    46  	// localUserThirdPartyBakery is the bakery.Bakery used by the
    47  	// controller for discharging third-party caveats for local users.
    48  	localUserThirdPartyBakery *bakery.Bakery
    49  	// localUserThirdPartyBakeryKey is the bakery.Bakery's key.
    50  	localUserThirdPartyBakeryKey *bakery.KeyPair
    51  
    52  	// localUserInteractions maintains a set of in-progress local user
    53  	// authentication interactions.
    54  	localUserInteractions *authentication.Interactions
    55  
    56  	// macaroonAuthOnce guards the fields below it.
    57  	macaroonAuthOnce   sync.Once
    58  	_macaroonAuth      *authentication.ExternalMacaroonAuthenticator
    59  	_macaroonAuthError error
    60  }
    61  
    62  // OpenLoginAuthorizer authorises any login operation presented to it.
    63  type OpenLoginAuthorizer struct{}
    64  
    65  // AuthorizeOps implements OpsAuthorizer.AuthorizeOps.
    66  func (OpenLoginAuthorizer) AuthorizeOps(ctx context.Context, authorizedOp bakery.Op, queryOps []bakery.Op) ([]bool, []checkers.Caveat, error) {
    67  	logger.Debugf("authorize query ops check for %v: %v", authorizedOp, queryOps)
    68  	allowed := make([]bool, len(queryOps))
    69  	for i := range allowed {
    70  		allowed[i] = queryOps[i] == identchecker.LoginOp
    71  	}
    72  	return allowed, nil, nil
    73  }
    74  
    75  // newAuthContext creates a new authentication context for st.
    76  func newAuthContext(
    77  	st *state.State,
    78  	clock clock.Clock,
    79  ) (*authContext, error) {
    80  	ctxt := &authContext{
    81  		st:                    st,
    82  		clock:                 clock,
    83  		localUserInteractions: authentication.NewInteractions(),
    84  	}
    85  
    86  	// Create a bakery for discharging third-party caveats for
    87  	// local user authentication. This service does not persist keys;
    88  	// its macaroons should be very short-lived.
    89  	checker := checkers.New(macaroon.MacaroonNamespace)
    90  	checker.Register("is-authenticated-user", macaroon.MacaroonURI,
    91  		// Having a macaroon with an is-authenticated-user
    92  		// caveat is proof that the user is "logged in".
    93  		// "is-authenticated-user",
    94  		func(ctx context.Context, cond, arg string) error { return nil },
    95  	)
    96  
    97  	bakeryConfig := st.NewBakeryConfig()
    98  	location := "juju model " + st.ModelUUID()
    99  	var err error
   100  	ctxt.localUserThirdPartyBakeryKey, err = bakeryConfig.GetLocalUsersThirdPartyKey()
   101  	if err != nil {
   102  		return nil, errors.Annotate(err, "generating key for local user third party bakery key")
   103  	}
   104  	ctxt.localUserThirdPartyBakery = bakery.New(
   105  		bakery.BakeryParams{
   106  			Checker:       checker,
   107  			Key:           ctxt.localUserThirdPartyBakeryKey,
   108  			OpsAuthorizer: OpenLoginAuthorizer{},
   109  			Location:      location,
   110  		})
   111  
   112  	// Create a bakery service for local user authentication. This service
   113  	// persists keys into MongoDB in a TTL collection.
   114  	store, err := st.NewBakeryStorage()
   115  	if err != nil {
   116  		return nil, errors.Trace(err)
   117  	}
   118  	locator := bakeryutil.BakeryThirdPartyLocator{PublicKey: ctxt.localUserThirdPartyBakeryKey.Public}
   119  	localUserBakeryKey, err := bakeryConfig.GetLocalUsersKey()
   120  	if err != nil {
   121  		return nil, errors.Annotate(err, "generating key for local user bakery key")
   122  	}
   123  	localUserBakery := bakery.New(
   124  		bakery.BakeryParams{
   125  			RootKeyStore:  store,
   126  			Key:           localUserBakeryKey,
   127  			OpsAuthorizer: OpenLoginAuthorizer{},
   128  			Location:      location,
   129  		})
   130  
   131  	ctxt.localUserBakery = &bakeryutil.ExpirableStorageBakery{
   132  		localUserBakery, location, localUserBakeryKey, store, locator,
   133  	}
   134  	return ctxt, nil
   135  }
   136  
   137  // CreateLocalLoginMacaroon creates a macaroon that may be provided to a user
   138  // as proof that they have logged in with a valid username and password. This
   139  // macaroon may then be used to obtain a discharge macaroon so that the user
   140  // can log in without presenting their password for a set amount of time.
   141  func (ctxt *authContext) CreateLocalLoginMacaroon(ctx context.Context, tag names.UserTag, version bakery.Version) (*bakery.Macaroon, error) {
   142  	return authentication.CreateLocalLoginMacaroon(ctx, tag, ctxt.localUserThirdPartyBakery.Oven, ctxt.clock, version)
   143  }
   144  
   145  // CheckLocalLoginCaveat parses and checks that the given caveat string is
   146  // valid for a local login request, and returns the tag of the local user
   147  // that the caveat asserts is logged in. checkers.ErrCaveatNotRecognized will
   148  // be returned if the caveat is not recognised.
   149  func (ctxt *authContext) CheckLocalLoginCaveat(caveat string) (names.UserTag, error) {
   150  	return authentication.CheckLocalLoginCaveat(caveat)
   151  }
   152  
   153  // CheckLocalLoginRequest checks that the given HTTP request contains at least
   154  // one valid local login macaroon minted using CreateLocalLoginMacaroon. It
   155  // returns an error with a *bakery.VerificationError cause if the macaroon
   156  // verification failed.
   157  func (ctxt *authContext) CheckLocalLoginRequest(ctx context.Context, req *http.Request) error {
   158  	return authentication.CheckLocalLoginRequest(ctx, ctxt.localUserThirdPartyBakery.Checker, req)
   159  }
   160  
   161  // DischargeCaveats returns the caveats to add to a login discharge macaroon.
   162  func (ctxt *authContext) DischargeCaveats(tag names.UserTag) []checkers.Caveat {
   163  	return authentication.DischargeCaveats(tag, ctxt.clock)
   164  }
   165  
   166  // authenticator returns an authenticator.Authenticator for the API
   167  // connection associated with the specified API server host.
   168  func (ctxt *authContext) authenticator(serverHost string) authenticator {
   169  	return authenticator{ctxt: ctxt, serverHost: serverHost}
   170  }
   171  
   172  // authenticator implements authenticator.Authenticator, delegating
   173  // to the appropriate authenticator based on the tag kind.
   174  type authenticator struct {
   175  	ctxt       *authContext
   176  	serverHost string
   177  }
   178  
   179  // Authenticate implements authentication.Authenticator
   180  // by choosing the right kind of authentication for the given
   181  // tag.
   182  func (a authenticator) Authenticate(
   183  	ctx context.Context,
   184  	entityFinder authentication.EntityFinder,
   185  	authParams authentication.AuthParams,
   186  ) (state.Entity, error) {
   187  	auth, err := a.authenticatorForTag(authParams.AuthTag)
   188  	if err != nil {
   189  		return nil, errors.Trace(err)
   190  	}
   191  	return auth.Authenticate(ctx, entityFinder, authParams)
   192  }
   193  
   194  // authenticatorForTag returns the authenticator appropriate
   195  // to use for a login with the given possibly-nil tag.
   196  func (a authenticator) authenticatorForTag(tag names.Tag) (authentication.EntityAuthenticator, error) {
   197  	if tag == nil || tag.Kind() == names.UserTagKind {
   198  		// Poorly written older controllers pass in an external user
   199  		// when doing api calls to the target controller during migration,
   200  		// so we need to check the user type.
   201  		if tag != nil && tag.(names.UserTag).IsLocal() {
   202  			return a.localUserAuth(), nil
   203  		}
   204  		auth, err := a.ctxt.externalMacaroonAuth(nil)
   205  		if errors.Cause(err) == errMacaroonAuthNotConfigured {
   206  			err = errors.Trace(apiservererrors.ErrNoCreds)
   207  		}
   208  		if err != nil {
   209  			return nil, errors.Trace(err)
   210  		}
   211  		return auth, nil
   212  	}
   213  	for _, agentKind := range AgentTags {
   214  		if tag.Kind() == agentKind {
   215  			return &a.ctxt.agentAuth, nil
   216  		}
   217  	}
   218  	return nil, errors.Annotatef(apiservererrors.ErrBadRequest, "unexpected login entity tag")
   219  }
   220  
   221  // localUserAuth returns an authenticator that can authenticate logins for
   222  // local users with either passwords or macaroons.
   223  func (a authenticator) localUserAuth() *authentication.LocalUserAuthenticator {
   224  	localUserIdentityLocation := url.URL{
   225  		Scheme: "https",
   226  		Host:   a.serverHost,
   227  		Path:   localUserIdentityLocationPath,
   228  	}
   229  	return &authentication.LocalUserAuthenticator{
   230  		Bakery:                    a.ctxt.localUserBakery,
   231  		Clock:                     a.ctxt.clock,
   232  		LocalUserIdentityLocation: localUserIdentityLocation.String(),
   233  	}
   234  }
   235  
   236  // externalMacaroonAuth returns an authenticator that can authenticate macaroon-based
   237  // logins for external users. If it fails once, it will always fail.
   238  func (ctxt *authContext) externalMacaroonAuth(identClient identchecker.IdentityClient) (authentication.EntityAuthenticator, error) {
   239  	ctxt.macaroonAuthOnce.Do(func() {
   240  		ctxt._macaroonAuth, ctxt._macaroonAuthError = newExternalMacaroonAuth(ctxt.st, ctxt.clock, externalLoginExpiryTime, identClient)
   241  	})
   242  	if ctxt._macaroonAuth == nil {
   243  		return nil, errors.Trace(ctxt._macaroonAuthError)
   244  	}
   245  	return ctxt._macaroonAuth, nil
   246  }
   247  
   248  var errMacaroonAuthNotConfigured = errors.New("macaroon authentication is not configured")
   249  
   250  const (
   251  	// TODO make this configurable via model config.
   252  	externalLoginExpiryTime = 24 * time.Hour
   253  )
   254  
   255  // newExternalMacaroonAuth returns an authenticator that can authenticate
   256  // macaroon-based logins for external users. This is just a helper function
   257  // for authCtxt.externalMacaroonAuth.
   258  func newExternalMacaroonAuth(st *state.State, clock clock.Clock, expiryTime time.Duration, identClient identchecker.IdentityClient) (*authentication.ExternalMacaroonAuthenticator, error) {
   259  	controllerCfg, err := st.ControllerConfig()
   260  	if err != nil {
   261  		return nil, errors.Annotate(err, "cannot get model config")
   262  	}
   263  	idURL := controllerCfg.IdentityURL()
   264  	if idURL == "" {
   265  		return nil, errMacaroonAuthNotConfigured
   266  	}
   267  	idPK := controllerCfg.IdentityPublicKey()
   268  	bakeryConfig := st.NewBakeryConfig()
   269  	key, err := bakeryConfig.GetExternalUsersThirdPartyKey()
   270  	if err != nil {
   271  		return nil, errors.Trace(err)
   272  	}
   273  
   274  	pkCache := bakery.NewThirdPartyStore()
   275  	pkLocator := httpbakery.NewThirdPartyLocator(nil, pkCache)
   276  	if idPK != nil {
   277  		pkCache.AddInfo(idURL, bakery.ThirdPartyInfo{
   278  			PublicKey: *idPK,
   279  			Version:   3,
   280  		})
   281  	}
   282  
   283  	auth := authentication.ExternalMacaroonAuthenticator{
   284  		Clock:            clock,
   285  		IdentityLocation: idURL,
   286  	}
   287  
   288  	store, err := st.NewBakeryStorage()
   289  	if err != nil {
   290  		return nil, errors.Trace(err)
   291  	}
   292  	store = store.ExpireAfter(expiryTime)
   293  	if identClient == nil {
   294  		identClient = &auth
   295  	}
   296  	identBakery := identchecker.NewBakery(identchecker.BakeryParams{
   297  		Checker:        httpbakery.NewChecker(),
   298  		Locator:        pkLocator,
   299  		Key:            key,
   300  		IdentityClient: identClient,
   301  		RootKeyStore:   store,
   302  		Authorizer: identchecker.ACLAuthorizer{
   303  			GetACL: func(ctx context.Context, op bakery.Op) ([]string, bool, error) {
   304  				return []string{identchecker.Everyone}, false, nil
   305  			},
   306  		},
   307  		Location: idURL,
   308  	})
   309  	auth.Bakery = identBakery
   310  	return &auth, nil
   311  }