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 }