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