focalboard/server/api/boards_and_blocks.go
Jesús Espino 61a8af8f34
Restoring guest account access and adding backend part of the guest accounts support (#2929)
Co-authored-by: Paul Esch-Laurent <paul.esch-laurent@mattermost.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
2022-08-24 17:08:58 -05:00

436 lines
12 KiB
Go

package api
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"github.com/gorilla/mux"
"github.com/mattermost/focalboard/server/app"
"github.com/mattermost/focalboard/server/model"
"github.com/mattermost/focalboard/server/services/audit"
"github.com/mattermost/mattermost-server/v6/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 := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
var newBab *model.BoardsAndBlocks
if err = json.Unmarshal(requestBody, &newBab); err != nil {
// a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
return
}
if len(newBab.Boards) == 0 {
message := "at least one board is required"
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
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 {
message := "cannot create boards for multiple teams"
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return
}
if board.ID == "" {
message := "boards need an ID to be referenced from the blocks"
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return
}
}
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"})
return
}
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
if isGuest {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"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.URL.Path, http.StatusBadRequest, message, nil)
return
}
if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return
}
if block.UpdateAt < 1 {
message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
return
}
if !boardIDs[block.BoardID] {
message := fmt.Sprintf("invalid BoardID %s (not exists in the created boards)", block.BoardID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
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.URL.Path, http.StatusBadRequest, err.Error(), err)
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.URL.Path, http.StatusInternalServerError, err.Error(), 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.URL.Path, http.StatusInternalServerError, err.Error(), 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 := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
var pbab *model.PatchBoardsAndBlocks
if err = json.Unmarshal(requestBody, &pbab); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
return
}
if err = pbab.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
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.URL.Path, http.StatusBadRequest, "", err)
return
}
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardProperties) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"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.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"})
return
}
}
board, err2 := a.app.GetBoard(boardID)
if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return
}
if teamID == "" {
teamID = board.TeamID
}
if teamID != board.TeamID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return
}
}
for _, blockID := range pbab.BlockIDs {
block, err2 := a.app.GetBlockByID(blockID)
if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2)
return
}
if block == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return
}
if _, ok := boardIDMap[block.BoardID]; !ok {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"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 errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
return
}
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", 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.URL.Path, http.StatusInternalServerError, "", 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 := ioutil.ReadAll(r.Body)
if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
return
}
var dbab *model.DeleteBoardsAndBlocks
if err = json.Unmarshal(requestBody, &dbab); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
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.URL.Path, http.StatusInternalServerError, "", err)
return
}
if board == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
return
}
if teamID == "" {
teamID = board.TeamID
}
if teamID != board.TeamID {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return
}
// permission check
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"})
return
}
}
for _, blockID := range dbab.Blocks {
block, err2 := a.app.GetBlockByID(blockID)
if err2 != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2)
return
}
if block == nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return
}
if _, ok := boardIDMap[block.BoardID]; !ok {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", nil)
return
}
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying cards"})
return
}
}
if err := dbab.IsValid(); err != nil {
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
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.URL.Path, http.StatusInternalServerError, "", 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()
}