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 }