1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-07-12 23:50:27 +02:00

Adding the permissions for commenter + viewer roles (#2882)

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Paul Esch-Laurent <paul.esch-laurent@mattermost.com>
This commit is contained in:
Jesús Espino
2022-08-24 22:36:28 +02:00
committed by GitHub
parent 03a6a963eb
commit 2b39745f68
13 changed files with 1112 additions and 51 deletions

View File

@ -221,13 +221,6 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
val := r.URL.Query().Get("disable_notify") val := r.URL.Query().Get("disable_notify")
disableNotify := val == True disableNotify := val == True
// in phase 1 we use "manage_board_cards", but we would have to
// check on specific actions for phase 2
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
return
}
requestBody, err := ioutil.ReadAll(r.Body) requestBody, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
@ -242,6 +235,8 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
return return
} }
hasComments := false
hasContents := false
for _, block := range blocks { for _, block := range blocks {
// Error checking // Error checking
if len(block.Type) < 1 { if len(block.Type) < 1 {
@ -250,6 +245,12 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
return return
} }
if block.Type == model.TypeComment {
hasComments = true
} else {
hasContents = true
}
if block.CreateAt < 1 { if block.CreateAt < 1 {
message := fmt.Sprintf("invalid createAt for block id %s", block.ID) message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil) a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
@ -269,6 +270,19 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
} }
} }
if hasContents {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
return
}
}
if hasComments {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to post card comments"})
return
}
}
blocks = model.GenerateBlockIDs(blocks, a.logger) blocks = model.GenerateBlockIDs(blocks, a.logger)
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail) auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
@ -748,10 +762,17 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
return return
} }
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) { if block.Type == model.TypeComment {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"}) if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to comment on board cards"})
return return
} }
} else {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board cards"})
return
}
}
auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail) auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec) defer a.audit.LogRecord(audit.LevelRead, auditRec)

View File

@ -1075,6 +1075,74 @@ func TestPermissionsCreateBoardBlocks(t *testing.T) {
}) })
} }
func TestPermissionsCreateBoardComments(t *testing.T) {
ttCasesF := func(testData TestData) []TestCase {
counter := 0
newBlockJSON := func(boardID string) string {
counter++
return toJSON(t, []*model.Block{{
ID: fmt.Sprintf("%d", counter),
Title: "Comment to create",
BoardID: boardID,
Type: model.TypeComment,
CreateAt: model.GetMillis(),
UpdateAt: model.GetMillis(),
}})
}
return []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.StatusOK, 1},
{"/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.StatusOK, 1},
{"/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.StatusOK, 1},
{"/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.StatusOK, 1},
{"/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},
}
}
t.Run("plugin", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
clients := setupClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(testData)
runTestCases(t, ttCases, testData, clients)
})
t.Run("local", func(t *testing.T) {
th := SetupTestHelperLocalMode(t)
defer th.TearDown()
clients := setupLocalClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(testData)
runTestCases(t, ttCases, testData, clients)
})
}
func TestPermissionsPatchBoardBlocks(t *testing.T) { func TestPermissionsPatchBoardBlocks(t *testing.T) {
newBlocksPatchJSON := func(blockID string) string { newBlocksPatchJSON := func(blockID string) string {
newTitle := "New Patch Block Title" newTitle := "New Patch Block Title"
@ -1420,37 +1488,104 @@ func TestPermissionsDuplicateBoardBlock(t *testing.T) {
} }
ttCases := []TestCase{ ttCases := []TestCase{
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0}, {"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/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-8/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-8/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-8/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-8/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-8/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-4/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, {"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/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-7/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-7/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-7/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-7/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-7/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-7/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, {"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/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-6/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-6/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-6/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-6/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-6/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-6/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-2/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, {"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/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-5/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-5/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-5/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-5/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-5/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-5/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-1/duplicate", methodPost, "", userAdmin, http.StatusOK, 1}, {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
// Invalid boardID/blockID combination
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusNotFound, 0},
}
t.Run("plugin", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
clients := setupClients(th)
testData := setupData(t, th)
extraSetup(t, th, testData)
runTestCases(t, ttCases, testData, clients)
})
t.Run("local", func(t *testing.T) {
th := SetupTestHelperLocalMode(t)
defer th.TearDown()
clients := setupLocalClients(th)
testData := setupData(t, th)
extraSetup(t, th, testData)
runTestCases(t, ttCases, testData, clients)
})
}
func TestPermissionsDuplicateBoardComment(t *testing.T) {
extraSetup := func(t *testing.T, th *TestHelper, testData TestData) {
err := th.Server.App().InsertBlock(model.Block{ID: "block-5", Title: "Test", Type: model.TypeComment, BoardID: testData.publicTemplate.ID}, userAdmin)
require.NoError(t, err)
err = th.Server.App().InsertBlock(model.Block{ID: "block-6", Title: "Test", Type: model.TypeComment, BoardID: testData.privateTemplate.ID}, userAdmin)
require.NoError(t, err)
err = th.Server.App().InsertBlock(model.Block{ID: "block-7", Title: "Test", Type: model.TypeComment, BoardID: testData.publicBoard.ID}, userAdmin)
require.NoError(t, err)
err = th.Server.App().InsertBlock(model.Block{ID: "block-8", Title: "Test", Type: model.TypeComment, BoardID: testData.privateBoard.ID}, userAdmin)
require.NoError(t, err)
}
ttCases := []TestCase{
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userCommenter, http.StatusOK, 1},
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
{"/boards/{PRIVATE_BOARD_ID}/blocks/block-8/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userCommenter, http.StatusOK, 1},
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
{"/boards/{PUBLIC_BOARD_ID}/blocks/block-7/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userCommenter, http.StatusOK, 1},
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
{"/boards/{PRIVATE_TEMPLATE_ID}/blocks/block-6/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userViewer, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userCommenter, http.StatusOK, 1},
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userEditor, http.StatusOK, 1},
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-5/duplicate", methodPost, "", userAdmin, http.StatusOK, 1},
// Invalid boardID/blockID combination // Invalid boardID/blockID combination
{"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusNotFound, 0}, {"/boards/{PUBLIC_TEMPLATE_ID}/blocks/block-3/duplicate", methodPost, "", userAdmin, http.StatusNotFound, 0},

View File

@ -17,4 +17,6 @@ var (
PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""} PermissionShareBoard = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""}
PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""} PermissionManageBoardCards = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""}
PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""} PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""}
PermissionCommentBoardCards = &mmModel.Permission{Id: "comment_board_cards", Name: "", Description: "", Scope: ""}
PermissionDeleteOthersComments = &mmModel.Permission{Id: "delete_others_comments", Name: "", Description: "", Scope: ""}
) )

View File

@ -67,10 +67,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
} }
switch permission { switch permission {
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard: case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
return member.SchemeAdmin return member.SchemeAdmin
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties: case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
return member.SchemeAdmin || member.SchemeEditor return member.SchemeAdmin || member.SchemeEditor
case model.PermissionCommentBoardCards:
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
case model.PermissionViewBoard: case model.PermissionViewBoard:
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
default: default:

View File

@ -99,10 +99,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
} }
switch permission { switch permission {
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard: case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
return member.SchemeAdmin return member.SchemeAdmin
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties: case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
return member.SchemeAdmin || member.SchemeEditor return member.SchemeAdmin || member.SchemeEditor
case model.PermissionCommentBoardCards:
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
case model.PermissionViewBoard: case model.PermissionViewBoard:
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
default: default:

View File

@ -61,6 +61,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
} }
}, [card.title, title]) }, [card.title, title])
const canEditBoardCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards]) const canEditBoardCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards])
const canCommentBoardCards = useHasCurrentBoardPermissions([Permission.CommentBoardCards])
const saveTitleRef = useRef<() => void>(saveTitle) const saveTitleRef = useRef<() => void>(saveTitle)
saveTitleRef.current = saveTitle saveTitleRef.current = saveTitle
@ -207,7 +208,7 @@ const CardDetail = (props: Props): JSX.Element|null => {
comments={comments} comments={comments}
boardId={card.boardId} boardId={card.boardId}
cardId={card.id} cardId={card.id}
readonly={props.readonly || !canEditBoardCards} readonly={props.readonly || !canCommentBoardCards}
/> />
</Fragment>} </Fragment>}
</div> </div>

View File

@ -57,6 +57,9 @@ describe('components/cardDetail/CommentsList', () => {
board_id_1: {title: 'Board'}, board_id_1: {title: 'Board'},
}, },
current: 'board_id_1', current: 'board_id_1',
myBoardMemberships: {
['board_id_1']: {userId: 'user_id_1', schemeAdmin: true},
},
}, },
cards: { cards: {
cards: { cards: {
@ -69,6 +72,9 @@ describe('components/cardDetail/CommentsList', () => {
featureFlags: {}, featureFlags: {},
}, },
}, },
teams: {
current: {id: 'team_id_1'},
},
}) })
const component = ( const component = (
@ -109,6 +115,18 @@ describe('components/cardDetail/CommentsList', () => {
{username: 'username_1'}, {username: 'username_1'},
], ],
}, },
boards: {
boards: {
board_id_1: {title: 'Board'},
},
current: 'board_id_1',
myBoardMemberships: {
['board_id_1']: {userId: 'user_id_1', schemeAdmin: true},
},
},
teams: {
current: {id: 'team_id_1'}
}
}) })
const component = ( const component = (

View File

@ -13,6 +13,8 @@ import {MarkdownEditor} from '../markdownEditor'
import {IUser} from '../../user' import {IUser} from '../../user'
import {getMe} from '../../store/users' import {getMe} from '../../store/users'
import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
import {Permission} from '../../constants'
import AddCommentTourStep from '../onboardingTour/addComments/addComments' import AddCommentTourStep from '../onboardingTour/addComments/addComments'
@ -30,6 +32,7 @@ type Props = {
const CommentsList = (props: Props) => { const CommentsList = (props: Props) => {
const [newComment, setNewComment] = useState('') const [newComment, setNewComment] = useState('')
const me = useAppSelector<IUser|null>(getMe) const me = useAppSelector<IUser|null>(getMe)
const canDeleteOthersComments = useHasCurrentBoardPermissions([Permission.DeleteOthersComments])
const onSendClicked = () => { const onSendClicked = () => {
const commentText = newComment const commentText = newComment
@ -88,15 +91,20 @@ const CommentsList = (props: Props) => {
{/* New comment */} {/* New comment */}
{!props.readonly && newCommentComponent} {!props.readonly && newCommentComponent}
{comments.slice(0).reverse().map((comment) => ( {comments.slice(0).reverse().map((comment) => {
// Only modify _own_ comments, EXCEPT for Admins, which can delete _any_ comment
// NOTE: editing comments will exist in the future (in addition to deleting)
const canDeleteComment: boolean = canDeleteOthersComments || me?.id === comment.modifiedBy
return (
<Comment <Comment
key={comment.id} key={comment.id}
comment={comment} comment={comment}
userImageUrl={Utils.getProfilePicture(comment.modifiedBy)} userImageUrl={Utils.getProfilePicture(comment.modifiedBy)}
userId={comment.modifiedBy} userId={comment.modifiedBy}
readonly={props.readonly} readonly={props.readonly || !canDeleteComment}
/> />
))} )})}
{/* horizontal divider below comments */} {/* horizontal divider below comments */}
{!(comments.length === 0 && props.readonly) && <hr className='CommentsList__divider'/>} {!(comments.length === 0 && props.readonly) && <hr className='CommentsList__divider'/>}

View File

@ -0,0 +1,683 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`src/components/shareBoard/userPermissionsRow should match snapshot 1`] = `
<div>
<div
class="user-item"
>
<div
class="user-item__content"
>
<div
class="ml-3"
>
<strong />
<strong
class="ml-2 text-light"
>
@username_1
</strong>
<strong
class="ml-2 text-light"
>
(You)
</strong>
</div>
</div>
<div>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="user-item__button"
>
Admin
<i
class="CompassIcon icon-chevron-down CompassIcon"
/>
</button>
<div
class="Menu noselect left "
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div>
<div
aria-label="Viewer"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Viewer
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Commenter"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Commenter
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Editor"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Editor
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Admin"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<svg
class="CheckIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
points="20,60 40,80 80,40"
/>
</svg>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Admin
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
class="MenuOption MenuSeparator menu-separator"
/>
</div>
<div>
<div
aria-label="Remove member"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Remove member
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Cancel
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/userPermissionsRow should match snapshot in plugin mode 1`] = `
<div>
<div
class="user-item"
>
<div
class="user-item__content"
>
<img
class="user-item__img"
/>
<div
class="ml-3"
>
<strong />
<strong
class="ml-2 text-light"
>
@username_1
</strong>
<strong
class="ml-2 text-light"
>
(You)
</strong>
</div>
</div>
<div>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="user-item__button"
>
Admin
<i
class="CompassIcon icon-chevron-down CompassIcon"
/>
</button>
<div
class="Menu noselect left "
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div>
<div
aria-label="Viewer"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Viewer
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Commenter"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Commenter
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Editor"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Editor
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Admin"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<svg
class="CheckIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
points="20,60 40,80 80,40"
/>
</svg>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Admin
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
class="MenuOption MenuSeparator menu-separator"
/>
</div>
<div>
<div
aria-label="Remove member"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Remove member
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Cancel
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`src/components/shareBoard/userPermissionsRow should match snapshot in template 1`] = `
<div>
<div
class="user-item"
>
<div
class="user-item__content"
>
<img
class="user-item__img"
/>
<div
class="ml-3"
>
<strong />
<strong
class="ml-2 text-light"
>
@username_1
</strong>
<strong
class="ml-2 text-light"
>
(You)
</strong>
</div>
</div>
<div>
<div
aria-label="menuwrapper"
class="MenuWrapper"
role="button"
>
<button
class="user-item__button"
>
Admin
<i
class="CompassIcon icon-chevron-down CompassIcon"
/>
</button>
<div
class="Menu noselect left "
>
<div
class="menu-contents"
>
<div
class="menu-options"
>
<div>
<div
aria-label="Viewer"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Viewer
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div />
<div>
<div
aria-label="Editor"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Editor
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
aria-label="Admin"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex menu-option__check"
>
<svg
class="CheckIcon Icon"
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
>
<polyline
points="20,60 40,80 80,40"
/>
</svg>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Admin
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
<div>
<div
class="MenuOption MenuSeparator menu-separator"
/>
</div>
<div>
<div
aria-label="Remove member"
class="MenuOption TextOption menu-option"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Remove member
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
<div
class="menu-spacer hideOnWidescreen"
/>
<div
class="menu-options hideOnWidescreen"
>
<div
aria-label="Cancel"
class="MenuOption TextOption menu-option menu-cancel"
role="button"
>
<div
class="d-flex"
>
<div
class="noicon"
/>
</div>
<div
class="menu-option__content"
>
<div
class="menu-name"
>
Cancel
</div>
</div>
<div
class="noicon"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,172 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {act, render} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {Provider as ReduxProvider} from 'react-redux'
import thunk from 'redux-thunk'
import React from 'react'
import {MemoryRouter} from 'react-router'
import {mocked} from 'jest-mock'
import {BoardMember} from '../../blocks/board'
import {IUser} from '../../user'
import {TestBlockFactory} from '../../test/testBlockFactory'
import {mockStateStore, wrapDNDIntl} from '../../testUtils'
import {Utils} from '../../utils'
import UserPermissionsRow from './userPermissionsRow'
jest.useFakeTimers()
const boardId = '1'
jest.mock('../../utils')
const mockedUtils = mocked(Utils, true)
const board = TestBlockFactory.createBoard()
board.id = boardId
board.teamId = 'team-id'
board.channelId = 'channel_1'
describe('src/components/shareBoard/userPermissionsRow', () => {
const me: IUser = {
id: 'user-id-1',
username: 'username_1',
email: '',
nickname: '',
firstname: '',
lastname: '',
props: {},
create_at: 0,
update_at: 0,
is_bot: false,
roles: 'system_user',
}
const state = {
teams: {
current: {id: 'team-id', title: 'Test Team'},
},
users: {
me,
boardUsers: [me],
blockSubscriptions: [],
},
boards: {
current: board.id,
boards: {
[board.id]: board,
},
templates: [],
membersInBoards: {
[board.id]: {},
},
myBoardMemberships: {
[board.id]: {userId: me.id, schemeAdmin: true},
},
},
}
beforeEach(() => {
jest.clearAllMocks()
})
test('should match snapshot', async () => {
let container: Element | undefined
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
const store = mockStateStore([thunk], state)
await act(async () => {
const result = render(
wrapDNDIntl(
<ReduxProvider store={store}>
<UserPermissionsRow
user={me}
isMe={true}
member={state.boards.myBoardMemberships[board.id] as BoardMember}
teammateNameDisplay={'test'}
onDeleteBoardMember={() => {}}
onUpdateBoardMember={() => {}}
/>
</ReduxProvider>),
{wrapper: MemoryRouter},
)
container = result.container
})
const buttonElement = container?.querySelector('.user-item__button')
expect(buttonElement).toBeDefined()
userEvent.click(buttonElement!)
expect(container).toMatchSnapshot()
})
test('should match snapshot in plugin mode', async () => {
let container: Element | undefined
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
const store = mockStateStore([thunk], state)
await act(async () => {
const result = render(
wrapDNDIntl(
<ReduxProvider store={store}>
<UserPermissionsRow
user={me}
isMe={true}
member={state.boards.myBoardMemberships[board.id] as BoardMember}
teammateNameDisplay={'test'}
onDeleteBoardMember={() => {}}
onUpdateBoardMember={() => {}}/>
</ReduxProvider>),
{wrapper: MemoryRouter},
)
container = result.container
})
const buttonElement = container?.querySelector('.user-item__button')
expect(buttonElement).toBeDefined()
userEvent.click(buttonElement!)
expect(container).toMatchSnapshot()
})
test('should match snapshot in template', async () => {
let container: Element | undefined
mockedUtils.isFocalboardPlugin.mockReturnValue(true)
const testState = {
...state,
boards: {
...state.boards,
boards: {},
templates: {
[board.id]: {...board, isTemplate: true},
}
}
}
const store = mockStateStore([thunk], testState)
await act(async () => {
const result = render(
wrapDNDIntl(
<ReduxProvider store={store}>
<UserPermissionsRow
user={me}
isMe={true}
member={state.boards.myBoardMemberships[board.id] as BoardMember}
teammateNameDisplay={'test'}
onDeleteBoardMember={() => {}}
onUpdateBoardMember={() => {}}
/>
</ReduxProvider>),
{wrapper: MemoryRouter},
)
container = result.container
})
const buttonElement = container?.querySelector('.user-item__button')
expect(buttonElement).toBeDefined()
userEvent.click(buttonElement!)
expect(container).toMatchSnapshot()
})
})

View File

@ -14,6 +14,8 @@ import {BoardMember} from '../../blocks/board'
import {IUser} from '../../user' import {IUser} from '../../user'
import {Utils} from '../../utils' import {Utils} from '../../utils'
import {Permission} from '../../constants' import {Permission} from '../../constants'
import {useAppSelector} from '../../store/hooks'
import {getCurrentBoard} from '../../store/boards'
import BoardPermissionGate from '../permissions/boardPermissionGate' import BoardPermissionGate from '../permissions/boardPermissionGate'
@ -28,6 +30,7 @@ type Props = {
const UserPermissionsRow = (props: Props): JSX.Element => { const UserPermissionsRow = (props: Props): JSX.Element => {
const intl = useIntl() const intl = useIntl()
const board = useAppSelector(getCurrentBoard)
const {user, member, isMe, teammateNameDisplay} = props const {user, member, isMe, teammateNameDisplay} = props
let currentRole = 'Viewer' let currentRole = 'Viewer'
if (member.schemeAdmin) { if (member.schemeAdmin) {
@ -71,6 +74,14 @@ const UserPermissionsRow = (props: Props): JSX.Element => {
name={intl.formatMessage({id: 'BoardMember.schemeViewer', defaultMessage: 'Viewer'})} name={intl.formatMessage({id: 'BoardMember.schemeViewer', defaultMessage: 'Viewer'})}
onClick={() => props.onUpdateBoardMember(member, 'Viewer')} onClick={() => props.onUpdateBoardMember(member, 'Viewer')}
/> />
{!board.isTemplate &&
<Menu.Text
id='Commenter'
check={true}
icon={currentRole === 'Commenter' ? <CheckIcon/> : null}
name={intl.formatMessage({id: 'BoardMember.schemeCommenter', defaultMessage: 'Commenter'})}
onClick={() => props.onUpdateBoardMember(member, 'Commenter')}
/>}
<Menu.Text <Menu.Text
id='Editor' id='Editor'
check={true} check={true}

View File

@ -10,7 +10,9 @@ enum Permission {
ManageBoardRoles = 'manage_board_roles', ManageBoardRoles = 'manage_board_roles',
ManageBoardCards = 'manage_board_cards', ManageBoardCards = 'manage_board_cards',
ManageBoardProperties = 'manage_board_properties', ManageBoardProperties = 'manage_board_properties',
CommentBoardCards = 'comment_board_cards',
ViewBoard = 'view_board', ViewBoard = 'view_board',
DeleteOthersComments = 'delete_others_comments'
} }
class Constants { class Constants {

View File

@ -22,8 +22,9 @@ export const useHasPermissions = (teamId: string, boardId: string, permissions:
return true return true
} }
const adminPermissions = [Permission.ManageBoardType, Permission.DeleteBoard, Permission.ShareBoard, Permission.ManageBoardRoles] const adminPermissions = [Permission.ManageBoardType, Permission.DeleteBoard, Permission.ShareBoard, Permission.ManageBoardRoles, Permission.DeleteOthersComments]
const editorPermissions = [Permission.ManageBoardCards, Permission.ManageBoardProperties] const editorPermissions = [Permission.ManageBoardCards, Permission.ManageBoardProperties]
const commenterPermissions = [Permission.CommentBoardCards]
const viewerPermissions = [Permission.ViewBoard] const viewerPermissions = [Permission.ViewBoard]
for (const permission of permissions) { for (const permission of permissions) {
@ -33,6 +34,9 @@ export const useHasPermissions = (teamId: string, boardId: string, permissions:
if (editorPermissions.includes(permission) && (member.schemeAdmin || member.schemeEditor)) { if (editorPermissions.includes(permission) && (member.schemeAdmin || member.schemeEditor)) {
return true return true
} }
if (commenterPermissions.includes(permission) && (member.schemeAdmin || member.schemeEditor || member.schemeCommenter)) {
return true
}
if (viewerPermissions.includes(permission) && (member.schemeAdmin || member.schemeEditor || member.schemeCommenter || member.schemeViewer)) { if (viewerPermissions.includes(permission) && (member.schemeAdmin || member.schemeEditor || member.schemeCommenter || member.schemeViewer)) {
return true return true
} }