github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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 "net/url" 9 "sync" 10 "time" 11 12 "github.com/juju/errors" 13 "github.com/juju/utils/clock" 14 "gopkg.in/juju/names.v2" 15 "gopkg.in/macaroon-bakery.v1/bakery" 16 "gopkg.in/macaroon-bakery.v1/bakery/checkers" 17 "gopkg.in/macaroon-bakery.v1/httpbakery" 18 "gopkg.in/macaroon.v1" 19 20 "github.com/juju/juju/apiserver/authentication" 21 "github.com/juju/juju/apiserver/common" 22 "github.com/juju/juju/apiserver/params" 23 "github.com/juju/juju/state" 24 "github.com/juju/juju/state/bakerystorage" 25 ) 26 27 const ( 28 localUserIdentityLocationPath = "/auth" 29 ) 30 31 // authContext holds authentication context shared 32 // between all API endpoints. 33 type authContext struct { 34 st *state.State 35 36 clock clock.Clock 37 agentAuth authentication.AgentAuthenticator 38 39 // localUserBakeryService is the bakery.Service used by the controller 40 // for authenticating local users. In time, we may want to use this for 41 // both local and external users. Note that this service does not 42 // discharge the third-party caveats. 43 localUserBakeryService *expirableStorageBakeryService 44 45 // localUserThirdPartyBakeryService is the bakery.Service used by the 46 // controller for discharging third-party caveats for local users. 47 localUserThirdPartyBakeryService *bakery.Service 48 49 // localUserInteractions maintains a set of in-progress local user 50 // authentication interactions. 51 localUserInteractions *authentication.Interactions 52 53 // macaroonAuthOnce guards the fields below it. 54 macaroonAuthOnce sync.Once 55 _macaroonAuth *authentication.ExternalMacaroonAuthenticator 56 _macaroonAuthError error 57 } 58 59 // newAuthContext creates a new authentication context for st. 60 func newAuthContext(st *state.State) (*authContext, error) { 61 ctxt := &authContext{ 62 st: st, 63 // TODO(fwereade) 2016-07-21 there should be a clock parameter 64 clock: clock.WallClock, 65 localUserInteractions: authentication.NewInteractions(), 66 } 67 68 // Create a bakery service for discharging third-party caveats for 69 // local user authentication. This service does not persist keys; 70 // its macaroons should be very short-lived. 71 localUserThirdPartyBakeryService, _, err := newBakeryService(st, nil, nil) 72 if err != nil { 73 return nil, errors.Trace(err) 74 } 75 ctxt.localUserThirdPartyBakeryService = localUserThirdPartyBakeryService 76 77 // Create a bakery service for local user authentication. This service 78 // persists keys into MongoDB in a TTL collection. 79 store, err := st.NewBakeryStorage() 80 if err != nil { 81 return nil, errors.Trace(err) 82 } 83 locator := bakeryServicePublicKeyLocator{ctxt.localUserThirdPartyBakeryService} 84 localUserBakeryService, localUserBakeryServiceKey, err := newBakeryService( 85 st, store, locator, 86 ) 87 if err != nil { 88 return nil, errors.Trace(err) 89 } 90 ctxt.localUserBakeryService = &expirableStorageBakeryService{ 91 localUserBakeryService, localUserBakeryServiceKey, store, locator, 92 } 93 return ctxt, nil 94 } 95 96 type bakeryServicePublicKeyLocator struct { 97 service *bakery.Service 98 } 99 100 // PublicKeyForLocation implements bakery.PublicKeyLocator. 101 func (b bakeryServicePublicKeyLocator) PublicKeyForLocation(string) (*bakery.PublicKey, error) { 102 return b.service.PublicKey(), nil 103 } 104 105 // CreateLocalLoginMacaroon creates a macaroon that may be provided to a user 106 // as proof that they have logged in with a valid username and password. This 107 // macaroon may then be used to obtain a discharge macaroon so that the user 108 // can log in without presenting their password for a set amount of time. 109 func (ctxt *authContext) CreateLocalLoginMacaroon(tag names.UserTag) (*macaroon.Macaroon, error) { 110 return authentication.CreateLocalLoginMacaroon(tag, ctxt.localUserThirdPartyBakeryService, ctxt.clock) 111 } 112 113 // CheckLocalLoginCaveat parses and checks that the given caveat string is 114 // valid for a local login request, and returns the tag of the local user 115 // that the caveat asserts is logged in. checkers.ErrCaveatNotRecognized will 116 // be returned if the caveat is not recognised. 117 func (ctxt *authContext) CheckLocalLoginCaveat(caveat string) (names.UserTag, error) { 118 return authentication.CheckLocalLoginCaveat(caveat) 119 } 120 121 // CheckLocalLoginRequest checks that the given HTTP request contains at least 122 // one valid local login macaroon minted using CreateLocalLoginMacaroon. It 123 // returns an error with a *bakery.VerificationError cause if the macaroon 124 // verification failed. If the macaroon is valid, CheckLocalLoginRequest returns 125 // a list of caveats to add to the discharge macaroon. 126 func (ctxt *authContext) CheckLocalLoginRequest(req *http.Request, tag names.UserTag) ([]checkers.Caveat, error) { 127 return authentication.CheckLocalLoginRequest(ctxt.localUserThirdPartyBakeryService, req, tag, ctxt.clock) 128 } 129 130 // authenticator returns an authenticator.EntityAuthenticator for the API 131 // connection associated with the specified API server host. 132 func (ctxt *authContext) authenticator(serverHost string) authenticator { 133 return authenticator{ctxt: ctxt, serverHost: serverHost} 134 } 135 136 // authenticator implements authenticator.EntityAuthenticator, delegating 137 // to the appropriate authenticator based on the tag kind. 138 type authenticator struct { 139 ctxt *authContext 140 serverHost string 141 } 142 143 // Authenticate implements authentication.EntityAuthenticator 144 // by choosing the right kind of authentication for the given 145 // tag. 146 func (a authenticator) Authenticate( 147 entityFinder authentication.EntityFinder, 148 tag names.Tag, 149 req params.LoginRequest, 150 ) (state.Entity, error) { 151 auth, err := a.authenticatorForTag(tag) 152 if err != nil { 153 return nil, errors.Trace(err) 154 } 155 return auth.Authenticate(entityFinder, tag, req) 156 } 157 158 // authenticatorForTag returns the authenticator appropriate 159 // to use for a login with the given possibly-nil tag. 160 func (a authenticator) authenticatorForTag(tag names.Tag) (authentication.EntityAuthenticator, error) { 161 if tag == nil { 162 auth, err := a.ctxt.externalMacaroonAuth() 163 if errors.Cause(err) == errMacaroonAuthNotConfigured { 164 err = errors.Trace(common.ErrNoCreds) 165 } 166 if err != nil { 167 return nil, errors.Trace(err) 168 } 169 return auth, nil 170 } 171 switch tag.Kind() { 172 case names.UnitTagKind, names.MachineTagKind: 173 return &a.ctxt.agentAuth, nil 174 case names.UserTagKind: 175 return a.localUserAuth(), nil 176 default: 177 return nil, errors.Annotatef(common.ErrBadRequest, "unexpected login entity tag") 178 } 179 } 180 181 // localUserAuth returns an authenticator that can authenticate logins for 182 // local users with either passwords or macaroons. 183 func (a authenticator) localUserAuth() *authentication.UserAuthenticator { 184 localUserIdentityLocation := url.URL{ 185 Scheme: "https", 186 Host: a.serverHost, 187 Path: localUserIdentityLocationPath, 188 } 189 return &authentication.UserAuthenticator{ 190 Service: a.ctxt.localUserBakeryService, 191 Clock: a.ctxt.clock, 192 LocalUserIdentityLocation: localUserIdentityLocation.String(), 193 } 194 } 195 196 // externalMacaroonAuth returns an authenticator that can authenticate macaroon-based 197 // logins for external users. If it fails once, it will always fail. 198 func (ctxt *authContext) externalMacaroonAuth() (authentication.EntityAuthenticator, error) { 199 ctxt.macaroonAuthOnce.Do(func() { 200 ctxt._macaroonAuth, ctxt._macaroonAuthError = newExternalMacaroonAuth(ctxt.st) 201 }) 202 if ctxt._macaroonAuth == nil { 203 return nil, errors.Trace(ctxt._macaroonAuthError) 204 } 205 return ctxt._macaroonAuth, nil 206 } 207 208 var errMacaroonAuthNotConfigured = errors.New("macaroon authentication is not configured") 209 210 // newExternalMacaroonAuth returns an authenticator that can authenticate 211 // macaroon-based logins for external users. This is just a helper function 212 // for authCtxt.externalMacaroonAuth. 213 func newExternalMacaroonAuth(st *state.State) (*authentication.ExternalMacaroonAuthenticator, error) { 214 controllerCfg, err := st.ControllerConfig() 215 if err != nil { 216 return nil, errors.Annotate(err, "cannot get model config") 217 } 218 idURL := controllerCfg.IdentityURL() 219 if idURL == "" { 220 return nil, errMacaroonAuthNotConfigured 221 } 222 // The identity server has been configured, 223 // so configure the bakery service appropriately. 224 idPK := controllerCfg.IdentityPublicKey() 225 if idPK == nil { 226 // No public key supplied - retrieve it from the identity manager. 227 idPK, err = httpbakery.PublicKeyForLocation(http.DefaultClient, idURL) 228 if err != nil { 229 return nil, errors.Annotate(err, "cannot get identity public key") 230 } 231 } 232 // We pass in nil for the storage, which leads to in-memory storage 233 // being used. We only use in-memory storage for now, since we never 234 // expire the keys, and don't want garbage to accumulate. 235 // 236 // TODO(axw) we should store the key in mongo, so that multiple servers 237 // can authenticate. That will require that we encode the server's ID 238 // in the macaroon ID so that servers don't overwrite each others' keys. 239 svc, _, err := newBakeryService(st, nil, bakery.PublicKeyLocatorMap{idURL: idPK}) 240 if err != nil { 241 return nil, errors.Annotate(err, "cannot make bakery service") 242 } 243 var auth authentication.ExternalMacaroonAuthenticator 244 auth.Service = svc 245 auth.Macaroon, err = svc.NewMacaroon("api-login", nil, nil) 246 if err != nil { 247 return nil, errors.Annotate(err, "cannot make macaroon") 248 } 249 auth.IdentityLocation = idURL 250 return &auth, nil 251 } 252 253 // newBakeryService creates a new bakery.Service. 254 func newBakeryService( 255 st *state.State, 256 store bakerystorage.ExpirableStorage, 257 locator bakery.PublicKeyLocator, 258 ) (*bakery.Service, *bakery.KeyPair, error) { 259 key, err := bakery.GenerateKey() 260 if err != nil { 261 return nil, nil, errors.Annotate(err, "generating key for bakery service") 262 } 263 service, err := bakery.NewService(bakery.NewServiceParams{ 264 Location: "juju model " + st.ModelUUID(), 265 Store: store, 266 Key: key, 267 Locator: locator, 268 }) 269 if err != nil { 270 return nil, nil, errors.Trace(err) 271 } 272 return service, key, nil 273 } 274 275 // expirableStorageBakeryService wraps bakery.Service, adding the ExpireStorageAt method. 276 type expirableStorageBakeryService struct { 277 *bakery.Service 278 key *bakery.KeyPair 279 store bakerystorage.ExpirableStorage 280 locator bakery.PublicKeyLocator 281 } 282 283 // ExpireStorageAt implements authentication.ExpirableStorageBakeryService. 284 func (s *expirableStorageBakeryService) ExpireStorageAt(t time.Time) (authentication.ExpirableStorageBakeryService, error) { 285 store := s.store.ExpireAt(t) 286 service, err := bakery.NewService(bakery.NewServiceParams{ 287 Location: s.Location(), 288 Store: store, 289 Key: s.key, 290 Locator: s.locator, 291 }) 292 if err != nil { 293 return nil, errors.Trace(err) 294 } 295 return &expirableStorageBakeryService{service, s.key, store, s.locator}, nil 296 }