github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/apiserver/authcontext.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver
     5  
     6  import (
     7  	"net/http"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/names"
    13  	"gopkg.in/macaroon-bakery.v1/bakery"
    14  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    15  
    16  	"github.com/juju/juju/apiserver/authentication"
    17  	"github.com/juju/juju/apiserver/common"
    18  	"github.com/juju/juju/apiserver/params"
    19  	"github.com/juju/juju/state"
    20  	"github.com/juju/juju/state/bakerystorage"
    21  )
    22  
    23  // authContext holds authentication context shared
    24  // between all API endpoints.
    25  type authContext struct {
    26  	st *state.State
    27  
    28  	agentAuth authentication.AgentAuthenticator
    29  	userAuth  authentication.UserAuthenticator
    30  
    31  	// macaroonAuthOnce guards the fields below it.
    32  	macaroonAuthOnce   sync.Once
    33  	_macaroonAuth      *authentication.ExternalMacaroonAuthenticator
    34  	_macaroonAuthError error
    35  }
    36  
    37  // newAuthContext creates a new authentication context for st.
    38  func newAuthContext(st *state.State) (*authContext, error) {
    39  	ctxt := &authContext{st: st}
    40  	store, err := st.NewBakeryStorage()
    41  	if err != nil {
    42  		return nil, errors.Trace(err)
    43  	}
    44  	// We use a non-nil, but empty key, because we don't use the
    45  	// key, and don't want to incur the overhead of generating one
    46  	// each time we create a service.
    47  	bakeryService, key, err := newBakeryService(st, store, nil)
    48  	if err != nil {
    49  		return nil, errors.Trace(err)
    50  	}
    51  	ctxt.userAuth.Service = &expirableStorageBakeryService{bakeryService, key, store, nil}
    52  	// TODO(fwereade): 2016-03-17 lp:1558657
    53  	ctxt.userAuth.Clock = state.GetClock()
    54  	return ctxt, nil
    55  }
    56  
    57  // Authenticate implements authentication.EntityAuthenticator
    58  // by choosing the right kind of authentication for the given
    59  // tag.
    60  func (ctxt *authContext) Authenticate(entityFinder authentication.EntityFinder, tag names.Tag, req params.LoginRequest) (state.Entity, error) {
    61  	auth, err := ctxt.authenticatorForTag(tag)
    62  	if err != nil {
    63  		return nil, errors.Trace(err)
    64  	}
    65  	return auth.Authenticate(entityFinder, tag, req)
    66  }
    67  
    68  // authenticatorForTag returns the authenticator appropriate
    69  // to use for a login with the given possibly-nil tag.
    70  func (ctxt *authContext) authenticatorForTag(tag names.Tag) (authentication.EntityAuthenticator, error) {
    71  	if tag == nil {
    72  		auth, err := ctxt.macaroonAuth()
    73  		if errors.Cause(err) == errMacaroonAuthNotConfigured {
    74  			// Make a friendlier error message.
    75  			err = errors.New("no credentials provided")
    76  		}
    77  		if err != nil {
    78  			return nil, errors.Trace(err)
    79  		}
    80  		return auth, nil
    81  	}
    82  	switch tag.Kind() {
    83  	case names.UnitTagKind, names.MachineTagKind:
    84  		return &ctxt.agentAuth, nil
    85  	case names.UserTagKind:
    86  		return &ctxt.userAuth, nil
    87  	default:
    88  		return nil, errors.Annotatef(common.ErrBadRequest, "unexpected login entity tag")
    89  	}
    90  }
    91  
    92  // macaroonAuth returns an authenticator that can authenticate macaroon-based
    93  // logins. If it fails once, it will always fail.
    94  func (ctxt *authContext) macaroonAuth() (authentication.EntityAuthenticator, error) {
    95  	ctxt.macaroonAuthOnce.Do(func() {
    96  		ctxt._macaroonAuth, ctxt._macaroonAuthError = newExternalMacaroonAuth(ctxt.st)
    97  	})
    98  	if ctxt._macaroonAuth == nil {
    99  		return nil, errors.Trace(ctxt._macaroonAuthError)
   100  	}
   101  	return ctxt._macaroonAuth, nil
   102  }
   103  
   104  var errMacaroonAuthNotConfigured = errors.New("macaroon authentication is not configured")
   105  
   106  // newExternalMacaroonAuth returns an authenticator that can authenticate
   107  // macaroon-based logins for external users. This is just a helper function
   108  // for authCtxt.macaroonAuth.
   109  func newExternalMacaroonAuth(st *state.State) (*authentication.ExternalMacaroonAuthenticator, error) {
   110  	envCfg, err := st.ModelConfig()
   111  	if err != nil {
   112  		return nil, errors.Annotate(err, "cannot get model config")
   113  	}
   114  	idURL := envCfg.IdentityURL()
   115  	if idURL == "" {
   116  		return nil, errMacaroonAuthNotConfigured
   117  	}
   118  	// The identity server has been configured,
   119  	// so configure the bakery service appropriately.
   120  	idPK := envCfg.IdentityPublicKey()
   121  	if idPK == nil {
   122  		// No public key supplied - retrieve it from the identity manager.
   123  		idPK, err = httpbakery.PublicKeyForLocation(http.DefaultClient, idURL)
   124  		if err != nil {
   125  			return nil, errors.Annotate(err, "cannot get identity public key")
   126  		}
   127  	}
   128  	// We pass in nil for the storage, which leads to in-memory storage
   129  	// being used. We only use in-memory storage for now, since we never
   130  	// expire the keys, and don't want garbage to accumulate.
   131  	//
   132  	// TODO(axw) we should store the key in mongo, so that multiple servers
   133  	// can authenticate. That will require that we encode the server's ID
   134  	// in the macaroon ID so that servers don't overwrite each others' keys.
   135  	svc, _, err := newBakeryService(st, nil, bakery.PublicKeyLocatorMap{idURL: idPK})
   136  	if err != nil {
   137  		return nil, errors.Annotate(err, "cannot make bakery service")
   138  	}
   139  	var auth authentication.ExternalMacaroonAuthenticator
   140  	auth.Service = svc
   141  	auth.Macaroon, err = svc.NewMacaroon("api-login", nil, nil)
   142  	if err != nil {
   143  		return nil, errors.Annotate(err, "cannot make macaroon")
   144  	}
   145  	auth.IdentityLocation = idURL
   146  	return &auth, nil
   147  }
   148  
   149  // newBakeryService creates a new bakery.Service.
   150  func newBakeryService(
   151  	st *state.State,
   152  	store bakerystorage.ExpirableStorage,
   153  	locator bakery.PublicKeyLocator,
   154  ) (*bakery.Service, *bakery.KeyPair, error) {
   155  	key, err := bakery.GenerateKey()
   156  	if err != nil {
   157  		return nil, nil, errors.Annotate(err, "generating key for bakery service")
   158  	}
   159  	service, err := bakery.NewService(bakery.NewServiceParams{
   160  		Location: "juju model " + st.ModelUUID(),
   161  		Store:    store,
   162  		Key:      key,
   163  		Locator:  locator,
   164  	})
   165  	if err != nil {
   166  		return nil, nil, errors.Trace(err)
   167  	}
   168  	return service, key, nil
   169  }
   170  
   171  // expirableStorageBakeryService wraps bakery.Service, adding the ExpireStorageAt method.
   172  type expirableStorageBakeryService struct {
   173  	*bakery.Service
   174  	key     *bakery.KeyPair
   175  	store   bakerystorage.ExpirableStorage
   176  	locator bakery.PublicKeyLocator
   177  }
   178  
   179  // ExpireStorageAt implements authentication.ExpirableStorageBakeryService.
   180  func (s *expirableStorageBakeryService) ExpireStorageAt(t time.Time) (authentication.ExpirableStorageBakeryService, error) {
   181  	store := s.store.ExpireAt(t)
   182  	service, err := bakery.NewService(bakery.NewServiceParams{
   183  		Location: s.Location(),
   184  		Store:    store,
   185  		Key:      s.key,
   186  		Locator:  s.locator,
   187  	})
   188  	if err != nil {
   189  		return nil, errors.Trace(err)
   190  	}
   191  	return &expirableStorageBakeryService{service, s.key, store, s.locator}, nil
   192  }