forked from influxdata/chronograf
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathauth.go
346 lines (305 loc) · 10.7 KB
/
auth.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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
package server
import (
"context"
"crypto"
"crypto/rsa"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/oauth2"
"github.com/influxdata/chronograf/organizations"
"github.com/influxdata/chronograf/roles"
)
// HasAuthorizedToken extracts the token from a request and validates it using the authenticator.
// It is used by routes that need access to the token to populate links request.
func HasAuthorizedToken(auth oauth2.Authenticator, r *http.Request) (oauth2.Principal, error) {
ctx := r.Context()
return auth.Validate(ctx, r)
}
// AuthorizedToken extracts the token and validates; if valid the next handler
// will be run. The principal will be sent to the next handler via the request's
// Context. It is up to the next handler to determine if the principal has access.
// On failure, will return http.StatusForbidden.
func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := logger.
WithField("component", "token_auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
if validSignature(log, r.Header.Get("Authorization")) {
next.ServeHTTP(w, r)
return
}
ctx := r.Context()
// We do not check the authorization of the principal. Those
// served further down the chain should do so.
principal, err := auth.Validate(ctx, r)
if err != nil {
log.Error("Invalid principal")
w.WriteHeader(http.StatusForbidden)
return
}
// If the principal is valid we will extend its lifespan
// into the future
principal, err = auth.Extend(ctx, w, principal)
if err != nil {
log.Error("Unable to extend principal")
w.WriteHeader(http.StatusForbidden)
return
}
// Send the principal to the next handler
ctx = context.WithValue(ctx, oauth2.PrincipalKey, principal)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// RawStoreAccess gives a super admin access to the data store without a facade.
func RawStoreAccess(logger chronograf.Logger, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if isServer := hasServerContext(ctx); isServer {
next(w, r)
return
}
log := logger.
WithField("component", "raw_store").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
r = r.WithContext(serverContext(ctx))
} else {
log.Error("User making request is not a SuperAdmin")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
next(w, r)
}
}
// nonce returns an nonce message to be signed.
func nonce(expires time.Duration) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Expires", msgLastSet.Add(expires).Format(time.RFC1123))
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(signerMessage))
}
}
var (
signerMessage = uuid.New().String() // signerMessage is the message to sign with the superadmin user's private key.
msgLastSet = time.Now()
)
func rotateSuperAdminNonce(ctx context.Context, expires time.Duration) {
tick := time.NewTicker(expires)
defer tick.Stop()
for {
select {
case <-tick.C:
msgLastSet = time.Now()
signerMessage = uuid.New().String()
case <-ctx.Done():
return
}
}
}
// validSignature validates the message was signed with the private key corresponding
// to the public key given to chronograf on start. Ideally, we would provide the
// message to be signed to the user in another call. This would allow old signature/msg
// pairs to be "expired".
func validSignature(log chronograf.Logger, authHeader string) bool {
if publicKey == nil || authHeader == "" {
return false
}
sig := strings.TrimSpace(strings.TrimPrefix(authHeader, "CHRONOGRAF-SHA256"))
h := crypto.SHA256.New()
h.Write([]byte(signerMessage))
d := h.Sum(nil)
data, err := base64.StdEncoding.DecodeString(sig)
if err != nil {
log.Debug("Failed to base64 decode signature")
return false
}
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, d, data)
if err != nil {
log.Debug("Failed to verify signature: ", err)
return false
}
return true
}
// AuthorizedUser extracts the user name and provider from context. If the
// user and provider can be found on the context, we look up the user by their
// name and provider. If the user is found, we verify that the user has at at
// least the role supplied.
func AuthorizedUser(
store DataStore,
useAuth bool,
role string,
logger chronograf.Logger,
next http.HandlerFunc,
) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
serverCtx := serverContext(ctx)
log := logger.
WithField("component", "role_auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
defaultOrg, err := store.Organizations(serverCtx).DefaultOrganization(serverCtx)
if err != nil {
log.Error(fmt.Sprintf("Failed to retrieve the default organization: %v", err))
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
if validSignature(log, r.Header.Get("Authorization")) {
// If there is super admin auth, then set the organization id to be the deault org id on context
// so that calls like hasOrganizationContext as used in Organization Config service
// method OrganizationConfig can successfully get the organization id
ctx = context.WithValue(ctx, organizations.ContextKey, defaultOrg.ID)
// And if there is super admin auth, then give the user raw access to the DataStore
r = r.WithContext(serverContext(ctx))
next(w, r)
return
}
if !useAuth {
// If there is no auth, then set the organization id to be the default org id on context
// so that calls like hasOrganizationContext as used in Organization Config service
// method OrganizationConfig can successfully get the organization id
ctx = context.WithValue(ctx, organizations.ContextKey, defaultOrg.ID)
// And if there is no auth, then give the user raw access to the DataStore
r = r.WithContext(serverContext(ctx))
next(w, r)
return
}
p, err := getValidPrincipal(ctx)
if err != nil {
log.Error("Failed to retrieve principal from context")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
scheme, err := getScheme(ctx)
if err != nil {
log.Error("Failed to retrieve scheme from context")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
// This is as if the user was logged into the default organization
if p.Organization == "" {
p.Organization = defaultOrg.ID
}
// validate that the organization exists
_, err = store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization})
if err != nil {
log.Error(fmt.Sprintf("Failed to retrieve organization %s from organizations store", p.Organization))
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization)
// TODO: seems silly to look up a user twice
u, err := store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{
Name: &p.Subject,
Provider: &p.Issuer,
Scheme: &scheme,
})
if err != nil {
log.Error("Failed to retrieve user")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
// In particular this is used by sever/users.go so that we know when and when not to
// allow users to make someone a super admin
ctx = context.WithValue(ctx, UserContextKey, u)
if u.SuperAdmin {
// To access resources (servers, sources, databases, layouts) within a DataStore,
// an organization and a role are required even if you are a super admin or are
// not using auth. Every user's current organization is set on context to filter
// the resources accessed within a DataStore, including for super admin or when
// not using auth. In this way, a DataStore can treat all requests the same,
// including those from a super admin and when not using auth.
//
// As for roles, in the case of super admin or when not using auth, the user's
// role on context (though not on their JWT or user) is set to be admin. In order
// to access all resources belonging to their current organization.
ctx = context.WithValue(ctx, roles.ContextKey, roles.AdminRoleName)
r = r.WithContext(ctx)
next(w, r)
return
}
u, err = store.Users(ctx).Get(ctx, chronograf.UserQuery{
Name: &p.Subject,
Provider: &p.Issuer,
Scheme: &scheme,
})
if err != nil {
log.Error("Failed to retrieve user")
Error(w, http.StatusForbidden, "User is not authorized", logger)
return
}
if hasAuthorizedRole(u, role) {
if len(u.Roles) != 1 {
msg := `User %d has too many role in organization. User: %#v.Please report this log at https://github.com/influxdata/chronograf/issues/new"`
log.Error(fmt.Sprint(msg, u.ID, u))
unknownErrorWithMessage(w, fmt.Errorf("please have administrator check logs and report error"), logger)
return
}
// use the first role, since there should only ever be one
// for any particular organization and hasAuthorizedRole
// should ensure that at least one role for the org exists
ctx = context.WithValue(ctx, roles.ContextKey, u.Roles[0].Name)
r = r.WithContext(ctx)
next(w, r)
return
}
Error(w, http.StatusForbidden, "User is not authorized", logger)
})
}
func hasAuthorizedRole(u *chronograf.User, role string) bool {
if u == nil {
return false
}
switch role {
case roles.MemberRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.MemberRoleName, roles.ReaderRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
return true
}
}
case roles.ReaderRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.ReaderRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
return true
}
}
case roles.ViewerRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
return true
}
}
case roles.EditorRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.EditorRoleName, roles.AdminRoleName:
return true
}
}
case roles.AdminRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.AdminRoleName:
return true
}
}
case roles.SuperAdminStatus:
// SuperAdmins should have been authorized before this.
// This is only meant to restrict access for non-superadmins.
return false
}
return false
}