github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/service/middleware.go (about) 1 package service 2 3 import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "time" 8 9 "github.com/codegangsta/negroni" 10 "github.com/evergreen-ci/evergreen" 11 "github.com/evergreen-ci/evergreen/auth" 12 "github.com/evergreen-ci/evergreen/model" 13 "github.com/evergreen-ci/evergreen/model/patch" 14 "github.com/evergreen-ci/evergreen/model/user" 15 "github.com/evergreen-ci/evergreen/plugin" 16 "github.com/evergreen-ci/evergreen/util" 17 "github.com/gorilla/context" 18 "github.com/gorilla/mux" 19 "github.com/mongodb/grip" 20 "github.com/pkg/errors" 21 ) 22 23 // Keys used for storing variables in request context with type safety. 24 type ( 25 RequestUserKey int 26 RequestCtxKey int 27 ) 28 29 type ( 30 // projectContext defines the set of common fields required across most UI requests. 31 projectContext struct { 32 model.Context 33 34 // AllProjects is a list of all available projects, limited to only the set of fields 35 // necessary for display. If user is logged in, this will include private projects. 36 AllProjects []UIProjectFields 37 38 // AuthRedirect indicates whether or not redirecting during authentication is necessary. 39 AuthRedirect bool 40 41 // IsAdmin indicates if the user is an admin for at least one of the projects 42 // listed in AllProjects. 43 IsAdmin bool 44 45 PluginNames []string 46 } 47 ) 48 49 type ( 50 // custom types used to attach specific values to request contexts, to prevent collisions. 51 reqUserKey int 52 reqTaskKey int 53 reqProjectContextKey int 54 ) 55 56 const ( 57 // Key values used to map user and project data to request context. 58 // These are private custom types to avoid key collisions. 59 RequestUser reqUserKey = 0 60 RequestTask reqTaskKey = 0 61 RequestProjectContext reqProjectContextKey = 0 62 ) 63 64 // GetUser returns a user if one is attached to the request. Returns nil if the user is not logged 65 // in, assuming that the middleware to lookup user information is enabled on the request handler. 66 func GetUser(r *http.Request) *user.DBUser { 67 if rv := context.Get(r, RequestUser); rv != nil { 68 return rv.(*user.DBUser) 69 } 70 return nil 71 } 72 73 // GetProjectContext fetches the projectContext associated with the request. Returns an error 74 // if no projectContext has been loaded and attached to the request. 75 func GetProjectContext(r *http.Request) (projectContext, error) { 76 if rv := context.Get(r, RequestProjectContext); rv != nil { 77 return rv.(projectContext), nil 78 } 79 return projectContext{}, errors.New("No context loaded") 80 } 81 82 // MustHaveProjectContext gets the projectContext from the request, 83 // or panics if it does not exist. 84 func MustHaveProjectContext(r *http.Request) projectContext { 85 pc, err := GetProjectContext(r) 86 if err != nil { 87 panic(err) 88 } 89 return pc 90 } 91 92 // MustHaveUser gets the user from the request or 93 // panics if it does not exist. 94 func MustHaveUser(r *http.Request) *user.DBUser { 95 u := GetUser(r) 96 if u == nil { 97 panic("no user attached to request") 98 } 99 return u 100 } 101 102 // ToPluginContext creates a UIContext from the projectContext data. 103 func (pc projectContext) ToPluginContext(settings evergreen.Settings, dbUser *user.DBUser) plugin.UIContext { 104 return plugin.UIContext{ 105 Settings: settings, 106 User: dbUser, 107 Task: pc.Task, 108 Build: pc.Build, 109 Version: pc.Version, 110 Patch: pc.Patch, 111 Project: pc.Project, 112 ProjectRef: pc.ProjectRef, 113 } 114 } 115 116 // GetSettings returns the global evergreen settings. 117 func (uis *UIServer) GetSettings() evergreen.Settings { 118 return uis.Settings 119 } 120 121 // withPluginUser takes a request and makes the user accessible to plugin code. 122 func withPluginUser(next http.Handler) http.HandlerFunc { 123 return func(w http.ResponseWriter, r *http.Request) { 124 u := GetUser(r) 125 plugin.SetUser(u, r) 126 next.ServeHTTP(w, r) 127 } 128 } 129 130 // requireAdmin takes in a request handler and returns a wrapped version which verifies that requests are 131 // authenticated and that the user is either a super user or is part of the project context's project's admins. 132 func (uis *UIServer) requireAdmin(next http.HandlerFunc) http.HandlerFunc { 133 return func(w http.ResponseWriter, r *http.Request) { 134 // get the project context 135 projCtx := MustHaveProjectContext(r) 136 if dbUser := GetUser(r); dbUser != nil { 137 if uis.isSuperUser(dbUser) || isAdmin(dbUser, projCtx.ProjectRef) { 138 next(w, r) 139 return 140 } 141 } 142 143 uis.RedirectToLogin(w, r) 144 return 145 } 146 } 147 148 // requireUser takes a request handler and returns a wrapped version which verifies that requests 149 // request are authenticated before proceeding. For a request which is not authenticated, it will 150 // execute the onFail handler. If onFail is nil, a simple "unauthorized" error will be sent. 151 func requireUser(onSuccess, onFail http.HandlerFunc) http.HandlerFunc { 152 return func(w http.ResponseWriter, r *http.Request) { 153 if GetUser(r) == nil { 154 if onFail != nil { 155 onFail(w, r) 156 return 157 } 158 http.Error(w, "Unauthorized", http.StatusUnauthorized) 159 return 160 } 161 onSuccess(w, r) 162 } 163 } 164 165 // requireSuperUser takes a request handler and returns a wrapped version which verifies that 166 // the requester is authenticated as a superuser. For a requester who isn't a super user, the 167 // request will be redirected to the login page instead. 168 func (uis *UIServer) requireSuperUser(next http.HandlerFunc) http.HandlerFunc { 169 return func(w http.ResponseWriter, r *http.Request) { 170 if len(uis.Settings.SuperUsers) == 0 { 171 f := requireUser(next, uis.RedirectToLogin) // Still must be user to proceed 172 f(w, r) 173 return 174 } 175 if uis.isSuperUser(GetUser(r)) { 176 next(w, r) 177 return 178 } 179 uis.RedirectToLogin(w, r) 180 return 181 } 182 } 183 184 // canEditPatch verifies that a user has permission to edit the given patch. 185 // A user has permission if they are a superuser, or if they are the author of the patch. 186 func (uis *UIServer) canEditPatch(currentUser *user.DBUser, currentPatch *patch.Patch) bool { 187 return currentUser.Id == currentPatch.Author || uis.isSuperUser(currentUser) 188 } 189 190 // isSuperUser verifies that a given user has super user permissions. 191 // A user has these permission if they are in the super users list or if the list is empty, 192 // in which case all users are super users. 193 func (uis *UIServer) isSuperUser(u *user.DBUser) bool { 194 if u == nil { 195 return false 196 } 197 if util.SliceContains(uis.Settings.SuperUsers, u.Id) || 198 len(uis.Settings.SuperUsers) == 0 { 199 return true 200 } 201 202 return false 203 204 } 205 206 // isAdmin returns false if the user is nil or if its id is not 207 // located in ProjectRef's Admins field. 208 func isAdmin(u *user.DBUser, project *model.ProjectRef) bool { 209 if u == nil { 210 return false 211 } 212 return util.SliceContains(project.Admins, u.Id) 213 } 214 215 // RedirectToLogin forces a redirect to the login page. The redirect param is set on the query 216 // so that the user will be returned to the original page after they login. 217 func (uis *UIServer) RedirectToLogin(w http.ResponseWriter, r *http.Request) { 218 querySep := "" 219 if r.URL.RawQuery != "" { 220 querySep = "?" 221 } 222 path := "/login#?" 223 if uis.UserManager.IsRedirect() { 224 path = "login/redirect?" 225 } 226 location := fmt.Sprintf("%v%vredirect=%v%v%v", 227 uis.Settings.Ui.Url, 228 path, 229 url.QueryEscape(r.URL.Path), 230 querySep, 231 r.URL.RawQuery) 232 http.Redirect(w, r, location, http.StatusFound) 233 } 234 235 // Loads all Task/Build/Version/Patch/Project metadata and attaches it to the request. 236 // If the project is private but the user is not logged in, redirects to the login page. 237 func (uis *UIServer) loadCtx(next http.HandlerFunc) http.HandlerFunc { 238 return func(w http.ResponseWriter, r *http.Request) { 239 projCtx, err := uis.LoadProjectContext(w, r) 240 if err != nil { 241 // Some database lookup failed when fetching the data - log it 242 uis.LoggedError(w, r, http.StatusInternalServerError, errors.Wrap(err, "Error loading project context")) 243 return 244 } 245 if projCtx.ProjectRef != nil && projCtx.ProjectRef.Private && GetUser(r) == nil { 246 uis.RedirectToLogin(w, r) 247 return 248 } 249 250 if projCtx.Patch != nil && GetUser(r) == nil { 251 uis.RedirectToLogin(w, r) 252 return 253 } 254 255 context.Set(r, RequestProjectContext, projCtx) 256 next(w, r) 257 } 258 } 259 260 // populateProjectRefs loads all project refs into the context. If includePrivate is true, 261 // all available projects will be included, otherwise only public projects will be loaded. 262 // Sets IsAdmin to true if the user id is located in a project's admin list. 263 func (pc *projectContext) populateProjectRefs(includePrivate, isSuperUser bool, dbUser *user.DBUser) error { 264 allProjs, err := model.FindAllTrackedProjectRefs() 265 if err != nil { 266 return err 267 } 268 pc.AllProjects = make([]UIProjectFields, 0, len(allProjs)) 269 // User is not logged in, so only include public projects. 270 for _, p := range allProjs { 271 if !p.Enabled { 272 continue 273 } 274 if !p.Private || includePrivate { 275 uiProj := UIProjectFields{ 276 DisplayName: p.DisplayName, 277 Identifier: p.Identifier, 278 Repo: p.Repo, 279 Owner: p.Owner, 280 } 281 pc.AllProjects = append(pc.AllProjects, uiProj) 282 } 283 284 if includePrivate && (isSuperUser || isAdmin(dbUser, &p)) { 285 pc.IsAdmin = true 286 } 287 } 288 return nil 289 } 290 291 // getRequestProjectId determines the projectId to associate with the request context, 292 // in cases where it could not be inferred from a task/build/version/patch etc. 293 // The projectId is determined using the following criteria in order of priority: 294 // 1. The projectId inferred by ProjectContext (checked outside this func) 295 // 2. The value of the project_id in the URL if present. 296 // 3. The value set in the request cookie, if present. 297 // 4. The default project in the UI settings, if present. 298 // 5. The first project in the complete list of all project refs. 299 func (uis *UIServer) getRequestProjectId(r *http.Request) string { 300 vars := mux.Vars(r) 301 projectId := vars["project_id"] 302 if len(projectId) > 0 { 303 return projectId 304 } 305 306 cookie, err := r.Cookie(ProjectCookieName) 307 if err == nil && len(cookie.Value) > 0 { 308 return cookie.Value 309 } 310 311 return uis.Settings.Ui.DefaultProject 312 } 313 314 // LoadProjectContext builds a projectContext from vars in the request's URL. 315 // This is done by reading in specific variables and inferring other required 316 // context variables when necessary (e.g. loading a project based on the task). 317 func (uis *UIServer) LoadProjectContext(rw http.ResponseWriter, r *http.Request) (projectContext, error) { 318 dbUser := GetUser(r) 319 320 vars := mux.Vars(r) 321 taskId := vars["task_id"] 322 buildId := vars["build_id"] 323 versionId := vars["version_id"] 324 patchId := vars["patch_id"] 325 326 projectId := uis.getRequestProjectId(r) 327 328 pc := projectContext{AuthRedirect: uis.UserManager.IsRedirect()} 329 isSuperUser := (dbUser != nil) && auth.IsSuperUser(uis.Settings.SuperUsers, dbUser) 330 err := pc.populateProjectRefs(dbUser != nil, isSuperUser, dbUser) 331 if err != nil { 332 return pc, err 333 } 334 335 // If we still don't have a default projectId, just use the first project in the list 336 // if there is one. 337 if len(projectId) == 0 && len(pc.AllProjects) > 0 { 338 projectId = pc.AllProjects[0].Identifier 339 } 340 341 // Build a model.Context using the data available. 342 ctx, err := model.LoadContext(taskId, buildId, versionId, patchId, projectId) 343 pc.Context = ctx 344 if err != nil { 345 return pc, err 346 } 347 348 // set the cookie for the next request if a project was found 349 if ctx.ProjectRef != nil { 350 ctx.Project, err = model.FindProject("", ctx.ProjectRef) 351 if err != nil { 352 return pc, err 353 } 354 355 // A project was found, update the project cookie for subsequent request. 356 http.SetCookie(rw, &http.Cookie{ 357 Name: ProjectCookieName, 358 Value: ctx.ProjectRef.Identifier, 359 Path: "/", 360 Expires: time.Now().Add(7 * 24 * time.Hour), 361 }) 362 } 363 364 if len(uis.GetAppPlugins()) > 0 { 365 pluginNames := []string{} 366 for _, p := range uis.GetAppPlugins() { 367 pluginNames = append(pluginNames, p.Name()) 368 } 369 pc.PluginNames = pluginNames 370 } 371 372 return pc, nil 373 } 374 375 // UserMiddleware is middleware which checks for session tokens on the Request 376 // and looks up and attaches a user for that token if one is found. 377 func UserMiddleware(um auth.UserManager) func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 378 return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 379 token := "" 380 var err error 381 // Grab token auth from cookies 382 for _, cookie := range r.Cookies() { 383 if cookie.Name == evergreen.AuthTokenCookie { 384 if token, err = url.QueryUnescape(cookie.Value); err == nil { 385 break 386 } 387 } 388 } 389 390 // Grab API auth details from header 391 var authDataAPIKey, authDataName string 392 if len(r.Header["Api-Key"]) > 0 { 393 authDataAPIKey = r.Header["Api-Key"][0] 394 } 395 if len(r.Header["Auth-Username"]) > 0 { 396 authDataName = r.Header["Auth-Username"][0] 397 } 398 if len(authDataName) == 0 && len(r.Header["Api-User"]) > 0 { 399 authDataName = r.Header["Api-User"][0] 400 } 401 402 if len(token) > 0 { 403 dbUser, err := um.GetUserByToken(token) 404 if err != nil { 405 grip.Infof("Error getting user %s: %+v", authDataName, err) 406 } else { 407 // Get the user's full details from the DB or create them if they don't exists 408 dbUser, err = model.GetOrCreateUser(dbUser.Username(), dbUser.DisplayName(), dbUser.Email()) 409 if err != nil { 410 grip.Infof("Error looking up user %s: %+v", dbUser.Username(), err) 411 } else { 412 context.Set(r, RequestUser, dbUser) 413 } 414 } 415 } else if len(authDataAPIKey) > 0 { 416 dbUser, err := user.FindOne(user.ById(authDataName)) 417 if dbUser != nil && err == nil { 418 if dbUser.APIKey != authDataAPIKey { 419 http.Error(rw, "Unauthorized - invalid API key", http.StatusUnauthorized) 420 return 421 } 422 context.Set(r, RequestUser, dbUser) 423 } else { 424 grip.Errorln("Error getting user:", err) 425 } 426 } 427 next(rw, r) 428 } 429 } 430 431 // Logger is a middleware handler that logs the request as it goes in and the response as it goes out. 432 type Logger struct { 433 // ids is a channel producing unique, autoincrementing request ids that are included in logs. 434 ids chan int 435 } 436 437 // NewLogger returns a new Logger instance 438 func NewLogger() *Logger { 439 ids := make(chan int, 100) 440 go func() { 441 reqId := 0 442 for { 443 ids <- reqId 444 reqId++ 445 } 446 }() 447 448 return &Logger{ids} 449 } 450 451 func (l *Logger) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 452 start := time.Now() 453 reqId := <-l.ids 454 455 grip.Infof("Started (%v) %s %s %s", reqId, r.Method, r.URL.Path, r.RemoteAddr) 456 457 next(rw, r) 458 459 res := rw.(negroni.ResponseWriter) 460 grip.Infof("Completed (%v) %v %s in %v", reqId, res.Status(), http.StatusText(res.Status()), time.Since(start)) 461 }