forked from evergreen-ci/evergreen
-
Notifications
You must be signed in to change notification settings - Fork 0
/
middleware.go
350 lines (303 loc) · 10.7 KB
/
middleware.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
347
348
349
350
package service
import (
"fmt"
"net/http"
"net/url"
"time"
"github.com/evergreen-ci/evergreen"
"github.com/evergreen-ci/evergreen/auth"
"github.com/evergreen-ci/evergreen/model"
"github.com/evergreen-ci/evergreen/model/user"
"github.com/evergreen-ci/evergreen/plugin"
"github.com/evergreen-ci/evergreen/util"
"github.com/evergreen-ci/gimlet"
"github.com/gorilla/csrf"
"github.com/mongodb/grip"
"github.com/mongodb/grip/message"
"github.com/pkg/errors"
)
// Key used for storing variables in request context with type safety.
type reqCtxKey int
const (
ProjectCookieName string = "mci-project-cookie"
// Key values used to map user and project data to request context.
// These are private custom types to avoid key collisions.
RequestTask reqCtxKey = iota
RequestProjectContext
)
// projectContext defines the set of common fields required across most UI requests.
type projectContext struct {
model.Context
// AllProjects is a list of all available projects, limited to only the set of fields
// necessary for display. If user is logged in, this will include private projects.
AllProjects []UIProjectFields
// AuthRedirect indicates whether or not redirecting during authentication is necessary.
AuthRedirect bool
// IsAdmin indicates if the user is an admin for at least one of the projects
// listed in AllProjects.
IsAdmin bool
PluginNames []string
}
// MustHaveProjectContext gets the projectContext from the request,
// or panics if it does not exist.
func MustHaveProjectContext(r *http.Request) projectContext {
pc, err := GetProjectContext(r)
if err != nil {
panic(err)
}
return pc
}
// MustHaveUser gets the user from the request or
// panics if it does not exist.
func MustHaveUser(r *http.Request) *user.DBUser {
u := gimlet.GetUser(r.Context())
if u == nil {
panic("no user attached to request")
}
usr, ok := u.(*user.DBUser)
if !ok {
panic("malformed user attached to request")
}
return usr
}
// ToPluginContext creates a UIContext from the projectContext data.
func (pc projectContext) ToPluginContext(settings evergreen.Settings, usr gimlet.User) plugin.UIContext {
dbUser, ok := usr.(*user.DBUser)
grip.CriticalWhen(!ok && usr != nil, message.Fields{
"message": "problem converting user interface to db record",
"location": "service/middleware.ToPluginContext",
"cause": "programmer error",
"type": fmt.Sprintf("%T", usr),
})
return plugin.UIContext{
Settings: settings,
User: dbUser,
Task: pc.Task,
Build: pc.Build,
Version: pc.Version,
Patch: pc.Patch,
ProjectRef: pc.ProjectRef,
}
}
// GetSettings returns the global evergreen settings.
func (uis *UIServer) GetSettings() evergreen.Settings {
return uis.Settings
}
// requireAdmin takes in a request handler and returns a wrapped version which verifies that requests are
// authenticated and that the user is either a super user or is part of the project context's project's admins.
func (uis *UIServer) requireAdmin(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// get the project context
projCtx := MustHaveProjectContext(r)
if dbUser := gimlet.GetUser(ctx); dbUser != nil {
if uis.isSuperUser(dbUser) || isAdmin(dbUser, projCtx.ProjectRef) {
next(w, r)
return
}
}
uis.RedirectToLogin(w, r)
}
}
// requireUser takes a request handler and returns a wrapped version which verifies that requests
// request are authenticated before proceeding. For a request which is not authenticated, it will
// execute the onFail handler. If onFail is nil, a simple "unauthorized" error will be sent.
func requireUser(onSuccess, onFail http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if user := gimlet.GetUser(r.Context()); user == nil {
if onFail != nil {
onFail(w, r)
return
}
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
onSuccess(w, r)
}
}
// requireSuperUser takes a request handler and returns a wrapped version which verifies that
// the requester is authenticated as a superuser. For a requester who isn't a super user, the
// request will be redirected to the login page instead.
func (uis *UIServer) requireSuperUser(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if len(uis.Settings.SuperUsers) == 0 {
f := requireUser(next, uis.RedirectToLogin) // Still must be user to proceed
f(w, r)
return
}
usr := gimlet.GetUser(r.Context())
if usr != nil && uis.isSuperUser(usr) {
next(w, r)
return
}
uis.RedirectToLogin(w, r)
}
}
func (uis *UIServer) requireLogin(next http.HandlerFunc) http.HandlerFunc {
return requireUser(next, uis.RedirectToLogin)
}
// isSuperUser verifies that a given user has super user permissions.
// A user has these permission if they are in the super users list or if the list is empty,
// in which case all users are super users.
func (uis *UIServer) isSuperUser(u gimlet.User) bool {
if util.StringSliceContains(uis.Settings.SuperUsers, u.Username()) ||
len(uis.Settings.SuperUsers) == 0 {
return true
}
return false
}
// isAdmin returns false if the user is nil or if its id is not
// located in ProjectRef's Admins field.
func isAdmin(u gimlet.User, project *model.ProjectRef) bool {
return util.StringSliceContains(project.Admins, u.Username())
}
// RedirectToLogin forces a redirect to the login page. The redirect param is set on the query
// so that the user will be returned to the original page after they login.
func (uis *UIServer) RedirectToLogin(w http.ResponseWriter, r *http.Request) {
querySep := ""
if r.URL.RawQuery != "" {
querySep = "?"
}
path := "/login#?"
if uis.UserManager.IsRedirect() {
path = "login/redirect?"
}
location := fmt.Sprintf("%v%vredirect=%v%v%v",
uis.Settings.Ui.Url,
path,
url.QueryEscape(r.URL.Path),
querySep,
r.URL.RawQuery)
http.Redirect(w, r, location, http.StatusFound)
}
// Loads all Task/Build/Version/Patch/Project metadata and attaches it to the request.
// If the project is private but the user is not logged in, redirects to the login page.
func (uis *UIServer) loadCtx(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
projCtx, err := uis.LoadProjectContext(w, r)
if err != nil {
// Some database lookup failed when fetching the data - log it
uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrap(err, "Error loading project context"))
return
}
usr := gimlet.GetUser(r.Context())
if usr == nil && (projCtx.ProjectRef != nil && projCtx.ProjectRef.Private) {
uis.RedirectToLogin(w, r)
return
}
if usr == nil && projCtx.Patch != nil {
uis.RedirectToLogin(w, r)
return
}
r = setUIRequestContext(r, projCtx)
next(w, r)
}
}
// populateProjectRefs loads all project refs into the context. If includePrivate is true,
// all available projects will be included, otherwise only public projects will be loaded.
// Sets IsAdmin to true if the user id is located in a project's admin list.
func (pc *projectContext) populateProjectRefs(includePrivate, isSuperUser bool, user gimlet.User) error {
allProjs, err := model.FindAllTrackedProjectRefs()
if err != nil {
return err
}
pc.AllProjects = make([]UIProjectFields, 0, len(allProjs))
// User is not logged in, so only include public projects.
for _, p := range allProjs {
if includePrivate && (isSuperUser || isAdmin(user, &p)) {
pc.IsAdmin = true
}
if !p.Enabled {
continue
}
if !p.Private || includePrivate {
uiProj := UIProjectFields{
DisplayName: p.DisplayName,
Identifier: p.Identifier,
Repo: p.Repo,
Owner: p.Owner,
}
pc.AllProjects = append(pc.AllProjects, uiProj)
}
}
return nil
}
// getRequestProjectId determines the projectId to associate with the request context,
// in cases where it could not be inferred from a task/build/version/patch etc.
// The projectId is determined using the following criteria in order of priority:
// 1. The projectId inferred by ProjectContext (checked outside this func)
// 2. The value of the project_id in the URL if present.
// 3. The value set in the request cookie, if present.
// 4. The default project in the UI settings, if present.
// 5. The first project in the complete list of all project refs.
func (uis *UIServer) getRequestProjectId(r *http.Request) string {
projectId := gimlet.GetVars(r)["project_id"]
if len(projectId) > 0 {
return projectId
}
cookie, err := r.Cookie(ProjectCookieName)
if err == nil && len(cookie.Value) > 0 {
return cookie.Value
}
return uis.Settings.Ui.DefaultProject
}
// LoadProjectContext builds a projectContext from vars in the request's URL.
// This is done by reading in specific variables and inferring other required
// context variables when necessary (e.g. loading a project based on the task).
func (uis *UIServer) LoadProjectContext(rw http.ResponseWriter, r *http.Request) (projectContext, error) {
dbUser := gimlet.GetUser(r.Context())
vars := gimlet.GetVars(r)
taskId := vars["task_id"]
buildId := vars["build_id"]
versionId := vars["version_id"]
patchId := vars["patch_id"]
projectId := uis.getRequestProjectId(r)
pc := projectContext{AuthRedirect: uis.UserManager.IsRedirect()}
isSuperUser := (dbUser != nil) && auth.IsSuperUser(uis.Settings.SuperUsers, dbUser)
err := pc.populateProjectRefs(dbUser != nil, isSuperUser, dbUser)
if err != nil {
return pc, err
}
// If we still don't have a default projectId, just use the first project in the list
// if there is one.
if len(projectId) == 0 && len(pc.AllProjects) > 0 {
projectId = pc.AllProjects[0].Identifier
}
// Build a model.Context using the data available.
ctx, err := model.LoadContext(taskId, buildId, versionId, patchId, projectId)
pc.Context = ctx
if err != nil {
return pc, err
}
// set the cookie for the next request if a project was found
if ctx.ProjectRef != nil {
http.SetCookie(rw, &http.Cookie{
Name: ProjectCookieName,
Value: ctx.ProjectRef.Identifier,
Path: "/",
Expires: time.Now().Add(7 * 24 * time.Hour),
})
}
return pc, nil
}
func GetUserMiddlewareConf() gimlet.UserMiddlewareConfiguration {
return gimlet.UserMiddlewareConfiguration{
CookieName: evergreen.AuthTokenCookie,
HeaderKeyName: evergreen.APIKeyHeader,
HeaderUserName: evergreen.APIUserHeader,
}
}
// ForbiddenHandler logs a rejected request befure returning a 403 to the client
func ForbiddenHandler(w http.ResponseWriter, r *http.Request) {
reason := csrf.FailureReason(r)
grip.Warning(message.Fields{
"action": "forbidden",
"method": r.Method,
"remote": r.RemoteAddr,
"path": r.URL.Path,
"reason": reason.Error(),
})
http.Error(w, fmt.Sprintf("%s - %s",
http.StatusText(http.StatusForbidden), reason),
http.StatusForbidden)
}