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  }