diff options
Diffstat (limited to 'api4/context.go')
-rw-r--r-- | api4/context.go | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/api4/context.go b/api4/context.go new file mode 100644 index 000000000..1a3011795 --- /dev/null +++ b/api4/context.go @@ -0,0 +1,360 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package api4 + +import ( + "fmt" + "net/http" + "strings" + "time" + + l4g "github.com/alecthomas/log4go" + goi18n "github.com/nicksnyder/go-i18n/i18n" + + "github.com/mattermost/platform/app" + "github.com/mattermost/platform/einterfaces" + "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" +) + +type Context struct { + Session model.Session + Params *ApiParams + Err *model.AppError + T goi18n.TranslateFunc + RequestId string + IpAddress string + Path string + siteURL string +} + +func ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{ + handleFunc: h, + requireSession: false, + trustRequester: false, + requireMfa: false, + } +} + +func ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{ + handleFunc: h, + requireSession: true, + trustRequester: false, + requireMfa: true, + } +} + +func ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{ + handleFunc: h, + requireSession: true, + trustRequester: false, + requireMfa: false, + } +} + +func ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{ + handleFunc: h, + requireSession: false, + trustRequester: true, + requireMfa: false, + } +} + +func ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler { + return &handler{ + handleFunc: h, + requireSession: true, + trustRequester: true, + requireMfa: true, + } +} + +type handler struct { + handleFunc func(*Context, http.ResponseWriter, *http.Request) + requireSession bool + trustRequester bool + requireMfa bool +} + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + now := time.Now() + l4g.Debug("%v - %v", r.Method, r.URL.Path) + + c := &Context{} + c.T, _ = utils.GetTranslationsAndLocale(w, r) + c.RequestId = model.NewId() + c.IpAddress = utils.GetIpAddress(r) + c.Params = ApiParamsFromRequest(r) + + token := "" + isTokenFromQueryString := false + + // Attempt to parse token out of the header + authHeader := r.Header.Get(model.HEADER_AUTH) + if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER { + // Default session token + token = authHeader[7:] + + } else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN { + // OAuth token + token = authHeader[6:] + } + + // Attempt to parse the token from the cookie + if len(token) == 0 { + if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil { + token = cookie.Value + + if h.requireSession && !h.trustRequester { + if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML { + c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt") + token = "" + } + } + } + } + + // Attempt to parse token out of the query string + if len(token) == 0 { + token = r.URL.Query().Get("access_token") + isTokenFromQueryString = true + } + + if utils.GetSiteURL() == "" { + protocol := app.GetProtocol(r) + c.SetSiteURL(protocol + "://" + r.Host) + } + + w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId) + w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.CfgHash)) + if einterfaces.GetClusterInterface() != nil { + w.Header().Set(model.HEADER_CLUSTER_ID, einterfaces.GetClusterInterface().GetClusterId()) + } + + w.Header().Set("Content-Type", "application/json") + + if r.Method == "GET" { + w.Header().Set("Expires", "0") + } + + if len(token) != 0 { + session, err := app.GetSession(token) + + if err != nil { + l4g.Error(utils.T("api.context.invalid_session.error"), err.Error()) + c.RemoveSessionCookie(w, r) + if h.requireSession { + c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token) + c.Err.StatusCode = http.StatusUnauthorized + } + } else if !session.IsOAuth && isTokenFromQueryString { + c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token) + c.Err.StatusCode = http.StatusUnauthorized + } else { + c.Session = *session + } + } + + c.Path = r.URL.Path + + if c.Err == nil && h.requireSession { + c.SessionRequired() + } + + if c.Err == nil && h.requireMfa { + c.MfaRequired() + } + + if c.Err == nil { + h.handleFunc(c, w, r) + } + + // Handle errors that have occured + if c.Err != nil { + c.Err.Translate(c.T) + c.Err.RequestId = c.RequestId + c.LogError(c.Err) + c.Err.Where = r.URL.Path + + // Block out detailed error when not in developer mode + if !*utils.Cfg.ServiceSettings.EnableDeveloper { + c.Err.DetailedError = "" + } + + w.WriteHeader(c.Err.StatusCode) + w.Write([]byte(c.Err.ToJson())) + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementHttpError() + } + } + + if einterfaces.GetMetricsInterface() != nil { + einterfaces.GetMetricsInterface().IncrementHttpRequest() + + if r.URL.Path != model.API_URL_SUFFIX+"/users/websocket" { + elapsed := float64(time.Since(now)) / float64(time.Second) + einterfaces.GetMetricsInterface().ObserveHttpRequestDuration(elapsed) + } + } +} + +func (c *Context) LogAudit(extraInfo string) { + audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id} + if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil { + c.LogError(r.Err) + } +} + +func (c *Context) LogAuditWithUserId(userId, extraInfo string) { + + if len(c.Session.UserId) > 0 { + extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.Session.UserId) + } + + audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id} + if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil { + c.LogError(r.Err) + } +} + +func (c *Context) LogError(err *model.AppError) { + + // filter out endless reconnects + if c.Path == "/api/v3/users/websocket" && err.StatusCode == 401 || err.Id == "web.check_browser_compatibility.app_error" { + c.LogDebug(err) + } else { + l4g.Error(utils.T("api.context.log.error"), c.Path, err.Where, err.StatusCode, + c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.T), err.DetailedError) + } +} + +func (c *Context) LogDebug(err *model.AppError) { + l4g.Debug(utils.T("api.context.log.error"), c.Path, err.Where, err.StatusCode, + c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.T), err.DetailedError) +} + +func (c *Context) IsSystemAdmin() bool { + return app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) +} + +func (c *Context) SessionRequired() { + if len(c.Session.UserId) == 0 { + c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "UserRequired") + c.Err.StatusCode = http.StatusUnauthorized + return + } +} + +func (c *Context) MfaRequired() { + // Must be licensed for MFA and have it configured for enforcement + if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication { + return + } + + // OAuth integrations are excepted + if c.Session.IsOAuth { + return + } + + if user, err := app.GetUser(c.Session.UserId); err != nil { + c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "MfaRequired") + c.Err.StatusCode = http.StatusUnauthorized + return + } else { + // Only required for email and ldap accounts + if user.AuthService != "" && + user.AuthService != model.USER_AUTH_SERVICE_EMAIL && + user.AuthService != model.USER_AUTH_SERVICE_LDAP { + return + } + + if !user.MfaActive { + c.Err = model.NewLocAppError("", "api.context.mfa_required.app_error", nil, "MfaRequired") + c.Err.StatusCode = http.StatusUnauthorized + return + } + } +} + +func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { + cookie := &http.Cookie{ + Name: model.SESSION_COOKIE_TOKEN, + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + } + + http.SetCookie(w, cookie) +} + +func (c *Context) SetInvalidParam(parameter string) { + c.Err = NewInvalidParamError(parameter) +} + +func (c *Context) SetInvalidUrlParam(parameter string) { + c.Err = NewInvalidUrlParamError(parameter) +} + +func NewInvalidParamError(parameter string) *model.AppError { + err := model.NewLocAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": parameter}, "") + err.StatusCode = http.StatusBadRequest + return err +} +func NewInvalidUrlParamError(parameter string) *model.AppError { + err := model.NewLocAppError("Context", "api.context.invalid_url_param.app_error", map[string]interface{}{"Name": parameter}, "") + err.StatusCode = http.StatusBadRequest + return err +} + +func (c *Context) SetPermissionError(permission *model.Permission) { + c.Err = model.NewLocAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id) + c.Err.StatusCode = http.StatusForbidden +} + +func (c *Context) SetSiteURL(url string) { + c.siteURL = strings.TrimRight(url, "/") +} + +func (c *Context) GetSiteURL() string { + return c.siteURL +} + +func (c *Context) RequireUserId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.UserId) != 26 { + c.SetInvalidUrlParam("user_id") + } + return c +} + +func (c *Context) RequireTeamId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.TeamId) != 26 { + c.SetInvalidUrlParam("team_id") + } + return c +} + +func (c *Context) RequireChannelId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.ChannelId) != 26 { + c.SetInvalidUrlParam("channel_id") + } + return c +} |