Doug Lauder
2023-03-22 17:22:27 -04:00
коммит произвёл GitHub
родитель b61c096497
Коммит c943ed6859
13276 изменённых файлов: 1695615 добавлений и 223189 удалений

60
server/boards/api/admin.go Обычный файл
Просмотреть файл

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
type AdminSetPasswordData struct {
Password string `json:"password"`
}
func (a *API) handleAdminSetPassword(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
username := vars["username"]
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var requestData AdminSetPasswordData
err = json.Unmarshal(requestBody, &requestData)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "adminSetPassword", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
auditRec.AddMeta("username", username)
if !strings.Contains(requestData.Password, "") {
a.errorResponse(w, r, model.NewErrBadRequest("password is required"))
return
}
err = a.app.UpdateUserPassword(username, requestData.Password)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("AdminSetPassword, username: %s", mlog.String("username", username))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}

246
server/boards/api/api.go Обычный файл
Просмотреть файл

@@ -0,0 +1,246 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime/debug"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/app"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/services/permissions"
"github.com/mattermost/mattermost-server/v6/server/platform/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) { //nolint:unparam
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
fmt.Fprint(w, message)
}
func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { //nolint:unparam
setResponseHeader(w, "Content-Type", "application/json")
w.WriteHeader(code)
_, _ = w.Write(json)
}
func setResponseHeader(w http.ResponseWriter, key string, value string) { //nolint:unparam
header := w.Header()
if header == nil {
return
}
header.Set(key, value)
}

79
server/boards/api/api_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,79 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"database/sql"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func TestErrorResponse(t *testing.T) {
testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)}
testCases := []struct {
Name string
Error error
ResponseCode int
ResponseBody string
}{
// bad request
{"ErrBadRequest", model.NewErrBadRequest("bad field"), http.StatusBadRequest, "bad field"},
{"ErrViewsLimitReached", model.ErrViewsLimitReached, http.StatusBadRequest, "limit reached"},
{"ErrAuthParam", model.NewErrAuthParam("password is required"), http.StatusBadRequest, "password is required"},
{"ErrInvalidCategory", model.NewErrInvalidCategory("open"), http.StatusBadRequest, "open"},
{"ErrBoardMemberIsLastAdmin", model.ErrBoardMemberIsLastAdmin, http.StatusBadRequest, "no admins"},
{"ErrBoardIDMismatch", model.ErrBoardIDMismatch, http.StatusBadRequest, "Board IDs do not match"},
// unauthorized
{"ErrUnauthorized", model.NewErrUnauthorized("not enough permissions"), http.StatusUnauthorized, "not enough permissions"},
// forbidden
{"ErrForbidden", model.NewErrForbidden("not enough permissions"), http.StatusForbidden, "not enough permissions"},
{"ErrPermission", model.NewErrPermission("not enough permissions"), http.StatusForbidden, "not enough permissions"},
{"ErrPatchUpdatesLimitedCards", model.ErrPatchUpdatesLimitedCards, http.StatusForbidden, "cards that are limited"},
{"ErrCategoryPermissionDenied", model.ErrCategoryPermissionDenied, http.StatusForbidden, "doesn't belong to user"},
// not found
{"ErrNotFound", model.NewErrNotFound("board"), http.StatusNotFound, "board"},
{"ErrNotAllFound", model.NewErrNotAllFound("block", []string{"1", "2"}), http.StatusNotFound, "not all instances of {block} in {1, 2} found"},
{"sql.ErrNoRows", sql.ErrNoRows, http.StatusNotFound, "rows"},
{"ErrNotFound", model.ErrCategoryDeleted, http.StatusNotFound, "category is deleted"},
// request entity too large
{"ErrRequestEntityTooLarge", model.ErrRequestEntityTooLarge, http.StatusRequestEntityTooLarge, "entity too large"},
// not implemented
{"ErrNotFound", model.ErrInsufficientLicense, http.StatusNotImplemented, "appropriate license required"},
{"ErrNotImplemented", model.NewErrNotImplemented("not implemented in plugin mode"), http.StatusNotImplemented, "plugin mode"},
// internal server error
{"Any other error", ErrHandlerPanic, http.StatusInternalServerError, "internal server error"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s should be a %d code", tc.Name, tc.ResponseCode), func(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
testAPI.errorResponse(w, r, tc.Error)
res := w.Result()
require.Equal(t, tc.ResponseCode, res.StatusCode)
require.Equal(t, "application/json", res.Header.Get("Content-Type"))
b, rErr := io.ReadAll(res.Body)
require.NoError(t, rErr)
res.Body.Close()
require.Contains(t, string(b), tc.ResponseBody)
})
}
}

258
server/boards/api/archive.go Обычный файл
Просмотреть файл

@@ -0,0 +1,258 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
archiveExtension = ".boardarchive"
)
func (a *API) registerAchivesRoutes(r *mux.Router) {
// Archive APIs
r.HandleFunc("/boards/{boardID}/archive/export", a.sessionRequired(a.handleArchiveExportBoard)).Methods("GET")
r.HandleFunc("/teams/{teamID}/archive/import", a.sessionRequired(a.handleArchiveImport)).Methods("POST")
r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET")
}
func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/archive/export archiveExportBoard
//
// Exports an archive of all blocks for one boards.
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Id of board to export
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// content:
// application-octet-stream:
// type: string
// format: binary
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
userID := getUserID(r)
// check user has permission to board
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
// if this user has `manage_system` permission and there is a license with the compliance
// feature enabled, then we will allow the export.
license := a.app.GetLicense()
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) || license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("BoardID", boardID)
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
opts := model.ExportArchiveOptions{
TeamID: board.TeamID,
BoardIDs: []string{board.ID},
}
filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Content-Transfer-Encoding", "binary")
if err := a.app.ExportArchive(w, opts); err != nil {
a.errorResponse(w, r, err)
}
auditRec.Success()
}
func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/archive/import archiveImport
//
// Import an archive of boards.
//
// ---
// produces:
// - application/json
// consumes:
// - multipart/form-data
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: file
// in: formData
// description: archive file to import
// required: true
// type: file
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session, _ := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
teamID := vars["teamID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
file, handle, err := r.FormFile(UploadFormFileKey)
if err != nil {
fmt.Fprintf(w, "%v", err)
return
}
defer file.Close()
auditRec := a.makeAuditRecord(r, "import", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("filename", handle.Filename)
auditRec.AddMeta("size", handle.Size)
opt := model.ImportArchiveOptions{
TeamID: teamID,
ModifiedBy: userID,
}
if err := a.app.ImportArchive(file, opt); err != nil {
a.logger.Debug("Error importing archive",
mlog.String("team_id", teamID),
mlog.Err(err),
)
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/archive/export archiveExportTeam
//
// Exports an archive of all blocks for all the boards in a team.
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Id of team
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// content:
// application-octet-stream:
// type: string
// format: binary
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
vars := mux.Vars(r)
teamID := vars["teamID"]
ctx := r.Context()
session, _ := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("TeamID", teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
if err != nil {
a.errorResponse(w, r, err)
return
}
ids := []string{}
for _, board := range boards {
ids = append(ids, board.ID)
}
opts := model.ExportArchiveOptions{
TeamID: teamID,
BoardIDs: ids,
}
filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
w.Header().Set("Content-Transfer-Encoding", "binary")
if err := a.app.ExportArchive(w, opts); err != nil {
a.errorResponse(w, r, err)
}
auditRec.Success()
}

36
server/boards/api/audit.go Обычный файл
Просмотреть файл

@@ -0,0 +1,36 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
)
// makeAuditRecord creates an audit record pre-populated with data from the request.
func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus string) *audit.Record { //nolint:unparam
ctx := r.Context()
var sessionID string
var userID string
if session, ok := ctx.Value(sessionContextKey).(*model.Session); ok {
sessionID = session.ID
userID = session.UserID
}
teamID := "unknown"
rec := &audit.Record{
APIPath: r.URL.Path,
Event: event,
Status: initialStatus,
UserID: userID,
SessionID: sessionID,
Client: r.UserAgent(),
IPAddress: r.RemoteAddr,
Meta: []audit.Meta{{K: audit.KeyTeamID, V: teamID}},
}
return rec
}

416
server/boards/api/auth.go Обычный файл
Просмотреть файл

@@ -0,0 +1,416 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/services/auth"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerAuthRoutes(r *mux.Router) {
// personal-server specific routes. These are not needed in plugin mode.
if !a.isPlugin {
r.HandleFunc("/login", a.handleLogin).Methods("POST")
r.HandleFunc("/logout", a.sessionRequired(a.handleLogout)).Methods("POST")
r.HandleFunc("/register", a.handleRegister).Methods("POST")
r.HandleFunc("/teams/{teamID}/regenerate_signup_token", a.sessionRequired(a.handlePostTeamRegenerateSignupToken)).Methods("POST")
r.HandleFunc("/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST")
}
}
func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /login login
//
// Login user
//
// ---
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// description: Login request
// required: true
// schema:
// "$ref": "#/definitions/LoginRequest"
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/LoginResponse"
// '401':
// description: invalid login
// schema:
// "$ref": "#/definitions/ErrorResponse"
// '500':
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
if a.singleUserToken != "" {
// Not permitted in single-user mode
a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var loginData model.LoginRequest
err = json.Unmarshal(requestBody, &loginData)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "login", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
auditRec.AddMeta("username", loginData.Username)
auditRec.AddMeta("type", loginData.Type)
if loginData.Type == "normal" {
token, err := a.app.Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken)
if err != nil {
a.errorResponse(w, r, model.NewErrUnauthorized("incorrect login"))
return
}
json, err := json.Marshal(model.LoginResponse{Token: token})
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.Success()
return
}
a.errorResponse(w, r, model.NewErrBadRequest("invalid login type"))
}
func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /logout logout
//
// Logout user
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '500':
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
if a.singleUserToken != "" {
// Not permitted in single-user mode
a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
auditRec := a.makeAuditRecord(r, "logout", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
auditRec.AddMeta("userID", session.UserID)
if err := a.app.Logout(session.ID); err != nil {
a.errorResponse(w, r, model.NewErrUnauthorized("incorrect logout"))
return
}
auditRec.AddMeta("sessionID", session.ID)
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /register register
//
// Register new user
//
// ---
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// description: Register request
// required: true
// schema:
// "$ref": "#/definitions/RegisterRequest"
// responses:
// '200':
// description: success
// '401':
// description: invalid registration token
// '500':
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
if a.singleUserToken != "" {
// Not permitted in single-user mode
a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var registerData model.RegisterRequest
err = json.Unmarshal(requestBody, &registerData)
if err != nil {
a.errorResponse(w, r, err)
return
}
registerData.Email = strings.TrimSpace(registerData.Email)
registerData.Username = strings.TrimSpace(registerData.Username)
// Validate token
if registerData.Token != "" {
team, err2 := a.app.GetRootTeam()
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if registerData.Token != team.SignupToken {
a.errorResponse(w, r, model.NewErrUnauthorized("invalid token"))
return
}
} else {
// No signup token, check if no active users
userCount, err2 := a.app.GetRegisteredUserCount()
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if userCount > 0 {
a.errorResponse(w, r, model.NewErrUnauthorized("no sign-up token and user(s) already exist"))
return
}
}
if err = registerData.IsValid(); err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "register", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
auditRec.AddMeta("username", registerData.Username)
err = a.app.RegisterUser(registerData.Username, registerData.Email, registerData.Password)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /users/{userID}/changepassword changePassword
//
// Change a user's password
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// - name: body
// in: body
// description: Change password request
// required: true
// schema:
// "$ref": "#/definitions/ChangePasswordRequest"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '400':
// description: invalid request
// schema:
// "$ref": "#/definitions/ErrorResponse"
// '500':
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
if a.singleUserToken != "" {
// Not permitted in single-user mode
a.errorResponse(w, r, model.NewErrUnauthorized("not permitted in single-user mode"))
return
}
vars := mux.Vars(r)
userID := vars["userID"]
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var requestData model.ChangePasswordRequest
if err = json.Unmarshal(requestBody, &requestData); err != nil {
a.errorResponse(w, r, err)
return
}
if err = requestData.IsValid(); err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "changePassword", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
if err = a.app.ChangePassword(userID, requestData.OldPassword, requestData.NewPassword); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) sessionRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return a.attachSession(handler, true)
}
func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request), required bool) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
token, _ := auth.ParseAuthTokenFromRequest(r)
a.logger.Debug(`attachSession`, mlog.Bool("single_user", a.singleUserToken != ""))
if a.singleUserToken != "" {
if required && (token != a.singleUserToken) {
a.errorResponse(w, r, model.NewErrUnauthorized("invalid single user token"))
return
}
now := utils.GetMillis()
session := &model.Session{
ID: model.SingleUser,
Token: token,
UserID: model.SingleUser,
AuthService: a.authService,
Props: map[string]interface{}{},
CreateAt: now,
UpdateAt: now,
}
ctx := context.WithValue(r.Context(), sessionContextKey, session)
handler(w, r.WithContext(ctx))
return
}
if a.MattermostAuth && r.Header.Get("Mattermost-User-Id") != "" {
userID := r.Header.Get("Mattermost-User-Id")
now := utils.GetMillis()
session := &model.Session{
ID: userID,
Token: userID,
UserID: userID,
AuthService: a.authService,
Props: map[string]interface{}{},
CreateAt: now,
UpdateAt: now,
}
ctx := context.WithValue(r.Context(), sessionContextKey, session)
handler(w, r.WithContext(ctx))
return
}
session, err := a.app.GetSession(token)
if err != nil {
if required {
a.errorResponse(w, r, model.NewErrUnauthorized(err.Error()))
return
}
handler(w, r)
return
}
authService := session.AuthService
if authService != a.authService {
msg := `Session authService mismatch`
a.logger.Error(msg,
mlog.String("sessionID", session.ID),
mlog.String("want", a.authService),
mlog.String("got", authService),
)
a.errorResponse(w, r, model.NewErrUnauthorized(msg))
return
}
ctx := context.WithValue(r.Context(), sessionContextKey, session)
handler(w, r.WithContext(ctx))
}
}
func (a *API) adminRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// Currently, admin APIs require local unix connections
conn := GetContextConn(r)
if _, isUnix := conn.(*net.UnixConn); !isUnix {
a.errorResponse(w, r, model.NewErrUnauthorized("not a local unix connection"))
return
}
handler(w, r)
}
}

788
server/boards/api/blocks.go Обычный файл
Просмотреть файл

@@ -0,0 +1,788 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerBlocksRoutes(r *mux.Router) {
// Blocks APIs
r.HandleFunc("/boards/{boardID}/blocks", a.attachSession(a.handleGetBlocks, false)).Methods("GET")
r.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST")
r.HandleFunc("/boards/{boardID}/blocks", a.sessionRequired(a.handlePatchBlocks)).Methods("PATCH")
r.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
r.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
r.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
r.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.sessionRequired(a.handleDuplicateBlock)).Methods("POST")
}
func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/blocks getBlocks
//
// Returns blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: parent_id
// in: query
// description: ID of parent block, omit to specify all blocks
// required: false
// type: string
// - name: type
// in: query
// description: Type of blocks to return, omit to specify all types
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
parentID := query.Get("parent_id")
blockType := query.Get("type")
all := query.Get("all")
blockID := query.Get("block_id")
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if !hasValidReadToken {
if board.IsTemplate && board.Type == model.BoardTypeOpen {
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board template"))
return
}
} else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
if board.IsTemplate {
var isGuest bool
isGuest, err = a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("guest are not allowed to get board templates"))
return
}
}
}
auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("parentID", parentID)
auditRec.AddMeta("blockType", blockType)
auditRec.AddMeta("all", all)
auditRec.AddMeta("blockID", blockID)
var blocks []*model.Block
var block *model.Block
switch {
case all != "":
blocks, err = a.app.GetBlocksForBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
case blockID != "":
block, err = a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if block.BoardID != boardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
blocks = append(blocks, block)
default:
blocks, err = a.app.GetBlocks(boardID, parentID, blockType)
if err != nil {
a.errorResponse(w, r, err)
return
}
}
a.logger.Debug("GetBlocks",
mlog.String("boardID", boardID),
mlog.String("parentID", parentID),
mlog.String("blockType", blockType),
mlog.String("blockID", blockID),
mlog.Int("block_count", len(blocks)),
)
var bErr error
blocks, bErr = a.app.ApplyCloudLimits(blocks)
if bErr != nil {
a.errorResponse(w, r, err)
return
}
json, err := json.Marshal(blocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("blockCount", len(blocks))
auditRec.Success()
}
func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/blocks updateBlocks
//
// Insert blocks. The specified IDs will only be used to link
// blocks with existing ones, the rest will be replaced by server
// generated IDs
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk inserting)
// required: false
// type: bool
// - name: Body
// in: body
// description: array of blocks to insert or update
// required: true
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// items:
// $ref: '#/definitions/Block'
// type: array
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var blocks []*model.Block
err = json.Unmarshal(requestBody, &blocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
hasComments := false
hasContents := false
for _, block := range blocks {
// Error checking
if len(block.Type) < 1 {
message := fmt.Sprintf("missing type for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.Type == model.TypeComment {
hasComments = true
} else {
hasContents = true
}
if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.UpdateAt < 1 {
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.BoardID != boardID {
message := fmt.Sprintf("invalid BoardID for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
}
if hasContents {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return
}
}
if hasComments {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to post card comments"))
return
}
}
blocks = model.GenerateBlockIDs(blocks, a.logger)
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("disable_notify", disableNotify)
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
model.StampModificationMetadata(userID, blocks, auditRec)
// this query param exists when creating template from board, or board from template
sourceBoardID := r.URL.Query().Get("sourceBoardID")
if sourceBoardID != "" {
if updateFileIDsErr := a.app.CopyCardFiles(sourceBoardID, blocks); updateFileIDsErr != nil {
a.errorResponse(w, r, updateFileIDsErr)
return
}
}
newBlocks, err := a.app.InsertBlocksAndNotify(blocks, session.UserID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("POST Blocks",
mlog.Int("block_count", len(blocks)),
mlog.Bool("disable_notify", disableNotify),
)
json, err := json.Marshal(newBlocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("blockCount", len(blocks))
auditRec.Success()
}
func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /boards/{boardID}/blocks/{blockID} deleteBlock
//
// Deletes a block
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: blockID
// in: path
// description: ID of block to delete
// required: true
// type: string
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk deletion)
// required: false
// type: bool
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
boardID := vars["boardID"]
blockID := vars["blockID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return
}
block, err := a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if block.BoardID != boardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
auditRec := a.makeAuditRecord(r, "deleteBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("blockID", blockID)
err = a.app.DeleteBlockAndNotify(blockID, userID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("DELETE Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/blocks/{blockID}/undelete undeleteBlock
//
// Undeletes a block
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: blockID
// in: path
// description: ID of block to undelete
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/BlockPatch"
// '404':
// description: block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
blockID := vars["blockID"]
boardID := vars["boardID"]
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
block, err := a.app.GetLastBlockHistoryEntry(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if board.ID != block.BoardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, board.ID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}
auditRec := a.makeAuditRecord(r, "undeleteBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("blockID", blockID)
undeletedBlock, err := a.app.UndeleteBlock(blockID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
undeletedBlockData, err := json.Marshal(undeletedBlock)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("UNDELETE Block", mlog.String("blockID", blockID))
jsonBytesResponse(w, http.StatusOK, undeletedBlockData)
auditRec.Success()
}
func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /boards/{boardID}/blocks/{blockID} patchBlock
//
// Partially updates a block
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: blockID
// in: path
// description: ID of block to patch
// required: true
// type: string
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk patching)
// required: false
// type: bool
// - name: Body
// in: body
// description: block patch to apply
// required: true
// schema:
// "$ref": "#/definitions/BlockPatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
boardID := vars["boardID"]
blockID := vars["blockID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return
}
block, err := a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if block.BoardID != boardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, boardID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var patch *model.BlockPatch
err = json.Unmarshal(requestBody, &patch)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "patchBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("blockID", blockID)
if _, err = a.app.PatchBlockAndNotify(blockID, patch, userID, disableNotify); err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PATCH Block", mlog.String("boardID", boardID), mlog.String("blockID", blockID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /boards/{boardID}/blocks/ patchBlocks
//
// Partially updates batch of blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Workspace ID
// required: true
// type: string
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk patching)
// required: false
// type: bool
// - name: Body
// in: body
// description: block Ids and block patches to apply
// required: true
// schema:
// "$ref": "#/definitions/BlockPatchBatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
teamID := vars["teamID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var patches *model.BlockPatchBatch
err = json.Unmarshal(requestBody, &patches)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "patchBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
for i := range patches.BlockIDs {
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), patches.BlockIDs[i])
}
for _, blockID := range patches.BlockIDs {
var block *model.Block
block, err = a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, model.NewErrForbidden("access denied to make board changes"))
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changesa"))
return
}
}
err = a.app.PatchBlocksAndNotify(teamID, patches, userID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PATCH Blocks", mlog.String("patches", strconv.Itoa(len(patches.BlockIDs))))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/blocks/{blockID}/duplicate duplicateBlock
//
// Returns the new created blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: blockID
// in: path
// description: Block ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// '404':
// description: board or block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
blockID := mux.Vars(r)["blockID"]
userID := getUserID(r)
query := r.URL.Query()
asTemplate := query.Get("asTemplate")
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if userID == "" {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
block, err := a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if board.ID != block.BoardID {
message := fmt.Sprintf("block ID=%s on BoardID=%s", block.ID, board.ID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
if block.Type == model.TypeComment {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to comment on board cards"))
return
}
} else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board cards"))
return
}
}
auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("blockID", blockID)
a.logger.Debug("DuplicateBlock",
mlog.String("boardID", boardID),
mlog.String("blockID", blockID),
)
blocks, err := a.app.DuplicateBlock(boardID, blockID, userID, asTemplate == True)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(blocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}

679
server/boards/api/boards.go Обычный файл
Просмотреть файл

@@ -0,0 +1,679 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerBoardsRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/boards", a.sessionRequired(a.handleGetBoards)).Methods("GET")
r.HandleFunc("/boards", a.sessionRequired(a.handleCreateBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}", a.attachSession(a.handleGetBoard, false)).Methods("GET")
r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handlePatchBoard)).Methods("PATCH")
r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handleDeleteBoard)).Methods("DELETE")
r.HandleFunc("/boards/{boardID}/duplicate", a.sessionRequired(a.handleDuplicateBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}/undelete", a.sessionRequired(a.handleUndeleteBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
}
func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards getBoards
//
// Returns team boards
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "getBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
// retrieve boards list
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBoards",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
)
data, err := json.Marshal(boards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("boardsCount", len(boards))
auditRec.Success()
}
func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards createBoard
//
// Creates a new board
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: the board to create
// required: true
// schema:
// "$ref": "#/definitions/Board"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Board'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newBoard *model.Board
if err = json.Unmarshal(requestBody, &newBoard); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if newBoard.Type == model.BoardTypeOpen {
if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePublicChannel) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create public boards"))
return
}
} else {
if !a.permissions.HasPermissionToTeam(userID, newBoard.TeamID, model.PermissionCreatePrivateChannel) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create private boards"))
return
}
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
if err = newBoard.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "createBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("teamID", newBoard.TeamID)
auditRec.AddMeta("boardType", newBoard.Type)
// create board
board, err := a.app.CreateBoard(newBoard, userID, true)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("CreateBoard",
mlog.String("teamID", board.TeamID),
mlog.String("boardID", board.ID),
mlog.String("boardType", string(board.Type)),
mlog.String("userID", userID),
)
data, err := json.Marshal(board)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID} getBoard
//
// Returns a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Board"
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if !hasValidReadToken {
if board.Type == model.BoardTypePrivate {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
} else {
var isGuest bool
isGuest, err = a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
}
auditRec := a.makeAuditRecord(r, "getBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
a.logger.Debug("GetBoard",
mlog.String("boardID", boardID),
)
data, err := json.Marshal(board)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /boards/{boardID} patchBoard
//
// Partially updates a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: Body
// in: body
// description: board patch to apply
// required: true
// schema:
// "$ref": "#/definitions/BoardPatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Board'
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
if _, err := a.app.GetBoard(boardID); err != nil {
a.errorResponse(w, r, err)
return
}
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var patch *model.BoardPatch
if err = json.Unmarshal(requestBody, &patch); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if err = patch.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board properties"))
return
}
if patch.Type != nil || patch.MinimumRole != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board type"))
return
}
}
if patch.ChannelID != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board access"))
return
}
}
auditRec := a.makeAuditRecord(r, "patchBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("userID", userID)
// patch board
updatedBoard, err := a.app.PatchBoard(patch, boardID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PatchBoard",
mlog.String("boardID", boardID),
mlog.String("userID", userID),
)
data, err := json.Marshal(updatedBoard)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /boards/{boardID} deleteBoard
//
// Removes a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
// Check if board exists
if _, err := a.app.GetBoard(boardID); err != nil {
a.errorResponse(w, r, err)
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to delete board"))
return
}
auditRec := a.makeAuditRecord(r, "deleteBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
if err := a.app.DeleteBoard(boardID, userID); err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("DELETE Board", mlog.String("boardID", boardID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/duplicate duplicateBoard
//
// Returns the new created board and all the blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardsAndBlocks'
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
query := r.URL.Query()
asTemplate := query.Get("asTemplate")
toTeam := query.Get("toTeam")
if userID == "" {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if toTeam == "" {
toTeam = board.TeamID
}
if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if board.IsTemplate && board.Type == model.BoardTypeOpen {
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
} else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
a.logger.Debug("DuplicateBoard",
mlog.String("boardID", boardID),
)
boardsAndBlocks, _, err := a.app.DuplicateBoard(boardID, userID, toTeam, asTemplate == True)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(boardsAndBlocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleUndeleteBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/undelete undeleteBoard
//
// Undeletes a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: ID of board to undelete
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
boardID := vars["boardID"]
auditRec := a.makeAuditRecord(r, "undeleteBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to undelete board"))
return
}
err := a.app.UndeleteBoard(boardID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("UNDELETE Board", mlog.String("boardID", boardID))
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleGetBoardMetadata(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/metadata getBoardMetadata
//
// Returns a board's metadata
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/BoardMetadata"
// '404':
// description: board not found
// '501':
// description: required license not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
board, boardMetadata, err := a.app.GetBoardMetadata(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if board == nil || boardMetadata == nil {
a.errorResponse(w, r, model.NewErrNotFound("board metadata BoardID="+boardID))
return
}
if board.Type == model.BoardTypePrivate {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
} else {
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
}
auditRec := a.makeAuditRecord(r, "getBoardMetadata", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
data, err := json.Marshal(boardMetadata)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}

414
server/boards/api/boards_and_blocks.go Обычный файл
Просмотреть файл

@@ -0,0 +1,414 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerBoardsAndBlocksRoutes(r *mux.Router) {
// BoardsAndBlocks APIs
r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleCreateBoardsAndBlocks)).Methods("POST")
r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handlePatchBoardsAndBlocks)).Methods("PATCH")
r.HandleFunc("/boards-and-blocks", a.sessionRequired(a.handleDeleteBoardsAndBlocks)).Methods("DELETE")
}
func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards-and-blocks insertBoardsAndBlocks
//
// Creates new boards and blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: the boards and blocks to create
// required: true
// schema:
// "$ref": "#/definitions/BoardsAndBlocks"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardsAndBlocks'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newBab *model.BoardsAndBlocks
if err = json.Unmarshal(requestBody, &newBab); err != nil {
a.errorResponse(w, r, err)
return
}
if len(newBab.Boards) == 0 {
a.errorResponse(w, r, model.NewErrBadRequest("at least one board is required"))
return
}
teamID := ""
boardIDs := map[string]bool{}
for _, board := range newBab.Boards {
boardIDs[board.ID] = true
if teamID == "" {
teamID = board.TeamID
continue
}
if teamID != board.TeamID {
a.errorResponse(w, r, model.NewErrBadRequest("cannot create boards for multiple teams"))
return
}
if board.ID == "" {
a.errorResponse(w, r, model.NewErrBadRequest("boards need an ID to be referenced from the blocks"))
return
}
}
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board template"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
for _, block := range newBab.Blocks {
// Error checking
if len(block.Type) < 1 {
message := fmt.Sprintf("missing type for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if block.UpdateAt < 1 {
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if !boardIDs[block.BoardID] {
message := fmt.Sprintf("invalid BoardID %s (not exists in the created boards)", block.BoardID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
}
// IDs of boards and blocks are used to confirm that they're
// linked and then regenerated by the server
newBab, err = model.GenerateBoardsAndBlocksIDs(newBab, a.logger)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("teamID", teamID)
auditRec.AddMeta("userID", userID)
auditRec.AddMeta("boardsCount", len(newBab.Boards))
auditRec.AddMeta("blocksCount", len(newBab.Blocks))
// create boards and blocks
bab, err := a.app.CreateBoardsAndBlocks(newBab, userID, true)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("CreateBoardsAndBlocks",
mlog.String("teamID", teamID),
mlog.String("userID", userID),
mlog.Int("boardCount", len(bab.Boards)),
mlog.Int("blockCount", len(bab.Blocks)),
)
data, err := json.Marshal(bab)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /boards-and-blocks patchBoardsAndBlocks
//
// Patches a set of related boards and blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: the patches for the boards and blocks
// required: true
// schema:
// "$ref": "#/definitions/PatchBoardsAndBlocks"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardsAndBlocks'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var pbab *model.PatchBoardsAndBlocks
if err = json.Unmarshal(requestBody, &pbab); err != nil {
a.errorResponse(w, r, err)
return
}
if err = pbab.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
teamID := ""
boardIDMap := map[string]bool{}
for i, boardID := range pbab.BoardIDs {
boardIDMap[boardID] = true
patch := pbab.BoardPatches[i]
if err = patch.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board properties"))
return
}
if patch.Type != nil || patch.MinimumRole != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying board type"))
return
}
}
board, err2 := a.app.GetBoard(boardID)
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if teamID == "" {
teamID = board.TeamID
}
if teamID != board.TeamID {
a.errorResponse(w, r, model.NewErrBadRequest("mismatched team ID"))
return
}
}
for _, blockID := range pbab.BlockIDs {
block, err2 := a.app.GetBlockByID(blockID)
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if _, ok := boardIDMap[block.BoardID]; !ok {
a.errorResponse(w, r, model.NewErrBadRequest("missing BoardID="+block.BoardID))
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying cards"))
return
}
}
auditRec := a.makeAuditRecord(r, "patchBoardsAndBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardsCount", len(pbab.BoardIDs))
auditRec.AddMeta("blocksCount", len(pbab.BlockIDs))
bab, err := a.app.PatchBoardsAndBlocks(pbab, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PATCH BoardsAndBlocks",
mlog.Int("boardsCount", len(pbab.BoardIDs)),
mlog.Int("blocksCount", len(pbab.BlockIDs)),
)
data, err := json.Marshal(bab)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /boards-and-blocks deleteBoardsAndBlocks
//
// Deletes boards and blocks
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: the boards and blocks to delete
// required: true
// schema:
// "$ref": "#/definitions/DeleteBoardsAndBlocks"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var dbab *model.DeleteBoardsAndBlocks
if err = json.Unmarshal(requestBody, &dbab); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
// user must have permission to delete all the boards, and that
// would include the permission to manage their blocks
teamID := ""
boardIDMap := map[string]bool{}
for _, boardID := range dbab.Boards {
boardIDMap[boardID] = true
// all boards in the request should belong to the same team
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if teamID == "" {
teamID = board.TeamID
}
if teamID != board.TeamID {
a.errorResponse(w, r, model.NewErrBadRequest("all boards should belong to the same team"))
return
}
// permission check
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to delete board"))
return
}
}
for _, blockID := range dbab.Blocks {
block, err2 := a.app.GetBlockByID(blockID)
if err2 != nil {
a.errorResponse(w, r, err2)
return
}
if _, ok := boardIDMap[block.BoardID]; !ok {
a.errorResponse(w, r, model.NewErrBadRequest("missing BoardID="+block.BoardID))
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modifying cards"))
return
}
}
if err := dbab.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "deleteBoardsAndBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardsCount", len(dbab.Boards))
auditRec.AddMeta("blocksCount", len(dbab.Blocks))
if err := a.app.DeleteBoardsAndBlocks(dbab, userID); err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("DELETE BoardsAndBlocks",
mlog.Int("boardsCount", len(dbab.Boards)),
mlog.Int("blocksCount", len(dbab.Blocks)),
)
// response
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}

393
server/boards/api/cards.go Обычный файл
Просмотреть файл

@@ -0,0 +1,393 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
defaultPage = "0"
defaultPerPage = "100"
)
func (a *API) registerCardsRoutes(r *mux.Router) {
// Cards APIs
r.HandleFunc("/boards/{boardID}/cards", a.sessionRequired(a.handleCreateCard)).Methods("POST")
r.HandleFunc("/boards/{boardID}/cards", a.sessionRequired(a.handleGetCards)).Methods("GET")
r.HandleFunc("/cards/{cardID}", a.sessionRequired(a.handlePatchCard)).Methods("PATCH")
r.HandleFunc("/cards/{cardID}", a.sessionRequired(a.handleGetCard)).Methods("GET")
}
func (a *API) handleCreateCard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/cards createCard
//
// Creates a new card for the specified board.
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: Body
// in: body
// description: the card to create
// required: true
// schema:
// "$ref": "#/definitions/Card"
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk data inserting)
// required: false
// type: bool
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Card'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
boardID := mux.Vars(r)["boardID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newCard *model.Card
if err = json.Unmarshal(requestBody, &newCard); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create card"))
return
}
if newCard.BoardID != "" && newCard.BoardID != boardID {
a.errorResponse(w, r, model.ErrBoardIDMismatch)
return
}
newCard.PopulateWithBoardID(boardID)
if err = newCard.CheckValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "createCard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
// create card
card, err := a.app.CreateCard(newCard, boardID, userID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("CreateCard",
mlog.String("boardID", boardID),
mlog.String("cardID", card.ID),
mlog.String("userID", userID),
)
data, err := json.Marshal(card)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetCards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/cards getCards
//
// Fetches cards for the specified board.
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of cards to return per page(default=100)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Card"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
boardID := mux.Vars(r)["boardID"]
query := r.URL.Query()
strPage := query.Get("page")
strPerPage := query.Get("per_page")
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to fetch cards"))
return
}
if strPage == "" {
strPage = defaultPage
}
if strPerPage == "" {
strPerPage = defaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
}
auditRec := a.makeAuditRecord(r, "getCards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("page", page)
auditRec.AddMeta("per_page", perPage)
cards, err := a.app.GetCardsForBoard(boardID, page, perPage)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetCards",
mlog.String("boardID", boardID),
mlog.String("userID", userID),
mlog.Int("page", page),
mlog.Int("per_page", perPage),
mlog.Int("count", len(cards)),
)
data, err := json.Marshal(cards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handlePatchCard(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /cards/{cardID}/cards patchCard
//
// Patches the specified card.
//
// ---
// produces:
// - application/json
// parameters:
// - name: cardID
// in: path
// description: Card ID
// required: true
// type: string
// - name: Body
// in: body
// description: the card patch
// required: true
// schema:
// "$ref": "#/definitions/CardPatch"
// - name: disable_notify
// in: query
// description: Disables notifications (for bulk data patching)
// required: false
// type: bool
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Card'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
cardID := mux.Vars(r)["cardID"]
val := r.URL.Query().Get("disable_notify")
disableNotify := val == True
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
card, err := a.app.GetCardByID(cardID)
if err != nil {
message := fmt.Sprintf("could not fetch card %s: %s", cardID, err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to patch card"))
return
}
var patch *model.CardPatch
if err = json.Unmarshal(requestBody, &patch); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
auditRec := a.makeAuditRecord(r, "patchCard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", card.BoardID)
auditRec.AddMeta("cardID", card.ID)
// patch card
cardPatched, err := a.app.PatchCard(patch, card.ID, userID, disableNotify)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PatchCard",
mlog.String("boardID", cardPatched.BoardID),
mlog.String("cardID", cardPatched.ID),
mlog.String("userID", userID),
)
data, err := json.Marshal(cardPatched)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetCard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /cards/{cardID} getCard
//
// Fetches the specified card.
//
// ---
// produces:
// - application/json
// parameters:
// - name: cardID
// in: path
// description: Card ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/Card'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
cardID := mux.Vars(r)["cardID"]
card, err := a.app.GetCardByID(cardID)
if err != nil {
message := fmt.Sprintf("could not fetch card %s: %s", cardID, err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if !a.permissions.HasPermissionToBoard(userID, card.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to fetch card"))
return
}
auditRec := a.makeAuditRecord(r, "getCard", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", card.BoardID)
auditRec.AddMeta("cardID", card.ID)
a.logger.Debug("GetCard",
mlog.String("boardID", card.BoardID),
mlog.String("cardID", card.ID),
mlog.String("userID", userID),
)
data, err := json.Marshal(card)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}

676
server/boards/api/categories.go Обычный файл
Просмотреть файл

@@ -0,0 +1,676 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
)
func (a *API) registerCategoriesRoutes(r *mux.Router) {
// Category APIs
r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleCreateCategory)).Methods(http.MethodPost)
r.HandleFunc("/teams/{teamID}/categories/reorder", a.sessionRequired(a.handleReorderCategories)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleUpdateCategory)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}", a.sessionRequired(a.handleDeleteCategory)).Methods(http.MethodDelete)
r.HandleFunc("/teams/{teamID}/categories", a.sessionRequired(a.handleGetUserCategoryBoards)).Methods(http.MethodGet)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/reorder", a.sessionRequired(a.handleReorderCategoryBoards)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}", a.sessionRequired(a.handleUpdateCategoryBoard)).Methods(http.MethodPost)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide", a.sessionRequired(a.handleHideBoard)).Methods(http.MethodPut)
r.HandleFunc("/teams/{teamID}/categories/{categoryID}/boards/{boardID}/unhide", a.sessionRequired(a.handleUnhideBoard)).Methods(http.MethodPut)
}
func (a *API) handleCreateCategory(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories createCategory
//
// Create a category for boards
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: Body
// in: body
// description: category to create
// required: true
// schema:
// "$ref": "#/definitions/Category"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var category model.Category
err = json.Unmarshal(requestBody, &category)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "createCategory", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
// user can only create category for themselves
if category.UserID != session.UserID {
message := fmt.Sprintf("userID %s and category userID %s mismatch", session.UserID, category.UserID)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
vars := mux.Vars(r)
teamID := vars["teamID"]
if category.TeamID != teamID {
a.errorResponse(w, r, model.NewErrBadRequest("teamID mismatch"))
return
}
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
createdCategory, err := a.app.CreateCategory(&category)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(createdCategory)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("categoryID", createdCategory.ID)
auditRec.Success()
}
func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /teams/{teamID}/categories/{categoryID} updateCategory
//
// Create a category for boards
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID
// required: true
// type: string
// - name: Body
// in: body
// description: category to update
// required: true
// schema:
// "$ref": "#/definitions/Category"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
categoryID := vars["categoryID"]
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var category model.Category
err = json.Unmarshal(requestBody, &category)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "updateCategory", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
if categoryID != category.ID {
a.errorResponse(w, r, model.NewErrBadRequest("categoryID mismatch in patch and body"))
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
// user can only update category for themselves
if category.UserID != session.UserID {
a.errorResponse(w, r, model.NewErrBadRequest("user ID mismatch in session and category"))
return
}
teamID := vars["teamID"]
if category.TeamID != teamID {
a.errorResponse(w, r, model.NewErrBadRequest("teamID mismatch"))
return
}
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
updatedCategory, err := a.app.UpdateCategory(&category)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(updatedCategory)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleDeleteCategory(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /teams/{teamID}/categories/{categoryID} deleteCategory
//
// Delete a category
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
vars := mux.Vars(r)
userID := session.UserID
teamID := vars["teamID"]
categoryID := vars["categoryID"]
auditRec := a.makeAuditRecord(r, "deleteCategory", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(deletedCategory)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetUserCategoryBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/categories getUserCategoryBoards
//
// Gets the user's board categories
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// items:
// "$ref": "#/definitions/CategoryBoards"
// type: array
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
vars := mux.Vars(r)
teamID := vars["teamID"]
auditRec := a.makeAuditRecord(r, "getUserCategoryBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
categoryBlocks, err := a.app.GetUserCategoryBoards(userID, teamID)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(categoryBlocks)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleUpdateCategoryBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID} updateCategoryBoard
//
// Set the category of a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID
// required: true
// type: string
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
auditRec := a.makeAuditRecord(r, "updateCategoryBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
vars := mux.Vars(r)
categoryID := vars["categoryID"]
boardID := vars["boardID"]
teamID := vars["teamID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
if !a.permissions.HasPermissionToTeam(session.UserID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
// TODO: Check the category and the team matches
err := a.app.AddUpdateUserCategoryBoard(teamID, userID, categoryID, []string{boardID})
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, []byte("ok"))
auditRec.Success()
}
func (a *API) handleReorderCategories(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /teams/{teamID}/categories/reorder handleReorderCategories
//
// Updated sidebar category order
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newCategoryOrder []string
err = json.Unmarshal(requestBody, &newCategoryOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "reorderCategories", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("TeamID", teamID)
auditRec.AddMeta("CategoryCount", len(newCategoryOrder))
updatedCategoryOrder, err := a.app.ReorderCategories(userID, teamID, newCategoryOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(updatedCategoryOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleReorderCategoryBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /teams/{teamID}/categories/{categoryID}/boards/reorder handleReorderCategoryBoards
//
// Updates order of boards inside a sidebar category
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
categoryID := vars["categoryID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
userID := session.UserID
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
category, err := a.app.GetCategory(categoryID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if category.UserID != userID {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var newBoardsOrder []string
err = json.Unmarshal(requestBody, &newBoardsOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "reorderCategoryBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
updatedBoardsOrder, err := a.app.ReorderCategoryBoards(userID, teamID, categoryID, newBoardsOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(updatedBoardsOrder)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleHideBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide hideBoard
//
// Hide the specified board for the user
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID to which the board to be hidden belongs to
// required: true
// type: string
// - name: boardID
// in: path
// description: ID of board to be hidden
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
teamID := vars["teamID"]
boardID := vars["boardID"]
categoryID := vars["categoryID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
auditRec := a.makeAuditRecord(r, "hideBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("board_id", boardID)
auditRec.AddMeta("team_id", teamID)
auditRec.AddMeta("category_id", categoryID)
if err := a.app.SetBoardVisibility(teamID, userID, categoryID, boardID, false); err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleUnhideBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/categories/{categoryID}/boards/{boardID}/hide unhideBoard
//
// Unhides the specified board for the user
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: categoryID
// in: path
// description: Category ID to which the board to be unhidden belongs to
// required: true
// type: string
// - name: boardID
// in: path
// description: ID of board to be unhidden
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Category"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
vars := mux.Vars(r)
teamID := vars["teamID"]
boardID := vars["boardID"]
categoryID := vars["categoryID"]
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to category"))
return
}
auditRec := a.makeAuditRecord(r, "unhideBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
if err := a.app.SetBoardVisibility(teamID, userID, categoryID, boardID, true); err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}

110
server/boards/api/channels.go Обычный файл
Просмотреть файл

@@ -0,0 +1,110 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerChannelsRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/channels/{channelID}", a.sessionRequired(a.handleGetChannel)).Methods("GET")
}
func (a *API) handleGetChannel(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/channels/{channelID} getChannel
//
// Returns the requested channel
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: channelID
// in: path
// description: Channel ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Channel"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
teamID := mux.Vars(r)["teamID"]
channelID := mux.Vars(r)["channelID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if !a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) {
a.errorResponse(w, r, model.NewErrPermission("access denied to channel"))
return
}
auditRec := a.makeAuditRecord(r, "getChannel", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
auditRec.AddMeta("channelID", teamID)
channel, err := a.app.GetChannel(teamID, channelID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetChannel",
mlog.String("teamID", teamID),
mlog.String("channelID", channelID),
)
if channel.TeamId != teamID {
if channel.Type != mm_model.ChannelTypeDirect && channel.Type != mm_model.ChannelTypeGroup {
message := fmt.Sprintf("channel ID=%s on TeamID=%s", channel.Id, teamID)
a.errorResponse(w, r, model.NewErrNotFound(message))
return
}
}
data, err := json.Marshal(channel)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}

451
server/boards/api/compliance.go Обычный файл
Просмотреть файл

@@ -0,0 +1,451 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
const (
complianceDefaultPage = "0"
complianceDefaultPerPage = "60"
)
func (a *API) registerComplianceRoutes(r *mux.Router) {
// Compliance APIs
r.HandleFunc("/admin/boards", a.sessionRequired(a.handleGetBoardsForCompliance)).Methods("GET")
r.HandleFunc("/admin/boards_history", a.sessionRequired(a.handleGetBoardsComplianceHistory)).Methods("GET")
r.HandleFunc("/admin/blocks_history", a.sessionRequired(a.handleGetBlocksComplianceHistory)).Methods("GET")
}
func (a *API) handleGetBoardsForCompliance(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/boards getBoardsForCompliance
//
// Returns boards for a specific team, or all teams.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: team_id
// in: query
// description: Team ID. If empty then boards across all teams are included.
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of boards to return per page(default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BoardsComplianceResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
teamID := query.Get("team_id")
strPage := query.Get("page")
strPerPage := query.Get("per_page")
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getAllBoards"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getAllBoards"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBoardsForComplianceOptions{
TeamID: teamID,
Page: page,
PerPage: perPage,
}
boards, more, err := a.app.GetBoardsForCompliance(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBoardsForCompliance",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
mlog.Bool("hasNext", more),
)
response := model.BoardsComplianceResponse{
HasNext: more,
Results: boards,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleGetBoardsComplianceHistory(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/boards_history getBoardsComplianceHistory
//
// Returns boards histories for a specific team, or all teams.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: modified_since
// in: query
// description: Filters for boards modified since timestamp; Unix time in milliseconds
// required: true
// type: integer
// - name: include_deleted
// in: query
// description: When true then deleted boards are included. Default=false
// required: false
// type: boolean
// - name: team_id
// in: query
// description: Team ID. If empty then board histories across all teams are included
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of board histories to return per page (default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BoardsComplianceHistoryResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
strModifiedSince := query.Get("modified_since") // required, everything else optional
includeDeleted := query.Get("include_deleted") == "true"
strPage := query.Get("page")
strPerPage := query.Get("per_page")
teamID := query.Get("team_id")
if strModifiedSince == "" {
a.errorResponse(w, r, model.NewErrBadRequest("`modified_since` parameter required"))
return
}
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getBoardsHistory"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getBoardsHistory"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
modifiedSince, err := strconv.ParseInt(strModifiedSince, 10, 64)
if err != nil {
message := fmt.Sprintf("invalid `modified_since` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBoardsComplianceHistoryOptions{
ModifiedSince: modifiedSince,
IncludeDeleted: includeDeleted,
TeamID: teamID,
Page: page,
PerPage: perPage,
}
boards, more, err := a.app.GetBoardsComplianceHistory(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBoardsComplianceHistory",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
mlog.Bool("hasNext", more),
)
response := model.BoardsComplianceHistoryResponse{
HasNext: more,
Results: boards,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleGetBlocksComplianceHistory(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /admin/blocks_history getBlocksComplianceHistory
//
// Returns block histories for a specific team, specific board, or all teams and boards.
//
// Requires a license that includes Compliance feature. Caller must have `manage_system` permissions.
//
// ---
// produces:
// - application/json
// parameters:
// - name: modified_since
// in: query
// description: Filters for boards modified since timestamp; Unix time in milliseconds
// required: true
// type: integer
// - name: include_deleted
// in: query
// description: When true then deleted boards are included. Default=false
// required: false
// type: boolean
// - name: team_id
// in: query
// description: Team ID. If empty then block histories across all teams are included
// required: false
// type: string
// - name: board_id
// in: query
// description: Board ID. If empty then block histories for all boards are included
// required: false
// type: string
// - name: page
// in: query
// description: The page to select (default=0)
// required: false
// type: integer
// - name: per_page
// in: query
// description: Number of block histories to return per page (default=60)
// required: false
// type: integer
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// items:
// "$ref": "#/definitions/BlocksComplianceHistoryResponse"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
strModifiedSince := query.Get("modified_since") // required, everything else optional
includeDeleted := query.Get("include_deleted") == "true"
strPage := query.Get("page")
strPerPage := query.Get("per_page")
teamID := query.Get("team_id")
boardID := query.Get("board_id")
if strModifiedSince == "" {
a.errorResponse(w, r, model.NewErrBadRequest("`modified_since` parameter required"))
return
}
// check for permission `manage_system`
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionManageSystem) {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied Compliance Export getBlocksHistory"))
return
}
// check for valid license feature: compliance
license := a.app.GetLicense()
if license == nil || !(*license.Features.Compliance) {
a.errorResponse(w, r, model.NewErrNotImplemented("insufficient license Compliance Export getBlocksHistory"))
return
}
// check for valid team if specified
if teamID != "" {
_, err := a.app.GetTeam(teamID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid team id: "+teamID))
return
}
}
// check for valid board if specified
if boardID != "" {
_, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("invalid board id: "+boardID))
return
}
}
if strPage == "" {
strPage = complianceDefaultPage
}
if strPerPage == "" {
strPerPage = complianceDefaultPerPage
}
page, err := strconv.Atoi(strPage)
if err != nil {
message := fmt.Sprintf("invalid `page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
perPage, err := strconv.Atoi(strPerPage)
if err != nil {
message := fmt.Sprintf("invalid `per_page` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
modifiedSince, err := strconv.ParseInt(strModifiedSince, 10, 64)
if err != nil {
message := fmt.Sprintf("invalid `modified_since` parameter: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
opts := model.QueryBlocksComplianceHistoryOptions{
ModifiedSince: modifiedSince,
IncludeDeleted: includeDeleted,
TeamID: teamID,
BoardID: boardID,
Page: page,
PerPage: perPage,
}
blocks, more, err := a.app.GetBlocksComplianceHistory(opts)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetBlocksComplianceHistory",
mlog.String("teamID", teamID),
mlog.String("boardID", boardID),
mlog.Int("blocksCount", len(blocks)),
mlog.Bool("hasNext", more),
)
response := model.BlocksComplianceHistoryResponse{
HasNext: more,
Results: blocks,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}

44
server/boards/api/config.go Обычный файл
Просмотреть файл

@@ -0,0 +1,44 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
func (a *API) registerConfigRoutes(r *mux.Router) {
// Config APIs
r.HandleFunc("/clientConfig", a.getClientConfig).Methods("GET")
}
func (a *API) getClientConfig(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /clientConfig getClientConfig
//
// Returns the client configuration
//
// ---
// produces:
// - application/json
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/ClientConfig"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
clientConfig := a.app.GetClientConfig()
configData, err := json.Marshal(clientConfig)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, configData)
}

107
server/boards/api/content_blocks.go Обычный файл
Просмотреть файл

@@ -0,0 +1,107 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
)
func (a *API) registerContentBlocksRoutes(r *mux.Router) {
// Blocks APIs
r.HandleFunc("/content-blocks/{blockID}/moveto/{where}/{dstBlockID}", a.sessionRequired(a.handleMoveBlockTo)).Methods("POST")
}
func (a *API) handleMoveBlockTo(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /content-blocks/{blockID}/move/{where}/{dstBlockID} moveBlockTo
//
// Move a block after another block in the parent card
//
// ---
// produces:
// - application/json
// parameters:
// - name: blockID
// in: path
// description: Block ID
// required: true
// type: string
// - name: where
// in: path
// description: Relative location respect destination block (after or before)
// required: true
// type: string
// - name: dstBlockID
// in: path
// description: Destination Block ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Block"
// '404':
// description: board or block not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
blockID := mux.Vars(r)["blockID"]
dstBlockID := mux.Vars(r)["dstBlockID"]
where := mux.Vars(r)["where"]
userID := getUserID(r)
block, err := a.app.GetBlockByID(blockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
dstBlock, err := a.app.GetBlockByID(dstBlockID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if where != "after" && where != "before" {
a.errorResponse(w, r, model.NewErrBadRequest("invalid where parameter, use before or after"))
return
}
if userID == "" {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board cards"))
return
}
auditRec := a.makeAuditRecord(r, "moveBlockTo", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("blockID", blockID)
auditRec.AddMeta("dstBlockID", dstBlockID)
err = a.app.MoveContentBlock(block, dstBlock, where, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}

32
server/boards/api/context.go Обычный файл
Просмотреть файл

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"context"
"net"
"net/http"
)
type contextKey int
const (
httpConnContextKey contextKey = iota
sessionContextKey
)
// SetContextConn stores the connection in the request context.
func SetContextConn(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, httpConnContextKey, c)
}
// GetContextConn gets the stored connection from the request context.
func GetContextConn(r *http.Request) net.Conn {
value := r.Context().Value(httpConnContextKey)
if value == nil {
return nil
}
return value.(net.Conn)
}

329
server/boards/api/files.go Обычный файл
Просмотреть файл

@@ -0,0 +1,329 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"time"
"github.com/mattermost/mattermost-server/v6/server/boards/app"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/web"
)
// FileUploadResponse is the response to a file upload
// swagger:model
type FileUploadResponse struct {
// The FileID to retrieve the uploaded file
// required: true
FileID string `json:"fileId"`
}
func FileUploadResponseFromJSON(data io.Reader) (*FileUploadResponse, error) {
var fileUploadResponse FileUploadResponse
if err := json.NewDecoder(data).Decode(&fileUploadResponse); err != nil {
return nil, err
}
return &fileUploadResponse, nil
}
func FileInfoResponseFromJSON(data io.Reader) (*mm_model.FileInfo, error) {
var fileInfo mm_model.FileInfo
if err := json.NewDecoder(data).Decode(&fileInfo); err != nil {
return nil, err
}
return &fileInfo, nil
}
func (a *API) registerFilesRoutes(r *mux.Router) {
// Files API
r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
r.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}/info", a.attachSession(a.getFileInfo, false)).Methods("GET")
r.HandleFunc("/teams/{teamID}/{boardID}/files", a.sessionRequired(a.handleUploadFile)).Methods("POST")
}
func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /files/teams/{teamID}/{boardID}/{filename} getFile
//
// Returns the contents of an uploaded file
//
// ---
// produces:
// - application/json
// - image/jpg
// - image/png
// - image/gif
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: filename
// in: path
// description: name of the file
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: file not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
filename := vars["filename"]
userID := getUserID(r)
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "getFile", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("teamID", board.TeamID)
auditRec.AddMeta("filename", filename)
fileInfo, fileReader, err := a.app.GetFile(board.TeamID, boardID, filename)
if err != nil && !model.IsErrNotFound(err) {
a.errorResponse(w, r, err)
return
}
if errors.Is(err, app.ErrFileNotFound) && board.ChannelID != "" {
// prior to moving from workspaces to teams, the filepath was constructed from
// workspaceID, which is the channel ID in plugin mode.
// If a file is not found from team ID as we tried above, try looking for it via
// channel ID.
fileReader, err = a.app.GetFileReader(board.ChannelID, boardID, filename)
if err != nil {
a.errorResponse(w, r, err)
return
}
// move file to team location
// nothing to do if there is an error
_ = a.app.MoveFile(board.ChannelID, board.TeamID, boardID, filename)
}
defer fileReader.Close()
mimeType := ""
var fileSize int64
if fileInfo != nil {
mimeType = fileInfo.MimeType
fileSize = fileInfo.Size
}
web.WriteFileResponse(filename, mimeType, fileSize, time.Now(), "", fileReader, false, w, r)
auditRec.Success()
}
func (a *API) getFileInfo(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /files/teams/{teamID}/{boardID}/{filename}/info getFile
//
// Returns the metadata of an uploaded file
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: filename
// in: path
// description: name of the file
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: file not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
teamID := vars["teamID"]
filename := vars["filename"]
userID := getUserID(r)
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
if userID == "" && !hasValidReadToken {
a.errorResponse(w, r, model.NewErrUnauthorized("access denied to board"))
return
}
if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
auditRec := a.makeAuditRecord(r, "getFile", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("teamID", teamID)
auditRec.AddMeta("filename", filename)
fileInfo, err := a.app.GetFileInfo(filename)
if err != nil && !model.IsErrNotFound(err) {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(fileInfo)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/boards/{boardID}/files uploadFile
//
// Upload a binary file, attached to a root block
//
// ---
// consumes:
// - multipart/form-data
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: ID of the team
// required: true
// type: string
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: uploaded file
// in: formData
// type: file
// description: The file to upload
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/FileUploadResponse"
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r, model.NewErrPermission("access denied to make board changes"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if a.app.GetConfig().MaxFileSize > 0 {
r.Body = http.MaxBytesReader(w, r.Body, a.app.GetConfig().MaxFileSize)
}
file, handle, err := r.FormFile(UploadFormFileKey)
if err != nil {
if strings.HasSuffix(err.Error(), "http: request body too large") {
a.errorResponse(w, r, model.ErrRequestEntityTooLarge)
return
}
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
defer file.Close()
auditRec := a.makeAuditRecord(r, "uploadFile", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("teamID", board.TeamID)
auditRec.AddMeta("filename", handle.Filename)
fileID, err := a.app.SaveFile(file, board.TeamID, boardID, handle.Filename)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("uploadFile",
mlog.String("filename", handle.Filename),
mlog.String("fileID", fileID),
)
data, err := json.Marshal(FileUploadResponse{FileID: fileID})
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("fileID", fileID)
auditRec.Success()
}

262
server/boards/api/insights.go Обычный файл
Просмотреть файл

@@ -0,0 +1,262 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
mm_model "github.com/mattermost/mattermost-server/v6/model"
)
func (a *API) registerInsightsRoutes(r *mux.Router) {
// Insights APIs
r.HandleFunc("/teams/{teamID}/boards/insights", a.sessionRequired(a.handleTeamBoardsInsights)).Methods("GET")
r.HandleFunc("/users/me/boards/insights", a.sessionRequired(a.handleUserBoardsInsights)).Methods("GET")
}
func (a *API) handleTeamBoardsInsights(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards/insights handleTeamBoardsInsights
//
// Returns team boards insights
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: time_range
// in: query
// description: duration of data to calculate insights for
// required: true
// type: string
// - name: page
// in: query
// description: page offset for top boards
// required: true
// type: string
// - name: per_page
// in: query
// description: limit for boards in a page.
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/BoardInsight"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := getUserID(r)
query := r.URL.Query()
timeRange := query.Get("time_range")
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "getTeamBoardsInsights", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
page, err := strconv.Atoi(query.Get("page"))
if err != nil {
message := fmt.Sprintf("error converting page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if page < 0 {
a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
}
perPage, err := strconv.Atoi(query.Get("per_page"))
if err != nil {
message := fmt.Sprintf("error converting per_page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if perPage < 0 {
a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
}
userTimezone, aErr := a.app.GetUserTimezone(userID)
if aErr != nil {
message := fmt.Sprintf("Error getting time zone of user: %s", aErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
userLocation, _ := time.LoadLocation(userTimezone)
if userLocation == nil {
userLocation = time.Now().UTC().Location()
}
// get unix time for duration
startTime, appErr := mm_model.GetStartOfDayForTimeRange(timeRange, userLocation)
if appErr != nil {
a.errorResponse(w, r, model.NewErrBadRequest(appErr.Message))
return
}
boardsInsights, err := a.app.GetTeamBoardsInsights(userID, teamID, &mm_model.InsightsOpts{
StartUnixMilli: mm_model.GetMillisForTime(*startTime),
Page: page,
PerPage: perPage,
})
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(boardsInsights)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("teamBoardsInsightCount", len(boardsInsights.Items))
auditRec.Success()
}
func (a *API) handleUserBoardsInsights(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/me/boards/insights getUserBoardsInsights
//
// Returns user boards insights
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: time_range
// in: query
// description: duration of data to calculate insights for
// required: true
// type: string
// - name: page
// in: query
// description: page offset for top boards
// required: true
// type: string
// - name: per_page
// in: query
// description: limit for boards in a page.
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/BoardInsight"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
userID := getUserID(r)
query := r.URL.Query()
teamID := query.Get("team_id")
timeRange := query.Get("time_range")
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "getUserBoardsInsights", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
page, err := strconv.Atoi(query.Get("page"))
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest("error converting page parameter to integer"))
return
}
if page < 0 {
a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
}
perPage, err := strconv.Atoi(query.Get("per_page"))
if err != nil {
message := fmt.Sprintf("error converting per_page parameter to integer: %s", err)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
if perPage < 0 {
a.errorResponse(w, r, model.NewErrBadRequest("Invalid page parameter"))
}
userTimezone, aErr := a.app.GetUserTimezone(userID)
if aErr != nil {
message := fmt.Sprintf("Error getting time zone of user: %s", aErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
userLocation, _ := time.LoadLocation(userTimezone)
if userLocation == nil {
userLocation = time.Now().UTC().Location()
}
// get unix time for duration
startTime, appErr := mm_model.GetStartOfDayForTimeRange(timeRange, userLocation)
if appErr != nil {
a.errorResponse(w, r, model.NewErrBadRequest(appErr.Message))
return
}
boardsInsights, err := a.app.GetUserBoardsInsights(userID, teamID, &mm_model.InsightsOpts{
StartUnixMilli: mm_model.GetMillisForTime(*startTime),
Page: page,
PerPage: perPage,
})
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(boardsInsights)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("userBoardInsightCount", len(boardsInsights.Items))
auditRec.Success()
}

91
server/boards/api/limits.go Обычный файл
Просмотреть файл

@@ -0,0 +1,91 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *API) registerLimitsRoutes(r *mux.Router) {
// limits
r.HandleFunc("/limits", a.sessionRequired(a.handleCloudLimits)).Methods("GET")
r.HandleFunc("/teams/{teamID}/notifyadminupgrade", a.sessionRequired(a.handleNotifyAdminUpgrade)).Methods(http.MethodPost)
}
func (a *API) handleCloudLimits(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /limits cloudLimits
//
// Fetches the cloud limits of the server.
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/BoardsCloudLimits"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardsCloudLimits, err := a.app.GetBoardsCloudLimits()
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(boardsCloudLimits)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}
func (a *API) handleNotifyAdminUpgrade(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /api/v2/teams/{teamID}/notifyadminupgrade handleNotifyAdminUpgrade
//
// Notifies admins for upgrade request.
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
vars := mux.Vars(r)
teamID := vars["teamID"]
if err := a.app.NotifyPortalAdminsUpgradeRequest(teamID); err != nil {
jsonStringResponse(w, http.StatusOK, "{}")
}
}

549
server/boards/api/members.go Обычный файл
Просмотреть файл

@@ -0,0 +1,549 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerMembersRoutes(r *mux.Router) {
// Member APIs
r.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleGetMembersForBoard)).Methods("GET")
r.HandleFunc("/boards/{boardID}/members", a.sessionRequired(a.handleAddMember)).Methods("POST")
r.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT")
r.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE")
r.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}/leave", a.sessionRequired(a.handleLeaveBoard)).Methods("POST")
}
func (a *API) handleGetMembersForBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/members getMembersForBoard
//
// Returns the members of the board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/BoardMember"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board members"))
return
}
auditRec := a.makeAuditRecord(r, "getMembersForBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
members, err := a.app.GetMembersForBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetMembersForBoard",
mlog.String("boardID", boardID),
mlog.Int("membersCount", len(members)),
)
data, err := json.Marshal(members)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/members addMember
//
// Adds a new member to a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: Body
// in: body
// description: membership to replace the current one with
// required: true
// schema:
// "$ref": "#/definitions/BoardMember"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardMember'
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) &&
!(board.Type == model.BoardTypeOpen && a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties)) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var reqBoardMember *model.BoardMember
if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
if reqBoardMember.UserID == "" {
a.errorResponse(w, r, model.NewErrBadRequest("empty userID"))
return
}
if !a.permissions.HasPermissionToTeam(reqBoardMember.UserID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
newBoardMember := &model.BoardMember{
UserID: reqBoardMember.UserID,
BoardID: boardID,
SchemeEditor: reqBoardMember.SchemeEditor,
SchemeAdmin: reqBoardMember.SchemeAdmin,
SchemeViewer: reqBoardMember.SchemeViewer,
SchemeCommenter: reqBoardMember.SchemeCommenter,
}
auditRec := a.makeAuditRecord(r, "addMember", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", reqBoardMember.UserID)
member, err := a.app.AddMemberToBoard(newBoardMember)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("AddMember",
mlog.String("boardID", board.ID),
mlog.String("addedUserID", reqBoardMember.UserID),
)
data, err := json.Marshal(member)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/join joinBoard
//
// Become a member of a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: allow_admin
// in: path
// description: allows admin users to join private boards
// required: false
// type: boolean
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardMember'
// '404':
// description: board not found
// '403':
// description: access denied
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
allowAdmin := query.Has("allow_admin")
userID := getUserID(r)
if userID == "" {
a.errorResponse(w, r, model.NewErrBadRequest("missing user ID"))
return
}
boardID := mux.Vars(r)["boardID"]
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
isAdmin := false
if board.Type != model.BoardTypeOpen {
if !allowAdmin || !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) {
a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board"))
return
}
isAdmin = true
}
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("guests not allowed to join boards"))
return
}
newBoardMember := &model.BoardMember{
UserID: userID,
BoardID: boardID,
SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin || isAdmin,
SchemeEditor: board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor,
SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter,
SchemeViewer: board.MinimumRole == model.BoardRoleViewer,
}
auditRec := a.makeAuditRecord(r, "joinBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", userID)
member, err := a.app.AddMemberToBoard(newBoardMember)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("JoinBoard",
mlog.String("boardID", board.ID),
mlog.String("addedUserID", userID),
)
data, err := json.Marshal(member)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleLeaveBoard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/leave leaveBoard
//
// Remove your own membership from a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: board not found
// '403':
// description: access denied
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
if userID == "" {
a.errorResponse(w, r, model.NewErrBadRequest("invalid session"))
return
}
boardID := mux.Vars(r)["boardID"]
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to board"))
return
}
board, err := a.app.GetBoard(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "leaveBoard", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", userID)
err = a.app.DeleteBoardMember(boardID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("LeaveBoard",
mlog.String("boardID", board.ID),
mlog.String("addedUserID", userID),
)
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
// swagger:operation PUT /boards/{boardID}/members/{userID} updateMember
//
// Updates a board member
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// - name: Body
// in: body
// description: membership to replace the current one with
// required: true
// schema:
// "$ref": "#/definitions/BoardMember"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// $ref: '#/definitions/BoardMember'
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
paramsUserID := mux.Vars(r)["userID"]
userID := getUserID(r)
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var reqBoardMember *model.BoardMember
if err = json.Unmarshal(requestBody, &reqBoardMember); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
newBoardMember := &model.BoardMember{
UserID: paramsUserID,
BoardID: boardID,
SchemeAdmin: reqBoardMember.SchemeAdmin,
SchemeEditor: reqBoardMember.SchemeEditor,
SchemeCommenter: reqBoardMember.SchemeCommenter,
SchemeViewer: reqBoardMember.SchemeViewer,
}
isGuest, err := a.userIsGuest(paramsUserID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
newBoardMember.SchemeAdmin = false
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}
auditRec := a.makeAuditRecord(r, "patchMember", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("patchedUserID", paramsUserID)
member, err := a.app.UpdateBoardMember(newBoardMember)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("PatchMember",
mlog.String("boardID", boardID),
mlog.String("patchedUserID", paramsUserID),
)
data, err := json.Marshal(member)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /boards/{boardID}/members/{userID} deleteMember
//
// Deletes a member from a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
paramsUserID := mux.Vars(r)["userID"]
userID := getUserID(r)
if _, err := a.app.GetBoard(boardID); err != nil {
a.errorResponse(w, r, err)
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
a.errorResponse(w, r, model.NewErrPermission("access denied to modify board members"))
return
}
auditRec := a.makeAuditRecord(r, "deleteMember", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("boardID", boardID)
auditRec.AddMeta("addedUserID", paramsUserID)
deleteErr := a.app.DeleteBoardMember(boardID, paramsUserID)
if deleteErr != nil {
a.errorResponse(w, r, deleteErr)
return
}
a.logger.Debug("DeleteMember",
mlog.String("boardID", boardID),
mlog.String("addedUserID", paramsUserID),
)
// response
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}

87
server/boards/api/onboarding.go Обычный файл
Просмотреть файл

@@ -0,0 +1,87 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *API) registerOnboardingRoutes(r *mux.Router) {
// Onboarding tour endpoints APIs
r.HandleFunc("/teams/{teamID}/onboard", a.sessionRequired(a.handleOnboard)).Methods(http.MethodPost)
}
func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /team/{teamID}/onboard onboard
//
// Onboards a user on Boards.
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: object
// properties:
// teamID:
// type: string
// description: Team ID
// boardID:
// type: string
// description: Board ID
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to create board"))
return
}
teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID)
if err != nil {
a.errorResponse(w, r, err)
return
}
response := map[string]string{
"teamID": teamID,
"boardID": boardID,
}
data, err := json.Marshal(response)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}

355
server/boards/api/search.go Обычный файл
Просмотреть файл

@@ -0,0 +1,355 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerSearchRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/channels", a.sessionRequired(a.handleSearchMyChannels)).Methods("GET")
r.HandleFunc("/teams/{teamID}/boards/search", a.sessionRequired(a.handleSearchBoards)).Methods("GET")
r.HandleFunc("/teams/{teamID}/boards/search/linkable", a.sessionRequired(a.handleSearchLinkableBoards)).Methods("GET")
r.HandleFunc("/boards/search", a.sessionRequired(a.handleSearchAllBoards)).Methods("GET")
}
func (a *API) handleSearchMyChannels(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/channels searchMyChannels
//
// Returns the user available channels
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: search
// in: query
// description: string to filter channels list
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Channel"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
query := r.URL.Query()
searchQuery := query.Get("search")
teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "searchMyChannels", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
channels, err := a.app.SearchUserChannels(teamID, userID, searchQuery)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GetUserChannels",
mlog.String("teamID", teamID),
mlog.Int("channelsCount", len(channels)),
)
data, err := json.Marshal(channels)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("channelsCount", len(channels))
auditRec.Success()
}
func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards/search searchBoards
//
// Returns the boards that match with a search term in the team
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: q
// in: query
// description: The search term. Must have at least one character
// required: true
// type: string
// - name: field
// in: query
// description: The field to search on for search term. Can be `title`, `property_name`. Defaults to `title`
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
var err error
teamID := mux.Vars(r)["teamID"]
term := r.URL.Query().Get("q")
searchFieldText := r.URL.Query().Get("field")
searchField := model.BoardSearchFieldTitle
if searchFieldText != "" {
searchField, err = model.BoardSearchFieldFromString(searchFieldText)
if err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
}
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if term == "" {
jsonStringResponse(w, http.StatusOK, "[]")
return
}
auditRec := a.makeAuditRecord(r, "searchBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
// retrieve boards list
boards, err := a.app.SearchBoardsForUser(term, searchField, userID, !isGuest)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("SearchBoards",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(boards)),
)
data, err := json.Marshal(boards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("boardsCount", len(boards))
auditRec.Success()
}
func (a *API) handleSearchLinkableBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards/search/linkable searchLinkableBoards
//
// Returns the boards that match with a search term in the team and the
// user has permission to manage members
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: q
// in: query
// description: The search term. Must have at least one character
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
teamID := mux.Vars(r)["teamID"]
term := r.URL.Query().Get("q")
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
if term == "" {
jsonStringResponse(w, http.StatusOK, "[]")
return
}
auditRec := a.makeAuditRecord(r, "searchLinkableBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
// retrieve boards list
boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
linkableBoards := []*model.Board{}
for _, board := range boards {
if a.permissions.HasPermissionToBoard(userID, board.ID, model.PermissionManageBoardRoles) {
linkableBoards = append(linkableBoards, board)
}
}
a.logger.Debug("SearchLinkableBoards",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(linkableBoards)),
)
data, err := json.Marshal(linkableBoards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("boardsCount", len(linkableBoards))
auditRec.Success()
}
func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/search searchAllBoards
//
// Returns the boards that match with a search term
//
// ---
// produces:
// - application/json
// parameters:
// - name: q
// in: query
// description: The search term. Must have at least one character
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
term := r.URL.Query().Get("q")
userID := getUserID(r)
if term == "" {
jsonStringResponse(w, http.StatusOK, "[]")
return
}
auditRec := a.makeAuditRecord(r, "searchAllBoards", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
// retrieve boards list
boards, err := a.app.SearchBoardsForUser(term, model.BoardSearchFieldTitle, userID, !isGuest)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("SearchAllBoards",
mlog.Int("boardsCount", len(boards)),
)
data, err := json.Marshal(boards)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("boardsCount", len(boards))
auditRec.Success()
}

184
server/boards/api/sharing.go Обычный файл
Просмотреть файл

@@ -0,0 +1,184 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"errors"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
var ErrTurningOnSharing = errors.New("turning on sharing for board failed, see log for details")
func (a *API) registerSharingRoutes(r *mux.Router) {
// Sharing APIs
r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST")
r.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handleGetSharing)).Methods("GET")
}
func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /boards/{boardID}/sharing getSharing
//
// Returns sharing information for a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Sharing"
// '404':
// description: board not found
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
boardID := vars["boardID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to sharing the board"))
return
}
auditRec := a.makeAuditRecord(r, "getSharing", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("boardID", boardID)
sharing, err := a.app.GetSharing(boardID)
if err != nil {
a.errorResponse(w, r, err)
return
}
sharingData, err := json.Marshal(sharing)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, sharingData)
a.logger.Debug("GET sharing",
mlog.String("boardID", boardID),
mlog.String("shareID", sharing.ID),
mlog.Bool("enabled", sharing.Enabled),
)
auditRec.AddMeta("shareID", sharing.ID)
auditRec.AddMeta("enabled", sharing.Enabled)
auditRec.Success()
}
func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /boards/{boardID}/sharing postSharing
//
// Sets sharing information for a board
//
// ---
// produces:
// - application/json
// parameters:
// - name: boardID
// in: path
// description: Board ID
// required: true
// type: string
// - name: Body
// in: body
// description: sharing information for a root block
// required: true
// schema:
// "$ref": "#/definitions/Sharing"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
boardID := mux.Vars(r)["boardID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) {
a.errorResponse(w, r, model.NewErrPermission("access denied to sharing the board"))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var sharing model.Sharing
err = json.Unmarshal(requestBody, &sharing)
if err != nil {
a.errorResponse(w, r, err)
return
}
// Stamp boardID from the URL
sharing.ID = boardID
auditRec := a.makeAuditRecord(r, "postSharing", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("shareID", sharing.ID)
auditRec.AddMeta("enabled", sharing.Enabled)
// Stamp ModifiedBy
modifiedBy := userID
if userID == model.SingleUser {
modifiedBy = ""
}
sharing.ModifiedBy = modifiedBy
if userID == model.SingleUser {
userID = ""
}
if !a.app.GetClientConfig().EnablePublicSharedBoards {
a.logger.Warn(
"Attempt to turn on sharing for board via API failed, sharing off in configuration.",
mlog.String("boardID", sharing.ID),
mlog.String("userID", userID))
a.errorResponse(w, r, ErrTurningOnSharing)
return
}
sharing.ModifiedBy = userID
err = a.app.UpsertSharing(sharing)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
a.logger.Debug("POST sharing", mlog.String("sharingID", sharing.ID))
auditRec.Success()
}

74
server/boards/api/statistics.go Обычный файл
Просмотреть файл

@@ -0,0 +1,74 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
mm_model "github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
)
func (a *API) registerStatisticsRoutes(r *mux.Router) {
// statistics
r.HandleFunc("/statistics", a.sessionRequired(a.handleStatistics)).Methods("GET")
}
func (a *API) handleStatistics(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /statistics handleStatistics
//
// Fetches the statistic of the server.
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/BoardStatistics"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if !a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in standalone mode"))
return
}
// user must have right to access analytics
userID := getUserID(r)
if !a.permissions.HasPermissionTo(userID, mm_model.PermissionGetAnalytics) {
a.errorResponse(w, r, model.NewErrPermission("access denied System Statistics"))
return
}
boardCount, err := a.app.GetBoardCount()
if err != nil {
a.errorResponse(w, r, err)
return
}
cardCount, err := a.app.GetUsedCardsCount()
if err != nil {
a.errorResponse(w, r, err)
return
}
stats := model.BoardsStatistics{
Boards: int(boardCount),
Cards: cardCount,
}
data, err := json.Marshal(stats)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
}

241
server/boards/api/subscriptions.go Обычный файл
Просмотреть файл

@@ -0,0 +1,241 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerSubscriptionsRoutes(r *mux.Router) {
// Subscription APIs
r.HandleFunc("/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST")
r.HandleFunc("/subscriptions/{blockID}/{subscriberID}", a.sessionRequired(a.handleDeleteSubscription)).Methods("DELETE")
r.HandleFunc("/subscriptions/{subscriberID}", a.sessionRequired(a.handleGetSubscriptions)).Methods("GET")
}
// subscriptions
func (a *API) handleCreateSubscription(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /subscriptions createSubscription
//
// Creates a subscription to a block for a user. The user will receive change notifications for the block.
//
// ---
// produces:
// - application/json
// parameters:
// - name: Body
// in: body
// description: subscription definition
// required: true
// schema:
// "$ref": "#/definitions/Subscription"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var sub model.Subscription
if err = json.Unmarshal(requestBody, &sub); err != nil {
a.errorResponse(w, r, err)
return
}
if err = sub.IsValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
auditRec := a.makeAuditRecord(r, "createSubscription", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("subscriber_id", sub.SubscriberID)
auditRec.AddMeta("block_id", sub.BlockID)
// User can only create subscriptions for themselves (for now)
if session.UserID != sub.SubscriberID {
a.errorResponse(w, r, model.NewErrBadRequest("userID and subscriberID mismatch"))
return
}
// check for valid block
_, bErr := a.app.GetBlockByID(sub.BlockID)
if bErr != nil {
message := fmt.Sprintf("invalid blockID: %s", bErr)
a.errorResponse(w, r, model.NewErrBadRequest(message))
return
}
subNew, err := a.app.CreateSubscription(&sub)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("CREATE subscription",
mlog.String("subscriber_id", subNew.SubscriberID),
mlog.String("block_id", subNew.BlockID),
)
json, err := json.Marshal(subNew)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.Success()
}
func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) {
// swagger:operation DELETE /subscriptions/{blockID}/{subscriberID} deleteSubscription
//
// Deletes a subscription a user has for a a block. The user will no longer receive change notifications for the block.
//
// ---
// produces:
// - application/json
// parameters:
// - name: blockID
// in: path
// description: Block ID
// required: true
// type: string
// - name: subscriberID
// in: path
// description: Subscriber ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
vars := mux.Vars(r)
blockID := vars["blockID"]
subscriberID := vars["subscriberID"]
auditRec := a.makeAuditRecord(r, "deleteSubscription", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
auditRec.AddMeta("block_id", blockID)
auditRec.AddMeta("subscriber_id", subscriberID)
// User can only delete subscriptions for themselves
if session.UserID != subscriberID {
a.errorResponse(w, r, model.NewErrPermission("access denied"))
return
}
if _, err := a.app.DeleteSubscription(blockID, subscriberID); err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("DELETE subscription",
mlog.String("blockID", blockID),
mlog.String("subscriberID", subscriberID),
)
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /subscriptions/{subscriberID} getSubscriptions
//
// Gets subscriptions for a user.
//
// ---
// produces:
// - application/json
// parameters:
// - name: subscriberID
// in: path
// description: Subscriber ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
vars := mux.Vars(r)
subscriberID := vars["subscriberID"]
auditRec := a.makeAuditRecord(r, "getSubscriptions", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("subscriber_id", subscriberID)
// User can only get subscriptions for themselves (for now)
if session.UserID != subscriberID {
a.errorResponse(w, r, model.NewErrPermission("access denied"))
return
}
subs, err := a.app.GetSubscriptions(subscriberID)
if err != nil {
a.errorResponse(w, r, err)
return
}
a.logger.Debug("GET subscriptions",
mlog.String("subscriberID", subscriberID),
mlog.Int("count", len(subs)),
)
json, err := json.Marshal(subs)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, json)
auditRec.AddMeta("subscription_count", len(subs))
auditRec.Success()
}

60
server/boards/api/system.go Обычный файл
Просмотреть файл

@@ -0,0 +1,60 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
)
func (a *API) registerSystemRoutes(r *mux.Router) {
// System APIs
r.HandleFunc("/hello", a.handleHello).Methods("GET")
r.HandleFunc("/ping", a.handlePing).Methods("GET")
}
func (a *API) handleHello(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /hello hello
//
// Responds with `Hello` if the web service is running.
//
// ---
// produces:
// - text/plain
// responses:
// '200':
// description: success
stringResponse(w, "Hello")
}
func (a *API) handlePing(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /ping ping
//
// Responds with server metadata if the web service is running.
//
// ---
// produces:
// - application/json
// responses:
// '200':
// description: success
serverMetadata := a.app.GetServerMetadata()
if a.singleUserToken != "" {
serverMetadata.SKU = "personal_desktop"
}
if serverMetadata.Edition == "plugin" {
serverMetadata.SKU = "suite"
}
bytes, err := json.Marshal(serverMetadata)
if err != nil {
a.errorResponse(w, r, err)
}
jsonStringResponse(w, 200, string(bytes))
}

146
server/boards/api/system_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,146 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"github.com/mattermost/mattermost-server/v6/server/boards/app"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func TestHello(t *testing.T) {
testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)}
t.Run("Returns 'Hello' on success", func(t *testing.T) {
request, _ := http.NewRequest(http.MethodGet, "/hello", nil)
response := httptest.NewRecorder()
testAPI.handleHello(response, request)
got := response.Body.String()
want := "Hello"
if got != want {
t.Errorf("got %q want %q", got, want)
}
if response.Code != http.StatusOK {
t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK)
}
})
}
func TestPing(t *testing.T) {
testAPI := API{logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug)}
t.Run("Returns metadata on success", func(t *testing.T) {
request, _ := http.NewRequest(http.MethodGet, "/ping", nil)
response := httptest.NewRecorder()
testAPI.handlePing(response, request)
var got app.ServerMetadata
err := json.NewDecoder(response.Body).Decode(&got)
if err != nil {
t.Fatalf("Unable to JSON decode response body %q", response.Body)
}
want := app.ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: model.Edition,
DBType: "",
DBVersion: "",
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "personal_server",
}
if got != want {
t.Errorf("got %q want %q", got, want)
}
if response.Code != http.StatusOK {
t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK)
}
})
t.Run("Sets SKU to 'personal_desktop' when in single-user mode", func(t *testing.T) {
testAPI.singleUserToken = "abc-123-xyz-456"
request, _ := http.NewRequest(http.MethodGet, "/ping", nil)
response := httptest.NewRecorder()
testAPI.handlePing(response, request)
var got app.ServerMetadata
err := json.NewDecoder(response.Body).Decode(&got)
if err != nil {
t.Fatalf("Unable to JSON decode response body %q", response.Body)
}
want := app.ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: model.Edition,
DBType: "",
DBVersion: "",
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "personal_desktop",
}
if got != want {
t.Errorf("got %q want %q", got, want)
}
if response.Code != http.StatusOK {
t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK)
}
})
t.Run("Sets SKU to 'suite' when in plugin mode", func(t *testing.T) {
model.Edition = "plugin"
request, _ := http.NewRequest(http.MethodGet, "/ping", nil)
response := httptest.NewRecorder()
testAPI.handlePing(response, request)
var got app.ServerMetadata
err := json.NewDecoder(response.Body).Decode(&got)
if err != nil {
t.Fatalf("Unable to JSON decode response body %q", response.Body)
}
want := app.ServerMetadata{
Version: model.CurrentVersion,
BuildNumber: model.BuildNumber,
BuildDate: model.BuildDate,
Commit: model.BuildHash,
Edition: "plugin",
DBType: "",
DBVersion: "",
OSType: runtime.GOOS,
OSArch: runtime.GOARCH,
SKU: "suite",
}
if got != want {
t.Errorf("got %q want %q", got, want)
}
if response.Code != http.StatusOK {
t.Errorf("got HTTP %d want %d", response.Code, http.StatusOK)
}
})
}

368
server/boards/api/teams.go Обычный файл
Просмотреть файл

@@ -0,0 +1,368 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
func (a *API) registerTeamsRoutes(r *mux.Router) {
// Team APIs
r.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET")
r.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET")
r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET")
r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsersByID)).Methods("POST")
r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET")
}
func (a *API) handleGetTeams(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams getTeams
//
// Returns information of all the teams
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Team"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
teams, err := a.app.GetTeamsForUser(userID)
if err != nil {
a.errorResponse(w, r, err)
}
auditRec := a.makeAuditRecord(r, "getTeams", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamCount", len(teams))
data, err := json.Marshal(teams)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetTeam(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID} getTeam
//
// Returns information of the root team
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Team"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
var team *model.Team
var err error
if a.MattermostAuth {
team, err = a.app.GetTeam(teamID)
if model.IsErrNotFound(err) {
a.errorResponse(w, r, model.NewErrUnauthorized("invalid team"))
}
if err != nil {
a.errorResponse(w, r, err)
}
} else {
team, err = a.app.GetRootTeam()
if err != nil {
a.errorResponse(w, r, err)
return
}
}
auditRec := a.makeAuditRecord(r, "getTeam", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("resultTeamID", team.ID)
data, err := json.Marshal(team)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/regenerate_signup_token regenerateSignupToken
//
// Regenerates the signup token for the root team
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
if a.MattermostAuth {
a.errorResponse(w, r, model.NewErrNotImplemented("not permitted in plugin mode"))
return
}
team, err := a.app.GetRootTeam()
if err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "regenerateSignupToken", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
team.SignupToken = utils.NewID(utils.IDTypeToken)
if err = a.app.UpsertTeamSignupToken(*team); err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, "{}")
auditRec.Success()
}
func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/users getTeamUsers
//
// Returns team users
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: search
// in: query
// description: string to filter users list
// required: false
// type: string
// - name: exclude_bots
// in: query
// description: exclude bot users
// required: false
// type: boolean
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := getUserID(r)
query := r.URL.Query()
searchQuery := query.Get("search")
excludeBots := r.URL.Query().Get("exclude_bots") == True
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
asGuestUser := ""
if isGuest {
asGuestUser = userID
}
users, err := a.app.SearchTeamUsers(teamID, searchQuery, asGuestUser, excludeBots)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(users)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("userCount", len(users))
auditRec.Success()
}
func (a *API) handleGetTeamUsersByID(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /teams/{teamID}/users getTeamUsersByID
//
// Returns a user[]
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// - name: Body
// in: body
// description: []UserIDs to return
// required: true
// type: []string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var userIDs []string
if err = json.Unmarshal(requestBody, &userIDs); err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "getTeamUsersByID", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
vars := mux.Vars(r)
teamID := vars["teamID"]
userID := getUserID(r)
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
var users []*model.User
var error error
if len(userIDs) == 0 {
a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty"))
return
}
if userIDs[0] == model.SingleUser {
ws, _ := a.app.GetRootTeam()
now := utils.GetMillis()
user := &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
CreateAt: ws.UpdateAt,
UpdateAt: now,
}
users = append(users, user)
} else {
users, error = a.app.GetUsersList(userIDs)
if error != nil {
a.errorResponse(w, r, error)
return
}
for i, u := range users {
if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) {
users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id)
}
if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) {
users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id)
}
}
}
usersList, err := json.Marshal(users)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, string(usersList))
auditRec.Success()
}

104
server/boards/api/templates.go Обычный файл
Просмотреть файл

@@ -0,0 +1,104 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
)
func (a *API) registerTemplatesRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/templates", a.sessionRequired(a.handleGetTemplates)).Methods("GET")
}
func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/templates getTemplates
//
// Returns team templates
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)
if teamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if isGuest {
a.errorResponse(w, r, model.NewErrPermission("access denied to templates"))
return
}
auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)
// retrieve boards list
boards, err := a.app.GetTemplateBoards(teamID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
results := []*model.Board{}
for _, board := range boards {
if board.Type == model.BoardTypeOpen {
results = append(results, board)
} else if a.permissions.HasPermissionToBoard(userID, board.ID, model.PermissionViewBoard) {
results = append(results, board)
}
}
a.logger.Debug("GetTemplates",
mlog.String("teamID", teamID),
mlog.Int("boardsCount", len(results)),
)
data, err := json.Marshal(results)
if err != nil {
a.errorResponse(w, r, err)
return
}
// response
jsonBytesResponse(w, http.StatusOK, data)
auditRec.AddMeta("templatesCount", len(results))
auditRec.Success()
}

408
server/boards/api/users.go Обычный файл
Просмотреть файл

@@ -0,0 +1,408 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api
import (
"encoding/json"
"io"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v6/server/boards/model"
"github.com/mattermost/mattermost-server/v6/server/boards/services/audit"
"github.com/mattermost/mattermost-server/v6/server/boards/utils"
)
func (a *API) registerUsersRoutes(r *mux.Router) {
// Users APIs
r.HandleFunc("/users", a.sessionRequired(a.handleGetUsersList)).Methods("POST")
r.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET")
r.HandleFunc("/users/me/memberships", a.sessionRequired(a.handleGetMyMemberships)).Methods("GET")
r.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET")
r.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut)
r.HandleFunc("/users/me/config", a.sessionRequired(a.handleGetUserPreferences)).Methods(http.MethodGet)
}
func (a *API) handleGetUsersList(w http.ResponseWriter, r *http.Request) {
// swagger:operation POST /users getUsersList
//
// Returns a user[]
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var userIDs []string
if err = json.Unmarshal(requestBody, &userIDs); err != nil {
a.errorResponse(w, r, err)
return
}
auditRec := a.makeAuditRecord(r, "getUsersList", audit.Fail)
defer a.audit.LogRecord(audit.LevelAuth, auditRec)
var users []*model.User
var error error
if len(userIDs) == 0 {
a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty"))
return
}
if userIDs[0] == model.SingleUser {
ws, _ := a.app.GetRootTeam()
now := utils.GetMillis()
user := &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
CreateAt: ws.UpdateAt,
UpdateAt: now,
}
users = append(users, user)
} else {
users, error = a.app.GetUsersList(userIDs)
if error != nil {
a.errorResponse(w, r, error)
return
}
}
usersList, err := json.Marshal(users)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonStringResponse(w, http.StatusOK, string(usersList))
auditRec.Success()
}
func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/me getMe
//
// Returns the currently logged-in user
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: false
// type: string
// - name: channelID
// in: path
// description: Channel ID
// required: false
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
query := r.URL.Query()
teamID := query.Get("teamID")
channelID := query.Get("channelID")
userID := getUserID(r)
var user *model.User
var err error
auditRec := a.makeAuditRecord(r, "getMe", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
if userID == model.SingleUser {
ws, _ := a.app.GetRootTeam()
now := utils.GetMillis()
user = &model.User{
ID: model.SingleUser,
Username: model.SingleUser,
Email: model.SingleUser,
CreateAt: ws.UpdateAt,
UpdateAt: now,
}
} else {
user, err = a.app.GetUser(userID)
if err != nil {
// ToDo: wrap with an invalid token error
a.errorResponse(w, r, err)
return
}
}
if teamID != "" && a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionManageTeam) {
user.Permissions = append(user.Permissions, model.PermissionManageTeam.Id)
}
if a.permissions.HasPermissionTo(userID, model.PermissionManageSystem) {
user.Permissions = append(user.Permissions, model.PermissionManageSystem.Id)
}
if channelID != "" && a.permissions.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) {
user.Permissions = append(user.Permissions, model.PermissionCreatePost.Id)
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.AddMeta("userID", user.ID)
auditRec.Success()
}
func (a *API) handleGetMyMemberships(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/me/memberships getMyMemberships
//
// Returns the currently users board memberships
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/BoardMember"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
auditRec := a.makeAuditRecord(r, "getMyBoardMemberships", audit.Fail)
auditRec.AddMeta("userID", userID)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
members, err := a.app.GetMembersForUser(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
membersData, err := json.Marshal(members)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, membersData)
auditRec.Success()
}
func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/{userID} getUser
//
// Returns a user
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/User"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
vars := mux.Vars(r)
userID := vars["userID"]
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("userID", userID)
user, err := a.app.GetUser(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
canSeeUser, err := a.app.CanSeeUser(session.UserID, userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
if !canSeeUser {
a.errorResponse(w, r, model.NewErrNotFound("user ID="+userID))
return
}
userData, err := json.Marshal(user)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, userData)
auditRec.Success()
}
func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
// swagger:operation PATCH /users/{userID}/config updateUserConfig
//
// Updates user config
//
// ---
// produces:
// - application/json
// parameters:
// - name: userID
// in: path
// description: User ID
// required: true
// type: string
// - name: Body
// in: body
// description: User config patch to apply
// required: true
// schema:
// "$ref": "#/definitions/UserPreferencesPatch"
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
requestBody, err := io.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r, err)
return
}
var patch *model.UserPreferencesPatch
err = json.Unmarshal(requestBody, &patch)
if err != nil {
a.errorResponse(w, r, err)
return
}
vars := mux.Vars(r)
userID := vars["userID"]
ctx := r.Context()
session := ctx.Value(sessionContextKey).(*model.Session)
auditRec := a.makeAuditRecord(r, "updateUserConfig", audit.Fail)
defer a.audit.LogRecord(audit.LevelModify, auditRec)
// a user can update only own config
if userID != session.UserID {
a.errorResponse(w, r, model.NewErrForbidden(""))
return
}
updatedConfig, err := a.app.UpdateUserConfig(userID, *patch)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(updatedConfig)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}
func (a *API) handleGetUserPreferences(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /users/me/config getUserConfig
//
// Returns an array of user preferences
//
// ---
// produces:
// - application/json
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// "$ref": "#/definitions/Preferences"
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"
userID := getUserID(r)
auditRec := a.makeAuditRecord(r, "getUserConfig", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
preferences, err := a.app.GetUserPreferences(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
data, err := json.Marshal(preferences)
if err != nil {
a.errorResponse(w, r, err)
return
}
jsonBytesResponse(w, http.StatusOK, data)
auditRec.Success()
}