Adding the board default role for public boards (#2884)

* Adding the default role concept in the backend

* Adding the interface part

* Fix golang-ci lint errors

* Adding local permissions tests

* Address PR review comments

* Improving the code a bit

* Another small fix

* Renaming DefaultRole to MinimumRole

* Setting the minimum role at minimum to check the permissions per roles in the integration tests

* Adding the new minimum role behavior

* Fixing some tests

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Jesús Espino 2022-05-16 18:09:11 +02:00 committed by GitHub
parent 11bd3720f1
commit d3edf2f698
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 504 additions and 36 deletions

View file

@ -2759,7 +2759,7 @@ func (a *API) handlePatchBoard(w http.ResponseWriter, r *http.Request) {
return
}
if patch.Type != nil {
if patch.Type != nil || patch.MinimumRole != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"})
return
@ -3429,12 +3429,13 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
return
}
// currently all memberships are created as editors by default
// TODO: Support different public roles
newBoardMember := &model.BoardMember{
UserID: userID,
BoardID: boardID,
SchemeEditor: true,
UserID: userID,
BoardID: boardID,
SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin,
SchemeEditor: board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor,
SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter,
SchemeViewer: board.MinimumRole == model.BoardRoleViewer,
}
auditRec := a.makeAuditRecord(r, "joinBoard", audit.Fail)
@ -3922,7 +3923,7 @@ func (a *API) handlePatchBoardsAndBlocks(w http.ResponseWriter, r *http.Request)
return
}
if patch.Type != nil {
if patch.Type != nil || patch.MinimumRole != nil {
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardType) {
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modifying board type"})
return

View file

@ -1945,7 +1945,83 @@ func TestJoinBoard(t *testing.T) {
me := th.GetUser1()
title := "Public board"
title := "Test Public board"
teamID := testTeamID
newBoard := &model.Board{
Title: title,
Type: model.BoardTypeOpen,
TeamID: teamID,
}
board, resp := th.Client.CreateBoard(newBoard)
th.CheckOK(resp)
require.NoError(t, resp.Error)
require.NotNil(t, board)
require.NotNil(t, board.ID)
require.Equal(t, title, board.Title)
require.Equal(t, model.BoardTypeOpen, board.Type)
require.Equal(t, teamID, board.TeamID)
require.Equal(t, me.ID, board.CreatedBy)
require.Equal(t, me.ID, board.ModifiedBy)
require.Equal(t, model.BoardRoleNone, board.MinimumRole)
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)
s, _ := json.MarshalIndent(member, "", "\t")
t.Log(string(s))
})
t.Run("create and join public board should match the minimumRole in the membership", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
me := th.GetUser1()
title := "Public board for commenters"
teamID := testTeamID
newBoard := &model.Board{
Title: title,
Type: model.BoardTypeOpen,
TeamID: teamID,
MinimumRole: model.BoardRoleCommenter,
}
board, resp := th.Client.CreateBoard(newBoard)
th.CheckOK(resp)
require.NoError(t, resp.Error)
require.NotNil(t, board)
require.NotNil(t, board.ID)
require.Equal(t, title, board.Title)
require.Equal(t, model.BoardTypeOpen, board.Type)
require.Equal(t, teamID, board.TeamID)
require.Equal(t, me.ID, board.CreatedBy)
require.Equal(t, me.ID, board.ModifiedBy)
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)
require.False(t, member.SchemeAdmin, "new member should not be admin")
require.False(t, member.SchemeEditor, "new member should not be editor")
require.True(t, member.SchemeCommenter, "new member should be commenter")
require.False(t, member.SchemeViewer, "new member should not be viewer")
s, _ := json.MarshalIndent(member, "", "\t")
t.Log(string(s))
})
t.Run("create and join public board should match editor role in the membership when MinimumRole is empty", func(t *testing.T) {
th := SetupTestHelper(t).InitBasic()
defer th.TearDown()
me := th.GetUser1()
title := "Public board for editors"
teamID := testTeamID
newBoard := &model.Board{
Title: title,
@ -1969,6 +2045,10 @@ func TestJoinBoard(t *testing.T) {
require.NotNil(t, member)
require.Equal(t, board.ID, member.BoardID)
require.Equal(t, th.GetUser2().ID, member.UserID)
require.False(t, member.SchemeAdmin, "new member should not be admin")
require.True(t, member.SchemeEditor, "new member should be editor")
require.False(t, member.SchemeCommenter, "new member should not be commenter")
require.False(t, member.SchemeViewer, "new member should not be viewer")
s, _ := json.MarshalIndent(member, "", "\t")
t.Log(string(s))

View file

@ -129,23 +129,27 @@ type TestData struct {
}
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}, userAdminID, true)
customTemplate1, err := th.Server.App().CreateBoard(
&model.Board{Title: "Custom template 1", TeamID: "test-team", IsTemplate: true, Type: model.BoardTypeOpen, MinimumRole: "viewer"},
userAdminID,
true,
)
require.NoError(t, err)
err = th.Server.App().InsertBlock(model.Block{ID: "block-1", Title: "Test", Type: "card", BoardID: customTemplate1.ID}, userAdminID)
require.NoError(t, err)
customTemplate2, err := th.Server.App().CreateBoard(
&model.Board{Title: "Custom template 2", TeamID: "test-team", IsTemplate: true, Type: model.BoardTypePrivate},
&model.Board{Title: "Custom template 2", TeamID: "test-team", IsTemplate: true, Type: model.BoardTypePrivate, MinimumRole: "viewer"},
userAdminID,
true)
require.NoError(t, err)
err = th.Server.App().InsertBlock(model.Block{ID: "block-2", Title: "Test", Type: "card", BoardID: customTemplate2.ID}, userAdminID)
require.NoError(t, err)
board1, err := th.Server.App().CreateBoard(&model.Board{Title: "Board 1", TeamID: "test-team", Type: model.BoardTypeOpen}, userAdminID, true)
board1, err := th.Server.App().CreateBoard(&model.Board{Title: "Board 1", TeamID: "test-team", Type: model.BoardTypeOpen, MinimumRole: "viewer"}, userAdminID, true)
require.NoError(t, err)
err = th.Server.App().InsertBlock(model.Block{ID: "block-3", Title: "Test", Type: "card", BoardID: board1.ID}, userAdminID)
require.NoError(t, err)
board2, err := th.Server.App().CreateBoard(&model.Board{Title: "Board 2", TeamID: "test-team", Type: model.BoardTypePrivate}, userAdminID, true)
board2, err := th.Server.App().CreateBoard(&model.Board{Title: "Board 2", TeamID: "test-team", Type: model.BoardTypePrivate, MinimumRole: "viewer"}, userAdminID, true)
require.NoError(t, err)
rBoard2, err := th.Server.App().GetBoard(board2.ID)
@ -558,6 +562,109 @@ func TestPermissionsPatchBoard(t *testing.T) {
})
}
func TestPermissionsPatchBoardType(t *testing.T) {
ttCases := []TestCase{
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userViewer, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userCommenter, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userEditor, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userAdmin, http.StatusOK, 1},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userViewer, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userCommenter, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userEditor, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, "{\"type\": \"P\"}", userAdmin, http.StatusOK, 1},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userViewer, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userCommenter, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userEditor, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userAdmin, http.StatusOK, 1},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userAnon, http.StatusUnauthorized, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userViewer, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userCommenter, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userEditor, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, "{\"type\": \"P\"}", userAdmin, http.StatusOK, 1},
}
t.Run("plugin", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
clients := setupClients(th)
testData := setupData(t, th)
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)
runTestCases(t, ttCases, testData, clients)
})
}
func TestPermissionsPatchBoardMinimumRole(t *testing.T) {
patch := toJSON(t, map[string]model.BoardRole{"minimumRole": model.BoardRoleViewer})
ttCases := []TestCase{
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0},
{"/boards/{PRIVATE_BOARD_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0},
{"/boards/{PUBLIC_BOARD_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0},
{"/boards/{PRIVATE_TEMPLATE_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userAnon, http.StatusUnauthorized, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userNoTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userTeamMember, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userViewer, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userCommenter, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userEditor, http.StatusForbidden, 0},
{"/boards/{PUBLIC_TEMPLATE_ID}", methodPatch, patch, userAdmin, http.StatusOK, 1},
}
t.Run("plugin", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
clients := setupClients(th)
testData := setupData(t, th)
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)
runTestCases(t, ttCases, testData, clients)
})
}
func TestPermissionsDeleteBoard(t *testing.T) {
ttCases := []TestCase{
{"/boards/{PRIVATE_BOARD_ID}", methodDelete, "", userAnon, http.StatusUnauthorized, 0},
@ -3095,3 +3202,167 @@ func TestPermissionsBoardArchiveImport(t *testing.T) {
runTestCases(t, ttCases, testData, clients)
})
}
func TestPermissionsMinimumRolesApplied(t *testing.T) {
ttCasesF := func(t *testing.T, th *TestHelper, minimumRole model.BoardRole, testData TestData) []TestCase {
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(),
}})
}
_, err := th.Server.App().PatchBoard(&model.BoardPatch{MinimumRole: &minimumRole}, testData.privateBoard.ID, userAdminID)
require.NoError(t, err)
_, err = th.Server.App().PatchBoard(&model.BoardPatch{MinimumRole: &minimumRole}, testData.publicBoard.ID, userAdminID)
require.NoError(t, err)
_, err = th.Server.App().PatchBoard(&model.BoardPatch{MinimumRole: &minimumRole}, testData.privateTemplate.ID, userAdminID)
require.NoError(t, err)
_, err = th.Server.App().PatchBoard(&model.BoardPatch{MinimumRole: &minimumRole}, testData.publicTemplate.ID, userAdminID)
require.NoError(t, err)
if minimumRole == "viewer" || minimumRole == "commenter" {
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.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},
}
} else {
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.StatusOK, 1},
{"/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.StatusOK, 1},
{"/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.StatusOK, 1},
{"/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.StatusOK, 1},
{"/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) {
t.Run("minimum role viewer", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
clients := setupClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(t, th, "viewer", testData)
runTestCases(t, ttCases, testData, clients)
})
t.Run("minimum role commenter", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
clients := setupClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(t, th, "commenter", testData)
runTestCases(t, ttCases, testData, clients)
})
t.Run("minimum role editor", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
clients := setupClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(t, th, "editor", testData)
runTestCases(t, ttCases, testData, clients)
})
t.Run("minimum role admin", func(t *testing.T) {
th := SetupTestHelperPluginMode(t)
defer th.TearDown()
clients := setupClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(t, th, "admin", testData)
runTestCases(t, ttCases, testData, clients)
})
})
t.Run("local", func(t *testing.T) {
t.Run("minimum role viewer", func(t *testing.T) {
th := SetupTestHelperLocalMode(t)
defer th.TearDown()
clients := setupLocalClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(t, th, "viewer", testData)
runTestCases(t, ttCases, testData, clients)
})
t.Run("minimum role commenter", func(t *testing.T) {
th := SetupTestHelperLocalMode(t)
defer th.TearDown()
clients := setupLocalClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(t, th, "commenter", testData)
runTestCases(t, ttCases, testData, clients)
})
t.Run("minimum role editor", func(t *testing.T) {
th := SetupTestHelperLocalMode(t)
defer th.TearDown()
clients := setupLocalClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(t, th, "editor", testData)
runTestCases(t, ttCases, testData, clients)
})
t.Run("minimum role admin", func(t *testing.T) {
th := SetupTestHelperLocalMode(t)
defer th.TearDown()
clients := setupLocalClients(th)
testData := setupData(t, th)
ttCases := ttCasesF(t, th, "admin", testData)
runTestCases(t, ttCases, testData, clients)
})
})
}

View file

@ -7,12 +7,21 @@ import (
)
type BoardType string
type BoardRole string
const (
BoardTypeOpen BoardType = "O"
BoardTypePrivate BoardType = "P"
)
const (
BoardRoleNone BoardRole = ""
BoardRoleViewer BoardRole = "viewer"
BoardRoleCommenter BoardRole = "commenter"
BoardRoleEditor BoardRole = "editor"
BoardRoleAdmin BoardRole = "admin"
)
// Board groups a set of blocks and its layout
// swagger:model
type Board struct {
@ -40,6 +49,10 @@ type Board struct {
// required: true
Type BoardType `json:"type"`
// The minimum role applied when somebody joins the board
// required: true
MinimumRole BoardRole `json:"minimumRole"`
// The title of the board
// required: false
Title string `json:"title"`
@ -92,6 +105,10 @@ type BoardPatch struct {
// required: false
Type *BoardType `json:"type"`
// The minimum role applied when somebody joins the board
// required: false
MinimumRole *BoardRole `json:"minimumRole"`
// The title of the board
// required: false
Title *string `json:"title"`
@ -140,6 +157,10 @@ type BoardMember struct {
// required: false
Roles string `json:"roles"`
// Minimum role because the board configuration
// required: false
MinimumRole string `json:"minimumRole"`
// Marks the user as an admin of the board
// required: true
SchemeAdmin bool `json:"schemeAdmin"`
@ -221,6 +242,10 @@ func (p *BoardPatch) Patch(board *Board) *Board {
board.Title = *p.Title
}
if p.MinimumRole != nil {
board.MinimumRole = *p.MinimumRole
}
if p.Description != nil {
board.Description = *p.Description
}
@ -296,11 +321,19 @@ func IsBoardTypeValid(t BoardType) bool {
return t == BoardTypeOpen || t == BoardTypePrivate
}
func IsBoardMinimumRoleValid(r BoardRole) bool {
return r == BoardRoleNone || r == BoardRoleAdmin || r == BoardRoleEditor || r == BoardRoleCommenter || r == BoardRoleViewer
}
func (p *BoardPatch) IsValid() error {
if p.Type != nil && !IsBoardTypeValid(*p.Type) {
return InvalidBoardErr{"invalid-board-type"}
}
if p.MinimumRole != nil && !IsBoardMinimumRoleValid(*p.MinimumRole) {
return InvalidBoardErr{"invalid-board-minimum-role"}
}
return nil
}
@ -320,6 +353,11 @@ func (b *Board) IsValid() error {
if !IsBoardTypeValid(b.Type) {
return InvalidBoardErr{"invalid-board-type"}
}
if !IsBoardMinimumRoleValid(b.MinimumRole) {
return InvalidBoardErr{"invalid-board-minimum-role"}
}
return nil
}

View file

@ -48,6 +48,17 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
return false
}
switch member.MinimumRole {
case "admin":
member.SchemeAdmin = true
case "editor":
member.SchemeEditor = true
case "commenter":
member.SchemeCommenter = true
case "viewer":
member.SchemeViewer = true
}
switch permission {
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
return member.SchemeAdmin

View file

@ -78,6 +78,17 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
return false
}
switch member.MinimumRole {
case "admin":
member.SchemeAdmin = true
case "editor":
member.SchemeEditor = true
case "commenter":
member.SchemeCommenter = true
case "viewer":
member.SchemeViewer = true
}
switch permission {
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
return member.SchemeAdmin

View file

@ -31,6 +31,7 @@ func boardFields(prefix string) []string {
"COALESCE(created_by, '')",
"modified_by",
"type",
"minimum_role",
"title",
"description",
"icon",
@ -67,6 +68,7 @@ func boardHistoryFields() []string {
"COALESCE(created_by, '')",
"COALESCE(modified_by, '')",
"type",
"minimum_role",
"COALESCE(title, '')",
"COALESCE(description, '')",
"COALESCE(icon, '')",
@ -84,13 +86,14 @@ func boardHistoryFields() []string {
}
var boardMemberFields = []string{
"board_id",
"user_id",
"roles",
"scheme_admin",
"scheme_editor",
"scheme_commenter",
"scheme_viewer",
"COALESCE(B.minimum_role, '')",
"BM.board_id",
"BM.user_id",
"BM.roles",
"BM.scheme_admin",
"BM.scheme_editor",
"BM.scheme_commenter",
"BM.scheme_viewer",
}
func (s *SQLStore) boardsFromRows(rows *sql.Rows) ([]*model.Board, error) {
@ -108,6 +111,7 @@ func (s *SQLStore) boardsFromRows(rows *sql.Rows) ([]*model.Board, error) {
&board.CreatedBy,
&board.ModifiedBy,
&board.Type,
&board.MinimumRole,
&board.Title,
&board.Description,
&board.Icon,
@ -149,6 +153,7 @@ func (s *SQLStore) boardMembersFromRows(rows *sql.Rows) ([]*model.BoardMember, e
var boardMember model.BoardMember
err := rows.Scan(
&boardMember.MinimumRole,
&boardMember.BoardID,
&boardMember.UserID,
&boardMember.Roles,
@ -308,6 +313,7 @@ func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID stri
"modified_by": userID,
"type": board.Type,
"title": board.Title,
"minimum_role": board.MinimumRole,
"description": board.Description,
"icon": board.Icon,
"show_description": board.ShowDescription,
@ -325,6 +331,7 @@ func (s *SQLStore) insertBoard(db sq.BaseRunner, board *model.Board, userID stri
Where(sq.Eq{"id": board.ID}).
Set("modified_by", userID).
Set("type", board.Type).
Set("minimum_role", board.MinimumRole).
Set("title", board.Title).
Set("description", board.Description).
Set("icon", board.Icon).
@ -398,6 +405,7 @@ func (s *SQLStore) deleteBoard(db sq.BaseRunner, boardID, userID string) error {
"created_by": board.CreatedBy,
"modified_by": userID,
"type": board.Type,
"minimum_role": board.MinimumRole,
"title": board.Title,
"description": board.Description,
"icon": board.Icon,
@ -536,9 +544,10 @@ func (s *SQLStore) deleteMember(db sq.BaseRunner, boardID, userID string) error
func (s *SQLStore) getMemberForBoard(db sq.BaseRunner, boardID, userID string) (*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members").
Where(sq.Eq{"board_id": boardID}).
Where(sq.Eq{"user_id": userID})
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.board_id": boardID}).
Where(sq.Eq{"BM.user_id": userID})
rows, err := query.Query()
if err != nil {
@ -562,8 +571,9 @@ func (s *SQLStore) getMemberForBoard(db sq.BaseRunner, boardID, userID string) (
func (s *SQLStore) getMembersForUser(db sq.BaseRunner, userID string) ([]*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members").
Where(sq.Eq{"user_id": userID})
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.user_id": userID})
rows, err := query.Query()
if err != nil {
@ -583,8 +593,9 @@ func (s *SQLStore) getMembersForUser(db sq.BaseRunner, userID string) ([]*model.
func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*model.BoardMember, error) {
query := s.getQueryBuilder(db).
Select(boardMemberFields...).
From(s.tablePrefix + "board_members").
Where(sq.Eq{"board_id": boardID})
From(s.tablePrefix + "board_members AS BM").
LeftJoin(s.tablePrefix + "boards AS B ON B.id=BM.board_id").
Where(sq.Eq{"BM.board_id": boardID})
rows, err := query.Query()
if err != nil {
@ -711,6 +722,7 @@ func (s *SQLStore) undeleteBoard(db sq.BaseRunner, boardID string, modifiedBy st
"modified_by",
"type",
"title",
"minimum_role",
"description",
"icon",
"show_description",
@ -730,6 +742,7 @@ func (s *SQLStore) undeleteBoard(db sq.BaseRunner, boardID string, modifiedBy st
board.CreatedBy,
modifiedBy,
board.Type,
board.MinimumRole,
board.Title,
board.Description,
board.Icon,

View file

@ -0,0 +1,3 @@
ALTER TABLE {{.prefix}}boards DROP COLUMN minimum_role;
ALTER TABLE {{.prefix}}boards_history DROP COLUMN minimum_role;

View file

@ -0,0 +1,4 @@
ALTER TABLE {{.prefix}}boards ADD COLUMN minimum_role VARCHAR(36) NOT NULL DEFAULT '';
ALTER TABLE {{.prefix}}boards_history ADD COLUMN minimum_role VARCHAR(36) NOT NULL DEFAULT '';
UPDATE {{.prefix}}boards SET minimum_role = 'editor';
UPDATE {{.prefix}}boards_history SET minimum_role = 'editor';

View file

@ -20,6 +20,7 @@ type Board = {
createdBy: string
modifiedBy: string
type: BoardTypes
minimumRole: string
title: string
description: string
@ -37,6 +38,7 @@ type Board = {
type BoardPatch = {
type?: BoardTypes
minimumRole?: string
title?: string
description?: string
icon?: string
@ -120,6 +122,7 @@ function createBoard(board?: Board): Board {
createdBy: board?.createdBy || '',
modifiedBy: board?.modifiedBy || '',
type: board?.type || BoardTypePrivate,
minimumRole: board?.minimumRole || '',
title: board?.title || '',
description: board?.description || '',
icon: board?.icon || '',

View file

@ -67,6 +67,7 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
description: 'test',
showDescription: false,
type: 'board',
minimumRole: 'editor',
isTemplate: true,
templateVersion: 0,
icon: '🚴🏻‍♂️',
@ -84,6 +85,7 @@ describe('components/boardTemplateSelector/boardTemplateSelectorItem', () => {
updateAt: 20,
deleteAt: 0,
type: 'board',
minimumRole: 'editor',
icon: '🚴🏻‍♂️',
description: 'test',
showDescription: false,

View file

@ -21,13 +21,14 @@ import BoardPermissionGate from '../permissions/boardPermissionGate'
import mutator from '../../mutator'
function updateBoardType(board: Board, newType: string) {
if (board.type === newType) {
function updateBoardType(board: Board, newType: string, newMinimumRole: string) {
if (board.type === newType && board.minimumRole == newMinimumRole) {
return
}
const newBoard = createBoard(board)
newBoard.type = newType
newBoard.minimumRole = newMinimumRole
mutator.updateBoard(newBoard, board, 'update board type')
}
@ -37,7 +38,16 @@ const TeamPermissionsRow = (): JSX.Element => {
const team = useAppSelector(getCurrentTeam)
const board = useAppSelector(getCurrentBoard)
const currentRole = board.type === BoardTypeOpen ? 'Editor' : 'None'
let currentRoleName = intl.formatMessage({id: 'BoardMember.schemeNone', defaultMessage: 'None'})
if (board.type === BoardTypeOpen && board.minimumRole === 'admin') {
currentRoleName = intl.formatMessage({id: 'BoardMember.schemeAdmin', defaultMessage: 'Admin'})
}else if (board.type === BoardTypeOpen && board.minimumRole === 'editor') {
currentRoleName = intl.formatMessage({id: 'BoardMember.schemeEditor', defaultMessage: 'Editor'})
}else if (board.type === BoardTypeOpen && board.minimumRole === 'commenter') {
currentRoleName = intl.formatMessage({id: 'BoardMember.schemeCommenter', defaultMessage: 'Commenter'})
}else if (board.type === BoardTypeOpen && board.minimumRole === 'viewer') {
currentRoleName = intl.formatMessage({id: 'BoardMember.schemeViewer', defaultMessage: 'Viewer'})
}
return (
<div className='user-item'>
@ -54,26 +64,47 @@ const TeamPermissionsRow = (): JSX.Element => {
<BoardPermissionGate permissions={[Permission.ManageBoardType]}>
<MenuWrapper>
<button className='user-item__button'>
{currentRole}
{currentRoleName}
<CompassIcon
icon='chevron-down'
className='CompassIcon'
/>
</button>
<Menu position='left'>
<Menu.Text
id='Admin'
check={board.minimumRole === 'admin'}
icon={board.type === BoardTypeOpen && board.minimumRole === 'admin' ? <CheckIcon/> : null}
name={intl.formatMessage({id: 'BoardMember.schemeAdmin', defaultMessage: 'Admin'})}
onClick={() => updateBoardType(board, BoardTypeOpen, 'admin')}
/>
<Menu.Text
id='Editor'
check={true}
icon={currentRole === 'Editor' ? <CheckIcon/> : null}
check={board.minimumRole === '' || board.minimumRole === 'editor' }
icon={board.type === BoardTypeOpen && board.minimumRole === 'editor' ? <CheckIcon/> : null}
name={intl.formatMessage({id: 'BoardMember.schemeEditor', defaultMessage: 'Editor'})}
onClick={() => updateBoardType(board, BoardTypeOpen)}
onClick={() => updateBoardType(board, BoardTypeOpen, 'editor')}
/>
<Menu.Text
id='Commenter'
check={board.minimumRole === 'commenter'}
icon={board.type === BoardTypeOpen && board.minimumRole === 'commenter' ? <CheckIcon/> : null}
name={intl.formatMessage({id: 'BoardMember.schemeCommenter', defaultMessage: 'Commenter'})}
onClick={() => updateBoardType(board, BoardTypeOpen, 'commenter')}
/>
<Menu.Text
id='Viewer'
check={board.minimumRole === 'viewer'}
icon={board.type === BoardTypeOpen && board.minimumRole === 'viewer' ? <CheckIcon/> : null}
name={intl.formatMessage({id: 'BoardMember.schemeViwer', defaultMessage: 'Viewer'})}
onClick={() => updateBoardType(board, BoardTypeOpen, 'viewer')}
/>
<Menu.Text
id='None'
check={true}
icon={currentRole === 'None' ? <CheckIcon/> : null}
icon={board.type === BoardTypePrivate ? <CheckIcon/> : null}
name={intl.formatMessage({id: 'BoardMember.schemeNone', defaultMessage: 'None'})}
onClick={() => updateBoardType(board, BoardTypePrivate)}
onClick={() => updateBoardType(board, BoardTypePrivate, 'editor')}
/>
</Menu>
</MenuWrapper>
@ -82,7 +113,7 @@ const TeamPermissionsRow = (): JSX.Element => {
permissions={[Permission.ManageBoardType]}
invert={true}
>
<span>{currentRole}</span>
<span>{currentRoleName}</span>
</BoardPermissionGate>
</div>
</div>