mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-11 18:13:52 +02:00
Permissions integration tests (#2697)
* Initial permissions review infrastructure
* Adding more tests cases
* Modifying a bit the tests approach and adding more tests
* Adding more tests
* Adding more tests for permissions
* Adding more tests
* Adding more permissions tests
* Adding more tests
* Adding more permission checks
* Adding more permissions tests
* Adding more permission tests
* Adding more tests
* Adding subscriptions tests
* Adding more permissions tests
* Adding tests for read tokens in the files
* Update APIs and fix unit tests
* Fix linter errors
* Auto-assign category id from the database (as expected because is serial/auto_increment integer field)
* Revert "Auto-assign category id from the database (as expected because is serial/auto_increment integer field)"
This reverts commit 5c98fd76a3
.
* Fixing Category scheme in postgres and MySQL
* Removing restriction about the channel_id and add it to all the databases
* Moving everything to a new migration
* Fix bad merge (?)
* Update 000021_fix_categories.up.sql
Fix Postgres ALTER COLUMN syntax
* Update 000021_fix_categories.down.sql
Fix Postgres ALTER COLUMN syntax
* Update 000021_fix_categories.up.sql
Remove unnecessary, and unsupported MODIFY COLUMNs for SQLite.
* Update 000021_fix_categories.up.sql
Remove not null from categories.channel_id
* Update 000021_fix_categories.down.sql
Migrate down removing not null from categories.channel_id
* Update 000021_fix_categories.up.sql
Fix drop not null on categories.channel_id
* Update 000021_fix_categories.down.sql
Fix down migration of drop not null from categories.channel_id.
* Restore default notification level to debug
Co-authored-by: Chen-I Lim <chenilim@gmail.com>
Co-authored-by: Chen-I Lim <46905241+chenilim@users.noreply.github.com>
This commit is contained in:
parent
0cff6e10b1
commit
a4ef8ec6bc
@ -87,7 +87,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}", a.sessionRequired(a.handlePatchBlock)).Methods("PATCH")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/undelete", a.sessionRequired(a.handleUndeleteBlock)).Methods("POST")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.attachSession(a.handleDuplicateBlock, false)).Methods("POST")
|
||||
apiv1.HandleFunc("/boards/{boardID}/blocks/{blockID}/duplicate", a.sessionRequired(a.handleDuplicateBlock)).Methods("POST")
|
||||
apiv1.HandleFunc("/boards/{boardID}/metadata", a.sessionRequired(a.handleGetBoardMetadata)).Methods("GET")
|
||||
|
||||
// Member APIs
|
||||
@ -96,6 +96,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleUpdateMember)).Methods("PUT")
|
||||
apiv1.HandleFunc("/boards/{boardID}/members/{userID}", a.sessionRequired(a.handleDeleteMember)).Methods("DELETE")
|
||||
apiv1.HandleFunc("/boards/{boardID}/join", a.sessionRequired(a.handleJoinBoard)).Methods("POST")
|
||||
apiv1.HandleFunc("/boards/{boardID}/leave", a.sessionRequired(a.handleLeaveBoard)).Methods("POST")
|
||||
|
||||
// Sharing APIs
|
||||
apiv1.HandleFunc("/boards/{boardID}/sharing", a.sessionRequired(a.handlePostSharing)).Methods("POST")
|
||||
@ -137,8 +138,7 @@ func (a *API) RegisterRoutes(r *mux.Router) {
|
||||
apiv1.HandleFunc("/teams/{teamID}/categories/{categoryID}/blocks/{blockID}", a.sessionRequired(a.handleUpdateCategoryBlock)).Methods(http.MethodPost)
|
||||
|
||||
// Get Files API
|
||||
files := r.PathPrefix("/files").Subrouter()
|
||||
files.HandleFunc("/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
|
||||
apiv1.HandleFunc("/files/teams/{teamID}/{boardID}/{filename}", a.attachSession(a.handleServeFile, false)).Methods("GET")
|
||||
|
||||
// Subscriptions
|
||||
apiv1.HandleFunc("/subscriptions", a.sessionRequired(a.handleCreateSubscription)).Methods("POST")
|
||||
@ -261,6 +261,8 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/Block"
|
||||
// '404':
|
||||
// description: board not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -275,6 +277,12 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID := getUserID(r)
|
||||
|
||||
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
|
||||
if userID == "" && !hasValidReadToken {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
}
|
||||
|
||||
board, err := a.app.GetBoard(boardID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -285,8 +293,8 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !a.hasValidReadTokenForBoard(r, boardID) {
|
||||
if board.IsTemplate {
|
||||
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.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"})
|
||||
return
|
||||
@ -474,6 +482,8 @@ func (a *API) handleUpdateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
case errors.Is(err, app.ErrorCategoryDeleted):
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", err)
|
||||
case errors.Is(err, app.ErrorCategoryPermissionDenied):
|
||||
// TODO: The permissions should be handled as much as possible at
|
||||
// the API level, this needs to be changed
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||
default:
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -505,9 +515,14 @@ func (a *API) handleDeleteCategory(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
deletedCategory, err := a.app.DeleteCategory(categoryID, userID, teamID)
|
||||
if err != nil {
|
||||
if errors.Is(err, app.ErrorCategoryPermissionDenied) {
|
||||
switch {
|
||||
case errors.Is(err, app.ErrorInvalidCategory):
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||
case errors.Is(err, app.ErrorCategoryPermissionDenied):
|
||||
// TODO: The permissions should be handled as much as possible at
|
||||
// the API level, this needs to be changed
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||
} else {
|
||||
default:
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
}
|
||||
return
|
||||
@ -563,6 +578,7 @@ func (a *API) handleUpdateCategoryBlock(w http.ResponseWriter, r *http.Request)
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
userID := session.UserID
|
||||
|
||||
// TODO: Check the category and the team matches
|
||||
err := a.app.AddUpdateUserCategoryBlock(teamID, userID, categoryID, blockID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -755,7 +771,7 @@ func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// a user can update only own config
|
||||
if userID != session.UserID {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
@ -950,6 +966,8 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// '404':
|
||||
// description: block not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -993,7 +1011,7 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation POST /api/v1/workspaces/{workspaceID}/blocks/{blockID}/undelete undeleteBlock
|
||||
// swagger:operation POST /api/v1/boards/{boardID}/blocks/{blockID}/undelete undeleteBlock
|
||||
//
|
||||
// Undeletes a block
|
||||
//
|
||||
@ -1001,9 +1019,9 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: workspaceID
|
||||
// - name: boardID
|
||||
// in: path
|
||||
// description: Workspace ID
|
||||
// description: Board ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: blockID
|
||||
@ -1016,6 +1034,10 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// "$ref": "#/definitions/BlockPatch"
|
||||
// '404':
|
||||
// description: block not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -1027,19 +1049,56 @@ func (a *API) handleUndeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
blockID := vars["blockID"]
|
||||
boardID := vars["boardID"]
|
||||
|
||||
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.StatusNotFound, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
block, err := a.app.GetLastBlockHistoryEntry(blockID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if block == nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if board.ID != block.BoardID {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"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)
|
||||
|
||||
err := a.app.UndeleteBlock(blockID, userID)
|
||||
undeletedBlock, err := a.app.UndeleteBlock(blockID, userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
undeletedBlockData, err := json.Marshal(undeletedBlock)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
a.logger.Debug("UNDELETE Block", mlog.String("blockID", blockID))
|
||||
jsonStringResponse(w, http.StatusOK, "{}")
|
||||
jsonBytesResponse(w, http.StatusOK, undeletedBlockData)
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
@ -1074,6 +1133,8 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// '404':
|
||||
// description: block not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -1185,6 +1246,19 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
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.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||
return
|
||||
}
|
||||
if !a.permissions.HasPermissionToBoard(userID, block.BoardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = a.app.PatchBlocks(teamID, patches, userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -1220,6 +1294,8 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
||||
// description: success
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Sharing"
|
||||
// '404':
|
||||
// description: board not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -1229,7 +1305,7 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) {
|
||||
boardID := vars["boardID"]
|
||||
|
||||
userID := getUserID(r)
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionShareBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to sharing the board"})
|
||||
return
|
||||
}
|
||||
@ -1500,6 +1576,11 @@ func (a *API) handlePostTeamRegenerateSignupToken(w http.ResponseWriter, r *http
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||
}
|
||||
|
||||
if a.MattermostAuth {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
team, err := a.app.GetRootTeam()
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -1555,6 +1636,8 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// '404':
|
||||
// description: file not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -1566,6 +1649,11 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) {
|
||||
userID := getUserID(r)
|
||||
|
||||
hasValidReadToken := a.hasValidReadTokenForBoard(r, boardID)
|
||||
if userID == "" && !hasValidReadToken {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasValidReadToken && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
@ -1659,6 +1747,8 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
// description: success
|
||||
// schema:
|
||||
// "$ref": "#/definitions/FileUploadResponse"
|
||||
// '404':
|
||||
// description: board not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -1685,7 +1775,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
file, handle, err := r.FormFile(UploadFormFileKey)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "%v", err)
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@ -1895,12 +1985,21 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
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(boards)),
|
||||
mlog.Int("boardsCount", len(results)),
|
||||
)
|
||||
|
||||
data, err := json.Marshal(boards)
|
||||
data, err := json.Marshal(results)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -1909,7 +2008,7 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
// response
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
|
||||
auditRec.AddMeta("templatesCount", len(boards))
|
||||
auditRec.AddMeta("templatesCount", len(results))
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
@ -2045,7 +2144,7 @@ func (a *API) handleDeleteSubscription(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// User can only delete subscriptions for themselves
|
||||
if session.UserID != subscriberID {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil)
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "access denied", nil)
|
||||
return
|
||||
}
|
||||
|
||||
@ -2103,7 +2202,7 @@ func (a *API) handleGetSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// User can only get subscriptions for themselves (for now)
|
||||
if session.UserID != subscriberID {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "userID and subscriberID mismatch", nil)
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "access denied", nil)
|
||||
return
|
||||
}
|
||||
|
||||
@ -2244,10 +2343,14 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
teamID := mux.Vars(r)["teamID"]
|
||||
ctx := r.Context()
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
userID := getUserID(r)
|
||||
|
||||
teamID, boardID, err := a.app.PrepareOnboardingTour(session.UserID, teamID)
|
||||
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||
return
|
||||
}
|
||||
|
||||
teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -2287,6 +2390,8 @@ func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// description: success
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Board"
|
||||
// '404':
|
||||
// description: board not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -2372,6 +2477,8 @@ func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// description: success
|
||||
// schema:
|
||||
// $ref: '#/definitions/Board'
|
||||
// '404':
|
||||
// description: board not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -2467,6 +2574,8 @@ func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// '404':
|
||||
// description: board not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -2475,6 +2584,17 @@ func (a *API) handleDeleteBoard(w http.ResponseWriter, r *http.Request) {
|
||||
boardID := mux.Vars(r)["boardID"]
|
||||
userID := getUserID(r)
|
||||
|
||||
// Check if board exists
|
||||
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.StatusNotFound, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionDeleteBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to delete board"})
|
||||
return
|
||||
@ -2516,6 +2636,8 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// description: success
|
||||
// schema:
|
||||
// $ref: '#/definitions/BoardsAndBlocks'
|
||||
// '404':
|
||||
// description: board not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -2527,11 +2649,6 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
||||
asTemplate := query.Get("asTemplate")
|
||||
toTeam := query.Get("toTeam")
|
||||
|
||||
if toTeam != "" && !a.permissions.HasPermissionToTeam(userID, toTeam, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to team"})
|
||||
return
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
@ -2547,13 +2664,18 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if board.Type == model.BoardTypePrivate {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
if toTeam == "" && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"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.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if board.TeamID != model.GlobalTeamID && !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
}
|
||||
@ -2613,6 +2735,8 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/Block"
|
||||
// '404':
|
||||
// description: board or block not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -2624,18 +2748,37 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
asTemplate := query.Get("asTemplate")
|
||||
|
||||
if userID == "" {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
}
|
||||
|
||||
board, err := a.app.GetBlockByID(blockID)
|
||||
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.StatusNotFound, "", nil)
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
}
|
||||
|
||||
block, err := a.app.GetBlockByID(blockID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if block == nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if board.ID != block.BoardID {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||
return
|
||||
}
|
||||
|
||||
@ -2751,11 +2894,6 @@ func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: boardID
|
||||
// in: path
|
||||
// description: Board ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: Team ID
|
||||
@ -2913,12 +3051,16 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
|
||||
// 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.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -2929,7 +3071,10 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
userID := getUserID(r)
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||
return
|
||||
}
|
||||
|
||||
requestBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@ -2955,11 +3100,6 @@ func (a *API) handleAddMember(w http.ResponseWriter, r *http.Request) {
|
||||
SchemeEditor: true,
|
||||
}
|
||||
|
||||
if board.Type == model.BoardTypePrivate && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "addMember", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||
auditRec.AddMeta("boardID", boardID)
|
||||
@ -3011,7 +3151,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
// $ref: '#/definitions/BoardMember'
|
||||
// '404':
|
||||
// description: board not found
|
||||
// '503':
|
||||
// '403':
|
||||
// description: access denied
|
||||
// default:
|
||||
// description: internal error
|
||||
@ -3063,7 +3203,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
a.logger.Debug("AddMember",
|
||||
a.logger.Debug("JoinBoard",
|
||||
mlog.String("boardID", board.ID),
|
||||
mlog.String("addedUserID", userID),
|
||||
)
|
||||
@ -3080,6 +3220,82 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
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.URL.Path, http.StatusBadRequest, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
boardID := mux.Vars(r)["boardID"]
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
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.StatusNotFound, "", nil)
|
||||
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 errors.Is(err, app.ErrBoardMemberIsLastAdmin) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, "", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", 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
|
||||
//
|
||||
@ -3203,6 +3419,8 @@ func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// '404':
|
||||
// description: board not found
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
@ -3212,11 +3430,6 @@ func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
|
||||
paramsUserID := mux.Vars(r)["userID"]
|
||||
userID := getUserID(r)
|
||||
|
||||
if paramsUserID != userID && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||
return
|
||||
}
|
||||
|
||||
board, err := a.app.GetBoard(boardID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -3227,6 +3440,11 @@ func (a *API) handleDeleteMember(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"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)
|
||||
@ -3295,38 +3513,16 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
||||
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 len(newBab.Boards) == 0 {
|
||||
message := "at least one board is required"
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// permission check
|
||||
createsPublicBoards := false
|
||||
createsPrivateBoards := false
|
||||
teamID := ""
|
||||
boardIDs := map[string]bool{}
|
||||
for _, board := range newBab.Boards {
|
||||
if board.Type == model.BoardTypeOpen {
|
||||
createsPublicBoards = true
|
||||
}
|
||||
if board.Type == model.BoardTypePrivate {
|
||||
createsPrivateBoards = true
|
||||
}
|
||||
boardIDs[board.ID] = true
|
||||
|
||||
if teamID == "" {
|
||||
teamID = board.TeamID
|
||||
@ -3346,6 +3542,38 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board template"})
|
||||
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)
|
||||
@ -3354,15 +3582,6 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
if createsPublicBoards && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionCreatePublicChannel) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create public boards"})
|
||||
return
|
||||
}
|
||||
|
||||
if createsPrivateBoards && !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionCreatePrivateChannel) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create private boards"})
|
||||
return
|
||||
}
|
||||
auditRec := a.makeAuditRecord(r, "createBoardsAndBlocks", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelModify, auditRec)
|
||||
auditRec.AddMeta("teamID", teamID)
|
||||
@ -3499,6 +3718,11 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
|
||||
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)
|
||||
@ -3571,7 +3795,9 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
||||
// 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 {
|
||||
@ -3597,6 +3823,28 @@ func (a *API) handleDeleteBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -46,6 +46,12 @@ func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
vars := mux.Vars(r)
|
||||
boardID := vars["boardID"]
|
||||
userID := getUserID(r)
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
@ -109,6 +115,11 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||
}
|
||||
|
||||
if a.MattermostAuth {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
teamID := vars["teamID"]
|
||||
|
||||
@ -185,6 +196,11 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
teamID := vars["teamID"]
|
||||
|
||||
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||
return
|
||||
}
|
||||
|
||||
file, handle, err := r.FormFile(UploadFormFileKey)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "%v", err)
|
||||
|
@ -170,6 +170,11 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||
}
|
||||
|
||||
if a.MattermostAuth {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(a.singleUserToken) > 0 {
|
||||
// Not permitted in single-user mode
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
||||
@ -235,6 +240,11 @@ func (a *API) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||
}
|
||||
|
||||
if a.MattermostAuth {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(a.singleUserToken) > 0 {
|
||||
// Not permitted in single-user mode
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
||||
@ -288,6 +298,11 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||
}
|
||||
|
||||
if a.MattermostAuth {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(a.singleUserToken) > 0 {
|
||||
// Not permitted in single-user mode
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
||||
@ -390,6 +405,11 @@ func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "not permitted in plugin mode", nil)
|
||||
}
|
||||
|
||||
if a.MattermostAuth {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotImplemented, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(a.singleUserToken) > 0 {
|
||||
// Not permitted in single-user mode
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "not permitted in single-user mode", nil)
|
||||
|
@ -282,35 +282,46 @@ func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UndeleteBlock(blockID string, modifiedBy string) error {
|
||||
func (a *App) GetLastBlockHistoryEntry(blockID string) (*model.Block, error) {
|
||||
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if len(blocks) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &blocks[0], nil
|
||||
}
|
||||
|
||||
func (a *App) UndeleteBlock(blockID string, modifiedBy string) (*model.Block, error) {
|
||||
blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(blocks) == 0 {
|
||||
// undeleting non-existing block not considered an error
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err = a.store.UndeleteBlock(blockID, modifiedBy)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if block == nil {
|
||||
a.logger.Error("Error loading the block after undelete, not propagating through websockets or notifications")
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
board, err := a.store.GetBoard(block.BoardID)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
@ -321,7 +332,7 @@ func (a *App) UndeleteBlock(blockID string, modifiedBy string) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func (a *App) GetBlockCountsByType() (map[string]int64, error) {
|
||||
|
@ -115,7 +115,7 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil)
|
||||
err := th.App.UndeleteBlock("block-id", "user-id-1")
|
||||
_, err := th.App.UndeleteBlock("block-id", "user-id-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@ -129,7 +129,7 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
).Return([]model.Block{block}, nil)
|
||||
th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"})
|
||||
th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil)
|
||||
err := th.App.UndeleteBlock("block-id", "user-id-1")
|
||||
_, err := th.App.UndeleteBlock("block-id", "user-id-1")
|
||||
require.Error(t, err, "error")
|
||||
})
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
var (
|
||||
ErrorCategoryPermissionDenied = errors.New("category doesn't belong to user")
|
||||
ErrorCategoryDeleted = errors.New("category is deleted")
|
||||
ErrorInvalidCategory = errors.New("invalid category")
|
||||
)
|
||||
|
||||
func (a *App) CreateCategory(category *model.Category) (*model.Category, error) {
|
||||
@ -86,6 +87,11 @@ func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category
|
||||
return nil, ErrorCategoryPermissionDenied
|
||||
}
|
||||
|
||||
// verify if category belongs to the team
|
||||
if existingCategory.TeamID != teamID {
|
||||
return nil, ErrorInvalidCategory
|
||||
}
|
||||
|
||||
if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -180,6 +180,10 @@ func (c *Client) GetJoinBoardRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/%s/join", c.GetBoardsRoute(), boardID)
|
||||
}
|
||||
|
||||
func (c *Client) GetLeaveBoardRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/%s/join", c.GetBoardsRoute(), boardID)
|
||||
}
|
||||
|
||||
func (c *Client) GetBlocksRoute(boardID string) string {
|
||||
return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID))
|
||||
}
|
||||
@ -548,6 +552,16 @@ func (c *Client) JoinBoard(boardID string) (*model.BoardMember, *Response) {
|
||||
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) LeaveBoard(boardID string) (*model.BoardMember, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetLeaveBoardRoute(boardID), "")
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BoardMemberFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, *Response) {
|
||||
r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member))
|
||||
if err != nil {
|
||||
|
@ -44,10 +44,12 @@ func TestGetBoards(t *testing.T) {
|
||||
teamID := "0"
|
||||
otherTeamID := "other-team-id"
|
||||
user1 := th.GetUser1()
|
||||
user2 := th.GetUser2()
|
||||
|
||||
board1 := &model.Board{
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
Title: "Board 1",
|
||||
}
|
||||
rBoard1, err := th.Server.App().CreateBoard(board1, user1.ID, true)
|
||||
require.NoError(t, err)
|
||||
@ -56,14 +58,16 @@ func TestGetBoards(t *testing.T) {
|
||||
board2 := &model.Board{
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
Title: "Board 2",
|
||||
}
|
||||
rBoard2, err := th.Server.App().CreateBoard(board2, user1.ID, false)
|
||||
rBoard2, err := th.Server.App().CreateBoard(board2, user2.ID, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rBoard2)
|
||||
|
||||
board3 := &model.Board{
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypePrivate,
|
||||
Title: "Board 3",
|
||||
}
|
||||
rBoard3, err := th.Server.App().CreateBoard(board3, user1.ID, true)
|
||||
require.NoError(t, err)
|
||||
@ -72,35 +76,43 @@ func TestGetBoards(t *testing.T) {
|
||||
board4 := &model.Board{
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypePrivate,
|
||||
Title: "Board 4",
|
||||
}
|
||||
rBoard4, err := th.Server.App().CreateBoard(board4, user1.ID, false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rBoard4)
|
||||
|
||||
board5 := &model.Board{
|
||||
TeamID: teamID,
|
||||
Type: model.BoardTypePrivate,
|
||||
Title: "Board 5",
|
||||
}
|
||||
rBoard5, err := th.Server.App().CreateBoard(board5, user2.ID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rBoard5)
|
||||
|
||||
board6 := &model.Board{
|
||||
TeamID: otherTeamID,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
rBoard5, err := th.Server.App().CreateBoard(board5, user1.ID, true)
|
||||
rBoard6, err := th.Server.App().CreateBoard(board6, user1.ID, true)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, rBoard5)
|
||||
require.NotNil(t, rBoard6)
|
||||
|
||||
boards, resp := th.Client.GetBoardsForTeam(teamID)
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, boards)
|
||||
require.Len(t, boards, 2)
|
||||
|
||||
boardIDs := []string{}
|
||||
for _, board := range boards {
|
||||
boardIDs = append(boardIDs, board.ID)
|
||||
}
|
||||
require.ElementsMatch(t, []string{rBoard1.ID, rBoard3.ID}, boardIDs)
|
||||
require.ElementsMatch(t, []*model.Board{
|
||||
rBoard1,
|
||||
rBoard2,
|
||||
rBoard3,
|
||||
}, boards)
|
||||
|
||||
boardsFromOtherTeam, resp := th.Client.GetBoardsForTeam(otherTeamID)
|
||||
th.CheckOK(resp)
|
||||
require.NotNil(t, boardsFromOtherTeam)
|
||||
require.Len(t, boardsFromOtherTeam, 1)
|
||||
require.Equal(t, rBoard5.ID, boardsFromOtherTeam[0].ID)
|
||||
require.Equal(t, rBoard6.ID, boardsFromOtherTeam[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
@ -815,7 +827,7 @@ func TestDeleteBoard(t *testing.T) {
|
||||
defer th.TearDown()
|
||||
|
||||
success, resp := th.Client.DeleteBoard("non-existing-board")
|
||||
th.CheckForbidden(resp)
|
||||
th.CheckNotFound(resp)
|
||||
require.False(t, success)
|
||||
})
|
||||
|
||||
@ -1023,15 +1035,26 @@ func TestAddMember(t *testing.T) {
|
||||
}
|
||||
|
||||
member, resp := th.Client2.AddMemberToBoard(newMember)
|
||||
th.CheckOK(resp)
|
||||
require.Equal(t, newMember.UserID, member.UserID)
|
||||
require.Equal(t, newMember.BoardID, member.BoardID)
|
||||
require.Equal(t, newMember.SchemeAdmin, member.SchemeAdmin)
|
||||
require.Equal(t, newMember.SchemeEditor, member.SchemeEditor)
|
||||
require.False(t, member.SchemeCommenter)
|
||||
require.False(t, member.SchemeViewer)
|
||||
th.CheckForbidden(resp)
|
||||
require.Nil(t, member)
|
||||
|
||||
members, resp := th.Client.GetMembersForBoard(board.ID)
|
||||
members, resp := th.Client2.GetMembersForBoard(board.ID)
|
||||
th.CheckForbidden(resp)
|
||||
require.Nil(t, members)
|
||||
|
||||
// Join board - will become an editor
|
||||
member, resp = th.Client2.JoinBoard(board.ID)
|
||||
th.CheckOK(resp)
|
||||
require.NoError(t, resp.Error)
|
||||
require.NotNil(t, member)
|
||||
require.Equal(t, board.ID, member.BoardID)
|
||||
require.Equal(t, th.GetUser2().ID, member.UserID)
|
||||
|
||||
member, resp = th.Client2.AddMemberToBoard(newMember)
|
||||
th.CheckForbidden(resp)
|
||||
require.Nil(t, member)
|
||||
|
||||
members, resp = th.Client2.GetMembersForBoard(board.ID)
|
||||
th.CheckOK(resp)
|
||||
require.Len(t, members, 2)
|
||||
})
|
||||
@ -1370,13 +1393,14 @@ func TestDeleteMember(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 2)
|
||||
|
||||
// Should fail - must call leave to leave a board
|
||||
success, resp := th.Client2.DeleteBoardMember(memberToDelete)
|
||||
th.CheckOK(resp)
|
||||
require.True(t, success)
|
||||
th.CheckForbidden(resp)
|
||||
require.False(t, success)
|
||||
|
||||
members, err = th.Server.App().GetMembersForBoard(board.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 1)
|
||||
require.Len(t, members, 2)
|
||||
})
|
||||
|
||||
//nolint:dupl
|
||||
|
@ -517,7 +517,7 @@ func TestPatchBoardsAndBlocks(t *testing.T) {
|
||||
|
||||
userID := th.GetUser1().ID
|
||||
initialTitle := "initial title"
|
||||
newTitle := "new title"
|
||||
newTitle := "new patched title"
|
||||
|
||||
newBoard1 := &model.Board{
|
||||
Title: initialTitle,
|
||||
@ -580,7 +580,7 @@ func TestPatchBoardsAndBlocks(t *testing.T) {
|
||||
|
||||
userID := th.GetUser1().ID
|
||||
initialTitle := "initial title"
|
||||
newTitle := "new title"
|
||||
newTitle := "new other title"
|
||||
|
||||
newBoard1 := &model.Board{
|
||||
Title: initialTitle,
|
||||
|
@ -13,9 +13,11 @@ import (
|
||||
"github.com/mattermost/focalboard/server/server"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/localpermissions"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/mmpermissions"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -27,6 +29,16 @@ const (
|
||||
password = "Pa$$word"
|
||||
)
|
||||
|
||||
const (
|
||||
userAnon string = "anon"
|
||||
userNoTeamMember string = "no-team-member"
|
||||
userTeamMember string = "team-member"
|
||||
userViewer string = "viewer"
|
||||
userCommenter string = "commenter"
|
||||
userEditor string = "editor"
|
||||
userAdmin string = "admin"
|
||||
)
|
||||
|
||||
type LicenseType int
|
||||
|
||||
const (
|
||||
@ -42,6 +54,19 @@ type TestHelper struct {
|
||||
Client2 *client.Client
|
||||
}
|
||||
|
||||
type FakePermissionPluginAPI struct{}
|
||||
|
||||
func (*FakePermissionPluginAPI) LogError(str string, params ...interface{}) {}
|
||||
func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool {
|
||||
if userID == userNoTeamMember {
|
||||
return false
|
||||
}
|
||||
if teamID == "empty-team" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getTestConfig() (*config.Configuration, error) {
|
||||
dbType, connectionString, err := sqlstore.PrepareNewTestDatabase()
|
||||
if err != nil {
|
||||
@ -135,6 +160,42 @@ func newTestServerWithLicense(singleUserToken string, licenseType LicenseType) *
|
||||
return srv
|
||||
}
|
||||
|
||||
func newTestServerPluginMode() *server.Server {
|
||||
cfg, err := getTestConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfg.AuthMode = "mattermost"
|
||||
cfg.EnablePublicSharedBoards = true
|
||||
|
||||
logger, _ := mlog.NewLogger()
|
||||
if err = logger.Configure("", cfg.LoggingCfgJSON, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
innerStore, err := server.NewStore(cfg, logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
db := NewPluginTestStore(innerStore)
|
||||
|
||||
permissionsService := mmpermissions.New(db, &FakePermissionPluginAPI{})
|
||||
|
||||
params := server.Params{
|
||||
Cfg: cfg,
|
||||
DBStore: db,
|
||||
Logger: logger,
|
||||
PermissionsService: permissionsService,
|
||||
}
|
||||
|
||||
srv, err := server.New(params)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
|
||||
sessionToken := "TESTTOKEN"
|
||||
th := &TestHelper{T: t}
|
||||
@ -148,6 +209,13 @@ func SetupTestHelper(t *testing.T) *TestHelper {
|
||||
return SetupTestHelperWithLicense(t, LicenseNone)
|
||||
}
|
||||
|
||||
func SetupTestHelperPluginMode(t *testing.T) *TestHelper {
|
||||
th := &TestHelper{T: t}
|
||||
th.Server = newTestServerPluginMode()
|
||||
th.Start()
|
||||
return th
|
||||
}
|
||||
|
||||
func SetupTestHelperWithLicense(t *testing.T, licenseType LicenseType) *TestHelper {
|
||||
th := &TestHelper{T: t}
|
||||
th.Server = newTestServerWithLicense("", licenseType)
|
||||
|
2148
server/integrationtests/permissions_test.go
Normal file
2148
server/integrationtests/permissions_test.go
Normal file
File diff suppressed because it is too large
Load Diff
198
server/integrationtests/pluginteststore.go
Normal file
198
server/integrationtests/pluginteststore.go
Normal file
@ -0,0 +1,198 @@
|
||||
package integrationtests
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
)
|
||||
|
||||
var errTestStore = errors.New("plugin test store error")
|
||||
|
||||
type PluginTestStore struct {
|
||||
store.Store
|
||||
users map[string]*model.User
|
||||
testTeam *model.Team
|
||||
otherTeam *model.Team
|
||||
emptyTeam *model.Team
|
||||
baseTeam *model.Team
|
||||
}
|
||||
|
||||
func NewPluginTestStore(innerStore store.Store) *PluginTestStore {
|
||||
return &PluginTestStore{
|
||||
Store: innerStore,
|
||||
users: map[string]*model.User{
|
||||
"no-team-member": {
|
||||
ID: "no-team-member",
|
||||
Props: map[string]interface{}{},
|
||||
Username: "no-team-member",
|
||||
Email: "no-team-member@sample.com",
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
},
|
||||
"team-member": {
|
||||
ID: "team-member",
|
||||
Props: map[string]interface{}{},
|
||||
Username: "team-member",
|
||||
Email: "team-member@sample.com",
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
},
|
||||
"viewer": {
|
||||
ID: "viewer",
|
||||
Props: map[string]interface{}{},
|
||||
Username: "viewer",
|
||||
Email: "viewer@sample.com",
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
},
|
||||
"commenter": {
|
||||
ID: "commenter",
|
||||
Props: map[string]interface{}{},
|
||||
Username: "commenter",
|
||||
Email: "commenter@sample.com",
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
},
|
||||
"editor": {
|
||||
ID: "editor",
|
||||
Props: map[string]interface{}{},
|
||||
Username: "editor",
|
||||
Email: "editor@sample.com",
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
},
|
||||
"admin": {
|
||||
ID: "admin",
|
||||
Props: map[string]interface{}{},
|
||||
Username: "admin",
|
||||
Email: "admin@sample.com",
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
},
|
||||
},
|
||||
testTeam: &model.Team{ID: "test-team", Title: "Test Team"},
|
||||
otherTeam: &model.Team{ID: "other-team", Title: "Other Team"},
|
||||
emptyTeam: &model.Team{ID: "empty-team", Title: "Empty Team"},
|
||||
baseTeam: &model.Team{ID: "0", Title: "Base Team"},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) GetTeam(id string) (*model.Team, error) {
|
||||
switch id {
|
||||
case "0":
|
||||
return s.baseTeam, nil
|
||||
case "other-team":
|
||||
return s.otherTeam, nil
|
||||
case "test-team":
|
||||
return s.testTeam, nil
|
||||
case "empty-team":
|
||||
return s.emptyTeam, nil
|
||||
}
|
||||
return nil, errTestStore
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) GetTeamsForUser(userID string) ([]*model.Team, error) {
|
||||
switch userID {
|
||||
case "no-team-member":
|
||||
return []*model.Team{}, nil
|
||||
case "team-member":
|
||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||
case "viewer":
|
||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||
case "commenter":
|
||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||
case "editor":
|
||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||
case "admin":
|
||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||
}
|
||||
return nil, errTestStore
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) GetUserByID(userID string) (*model.User, error) {
|
||||
user := s.users[userID]
|
||||
if user == nil {
|
||||
return nil, errTestStore
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) GetUserByEmail(email string) (*model.User, error) {
|
||||
for _, user := range s.users {
|
||||
if user.Email == email {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
return nil, errTestStore
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) GetUserByUsername(username string) (*model.User, error) {
|
||||
for _, user := range s.users {
|
||||
if user.Username == username {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
return nil, errTestStore
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) PatchUserProps(userID string, patch model.UserPropPatch) error {
|
||||
user, err := s.GetUserByID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
props := user.Props
|
||||
|
||||
for _, key := range patch.DeletedFields {
|
||||
delete(props, key)
|
||||
}
|
||||
|
||||
for key, value := range patch.UpdatedFields {
|
||||
props[key] = value
|
||||
}
|
||||
|
||||
user.Props = props
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
||||
switch {
|
||||
case teamID == s.testTeam.ID:
|
||||
return []*model.User{
|
||||
s.users["team-member"],
|
||||
s.users["viewer"],
|
||||
s.users["commenter"],
|
||||
s.users["editor"],
|
||||
s.users["admin"],
|
||||
}, nil
|
||||
case teamID == s.otherTeam.ID:
|
||||
return []*model.User{
|
||||
s.users["team-member"],
|
||||
s.users["viewer"],
|
||||
s.users["commenter"],
|
||||
s.users["editor"],
|
||||
s.users["admin"],
|
||||
}, nil
|
||||
case teamID == s.emptyTeam.ID:
|
||||
return []*model.User{}, nil
|
||||
}
|
||||
return nil, errTestStore
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
|
||||
users := []*model.User{}
|
||||
teamUsers, err := s.GetUsersByTeam(teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range teamUsers {
|
||||
if strings.Contains(user.Username, searchQuery) {
|
||||
users = append(users, user)
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
}
|
@ -11,15 +11,19 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/permissions"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin"
|
||||
)
|
||||
|
||||
type APIInterface interface {
|
||||
HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool
|
||||
LogError(string, ...interface{})
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
store permissions.Store
|
||||
api plugin.API
|
||||
api APIInterface
|
||||
}
|
||||
|
||||
func New(store permissions.Store, api plugin.API) *Service {
|
||||
func New(store permissions.Store, api APIInterface) *Service {
|
||||
return &Service{
|
||||
store: store,
|
||||
api: api,
|
||||
|
@ -241,11 +241,18 @@ func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, err
|
||||
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string) ([]*model.Board, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(boardFields("b.")...).
|
||||
Distinct().
|
||||
From(s.tablePrefix + "boards as b").
|
||||
Join(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
||||
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
||||
Where(sq.Eq{"b.team_id": teamID}).
|
||||
Where(sq.Eq{"bm.user_id": userID}).
|
||||
Where(sq.Eq{"b.is_template": false})
|
||||
Where(sq.Eq{"b.is_template": false}).
|
||||
Where(sq.Or{
|
||||
sq.Eq{"b.type": model.BoardTypeOpen},
|
||||
sq.And{
|
||||
sq.Eq{"b.type": model.BoardTypePrivate},
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
},
|
||||
})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
|
@ -6,7 +6,7 @@ CREATE TABLE {{.prefix}}categories (
|
||||
user_id varchar(32) NOT NULL,
|
||||
team_id varchar(32) NOT NULL,
|
||||
{{if not .sqlite}}
|
||||
channel_id varchar(32) NOT NULL,
|
||||
channel_id varchar(32) NOT NULL,
|
||||
{{end}}
|
||||
create_at BIGINT,
|
||||
update_at BIGINT,
|
||||
|
@ -0,0 +1,18 @@
|
||||
{{if .sqlite}}
|
||||
ALTER TABLE {{.prefix}}categories DROP COLUMN channel_id VARCHAR(36);
|
||||
{{end}}
|
||||
|
||||
{{if .mysql}}
|
||||
ALTER TABLE {{.prefix}}categories MODIFY COLUMN channel_id VARCHAR(36) NOT NULL;
|
||||
ALTER TABLE {{.prefix}}categories MODIFY COLUMN id INT NOT NULL UNIQUE AUTO_INCREMENT;
|
||||
ALTER TABLE {{.prefix}}category_blocks MODIFY COLUMN id INT NOT NULL UNIQUE AUTO_INCREMENT;
|
||||
{{end}}
|
||||
|
||||
{{if .postgres}}
|
||||
ALTER TABLE {{.prefix}}categories ALTER COLUMN id DROP NOT NULL;
|
||||
ALTER TABLE {{.prefix}}categories ALTER COLUMN id TYPE SERIAL;
|
||||
ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id DROP NOT NULL;
|
||||
ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id TYPE SERIAL;
|
||||
ALTER TABLE {{.prefix}}categories ALTER COLUMN channel_id TYPE VARCHAR(32);
|
||||
ALTER TABLE {{.prefix}}categories ALTER COLUMN channel_id SET NOT NULL;
|
||||
{{end}}
|
@ -0,0 +1,18 @@
|
||||
{{if .sqlite}}
|
||||
ALTER TABLE {{.prefix}}categories ADD COLUMN channel_id VARCHAR(36);
|
||||
{{end}}
|
||||
|
||||
{{if .mysql}}
|
||||
ALTER TABLE {{.prefix}}categories MODIFY COLUMN id VARCHAR(36) NOT NULL;
|
||||
ALTER TABLE {{.prefix}}category_blocks MODIFY COLUMN id VARCHAR(36) NOT NULL;
|
||||
ALTER TABLE {{.prefix}}categories MODIFY COLUMN channel_id VARCHAR(36);
|
||||
{{end}}
|
||||
|
||||
{{if .postgres}}
|
||||
ALTER TABLE {{.prefix}}categories ALTER COLUMN id TYPE VARCHAR(36);
|
||||
ALTER TABLE {{.prefix}}categories ALTER COLUMN id SET NOT NULL;
|
||||
ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id TYPE VARCHAR(36);
|
||||
ALTER TABLE {{.prefix}}category_blocks ALTER COLUMN id SET NOT NULL;
|
||||
ALTER TABLE {{.prefix}}categories ALTER COLUMN channel_id TYPE VARCHAR(36);
|
||||
ALTER TABLE {{.prefix}}categories ALTER COLUMN channel_id DROP NOT NULL;
|
||||
{{end}}
|
@ -120,7 +120,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
||||
TeamID: teamID1,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
_, _, err := store.InsertBoardWithAdmin(board1, userID)
|
||||
rBoard1, _, err := store.InsertBoardWithAdmin(board1, userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
board2 := &model.Board{
|
||||
@ -128,7 +128,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
||||
TeamID: teamID1,
|
||||
Type: model.BoardTypePrivate,
|
||||
}
|
||||
_, _, err = store.InsertBoardWithAdmin(board2, userID)
|
||||
rBoard2, _, err := store.InsertBoardWithAdmin(board2, userID)
|
||||
require.NoError(t, err)
|
||||
|
||||
board3 := &model.Board{
|
||||
@ -136,7 +136,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
||||
TeamID: teamID1,
|
||||
Type: model.BoardTypeOpen,
|
||||
}
|
||||
_, err = store.InsertBoard(board3, "other-user")
|
||||
rBoard3, err := store.InsertBoard(board3, "other-user")
|
||||
require.NoError(t, err)
|
||||
|
||||
board4 := &model.Board{
|
||||
@ -164,16 +164,14 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
||||
_, err = store.InsertBoard(board6, "other-user")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should only find the two boards that the user is a member of for team 1", func(t *testing.T) {
|
||||
t.Run("should only find the two boards that the user is a member of for team 1 plus the one open board", func(t *testing.T) {
|
||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, boards, 2)
|
||||
|
||||
boardIDs := []string{}
|
||||
for _, board := range boards {
|
||||
boardIDs = append(boardIDs, board.ID)
|
||||
}
|
||||
require.ElementsMatch(t, []string{board1.ID, board2.ID}, boardIDs)
|
||||
require.ElementsMatch(t, []*model.Board{
|
||||
rBoard1,
|
||||
rBoard2,
|
||||
rBoard3,
|
||||
}, boards)
|
||||
})
|
||||
|
||||
t.Run("should only find the board that the user is a member of for team 2", func(t *testing.T) {
|
||||
|
@ -260,6 +260,32 @@ definitions:
|
||||
- schemeViewer
|
||||
type: object
|
||||
x-go-package: github.com/mattermost/focalboard/server/model
|
||||
BoardMemberHistoryEntry:
|
||||
description: BoardMemberHistoryEntry stores the information of the membership of a user on a board
|
||||
properties:
|
||||
action:
|
||||
description: The action that added this history entry (created or deleted)
|
||||
type: string
|
||||
x-go-name: Action
|
||||
boardId:
|
||||
description: The ID of the board
|
||||
type: string
|
||||
x-go-name: BoardID
|
||||
insertAt:
|
||||
description: The insertion time
|
||||
format: int64
|
||||
type: integer
|
||||
x-go-name: InsertAt
|
||||
userId:
|
||||
description: The ID of the user
|
||||
type: string
|
||||
x-go-name: UserID
|
||||
required:
|
||||
- boardId
|
||||
- userId
|
||||
- insertAt
|
||||
type: object
|
||||
x-go-package: github.com/mattermost/focalboard/server/model
|
||||
BoardMetadata:
|
||||
description: BoardMetadata contains metadata for a Board
|
||||
properties:
|
||||
@ -882,6 +908,8 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -904,6 +932,8 @@ paths:
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/Board'
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -932,6 +962,8 @@ paths:
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/Board'
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -959,34 +991,6 @@ paths:
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Exports an archive of all blocks for one boards.
|
||||
/api/v1/boards/{boardID}/archive/import:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
operationId: archiveImport
|
||||
parameters:
|
||||
- description: Workspace ID
|
||||
in: path
|
||||
name: boardID
|
||||
required: true
|
||||
type: string
|
||||
- description: archive file to import
|
||||
in: formData
|
||||
name: file
|
||||
required: true
|
||||
type: file
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Import an archive of boards.
|
||||
/api/v1/boards/{boardID}/blocks:
|
||||
get:
|
||||
description: Returns blocks
|
||||
@ -1014,6 +1018,8 @@ paths:
|
||||
items:
|
||||
$ref: '#/definitions/Block'
|
||||
type: array
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -1102,6 +1108,8 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
"404":
|
||||
description: block not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -1133,6 +1141,8 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
"404":
|
||||
description: block not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -1163,6 +1173,8 @@ paths:
|
||||
items:
|
||||
$ref: '#/definitions/Block'
|
||||
type: array
|
||||
"404":
|
||||
description: board or block not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -1205,6 +1217,36 @@ paths:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/api/v1/boards/{boardID}/blocks/{blockID}/undelete:
|
||||
post:
|
||||
description: Undeletes a block
|
||||
operationId: undeleteBlock
|
||||
parameters:
|
||||
- description: Board ID
|
||||
in: path
|
||||
name: boardID
|
||||
required: true
|
||||
type: string
|
||||
- description: ID of block to undelete
|
||||
in: path
|
||||
name: blockID
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/BlockPatch'
|
||||
"404":
|
||||
description: block not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/api/v1/boards/{boardID}/blocks/export:
|
||||
get:
|
||||
description: Returns all blocks of a board
|
||||
@ -1276,6 +1318,8 @@ paths:
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/BoardsAndBlocks'
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -1327,6 +1371,8 @@ paths:
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -1377,6 +1423,8 @@ paths:
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/Sharing'
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -1633,6 +1681,34 @@ paths:
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Exports an archive of all blocks for all the boards in a team.
|
||||
/api/v1/teams/{teamID}/archive/import:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
operationId: archiveImport
|
||||
parameters:
|
||||
- description: Team ID
|
||||
in: path
|
||||
name: teamID
|
||||
required: true
|
||||
type: string
|
||||
- description: archive file to import
|
||||
in: formData
|
||||
name: file
|
||||
required: true
|
||||
type: file
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: Import an archive of boards.
|
||||
/api/v1/teams/{teamID}/boards:
|
||||
get:
|
||||
description: Returns team boards
|
||||
@ -1686,6 +1762,8 @@ paths:
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/FileUploadResponse'
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -1697,11 +1775,6 @@ paths:
|
||||
description: Returns the boards that match with a search term
|
||||
operationId: searchBoards
|
||||
parameters:
|
||||
- description: Board ID
|
||||
in: path
|
||||
name: boardID
|
||||
required: true
|
||||
type: string
|
||||
- description: Team ID
|
||||
in: path
|
||||
name: teamID
|
||||
@ -1919,66 +1992,6 @@ paths:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/api/v1/workspaces/{workspaceID}/blocks/{blockID}/undelete:
|
||||
post:
|
||||
description: Undeletes a block
|
||||
operationId: undeleteBlock
|
||||
parameters:
|
||||
- description: Workspace ID
|
||||
in: path
|
||||
name: workspaceID
|
||||
required: true
|
||||
type: string
|
||||
- description: ID of block to undelete
|
||||
in: path
|
||||
name: blockID
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/boards/{boardID}/{rootID}/{fileID}:
|
||||
get:
|
||||
description: Returns the contents of an uploaded file
|
||||
operationId: getFile
|
||||
parameters:
|
||||
- description: Board ID
|
||||
in: path
|
||||
name: boardID
|
||||
required: true
|
||||
type: string
|
||||
- description: ID of the root block
|
||||
in: path
|
||||
name: rootID
|
||||
required: true
|
||||
type: string
|
||||
- description: ID of the file
|
||||
in: path
|
||||
name: fileID
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
- image/jpg
|
||||
- image/png
|
||||
- image/gif
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/boards/{boardID}/join:
|
||||
post:
|
||||
description: Become a member of a board
|
||||
@ -1996,10 +2009,35 @@ paths:
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/BoardMember'
|
||||
"403":
|
||||
description: access denied
|
||||
"404":
|
||||
description: board not found
|
||||
"503":
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
$ref: '#/definitions/ErrorResponse'
|
||||
security:
|
||||
- BearerAuth: []
|
||||
/boards/{boardID}/leave:
|
||||
post:
|
||||
description: Remove your own membership from a board
|
||||
operationId: leaveBoard
|
||||
parameters:
|
||||
- description: Board ID
|
||||
in: path
|
||||
name: boardID
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: success
|
||||
"403":
|
||||
description: access denied
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
@ -2029,6 +2067,8 @@ paths:
|
||||
description: success
|
||||
schema:
|
||||
$ref: '#/definitions/BoardMember'
|
||||
"404":
|
||||
description: board not found
|
||||
default:
|
||||
description: internal error
|
||||
schema:
|
||||
|
Loading…
Reference in New Issue
Block a user