focalboard/server/api/api.go
Paul Esch-Laurent f662e35daa wip
2022-11-30 15:49:16 -06:00

243 lines
6.1 KiB
Go

package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime/debug"
"github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit"
"github.com/mattermost/focalboard/server/services/permissions"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
)
const (
HeaderRequestedWith = "X-Requested-With"
HeaderRequestedWithXML = "XMLHttpRequest"
UploadFormFileKey = "file"
True = "true"
ErrorNoTeamCode = 1000
ErrorNoTeamMessage = "No team"
)
var (
ErrHandlerPanic = errors.New("http handler panic")
)
// ----------------------------------------------------------------------------------------------------
// REST APIs
type API struct {
app *app.App
authService string
permissions permissions.PermissionsService
singleUserToken string
MattermostAuth bool
logger mlog.LoggerIFace
audit *audit.Audit
isPlugin bool
}
func NewAPI(
app *app.App,
singleUserToken string,
authService string,
permissions permissions.PermissionsService,
logger mlog.LoggerIFace,
audit *audit.Audit,
isPlugin bool,
) *API {
return &API{
app: app,
singleUserToken: singleUserToken,
authService: authService,
permissions: permissions,
logger: logger,
audit: audit,
isPlugin: isPlugin,
}
}
func (a *API) RegisterRoutes(r *mux.Router) {
apiv2 := r.PathPrefix("/api/v2").Subrouter()
apiv2.Use(a.panicHandler)
apiv2.Use(a.requireCSRFToken)
/* ToDo:
apiv3 := r.PathPrefix("/api/v3").Subrouter()
apiv3.Use(a.panicHandler)
apiv3.Use(a.requireCSRFToken)
*/
// V2 routes (ToDo: migrate these to V3 when ready to ship V3)
a.registerUsersRoutes(apiv2)
a.registerAuthRoutes(apiv2)
a.registerMembersRoutes(apiv2)
a.registerCategoriesRoutes(apiv2)
a.registerSharingRoutes(apiv2)
a.registerTeamsRoutes(apiv2)
a.registerAchivesRoutes(apiv2)
a.registerSubscriptionsRoutes(apiv2)
a.registerFilesRoutes(apiv2)
a.registerLimitsRoutes(apiv2)
a.registerInsightsRoutes(apiv2)
a.registerOnboardingRoutes(apiv2)
a.registerSearchRoutes(apiv2)
a.registerConfigRoutes(apiv2)
a.registerBoardsAndBlocksRoutes(apiv2)
a.registerChannelsRoutes(apiv2)
a.registerTemplatesRoutes(apiv2)
a.registerBoardsRoutes(apiv2)
a.registerBlocksRoutes(apiv2)
a.registerContentBlocksRoutes(apiv2)
a.registerStatisticsRoutes(apiv2)
a.registerComplianceRoutes(apiv2)
// V3 routes
a.registerCardsRoutes(apiv2)
// System routes are outside the /api/v2 path
a.registerSystemRoutes(r)
}
func (a *API) RegisterAdminRoutes(r *mux.Router) {
r.HandleFunc("/api/v2/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST")
}
func getUserID(r *http.Request) string {
ctx := r.Context()
session, ok := ctx.Value(sessionContextKey).(*model.Session)
if !ok {
return ""
}
return session.UserID
}
func (a *API) panicHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if p := recover(); p != nil {
a.logger.Error("Http handler panic",
mlog.Any("panic", p),
mlog.String("stack", string(debug.Stack())),
mlog.String("uri", r.URL.Path),
)
a.errorResponse(w, r, ErrHandlerPanic)
}
}()
next.ServeHTTP(w, r)
})
}
func (a *API) requireCSRFToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !a.checkCSRFToken(r) {
a.logger.Error("checkCSRFToken FAILED")
a.errorResponse(w, r, model.NewErrBadRequest("checkCSRFToken FAILED"))
return
}
next.ServeHTTP(w, r)
})
}
func (a *API) checkCSRFToken(r *http.Request) bool {
token := r.Header.Get(HeaderRequestedWith)
return token == HeaderRequestedWithXML
}
func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
query := r.URL.Query()
readToken := query.Get("read_token")
if len(readToken) < 1 {
return false
}
isValid, err := a.app.IsValidReadToken(boardID, readToken)
if err != nil {
a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err))
return false
}
return isValid
}
func (a *API) userIsGuest(userID string) (bool, error) {
if a.singleUserToken != "" {
return false, nil
}
return a.app.UserIsGuest(userID)
}
// Response helpers
func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) {
a.logger.Error(err.Error())
errorResponse := model.ErrorResponse{Error: err.Error()}
switch {
case model.IsErrBadRequest(err):
errorResponse.ErrorCode = http.StatusBadRequest
case model.IsErrUnauthorized(err):
errorResponse.ErrorCode = http.StatusUnauthorized
case model.IsErrForbidden(err):
errorResponse.ErrorCode = http.StatusForbidden
case model.IsErrNotFound(err):
errorResponse.ErrorCode = http.StatusNotFound
case model.IsErrRequestEntityTooLarge(err):
errorResponse.ErrorCode = http.StatusRequestEntityTooLarge
case model.IsErrNotImplemented(err):
errorResponse.ErrorCode = http.StatusNotImplemented
default:
a.logger.Error("API ERROR",
mlog.Int("code", http.StatusInternalServerError),
mlog.Err(err),
mlog.String("api", r.URL.Path),
)
errorResponse.Error = "internal server error"
errorResponse.ErrorCode = http.StatusInternalServerError
}
setResponseHeader(w, "Content-Type", "application/json")
data, err := json.Marshal(errorResponse)
if err != nil {
data = []byte("{}")
}
w.WriteHeader(errorResponse.ErrorCode)
_, _ = w.Write(data)
}
func stringResponse(w http.ResponseWriter, message string) {
setResponseHeader(w, "Content-Type", "text/plain")
_, _ = fmt.Fprint(w, message)
}
func jsonStringResponse(w http.ResponseWriter, code int, message string) {
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
fmt.Fprint(w, message)
}
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) {
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
_, _ = w.Write(json)
}
func setResponseHeader(w http.ResponseWriter, key string, value string) {
header := w.Header()
if header == nil {
return
}
header.Set(key, value)
}