2020-10-16 19:12:53 +02:00
|
|
|
package api
|
2020-10-16 11:41:56 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2022-03-22 16:24:34 +02:00
|
|
|
"errors"
|
2020-10-16 11:41:56 +02:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2022-01-10 22:33:05 +02:00
|
|
|
"runtime/debug"
|
2020-10-16 11:41:56 +02:00
|
|
|
|
|
|
|
"github.com/gorilla/mux"
|
2021-01-27 00:13:46 +02:00
|
|
|
"github.com/mattermost/focalboard/server/app"
|
|
|
|
"github.com/mattermost/focalboard/server/model"
|
2021-06-11 14:24:51 +02:00
|
|
|
"github.com/mattermost/focalboard/server/services/audit"
|
2022-03-22 16:24:34 +02:00
|
|
|
"github.com/mattermost/focalboard/server/services/permissions"
|
2021-08-25 22:08:01 +02:00
|
|
|
|
|
|
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
2020-10-16 11:41:56 +02:00
|
|
|
)
|
|
|
|
|
2021-02-03 02:54:15 +02:00
|
|
|
const (
|
2021-06-21 11:21:42 +02:00
|
|
|
HeaderRequestedWith = "X-Requested-With"
|
|
|
|
HeaderRequestedWithXML = "XMLHttpRequest"
|
2021-08-10 04:57:45 +02:00
|
|
|
UploadFormFileKey = "file"
|
2022-07-19 17:26:01 +02:00
|
|
|
True = "true"
|
2021-02-03 02:54:15 +02:00
|
|
|
)
|
|
|
|
|
2021-03-26 20:01:54 +02:00
|
|
|
const (
|
2022-03-22 16:24:34 +02:00
|
|
|
ErrorNoTeamCode = 1000
|
|
|
|
ErrorNoTeamMessage = "No team"
|
2021-03-26 20:01:54 +02:00
|
|
|
)
|
|
|
|
|
2022-06-29 14:35:24 +02:00
|
|
|
var errAPINotSupportedInStandaloneMode = errors.New("API not supported in standalone mode")
|
|
|
|
|
2021-07-09 03:09:02 +02:00
|
|
|
type PermissionError struct {
|
|
|
|
msg string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pe PermissionError) Error() string {
|
|
|
|
return pe.msg
|
|
|
|
}
|
|
|
|
|
2020-10-16 11:41:56 +02:00
|
|
|
// ----------------------------------------------------------------------------------------------------
|
|
|
|
// REST APIs
|
|
|
|
|
2020-10-16 16:21:42 +02:00
|
|
|
type API struct {
|
2021-06-18 16:03:42 +02:00
|
|
|
app *app.App
|
2021-05-24 19:06:11 +02:00
|
|
|
authService string
|
2022-03-22 16:24:34 +02:00
|
|
|
permissions permissions.PermissionsService
|
2021-05-24 19:06:11 +02:00
|
|
|
singleUserToken string
|
|
|
|
MattermostAuth bool
|
2022-07-18 19:21:57 +02:00
|
|
|
logger mlog.LoggerIFace
|
2021-06-11 14:24:51 +02:00
|
|
|
audit *audit.Audit
|
2022-06-16 12:46:35 +02:00
|
|
|
isPlugin bool
|
2020-10-16 16:21:42 +02:00
|
|
|
}
|
|
|
|
|
2022-06-16 12:46:35 +02:00
|
|
|
func NewAPI(
|
|
|
|
app *app.App,
|
|
|
|
singleUserToken string,
|
|
|
|
authService string,
|
|
|
|
permissions permissions.PermissionsService,
|
2022-07-18 19:21:57 +02:00
|
|
|
logger mlog.LoggerIFace,
|
2022-06-16 12:46:35 +02:00
|
|
|
audit *audit.Audit,
|
|
|
|
isPlugin bool,
|
|
|
|
) *API {
|
2021-02-09 22:27:34 +02:00
|
|
|
return &API{
|
2021-06-18 16:03:42 +02:00
|
|
|
app: app,
|
2021-02-09 22:27:34 +02:00
|
|
|
singleUserToken: singleUserToken,
|
2021-03-26 20:01:54 +02:00
|
|
|
authService: authService,
|
2022-03-22 16:24:34 +02:00
|
|
|
permissions: permissions,
|
2021-05-29 08:23:10 +02:00
|
|
|
logger: logger,
|
2021-06-11 14:24:51 +02:00
|
|
|
audit: audit,
|
2022-06-16 12:46:35 +02:00
|
|
|
isPlugin: isPlugin,
|
2021-02-09 22:27:34 +02:00
|
|
|
}
|
2020-10-16 16:21:42 +02:00
|
|
|
}
|
2020-10-16 11:41:56 +02:00
|
|
|
|
|
|
|
func (a *API) RegisterRoutes(r *mux.Router) {
|
2022-04-13 22:24:32 +02:00
|
|
|
apiv2 := r.PathPrefix("/api/v2").Subrouter()
|
|
|
|
apiv2.Use(a.panicHandler)
|
|
|
|
apiv2.Use(a.requireCSRFToken)
|
2020-12-07 21:40:16 +02:00
|
|
|
|
2022-08-10 14:20:42 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
// System routes are outside the /api/v2 path
|
|
|
|
a.registerSystemRoutes(r)
|
2020-10-16 11:41:56 +02:00
|
|
|
}
|
|
|
|
|
2021-01-20 23:52:25 +02:00
|
|
|
func (a *API) RegisterAdminRoutes(r *mux.Router) {
|
2022-04-13 22:24:32 +02:00
|
|
|
r.HandleFunc("/api/v2/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST")
|
2021-01-20 23:52:25 +02:00
|
|
|
}
|
|
|
|
|
2022-03-22 16:24:34 +02:00
|
|
|
func getUserID(r *http.Request) string {
|
|
|
|
ctx := r.Context()
|
|
|
|
session, ok := ctx.Value(sessionContextKey).(*model.Session)
|
|
|
|
if !ok {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return session.UserID
|
|
|
|
}
|
|
|
|
|
2022-01-10 22:33:05 +02:00
|
|
|
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.URL.Path, http.StatusInternalServerError, "", nil)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-02-05 20:28:52 +02:00
|
|
|
func (a *API) requireCSRFToken(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2021-02-03 02:54:15 +02:00
|
|
|
if !a.checkCSRFToken(r) {
|
2021-05-29 08:23:10 +02:00
|
|
|
a.logger.Error("checkCSRFToken FAILED")
|
2021-07-27 18:57:29 +02:00
|
|
|
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "checkCSRFToken FAILED", nil)
|
2021-02-03 02:54:15 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-05 20:28:52 +02:00
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
2021-02-03 02:54:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (a *API) checkCSRFToken(r *http.Request) bool {
|
2021-06-21 11:21:42 +02:00
|
|
|
token := r.Header.Get(HeaderRequestedWith)
|
|
|
|
return token == HeaderRequestedWithXML
|
2021-02-03 02:54:15 +02:00
|
|
|
}
|
|
|
|
|
2022-03-22 16:24:34 +02:00
|
|
|
func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
|
2021-03-30 01:27:35 +02:00
|
|
|
query := r.URL.Query()
|
|
|
|
readToken := query.Get("read_token")
|
|
|
|
|
|
|
|
if len(readToken) < 1 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2022-03-22 16:24:34 +02:00
|
|
|
isValid, err := a.app.IsValidReadToken(boardID, readToken)
|
2021-03-30 01:27:35 +02:00
|
|
|
if err != nil {
|
2022-03-22 16:24:34 +02:00
|
|
|
a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err))
|
2021-03-30 01:27:35 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return isValid
|
|
|
|
}
|
|
|
|
|
2022-03-22 16:24:34 +02:00
|
|
|
// Response helpers
|
|
|
|
|
|
|
|
func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message string, sourceError error) {
|
2022-04-18 14:21:44 +02:00
|
|
|
if code == http.StatusUnauthorized || code == http.StatusForbidden {
|
2022-04-06 13:54:40 +02:00
|
|
|
a.logger.Debug("API DEBUG",
|
|
|
|
mlog.Int("code", code),
|
|
|
|
mlog.Err(sourceError),
|
|
|
|
mlog.String("msg", message),
|
|
|
|
mlog.String("api", api),
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
a.logger.Error("API ERROR",
|
|
|
|
mlog.Int("code", code),
|
|
|
|
mlog.Err(sourceError),
|
|
|
|
mlog.String("msg", message),
|
|
|
|
mlog.String("api", api),
|
|
|
|
)
|
|
|
|
}
|
2022-03-22 16:24:34 +02:00
|
|
|
|
2022-08-03 17:21:55 +02:00
|
|
|
setResponseHeader(w, "Content-Type", "application/json")
|
2022-03-22 16:24:34 +02:00
|
|
|
data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: code})
|
|
|
|
if err != nil {
|
|
|
|
data = []byte("{}")
|
|
|
|
}
|
|
|
|
w.WriteHeader(code)
|
|
|
|
_, _ = w.Write(data)
|
|
|
|
}
|
|
|
|
|
2022-04-20 17:01:40 +02:00
|
|
|
func stringResponse(w http.ResponseWriter, message string) {
|
2022-08-03 17:21:55 +02:00
|
|
|
setResponseHeader(w, "Content-Type", "text/plain")
|
2022-04-20 17:01:40 +02:00
|
|
|
_, _ = fmt.Fprint(w, message)
|
|
|
|
}
|
|
|
|
|
2022-06-06 10:18:21 +02:00
|
|
|
func jsonStringResponse(w http.ResponseWriter, code int, message string) {
|
2022-08-03 17:21:55 +02:00
|
|
|
setResponseHeader(w, "Content-Type", "application/json")
|
2022-03-22 16:24:34 +02:00
|
|
|
w.WriteHeader(code)
|
|
|
|
fmt.Fprint(w, message)
|
|
|
|
}
|
|
|
|
|
2022-06-06 10:18:21 +02:00
|
|
|
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) {
|
2022-08-03 17:21:55 +02:00
|
|
|
setResponseHeader(w, "Content-Type", "application/json")
|
2022-03-22 16:24:34 +02:00
|
|
|
w.WriteHeader(code)
|
|
|
|
_, _ = w.Write(json)
|
2021-09-08 06:52:03 +02:00
|
|
|
}
|
2022-08-03 17:21:55 +02:00
|
|
|
|
|
|
|
func setResponseHeader(w http.ResponseWriter, key string, value string) {
|
|
|
|
header := w.Header()
|
|
|
|
if header == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
header.Set(key, value)
|
|
|
|
}
|