From a4ef8ec6bcc85503416935521ed0e26526d33614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 5 Apr 2022 17:00:04 +0200 Subject: [PATCH] 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 5c98fd76a32f1a7ef6a6258497ec7ac64e034640. * 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 Co-authored-by: Chen-I Lim <46905241+chenilim@users.noreply.github.com> --- server/api/api.go | 432 +++- server/api/archive.go | 16 + server/api/auth.go | 20 + server/app/blocks.go | 27 +- server/app/blocks_test.go | 4 +- server/app/category.go | 6 + server/client/client.go | 14 + server/integrationtests/board_test.go | 70 +- .../boards_and_blocks_test.go | 4 +- server/integrationtests/clienttestlib.go | 68 + server/integrationtests/permissions_test.go | 2148 +++++++++++++++++ server/integrationtests/pluginteststore.go | 198 ++ .../mmpermissions/mmpermissions.go | 10 +- server/services/store/sqlstore/board.go | 13 +- .../000018_populate_categories.up.sql | 2 +- .../migrations/000021_fix_categories.down.sql | 18 + .../migrations/000021_fix_categories.up.sql | 18 + server/services/store/storetests/boards.go | 20 +- server/swagger/swagger.yml | 228 +- 19 files changed, 3077 insertions(+), 239 deletions(-) create mode 100644 server/integrationtests/permissions_test.go create mode 100644 server/integrationtests/pluginteststore.go create mode 100644 server/services/store/sqlstore/migrations/000021_fix_categories.down.sql create mode 100644 server/services/store/sqlstore/migrations/000021_fix_categories.up.sql diff --git a/server/api/api.go b/server/api/api.go index 4b18a93b6..a02ed5d0b 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -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 diff --git a/server/api/archive.go b/server/api/archive.go index dd5e25bf3..25549a4d5 100644 --- a/server/api/archive.go +++ b/server/api/archive.go @@ -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) diff --git a/server/api/auth.go b/server/api/auth.go index b1b7e6ec5..a23efcc75 100644 --- a/server/api/auth.go +++ b/server/api/auth.go @@ -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) diff --git a/server/app/blocks.go b/server/app/blocks.go index 32c168aab..552c7f01b 100644 --- a/server/app/blocks.go +++ b/server/app/blocks.go @@ -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) { diff --git a/server/app/blocks_test.go b/server/app/blocks_test.go index b1f0f582c..e86057a7f 100644 --- a/server/app/blocks_test.go +++ b/server/app/blocks_test.go @@ -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") }) } diff --git a/server/app/category.go b/server/app/category.go index b80cf9f33..b571b138e 100644 --- a/server/app/category.go +++ b/server/app/category.go @@ -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 } diff --git a/server/client/client.go b/server/client/client.go index 2d5547c7f..a3094e621 100644 --- a/server/client/client.go +++ b/server/client/client.go @@ -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 { diff --git a/server/integrationtests/board_test.go b/server/integrationtests/board_test.go index dd593b686..32da490f5 100644 --- a/server/integrationtests/board_test.go +++ b/server/integrationtests/board_test.go @@ -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 diff --git a/server/integrationtests/boards_and_blocks_test.go b/server/integrationtests/boards_and_blocks_test.go index bd23161fd..f0079dd5d 100644 --- a/server/integrationtests/boards_and_blocks_test.go +++ b/server/integrationtests/boards_and_blocks_test.go @@ -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, diff --git a/server/integrationtests/clienttestlib.go b/server/integrationtests/clienttestlib.go index 3744f606a..3fb93432d 100644 --- a/server/integrationtests/clienttestlib.go +++ b/server/integrationtests/clienttestlib.go @@ -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) diff --git a/server/integrationtests/permissions_test.go b/server/integrationtests/permissions_test.go new file mode 100644 index 000000000..369337f54 --- /dev/null +++ b/server/integrationtests/permissions_test.go @@ -0,0 +1,2148 @@ +//nolint:dupl +package integrationtests + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/mattermost/focalboard/server/client" + "github.com/mattermost/focalboard/server/model" + "github.com/stretchr/testify/require" +) + +type Clients struct { + Anon *client.Client + NoTeamMember *client.Client + TeamMember *client.Client + Viewer *client.Client + Commenter *client.Client + Editor *client.Client + Admin *client.Client +} + +const ( + methodPost = "POST" + methodGet = "GET" + methodPut = "PUT" + methodDelete = "DELETE" + methodPatch = "PATCH" +) + +type TestCase struct { + url string + method string + body string + userRole string // userAnon, userNoTeamMember, userTeamMember, userViewer, userCommenter, userEditor or userAdmin + expectedStatusCode int + totalResults int +} + +func setupClients(th *TestHelper) Clients { + // user1 + clients := Clients{ + Anon: client.NewClient(th.Server.Config().ServerRoot, ""), + NoTeamMember: client.NewClient(th.Server.Config().ServerRoot, ""), + TeamMember: client.NewClient(th.Server.Config().ServerRoot, ""), + Viewer: client.NewClient(th.Server.Config().ServerRoot, ""), + Commenter: client.NewClient(th.Server.Config().ServerRoot, ""), + Editor: client.NewClient(th.Server.Config().ServerRoot, ""), + Admin: client.NewClient(th.Server.Config().ServerRoot, ""), + } + + clients.NoTeamMember.HTTPHeader["Mattermost-User-Id"] = userNoTeamMember + clients.TeamMember.HTTPHeader["Mattermost-User-Id"] = userTeamMember + clients.Viewer.HTTPHeader["Mattermost-User-Id"] = userViewer + clients.Commenter.HTTPHeader["Mattermost-User-Id"] = userCommenter + clients.Editor.HTTPHeader["Mattermost-User-Id"] = userEditor + clients.Admin.HTTPHeader["Mattermost-User-Id"] = userAdmin + + return clients +} + +func toJSON(t *testing.T, obj interface{}) string { + result, err := json.Marshal(obj) + require.NoError(t, err) + return string(result) +} + +type TestData struct { + publicBoard *model.Board + privateBoard *model.Board + publicTemplate *model.Board + privateTemplate *model.Board +} + +func setupData(t *testing.T, th *TestHelper) TestData { + customTemplate1, err := th.Server.App().CreateBoard(&model.Board{Title: "Custom template 1", TeamID: "test-team", IsTemplate: true, Type: model.BoardTypeOpen}, userAdmin, true) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-1", Title: "Test", Type: "card", BoardID: customTemplate1.ID}, userAdmin) + require.NoError(t, err) + customTemplate2, err := th.Server.App().CreateBoard(&model.Board{Title: "Custom template 2", TeamID: "test-team", IsTemplate: true, Type: model.BoardTypePrivate}, userAdmin, true) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-2", Title: "Test", Type: "card", BoardID: customTemplate2.ID}, userAdmin) + require.NoError(t, err) + + board1, err := th.Server.App().CreateBoard(&model.Board{Title: "Board 1", TeamID: "test-team", Type: model.BoardTypeOpen}, userAdmin, true) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-3", Title: "Test", Type: "card", BoardID: board1.ID}, userAdmin) + require.NoError(t, err) + board2, err := th.Server.App().CreateBoard(&model.Board{Title: "Board 2", TeamID: "test-team", Type: model.BoardTypePrivate}, userAdmin, true) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-4", Title: "Test", Type: "card", BoardID: board2.ID}, userAdmin) + require.NoError(t, err) + + err = th.Server.App().UpsertSharing(model.Sharing{ID: board2.ID, Enabled: true, Token: "valid", ModifiedBy: userAdmin, UpdateAt: model.GetMillis()}) + require.NoError(t, err) + + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate1.ID, UserID: userViewer, SchemeViewer: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate2.ID, UserID: userViewer, SchemeViewer: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate1.ID, UserID: userCommenter, SchemeCommenter: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate2.ID, UserID: userCommenter, SchemeCommenter: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate1.ID, UserID: userEditor, SchemeEditor: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate2.ID, UserID: userEditor, SchemeEditor: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate1.ID, UserID: userAdmin, SchemeAdmin: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: customTemplate2.ID, UserID: userAdmin, SchemeAdmin: true}) + require.NoError(t, err) + + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board1.ID, UserID: userViewer, SchemeViewer: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userViewer, SchemeViewer: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board1.ID, UserID: userCommenter, SchemeCommenter: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userCommenter, SchemeCommenter: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board1.ID, UserID: userEditor, SchemeEditor: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userEditor, SchemeEditor: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board1.ID, UserID: userAdmin, SchemeAdmin: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: board2.ID, UserID: userAdmin, SchemeAdmin: true}) + require.NoError(t, err) + + return TestData{ + publicBoard: board1, + privateBoard: board2, + publicTemplate: customTemplate1, + privateTemplate: customTemplate2, + } +} + +func runTestCases(t *testing.T, ttCases []TestCase, testData TestData, clients Clients) { + for _, tc := range ttCases { + t.Run(tc.userRole+": "+tc.method+" "+tc.url, func(t *testing.T) { + reqClient := clients.Anon + switch tc.userRole { + case userAnon: + reqClient = clients.Anon + case userNoTeamMember: + reqClient = clients.NoTeamMember + case userTeamMember: + reqClient = clients.TeamMember + case userViewer: + reqClient = clients.Viewer + case userCommenter: + reqClient = clients.Commenter + case userEditor: + reqClient = clients.Editor + case userAdmin: + reqClient = clients.Admin + } + + url := strings.ReplaceAll(tc.url, "{PRIVATE_BOARD_ID}", testData.privateBoard.ID) + url = strings.ReplaceAll(url, "{PUBLIC_BOARD_ID}", testData.publicBoard.ID) + url = strings.ReplaceAll(url, "{PUBLIC_TEMPLATE_ID}", testData.publicTemplate.ID) + url = strings.ReplaceAll(url, "{PRIVATE_TEMPLATE_ID}", testData.privateTemplate.ID) + + var response *http.Response + var err error + switch tc.method { + case methodGet: + response, err = reqClient.DoAPIGet(url, "") + defer response.Body.Close() + case methodPost: + response, err = reqClient.DoAPIPost(url, tc.body) + defer response.Body.Close() + case methodPatch: + response, err = reqClient.DoAPIPatch(url, tc.body) + defer response.Body.Close() + case methodPut: + response, err = reqClient.DoAPIPut(url, tc.body) + defer response.Body.Close() + case methodDelete: + response, err = reqClient.DoAPIDelete(url, tc.body) + defer response.Body.Close() + } + + require.Equal(t, tc.expectedStatusCode, response.StatusCode) + if tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 { + require.NoError(t, err) + } + if tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + require.Fail(t, err.Error()) + } + if strings.HasPrefix(string(body), "[") { + var data []interface{} + err = json.Unmarshal(body, &data) + if err != nil { + require.Fail(t, err.Error()) + } + require.Len(t, data, tc.totalResults) + } else { + if tc.totalResults > 0 { + require.Equal(t, 1, tc.totalResults) + require.Greater(t, len(string(body)), 2) + } else { + require.Len(t, string(body), 2) + } + } + } + }) + } +} + +func TestPermissionsGetTeamBoards(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team/boards", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/boards", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/boards", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/teams/test-team/boards", methodGet, "", userViewer, http.StatusOK, 2}, + {"/teams/test-team/boards", methodGet, "", userCommenter, http.StatusOK, 2}, + {"/teams/test-team/boards", methodGet, "", userEditor, http.StatusOK, 2}, + {"/teams/test-team/boards", methodGet, "", userAdmin, http.StatusOK, 2}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsSearchTeamBoards(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + // Search boards + {"/teams/test-team/boards/search?q=b", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/boards/search?q=b", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/boards/search?q=b", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/teams/test-team/boards/search?q=b", methodGet, "", userViewer, http.StatusOK, 2}, + {"/teams/test-team/boards/search?q=b", methodGet, "", userCommenter, http.StatusOK, 2}, + {"/teams/test-team/boards/search?q=b", methodGet, "", userEditor, http.StatusOK, 2}, + {"/teams/test-team/boards/search?q=b", methodGet, "", userAdmin, http.StatusOK, 2}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetTeamTemplates(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + builtInTemplateCount := 7 + + ttCases := []TestCase{ + // Get Team Boards + {"/teams/test-team/templates", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/templates", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/templates", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/teams/test-team/templates", methodGet, "", userViewer, http.StatusOK, 2}, + {"/teams/test-team/templates", methodGet, "", userCommenter, http.StatusOK, 2}, + {"/teams/test-team/templates", methodGet, "", userEditor, http.StatusOK, 2}, + {"/teams/test-team/templates", methodGet, "", userAdmin, http.StatusOK, 2}, + // Built-in templates + {"/teams/0/templates", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/0/templates", methodGet, "", userNoTeamMember, http.StatusOK, builtInTemplateCount}, + {"/teams/0/templates", methodGet, "", userTeamMember, http.StatusOK, builtInTemplateCount}, + {"/teams/0/templates", methodGet, "", userViewer, http.StatusOK, builtInTemplateCount}, + {"/teams/0/templates", methodGet, "", userCommenter, http.StatusOK, builtInTemplateCount}, + {"/teams/0/templates", methodGet, "", userEditor, http.StatusOK, builtInTemplateCount}, + {"/teams/0/templates", methodGet, "", userAdmin, http.StatusOK, builtInTemplateCount}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsCreateBoard(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + publicBoard := toJSON(t, model.Board{Title: "Board To Create", TeamID: "test-team", Type: model.BoardTypeOpen}) + privateBoard := toJSON(t, model.Board{Title: "Board To Create", TeamID: "test-team", Type: model.BoardTypeOpen}) + + ttCases := []TestCase{ + // Create Public boards + {"/boards", methodPost, publicBoard, userAnon, http.StatusUnauthorized, 0}, + {"/boards", methodPost, publicBoard, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards", methodPost, publicBoard, userTeamMember, http.StatusOK, 1}, + + // Create private boards + {"/boards", methodPost, privateBoard, userAnon, http.StatusUnauthorized, 0}, + {"/boards", methodPost, privateBoard, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards", methodPost, privateBoard, userTeamMember, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetBoard(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_BOARD_ID}?read_token=invalid", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}?read_token=valid", methodGet, "", userAnon, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}?read_token=invalid", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}?read_token=valid", methodGet, "", userTeamMember, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsPatchBoard(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"title\": \"test\"}", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"title\": \"test\"}", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsDeleteBoard(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}", methodDelete, "", userAdmin, http.StatusOK, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsDuplicateBoard(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + // In same team + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userTeamMember, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) + + // In other team + ttCases = []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/duplicate?toTeam=other-team", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userTeamMember, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/duplicate?toTeam=other-team", methodPost, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetBoardBlocks(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_BOARD_ID}/blocks?read_token=invalid", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks?read_token=valid", methodGet, "", userAnon, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/blocks?read_token=invalid", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks?read_token=valid", methodGet, "", userTeamMember, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsCreateBoardBlocks(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + counter := 0 + newBlockJSON := func(boardID string) string { + counter++ + return toJSON(t, []*model.Block{{ + ID: fmt.Sprintf("%d", counter), + Title: "Board To Create", + BoardID: boardID, + Type: "card", + CreateAt: model.GetMillis(), + UpdateAt: model.GetMillis(), + }}) + } + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPost, newBlockJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPost, newBlockJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsPatchBoardBlocks(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + counter := 0 + newBlocksPatchJSON := func(blockID string) string { + counter++ + newTitle := "New Title" + return toJSON(t, model.BlockPatchBatch{ + BlockIDs: []string{blockID}, + BlockPatches: []model.BlockPatch{ + {Title: &newTitle}, + }, + }) + } + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userEditor, http.StatusOK, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-4"), userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userEditor, http.StatusOK, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks", methodPatch, newBlocksPatchJSON("block-3"), userAdmin, http.StatusOK, 0}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userEditor, http.StatusOK, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-2"), userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userEditor, http.StatusOK, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks", methodPatch, newBlocksPatchJSON("block-1"), userAdmin, http.StatusOK, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsPatchBoardBlock(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + newTitle := "New Title" + patchJSON := toJSON(t, model.BlockPatch{Title: &newTitle}) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userEditor, http.StatusOK, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodPatch, patchJSON, userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userEditor, http.StatusOK, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodPatch, patchJSON, userAdmin, http.StatusOK, 0}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userEditor, http.StatusOK, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodPatch, patchJSON, userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userEditor, http.StatusOK, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodPatch, patchJSON, userAdmin, http.StatusOK, 0}, + + // Invalid boardID/blockID combination + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3", methodPatch, patchJSON, userAdmin, http.StatusNotFound, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsDeleteBoardBlock(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + err := th.Server.App().InsertBlock(model.Block{ID: "block-5", Title: "Test", Type: "card", BoardID: testData.publicTemplate.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-6", Title: "Test", Type: "card", BoardID: testData.privateTemplate.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-7", Title: "Test", Type: "card", BoardID: testData.publicBoard.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-8", Title: "Test", Type: "card", BoardID: testData.privateBoard.ID}, userAdmin) + require.NoError(t, err) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4", methodDelete, "", userEditor, http.StatusOK, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-8", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3", methodDelete, "", userEditor, http.StatusOK, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-7", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2", methodDelete, "", userEditor, http.StatusOK, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1", methodDelete, "", userEditor, http.StatusOK, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5", methodDelete, "", userAdmin, http.StatusOK, 0}, + + // Invalid boardID/blockID combination + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3", methodDelete, "", userAdmin, http.StatusNotFound, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsUndeleteBoardBlock(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + err := th.Server.App().InsertBlock(model.Block{ID: "block-5", Title: "Test", Type: "card", BoardID: testData.publicTemplate.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-6", Title: "Test", Type: "card", BoardID: testData.privateTemplate.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-7", Title: "Test", Type: "card", BoardID: testData.publicBoard.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-8", Title: "Test", Type: "card", BoardID: testData.privateBoard.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().DeleteBlock("block-1", userAdmin) + require.NoError(t, err) + err = th.Server.App().DeleteBlock("block-2", userAdmin) + require.NoError(t, err) + err = th.Server.App().DeleteBlock("block-3", userAdmin) + require.NoError(t, err) + err = th.Server.App().DeleteBlock("block-4", userAdmin) + require.NoError(t, err) + err = th.Server.App().DeleteBlock("block-5", userAdmin) + require.NoError(t, err) + err = th.Server.App().DeleteBlock("block-6", userAdmin) + require.NoError(t, err) + err = th.Server.App().DeleteBlock("block-7", userAdmin) + require.NoError(t, err) + err = th.Server.App().DeleteBlock("block-8", userAdmin) + require.NoError(t, err) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/undelete", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/undelete", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/undelete", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/undelete", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/undelete", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/undelete", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/undelete", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/undelete", methodPost, "", userAdmin, http.StatusOK, 1}, + + // Invalid boardID/blockID combination + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/undelete", methodPost, "", userAdmin, http.StatusNotFound, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsDuplicateBoardBlock(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + err := th.Server.App().InsertBlock(model.Block{ID: "block-5", Title: "Test", Type: "card", BoardID: testData.publicTemplate.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-6", Title: "Test", Type: "card", BoardID: testData.privateTemplate.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-7", Title: "Test", Type: "card", BoardID: testData.publicBoard.ID}, userAdmin) + require.NoError(t, err) + err = th.Server.App().InsertBlock(model.Block{ID: "block-8", Title: "Test", Type: "card", BoardID: testData.privateBoard.ID}, userAdmin) + require.NoError(t, err) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, + + // Invalid boardID/blockID combination + {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusNotFound, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetBoardMembers(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userViewer, http.StatusOK, 4}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userCommenter, http.StatusOK, 4}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userEditor, http.StatusOK, 4}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodGet, "", userAdmin, http.StatusOK, 4}, + + {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userViewer, http.StatusOK, 4}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userCommenter, http.StatusOK, 4}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userEditor, http.StatusOK, 4}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodGet, "", userAdmin, http.StatusOK, 4}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userViewer, http.StatusOK, 4}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userCommenter, http.StatusOK, 4}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userEditor, http.StatusOK, 4}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodGet, "", userAdmin, http.StatusOK, 4}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userViewer, http.StatusOK, 4}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userCommenter, http.StatusOK, 4}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userEditor, http.StatusOK, 4}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodGet, "", userAdmin, http.StatusOK, 4}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsCreateBoardMembers(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + boardMemberJSON := func(boardID string) string { + return toJSON(t, model.BoardMember{ + BoardID: boardID, + UserID: userTeamMember, + SchemeEditor: true, + }) + } + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members", methodPost, boardMemberJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members", methodPost, boardMemberJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members", methodPost, boardMemberJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsUpdateBoardMember(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + boardMemberJSON := func(boardID string) string { + return toJSON(t, model.BoardMember{ + BoardID: boardID, + UserID: userTeamMember, + SchemeEditor: false, + SchemeViewer: true, + }) + } + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateBoard.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateBoard.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateBoard.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateBoard.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateBoard.ID), userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateBoard.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicBoard.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicBoard.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicBoard.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicBoard.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicBoard.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicBoard.ID), userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicBoard.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateTemplate.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateTemplate.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateTemplate.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateTemplate.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateTemplate.ID), userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.privateTemplate.ID), userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicTemplate.ID), userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicTemplate.ID), userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicTemplate.ID), userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicTemplate.ID), userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicTemplate.ID), userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicTemplate.ID), userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/viewer", methodPut, boardMemberJSON(testData.publicTemplate.ID), userAdmin, http.StatusOK, 1}, + + // Invalid boardID/memberID combination + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodPut, "", userAdmin, http.StatusBadRequest, 0}, + + // Invalid boardID + {"/boards/invalid/members/viewer", methodPut, "", userAdmin, http.StatusBadRequest, 0}, + + // Invalid memberID + {"/boards/{PUBLIC_TEMPLATE_ID}/members/invalid", methodPut, "", userAdmin, http.StatusBadRequest, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsDeleteBoardMember(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + _, err := th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicBoard.ID, UserID: userTeamMember, SchemeViewer: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateBoard.ID, UserID: userTeamMember, SchemeViewer: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicTemplate.ID, UserID: userTeamMember, SchemeViewer: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateTemplate.ID, UserID: userTeamMember, SchemeViewer: true}) + require.NoError(t, err) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/members/team-member", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/team-member", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/team-member", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/team-member", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/team-member", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/team-member", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/members/team-member", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_BOARD_ID}/members/team-member", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/team-member", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/team-member", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/team-member", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/team-member", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/team-member", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/members/team-member", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/members/team-member", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/team-member", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/team-member", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/team-member", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/team-member", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/team-member", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/members/team-member", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodDelete, "", userAdmin, http.StatusOK, 0}, + + // Invalid boardID/memberID combination + {"/boards/{PUBLIC_TEMPLATE_ID}/members/team-member", methodDelete, "", userAdmin, http.StatusOK, 0}, + + // Invalid boardID + {"/boards/invalid/members/viewer", methodDelete, "", userAdmin, http.StatusNotFound, 0}, + + // Invalid memberID + {"/boards/{PUBLIC_TEMPLATE_ID}/members/invalid", methodDelete, "", userAdmin, http.StatusOK, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsJoinBoardAsMember(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + + // Do we want to forbid already existing members to join to the board or simply return the current membership? + {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/join", methodPost, "", userAdmin, http.StatusForbidden, 0}, + + {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userTeamMember, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/join", methodPost, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/join", methodPost, "", userAdmin, http.StatusForbidden, 0}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userTeamMember, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/join", methodPost, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsLeaveBoardAsMember(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + _, err := th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicBoard.ID, UserID: "not-real-user", SchemeAdmin: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateBoard.ID, UserID: "not-real-user", SchemeAdmin: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicTemplate.ID, UserID: "not-real-user", SchemeAdmin: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateTemplate.ID, UserID: "not-real-user", SchemeAdmin: true}) + require.NoError(t, err) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userViewer, http.StatusOK, 0}, + {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userCommenter, http.StatusOK, 0}, + {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userEditor, http.StatusOK, 0}, + {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userViewer, http.StatusOK, 0}, + {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userCommenter, http.StatusOK, 0}, + {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userEditor, http.StatusOK, 0}, + {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userViewer, http.StatusOK, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userCommenter, http.StatusOK, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userEditor, http.StatusOK, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userViewer, http.StatusOK, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userCommenter, http.StatusOK, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userEditor, http.StatusOK, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userAdmin, http.StatusOK, 0}, + } + runTestCases(t, ttCases, testData, clients) + + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicBoard.ID, UserID: userAdmin, SchemeAdmin: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateBoard.ID, UserID: userAdmin, SchemeAdmin: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.publicTemplate.ID, UserID: userAdmin, SchemeAdmin: true}) + require.NoError(t, err) + _, err = th.Server.App().AddMemberToBoard(&model.BoardMember{BoardID: testData.privateTemplate.ID, UserID: userAdmin, SchemeAdmin: true}) + require.NoError(t, err) + + require.NoError(t, th.Server.App().DeleteBoardMember(testData.publicBoard.ID, "not-real-user")) + require.NoError(t, th.Server.App().DeleteBoardMember(testData.privateBoard.ID, "not-real-user")) + require.NoError(t, th.Server.App().DeleteBoardMember(testData.publicTemplate.ID, "not-real-user")) + require.NoError(t, th.Server.App().DeleteBoardMember(testData.privateTemplate.ID, "not-real-user")) + + // Last admin leave should fail + ttCases = []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/leave", methodPost, "", userAdmin, http.StatusBadRequest, 0}, + {"/boards/{PUBLIC_BOARD_ID}/leave", methodPost, "", userAdmin, http.StatusBadRequest, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/leave", methodPost, "", userAdmin, http.StatusBadRequest, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/leave", methodPost, "", userAdmin, http.StatusBadRequest, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsShareBoard(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + sharing := toJSON(t, model.Sharing{Enabled: true, Token: "test-token"}) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodPost, sharing, userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodPost, sharing, userAdmin, http.StatusOK, 0}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodPost, sharing, userAdmin, http.StatusOK, 0}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodPost, sharing, userAdmin, http.StatusOK, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetSharedBoardInfo(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + clients.Admin.PostSharing(&model.Sharing{ID: testData.publicBoard.ID, Enabled: true, Token: "test-token"}) + clients.Admin.PostSharing(&model.Sharing{ID: testData.privateBoard.ID, Enabled: true, Token: "test-token"}) + clients.Admin.PostSharing(&model.Sharing{ID: testData.publicTemplate.ID, Enabled: true, Token: "test-token"}) + clients.Admin.PostSharing(&model.Sharing{ID: testData.privateTemplate.ID, Enabled: true, Token: "test-token"}) + + ttCases := []TestCase{ + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/sharing", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/sharing", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/sharing", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userViewer, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userCommenter, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userEditor, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/sharing", methodGet, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsListTeams(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams", methodGet, "", userNoTeamMember, http.StatusOK, 0}, + {"/teams", methodGet, "", userTeamMember, http.StatusOK, 2}, + {"/teams", methodGet, "", userViewer, http.StatusOK, 2}, + {"/teams", methodGet, "", userCommenter, http.StatusOK, 2}, + {"/teams", methodGet, "", userEditor, http.StatusOK, 2}, + {"/teams", methodGet, "", userAdmin, http.StatusOK, 2}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetTeam(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/teams/test-team", methodGet, "", userViewer, http.StatusOK, 1}, + {"/teams/test-team", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/teams/test-team", methodGet, "", userEditor, http.StatusOK, 1}, + {"/teams/test-team", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/teams/empty-team", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/empty-team", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/empty-team", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/teams/empty-team", methodGet, "", userViewer, http.StatusForbidden, 0}, + {"/teams/empty-team", methodGet, "", userCommenter, http.StatusForbidden, 0}, + {"/teams/empty-team", methodGet, "", userEditor, http.StatusForbidden, 0}, + {"/teams/empty-team", methodGet, "", userAdmin, http.StatusForbidden, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsRegenerateSignupTokenPluginMode(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team/regenerate_signup_token", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/regenerate_signup_token", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, + + {"/teams/empty-team/regenerate_signup_token", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/empty-team/regenerate_signup_token", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetTeamUsers(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team/users", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/users", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/users", methodGet, "", userTeamMember, http.StatusOK, 5}, + {"/teams/test-team/users", methodGet, "", userViewer, http.StatusOK, 5}, + {"/teams/test-team/users", methodGet, "", userCommenter, http.StatusOK, 5}, + {"/teams/test-team/users", methodGet, "", userEditor, http.StatusOK, 5}, + {"/teams/test-team/users", methodGet, "", userAdmin, http.StatusOK, 5}, + + {"/teams/empty-team/users", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/empty-team/users", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/empty-team/users", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/teams/empty-team/users", methodGet, "", userViewer, http.StatusForbidden, 0}, + {"/teams/empty-team/users", methodGet, "", userCommenter, http.StatusForbidden, 0}, + {"/teams/empty-team/users", methodGet, "", userEditor, http.StatusForbidden, 0}, + {"/teams/empty-team/users", methodGet, "", userAdmin, http.StatusForbidden, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsTeamArchiveExportPluginMode(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/archive/export", methodGet, "", userAdmin, http.StatusNotImplemented, 0}, + + {"/teams/empty-team/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/empty-team/archive/export", methodGet, "", userAdmin, http.StatusNotImplemented, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsUploadFile(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userEditor, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions + {"/teams/test-team/{PRIVATE_BOARD_ID}/files", methodPost, "", userAdmin, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions + + {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userEditor, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions + {"/teams/test-team/{PUBLIC_BOARD_ID}/files", methodPost, "", userAdmin, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions + + {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userEditor, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions + {"/teams/test-team/{PRIVATE_TEMPLATE_ID}/files", methodPost, "", userAdmin, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions + + {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userViewer, http.StatusForbidden, 0}, + {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userCommenter, http.StatusForbidden, 0}, + {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userEditor, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions + {"/teams/test-team/{PUBLIC_TEMPLATE_ID}/files", methodPost, "", userAdmin, http.StatusBadRequest, 1}, // Not checking the logic, only the permissions + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetMe(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/users/me", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/users/me", methodGet, "", userNoTeamMember, http.StatusOK, 1}, + {"/users/me", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/users/me", methodGet, "", userViewer, http.StatusOK, 1}, + {"/users/me", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/users/me", methodGet, "", userEditor, http.StatusOK, 1}, + {"/users/me", methodGet, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetMyMemberships(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/users/me/memberships", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/users/me/memberships", methodGet, "", userNoTeamMember, http.StatusOK, 0}, + {"/users/me/memberships", methodGet, "", userTeamMember, http.StatusOK, 0}, + {"/users/me/memberships", methodGet, "", userViewer, http.StatusOK, 4}, + {"/users/me/memberships", methodGet, "", userCommenter, http.StatusOK, 4}, + {"/users/me/memberships", methodGet, "", userEditor, http.StatusOK, 4}, + {"/users/me/memberships", methodGet, "", userAdmin, http.StatusOK, 4}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetUser(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/users/no-team-member", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/users/no-team-member", methodGet, "", userNoTeamMember, http.StatusOK, 1}, + {"/users/no-team-member", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/users/no-team-member", methodGet, "", userViewer, http.StatusOK, 1}, + {"/users/no-team-member", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/users/no-team-member", methodGet, "", userEditor, http.StatusOK, 1}, + {"/users/no-team-member", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/users/team-member", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/users/team-member", methodGet, "", userNoTeamMember, http.StatusOK, 1}, + {"/users/team-member", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/users/team-member", methodGet, "", userViewer, http.StatusOK, 1}, + {"/users/team-member", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/users/team-member", methodGet, "", userEditor, http.StatusOK, 1}, + {"/users/team-member", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/users/viewer", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/users/viewer", methodGet, "", userNoTeamMember, http.StatusOK, 1}, + {"/users/viewer", methodGet, "", userTeamMember, http.StatusOK, 1}, + {"/users/viewer", methodGet, "", userViewer, http.StatusOK, 1}, + {"/users/viewer", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/users/viewer", methodGet, "", userEditor, http.StatusOK, 1}, + {"/users/viewer", methodGet, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsUserChangePasswordPluginMode(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/users/admin/changepassword", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/users/admin/changepassword", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsUpdateUserConfig(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + patch := toJSON(t, model.UserPropPatch{UpdatedFields: map[string]string{"test": "test"}}) + + ttCases := []TestCase{ + {"/users/team-member/config", methodPut, patch, userAnon, http.StatusUnauthorized, 0}, + {"/users/team-member/config", methodPut, patch, userNoTeamMember, http.StatusForbidden, 0}, + {"/users/team-member/config", methodPut, patch, userTeamMember, http.StatusOK, 1}, + {"/users/team-member/config", methodPut, patch, userViewer, http.StatusForbidden, 0}, + {"/users/team-member/config", methodPut, patch, userCommenter, http.StatusForbidden, 0}, + {"/users/team-member/config", methodPut, patch, userEditor, http.StatusForbidden, 0}, + {"/users/team-member/config", methodPut, patch, userAdmin, http.StatusForbidden, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsCreateBoardsAndBlocks(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + bab := toJSON(t, model.BoardsAndBlocks{ + Boards: []*model.Board{{ID: "test", Title: "Test Board", TeamID: "test-team"}}, + Blocks: []model.Block{ + {ID: "test-block", BoardID: "test", Type: "card", CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + }, + }) + + ttCases := []TestCase{ + {"/boards-and-blocks", methodPost, bab, userAnon, http.StatusUnauthorized, 0}, + {"/boards-and-blocks", methodPost, bab, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPost, bab, userTeamMember, http.StatusOK, 1}, + {"/boards-and-blocks", methodPost, bab, userViewer, http.StatusOK, 1}, + {"/boards-and-blocks", methodPost, bab, userCommenter, http.StatusOK, 1}, + {"/boards-and-blocks", methodPost, bab, userEditor, http.StatusOK, 1}, + {"/boards-and-blocks", methodPost, bab, userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsUpdateBoardsAndBlocks(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + newTitle := "new title" + bab := toJSON(t, model.PatchBoardsAndBlocks{ + BoardIDs: []string{testData.publicBoard.ID}, + BoardPatches: []*model.BoardPatch{{Title: &newTitle}}, + BlockIDs: []string{"block-3"}, + BlockPatches: []*model.BlockPatch{{Title: &newTitle}}, + }) + + ttCases := []TestCase{ + {"/boards-and-blocks", methodPatch, bab, userAnon, http.StatusUnauthorized, 0}, + {"/boards-and-blocks", methodPatch, bab, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userTeamMember, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userViewer, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userCommenter, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userEditor, http.StatusOK, 1}, + {"/boards-and-blocks", methodPatch, bab, userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) + + newType := model.BoardTypePrivate + // With type change + bab = toJSON(t, model.PatchBoardsAndBlocks{ + BoardIDs: []string{testData.publicBoard.ID}, + BoardPatches: []*model.BoardPatch{{Type: &newType}}, + BlockIDs: []string{"block-3"}, + BlockPatches: []*model.BlockPatch{{Title: &newTitle}}, + }) + + ttCases = []TestCase{ + {"/boards-and-blocks", methodPatch, bab, userAnon, http.StatusUnauthorized, 0}, + {"/boards-and-blocks", methodPatch, bab, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userTeamMember, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userViewer, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userCommenter, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userEditor, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodPatch, bab, userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsDeleteBoardsAndBlocks(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + bab := toJSON(t, model.DeleteBoardsAndBlocks{ + Boards: []string{testData.publicBoard.ID}, + Blocks: []string{"block-3"}, + }) + + ttCases := []TestCase{ + {"/boards-and-blocks", methodDelete, bab, userAnon, http.StatusUnauthorized, 0}, + {"/boards-and-blocks", methodDelete, bab, userNoTeamMember, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodDelete, bab, userTeamMember, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodDelete, bab, userViewer, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodDelete, bab, userCommenter, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodDelete, bab, userEditor, http.StatusForbidden, 0}, + {"/boards-and-blocks", methodDelete, bab, userAdmin, http.StatusOK, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsLoginPluginMode(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/login", methodPost, "", userAnon, http.StatusNotImplemented, 0}, + {"/login", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsLogoutPluginMode(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/logout", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/logout", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsRegisterPluginMode(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/register", methodPost, "", userAnon, http.StatusNotImplemented, 0}, + {"/register", methodPost, "", userAdmin, http.StatusNotImplemented, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsClientConfig(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/clientConfig", methodGet, "", userAnon, http.StatusOK, 1}, + {"/clientConfig", methodGet, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetCategories(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team/categories", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/categories", methodGet, "", userNoTeamMember, http.StatusOK, 0}, + {"/teams/test-team/categories", methodGet, "", userTeamMember, http.StatusOK, 0}, + {"/teams/test-team/categories", methodGet, "", userViewer, http.StatusOK, 0}, + {"/teams/test-team/categories", methodGet, "", userCommenter, http.StatusOK, 0}, + {"/teams/test-team/categories", methodGet, "", userEditor, http.StatusOK, 0}, + {"/teams/test-team/categories", methodGet, "", userAdmin, http.StatusOK, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsCreateCategory(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + category := func(userID string) string { + return toJSON(t, model.Category{ + Name: "Test category", + TeamID: "test-team", + UserID: userID, + CreateAt: model.GetMillis(), + UpdateAt: model.GetMillis(), + }) + } + + ttCases := []TestCase{ + {"/teams/test-team/categories", methodPost, category(""), userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/categories", methodPost, category(userNoTeamMember), userNoTeamMember, http.StatusOK, 1}, + {"/teams/test-team/categories", methodPost, category(userTeamMember), userTeamMember, http.StatusOK, 1}, + {"/teams/test-team/categories", methodPost, category(userViewer), userViewer, http.StatusOK, 1}, + {"/teams/test-team/categories", methodPost, category(userCommenter), userCommenter, http.StatusOK, 1}, + {"/teams/test-team/categories", methodPost, category(userEditor), userEditor, http.StatusOK, 1}, + {"/teams/test-team/categories", methodPost, category(userAdmin), userAdmin, http.StatusOK, 1}, + + {"/teams/test-team/categories", methodPost, category("other"), userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/categories", methodPost, category("other"), userNoTeamMember, http.StatusBadRequest, 0}, + {"/teams/test-team/categories", methodPost, category("other"), userTeamMember, http.StatusBadRequest, 0}, + {"/teams/test-team/categories", methodPost, category("other"), userViewer, http.StatusBadRequest, 0}, + {"/teams/test-team/categories", methodPost, category("other"), userCommenter, http.StatusBadRequest, 0}, + {"/teams/test-team/categories", methodPost, category("other"), userEditor, http.StatusBadRequest, 0}, + {"/teams/test-team/categories", methodPost, category("other"), userAdmin, http.StatusBadRequest, 0}, + + {"/teams/other-team/categories", methodPost, category(""), userAnon, http.StatusUnauthorized, 0}, + {"/teams/other-team/categories", methodPost, category(userNoTeamMember), userNoTeamMember, http.StatusBadRequest, 0}, + {"/teams/other-team/categories", methodPost, category(userTeamMember), userTeamMember, http.StatusBadRequest, 0}, + {"/teams/other-team/categories", methodPost, category(userViewer), userViewer, http.StatusBadRequest, 0}, + {"/teams/other-team/categories", methodPost, category(userCommenter), userCommenter, http.StatusBadRequest, 0}, + {"/teams/other-team/categories", methodPost, category(userEditor), userEditor, http.StatusBadRequest, 0}, + {"/teams/other-team/categories", methodPost, category(userAdmin), userAdmin, http.StatusBadRequest, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsUpdateCategory(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + categoryNoTeamMember, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userNoTeamMember, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryTeamMember, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userTeamMember, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryViewer, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userViewer, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryCommenter, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userCommenter, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryEditor, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userEditor, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryAdmin, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userAdmin, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + + category := func(userID string, categoryID string) string { + return toJSON(t, model.Category{ + ID: categoryID, + Name: "Test category", + TeamID: "test-team", + UserID: userID, + CreateAt: model.GetMillis(), + UpdateAt: model.GetMillis(), + }) + } + + ttCases := []TestCase{ + {"/teams/test-team/categories/any", methodPut, category("", "any"), userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/categories/" + categoryNoTeamMember.ID, methodPut, category(userNoTeamMember, categoryNoTeamMember.ID), userNoTeamMember, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryTeamMember.ID, methodPut, category(userTeamMember, categoryTeamMember.ID), userTeamMember, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryViewer.ID, methodPut, category(userViewer, categoryViewer.ID), userViewer, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryCommenter.ID, methodPut, category(userCommenter, categoryCommenter.ID), userCommenter, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryEditor.ID, methodPut, category(userEditor, categoryEditor.ID), userEditor, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryAdmin.ID, methodPut, category(userAdmin, categoryAdmin.ID), userAdmin, http.StatusOK, 1}, + + {"/teams/test-team/categories/any", methodPut, category("other", "any"), userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/categories/" + categoryNoTeamMember.ID, methodPut, category("other", categoryNoTeamMember.ID), userNoTeamMember, http.StatusBadRequest, 0}, + {"/teams/test-team/categories/" + categoryTeamMember.ID, methodPut, category("other", categoryTeamMember.ID), userTeamMember, http.StatusBadRequest, 0}, + {"/teams/test-team/categories/" + categoryViewer.ID, methodPut, category("other", categoryViewer.ID), userViewer, http.StatusBadRequest, 0}, + {"/teams/test-team/categories/" + categoryCommenter.ID, methodPut, category("other", categoryCommenter.ID), userCommenter, http.StatusBadRequest, 0}, + {"/teams/test-team/categories/" + categoryEditor.ID, methodPut, category("other", categoryEditor.ID), userEditor, http.StatusBadRequest, 0}, + {"/teams/test-team/categories/" + categoryAdmin.ID, methodPut, category("other", categoryAdmin.ID), userAdmin, http.StatusBadRequest, 0}, + + {"/teams/other-team/categories/any", methodPut, category("", "any"), userAnon, http.StatusUnauthorized, 0}, + {"/teams/other-team/categories/" + categoryNoTeamMember.ID, methodPut, category(userNoTeamMember, categoryNoTeamMember.ID), userNoTeamMember, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryTeamMember.ID, methodPut, category(userTeamMember, categoryTeamMember.ID), userTeamMember, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryViewer.ID, methodPut, category(userViewer, categoryViewer.ID), userViewer, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryCommenter.ID, methodPut, category(userCommenter, categoryCommenter.ID), userCommenter, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryEditor.ID, methodPut, category(userEditor, categoryEditor.ID), userEditor, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryAdmin.ID, methodPut, category(userAdmin, categoryAdmin.ID), userAdmin, http.StatusBadRequest, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsDeleteCategory(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + categoryNoTeamMember, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userNoTeamMember, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryTeamMember, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userTeamMember, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryViewer, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userViewer, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryCommenter, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userCommenter, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryEditor, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userEditor, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryAdmin, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userAdmin, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + + ttCases := []TestCase{ + {"/teams/other-team/categories/any", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/other-team/categories/" + categoryNoTeamMember.ID, methodDelete, "", userNoTeamMember, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryTeamMember.ID, methodDelete, "", userTeamMember, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryViewer.ID, methodDelete, "", userViewer, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryCommenter.ID, methodDelete, "", userCommenter, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryEditor.ID, methodDelete, "", userEditor, http.StatusBadRequest, 0}, + {"/teams/other-team/categories/" + categoryAdmin.ID, methodDelete, "", userAdmin, http.StatusBadRequest, 0}, + + {"/teams/test-team/categories/any", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/categories/" + categoryNoTeamMember.ID, methodDelete, "", userNoTeamMember, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryTeamMember.ID, methodDelete, "", userTeamMember, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryViewer.ID, methodDelete, "", userViewer, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryCommenter.ID, methodDelete, "", userCommenter, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryEditor.ID, methodDelete, "", userEditor, http.StatusOK, 1}, + {"/teams/test-team/categories/" + categoryAdmin.ID, methodDelete, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsUpdateCategoryBlock(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + categoryNoTeamMember, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userNoTeamMember, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryTeamMember, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userTeamMember, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryViewer, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userViewer, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryCommenter, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userCommenter, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryEditor, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userEditor, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + categoryAdmin, err := th.Server.App().CreateCategory( + &model.Category{Name: "Test category", TeamID: "test-team", UserID: userAdmin, CreateAt: model.GetMillis(), UpdateAt: model.GetMillis()}, + ) + require.NoError(t, err) + + ttCases := []TestCase{ + {"/teams/test-team/categories/any/blocks/any", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/categories/" + categoryNoTeamMember.ID + "/blocks/" + testData.publicBoard.ID, methodPost, "", userNoTeamMember, http.StatusOK, 0}, + {"/teams/test-team/categories/" + categoryTeamMember.ID + "/blocks/" + testData.publicBoard.ID, methodPost, "", userTeamMember, http.StatusOK, 0}, + {"/teams/test-team/categories/" + categoryViewer.ID + "/blocks/" + testData.publicBoard.ID, methodPost, "", userViewer, http.StatusOK, 0}, + {"/teams/test-team/categories/" + categoryCommenter.ID + "/blocks/" + testData.publicBoard.ID, methodPost, "", userCommenter, http.StatusOK, 0}, + {"/teams/test-team/categories/" + categoryEditor.ID + "/blocks/" + testData.publicBoard.ID, methodPost, "", userEditor, http.StatusOK, 0}, + {"/teams/test-team/categories/" + categoryAdmin.ID + "/blocks/" + testData.publicBoard.ID, methodPost, "", userAdmin, http.StatusOK, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetFile(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + newFileID, err := th.Server.App().SaveFile(bytes.NewBuffer([]byte("test")), "test-team", testData.privateBoard.ID, "test.png") + require.NoError(t, err) + + ttCases := []TestCase{ + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID, methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID, methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID, methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID, methodGet, "", userViewer, http.StatusOK, 1}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID, methodGet, "", userCommenter, http.StatusOK, 1}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID, methodGet, "", userEditor, http.StatusOK, 1}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID, methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID + "?read_token=invalid", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID + "?read_token=valid", methodGet, "", userAnon, http.StatusOK, 1}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID + "?read_token=invalid", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/files/teams/test-team/{PRIVATE_BOARD_ID}/" + newFileID + "?read_token=valid", methodGet, "", userTeamMember, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsCreateSubscription(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + subscription := func(userID string) string { + return toJSON(t, model.Subscription{ + BlockType: "card", + BlockID: "block-3", + SubscriberType: "user", + SubscriberID: userID, + CreateAt: model.GetMillis(), + }) + } + ttCases := []TestCase{ + {"/subscriptions", methodPost, subscription(""), userAnon, http.StatusUnauthorized, 0}, + {"/subscriptions", methodPost, subscription(userNoTeamMember), userNoTeamMember, http.StatusOK, 1}, + {"/subscriptions", methodPost, subscription(userTeamMember), userTeamMember, http.StatusOK, 1}, + {"/subscriptions", methodPost, subscription(userViewer), userViewer, http.StatusOK, 1}, + {"/subscriptions", methodPost, subscription(userCommenter), userCommenter, http.StatusOK, 1}, + {"/subscriptions", methodPost, subscription(userEditor), userEditor, http.StatusOK, 1}, + {"/subscriptions", methodPost, subscription(userAdmin), userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsGetSubscriptions(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/subscriptions/anon", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/subscriptions/no-team-member", methodGet, "", userNoTeamMember, http.StatusOK, 0}, + {"/subscriptions/team-member", methodGet, "", userTeamMember, http.StatusOK, 0}, + {"/subscriptions/viewer", methodGet, "", userViewer, http.StatusOK, 0}, + {"/subscriptions/commenter", methodGet, "", userCommenter, http.StatusOK, 0}, + {"/subscriptions/editor", methodGet, "", userEditor, http.StatusOK, 0}, + {"/subscriptions/admin", methodGet, "", userAdmin, http.StatusOK, 0}, + + {"/subscriptions/other", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/subscriptions/other", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/subscriptions/other", methodGet, "", userViewer, http.StatusForbidden, 0}, + {"/subscriptions/other", methodGet, "", userCommenter, http.StatusForbidden, 0}, + {"/subscriptions/other", methodGet, "", userEditor, http.StatusForbidden, 0}, + {"/subscriptions/other", methodGet, "", userAdmin, http.StatusForbidden, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsDeleteSubscription(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + _, err := th.Server.App().CreateSubscription( + &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userNoTeamMember, CreateAt: model.GetMillis()}, + ) + require.NoError(t, err) + _, err = th.Server.App().CreateSubscription( + &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userTeamMember, CreateAt: model.GetMillis()}, + ) + require.NoError(t, err) + _, err = th.Server.App().CreateSubscription( + &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userViewer, CreateAt: model.GetMillis()}, + ) + require.NoError(t, err) + _, err = th.Server.App().CreateSubscription( + &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userCommenter, CreateAt: model.GetMillis()}, + ) + require.NoError(t, err) + _, err = th.Server.App().CreateSubscription( + &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userEditor, CreateAt: model.GetMillis()}, + ) + require.NoError(t, err) + _, err = th.Server.App().CreateSubscription( + &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: userAdmin, CreateAt: model.GetMillis()}, + ) + require.NoError(t, err) + _, err = th.Server.App().CreateSubscription( + &model.Subscription{BlockType: "card", BlockID: "block-3", SubscriberType: "user", SubscriberID: "other", CreateAt: model.GetMillis()}, + ) + require.NoError(t, err) + + ttCases := []TestCase{ + {"/subscriptions/block-3/anon", methodDelete, "", userAnon, http.StatusUnauthorized, 0}, + {"/subscriptions/block-3/no-team-member", methodDelete, "", userNoTeamMember, http.StatusOK, 0}, + {"/subscriptions/block-3/team-member", methodDelete, "", userTeamMember, http.StatusOK, 0}, + {"/subscriptions/block-3/viewer", methodDelete, "", userViewer, http.StatusOK, 0}, + {"/subscriptions/block-3/commenter", methodDelete, "", userCommenter, http.StatusOK, 0}, + {"/subscriptions/block-3/editor", methodDelete, "", userEditor, http.StatusOK, 0}, + {"/subscriptions/block-3/admin", methodDelete, "", userAdmin, http.StatusOK, 0}, + + {"/subscriptions/block-3/other", methodDelete, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/subscriptions/block-3/other", methodDelete, "", userTeamMember, http.StatusForbidden, 0}, + {"/subscriptions/block-3/other", methodDelete, "", userViewer, http.StatusForbidden, 0}, + {"/subscriptions/block-3/other", methodDelete, "", userCommenter, http.StatusForbidden, 0}, + {"/subscriptions/block-3/other", methodDelete, "", userEditor, http.StatusForbidden, 0}, + {"/subscriptions/block-3/other", methodDelete, "", userAdmin, http.StatusForbidden, 0}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsOnboard(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team/onboard", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/onboard", methodPost, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/teams/test-team/onboard", methodPost, "", userTeamMember, http.StatusOK, 1}, + {"/teams/test-team/onboard", methodPost, "", userViewer, http.StatusOK, 1}, + {"/teams/test-team/onboard", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/teams/test-team/onboard", methodPost, "", userEditor, http.StatusOK, 1}, + {"/teams/test-team/onboard", methodPost, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsBoardArchiveExport(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_BOARD_ID}/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_BOARD_ID}/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PUBLIC_TEMPLATE_ID}/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, + + {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userAnon, http.StatusUnauthorized, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userNoTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userTeamMember, http.StatusForbidden, 0}, + {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userViewer, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userCommenter, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userEditor, http.StatusOK, 1}, + {"/boards/{PRIVATE_TEMPLATE_ID}/archive/export", methodGet, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} + +func TestPermissionsBoardArchiveImport(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + testData := setupData(t, th) + clients := setupClients(th) + + ttCases := []TestCase{ + {"/teams/test-team/archive/import", methodPost, "", userAnon, http.StatusUnauthorized, 0}, + {"/teams/test-team/archive/import", methodPost, "", userNoTeamMember, http.StatusForbidden, 1}, + {"/teams/test-team/archive/import", methodPost, "", userTeamMember, http.StatusOK, 1}, + {"/teams/test-team/archive/import", methodPost, "", userViewer, http.StatusOK, 1}, + {"/teams/test-team/archive/import", methodPost, "", userCommenter, http.StatusOK, 1}, + {"/teams/test-team/archive/import", methodPost, "", userEditor, http.StatusOK, 1}, + {"/teams/test-team/archive/import", methodPost, "", userAdmin, http.StatusOK, 1}, + } + runTestCases(t, ttCases, testData, clients) +} diff --git a/server/integrationtests/pluginteststore.go b/server/integrationtests/pluginteststore.go new file mode 100644 index 000000000..a68a165b5 --- /dev/null +++ b/server/integrationtests/pluginteststore.go @@ -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 +} diff --git a/server/services/permissions/mmpermissions/mmpermissions.go b/server/services/permissions/mmpermissions/mmpermissions.go index a2c2b6588..e7bde3e6e 100644 --- a/server/services/permissions/mmpermissions/mmpermissions.go +++ b/server/services/permissions/mmpermissions/mmpermissions.go @@ -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, diff --git a/server/services/store/sqlstore/board.go b/server/services/store/sqlstore/board.go index 20c2cc006..a1d9e1584 100644 --- a/server/services/store/sqlstore/board.go +++ b/server/services/store/sqlstore/board.go @@ -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 { diff --git a/server/services/store/sqlstore/migrations/000018_populate_categories.up.sql b/server/services/store/sqlstore/migrations/000018_populate_categories.up.sql index fe6437694..cc9ea3899 100644 --- a/server/services/store/sqlstore/migrations/000018_populate_categories.up.sql +++ b/server/services/store/sqlstore/migrations/000018_populate_categories.up.sql @@ -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, diff --git a/server/services/store/sqlstore/migrations/000021_fix_categories.down.sql b/server/services/store/sqlstore/migrations/000021_fix_categories.down.sql new file mode 100644 index 000000000..8d49a03a6 --- /dev/null +++ b/server/services/store/sqlstore/migrations/000021_fix_categories.down.sql @@ -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}} diff --git a/server/services/store/sqlstore/migrations/000021_fix_categories.up.sql b/server/services/store/sqlstore/migrations/000021_fix_categories.up.sql new file mode 100644 index 000000000..67e967b0a --- /dev/null +++ b/server/services/store/sqlstore/migrations/000021_fix_categories.up.sql @@ -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}} diff --git a/server/services/store/storetests/boards.go b/server/services/store/storetests/boards.go index 07cf21699..e00d71148 100644 --- a/server/services/store/storetests/boards.go +++ b/server/services/store/storetests/boards.go @@ -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) { diff --git a/server/swagger/swagger.yml b/server/swagger/swagger.yml index 200252744..bbc08b3b3 100644 --- a/server/swagger/swagger.yml +++ b/server/swagger/swagger.yml @@ -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: