initial implementation of SysAdmin/TeamAdmin feature (#4537)
* initial implementation of SysAdmin/TeamAdmin feature * fix adminBadge tests * updating tests * more fixes for unit tests * lint fixes * update snapshots * update cypress test for call change * add additional unit tests * update test for lint errors * fix reviews implement tests * fix for merge, reset dialog before redirection * remove unused test code * fix more tests * fix swagger doc for missing parameters --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
759a8bb76a
commit
098868387e
32 changed files with 1031 additions and 80 deletions
|
@ -206,6 +206,11 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
|||
// description: Board ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: allow_admin
|
||||
// in: path
|
||||
// description: allows admin users to join private boards
|
||||
// required: false
|
||||
// type: boolean
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
|
@ -222,6 +227,9 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
|||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
query := r.URL.Query()
|
||||
allowAdmin := query.Has("allow_admin")
|
||||
|
||||
userID := getUserID(r)
|
||||
if userID == "" {
|
||||
a.errorResponse(w, r, model.NewErrBadRequest("missing user ID"))
|
||||
|
@ -234,9 +242,14 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
|||
a.errorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := false
|
||||
if board.Type != model.BoardTypeOpen {
|
||||
a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board"))
|
||||
return
|
||||
if !allowAdmin || !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) {
|
||||
a.errorResponse(w, r, model.NewErrPermission("cannot join a non Open board"))
|
||||
return
|
||||
}
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
|
@ -257,7 +270,7 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
|||
newBoardMember := &model.BoardMember{
|
||||
UserID: userID,
|
||||
BoardID: boardID,
|
||||
SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin,
|
||||
SchemeAdmin: board.MinimumRole == model.BoardRoleAdmin || isAdmin,
|
||||
SchemeEditor: board.MinimumRole == model.BoardRoleNone || board.MinimumRole == model.BoardRoleEditor,
|
||||
SchemeCommenter: board.MinimumRole == model.BoardRoleCommenter,
|
||||
SchemeViewer: board.MinimumRole == model.BoardRoleViewer,
|
||||
|
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
@ -15,6 +16,7 @@ func (a *API) registerTeamsRoutes(r *mux.Router) {
|
|||
r.HandleFunc("/teams", a.sessionRequired(a.handleGetTeams)).Methods("GET")
|
||||
r.HandleFunc("/teams/{teamID}", a.sessionRequired(a.handleGetTeam)).Methods("GET")
|
||||
r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsers)).Methods("GET")
|
||||
r.HandleFunc("/teams/{teamID}/users", a.sessionRequired(a.handleGetTeamUsersByID)).Methods("POST")
|
||||
r.HandleFunc("/teams/{teamID}/archive/export", a.sessionRequired(a.handleArchiveExportTeam)).Methods("GET")
|
||||
}
|
||||
|
||||
|
@ -257,3 +259,106 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
|
|||
auditRec.AddMeta("userCount", len(users))
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleGetTeamUsersByID(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation POST /teams/{teamID}/users getTeamUsersByID
|
||||
//
|
||||
// Returns a user[]
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: Team ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: Body
|
||||
// in: body
|
||||
// description: []UserIDs to return
|
||||
// required: true
|
||||
// type: []string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/User"
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
requestBody, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
var userIDs []string
|
||||
if err = json.Unmarshal(requestBody, &userIDs); err != nil {
|
||||
a.errorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "getTeamUsersByID", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
|
||||
vars := mux.Vars(r)
|
||||
teamID := vars["teamID"]
|
||||
userID := getUserID(r)
|
||||
|
||||
if !a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r, model.NewErrPermission("access denied to team"))
|
||||
return
|
||||
}
|
||||
|
||||
var users []*model.User
|
||||
var error error
|
||||
|
||||
if len(userIDs) == 0 {
|
||||
a.errorResponse(w, r, model.NewErrBadRequest("User IDs are empty"))
|
||||
return
|
||||
}
|
||||
|
||||
if userIDs[0] == model.SingleUser {
|
||||
ws, _ := a.app.GetRootTeam()
|
||||
now := utils.GetMillis()
|
||||
user := &model.User{
|
||||
ID: model.SingleUser,
|
||||
Username: model.SingleUser,
|
||||
Email: model.SingleUser,
|
||||
CreateAt: ws.UpdateAt,
|
||||
UpdateAt: now,
|
||||
}
|
||||
users = append(users, user)
|
||||
} else {
|
||||
users, error = a.app.GetUsersList(userIDs)
|
||||
if error != nil {
|
||||
a.errorResponse(w, r, error)
|
||||
return
|
||||
}
|
||||
|
||||
for i, u := range users {
|
||||
if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) {
|
||||
users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id)
|
||||
}
|
||||
if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) {
|
||||
users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usersList, err := json.Marshal(users)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonStringResponse(w, http.StatusOK, string(usersList))
|
||||
auditRec.Success()
|
||||
}
|
||||
|
|
|
@ -107,6 +107,12 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
|||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: teamID
|
||||
// in: path
|
||||
// description: Team ID
|
||||
// required: false
|
||||
// type: string
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
|
@ -118,6 +124,8 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
|||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
query := r.URL.Query()
|
||||
teamID := query.Get("teamID")
|
||||
|
||||
userID := getUserID(r)
|
||||
|
||||
|
@ -146,6 +154,13 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
if teamID != "" && a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionManageTeam) {
|
||||
user.Permissions = append(user.Permissions, model.PermissionManageTeam.Id)
|
||||
}
|
||||
if a.permissions.HasPermissionTo(userID, model.PermissionManageSystem) {
|
||||
user.Permissions = append(user.Permissions, model.PermissionManageSystem.Id)
|
||||
}
|
||||
|
||||
userData, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r, err)
|
||||
|
|
|
@ -502,11 +502,48 @@ func (a *App) DeleteBoard(boardID, userID string) error {
|
|||
}
|
||||
|
||||
func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) {
|
||||
return a.store.GetMembersForBoard(boardID)
|
||||
members, err := a.store.GetMembersForBoard(boardID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
board, err := a.store.GetBoard(boardID)
|
||||
if err != nil && !model.IsErrNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if board != nil {
|
||||
for i, m := range members {
|
||||
if !m.SchemeAdmin {
|
||||
if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) {
|
||||
members[i].SchemeAdmin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) {
|
||||
return a.store.GetMembersForUser(userID)
|
||||
members, err := a.store.GetMembersForUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, m := range members {
|
||||
if !m.SchemeAdmin {
|
||||
board, err := a.store.GetBoard(m.BoardID)
|
||||
if err != nil && !model.IsErrNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
if board != nil {
|
||||
if a.permissions.HasPermissionToTeam(m.UserID, board.TeamID, model.PermissionManageTeam) {
|
||||
// if system/team admin
|
||||
members[i].SchemeAdmin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (a *App) GetMemberForBoard(boardID string, userID string) (*model.BoardMember, error) {
|
||||
|
@ -536,6 +573,14 @@ func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, e
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if !newMember.SchemeAdmin {
|
||||
if board != nil {
|
||||
if a.permissions.HasPermissionToTeam(newMember.UserID, board.TeamID, model.PermissionManageTeam) {
|
||||
newMember.SchemeAdmin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !board.IsTemplate {
|
||||
if err = a.addBoardsToDefaultCategory(member.UserID, board.TeamID, []*model.Board{board}); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -127,6 +127,7 @@ func TestAddMemberToBoard(t *testing.T) {
|
|||
},
|
||||
}, nil).Times(2)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id_1", "default_category_id", []string{"board_id_1"}).Return(nil)
|
||||
th.API.EXPECT().HasPermissionToTeam("user_id_1", "team_id_1", model.PermissionManageTeam).Return(false).Times(1)
|
||||
|
||||
addedBoardMember, err := th.App.AddMemberToBoard(boardMember)
|
||||
require.NoError(t, err)
|
||||
|
@ -180,7 +181,7 @@ func TestPatchBoard(t *testing.T) {
|
|||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(2)
|
||||
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil)
|
||||
|
@ -218,7 +219,7 @@ func TestPatchBoard(t *testing.T) {
|
|||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(2)
|
||||
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{}, nil)
|
||||
|
@ -256,7 +257,7 @@ func TestPatchBoard(t *testing.T) {
|
|||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(2)
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
|
||||
|
@ -294,7 +295,7 @@ func TestPatchBoard(t *testing.T) {
|
|||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(2)
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
|
||||
|
@ -332,7 +333,10 @@ func TestPatchBoard(t *testing.T) {
|
|||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
}, nil).Times(3)
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
|
||||
|
@ -370,7 +374,11 @@ func TestPatchBoard(t *testing.T) {
|
|||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
IsTemplate: true,
|
||||
}, nil)
|
||||
ChannelID: "",
|
||||
}, nil).Times(1)
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
|
||||
// Type not null will retrieve team members
|
||||
th.Store.EXPECT().GetUsersByTeam(teamID, "", false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
|
||||
|
@ -566,3 +574,99 @@ func TestDuplicateBoard(t *testing.T) {
|
|||
assert.NotNil(t, members)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMembersForBoard(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
const boardID = "board_id_1"
|
||||
const userID = "user_id_1"
|
||||
const teamID = "team_id_1"
|
||||
|
||||
th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{
|
||||
{
|
||||
BoardID: boardID,
|
||||
UserID: userID,
|
||||
SchemeEditor: true,
|
||||
},
|
||||
}, nil).Times(3)
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(nil, nil).Times(1)
|
||||
t.Run("-base case", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForBoard(boardID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
assert.False(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
board := &model.Board{
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
}
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2)
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
|
||||
t.Run("-team check false ", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForBoard(boardID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
|
||||
assert.False(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1)
|
||||
t.Run("-team check true", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForBoard(boardID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
|
||||
assert.True(t, members[0].SchemeAdmin)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMembersForUser(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
const boardID = "board_id_1"
|
||||
const userID = "user_id_1"
|
||||
const teamID = "team_id_1"
|
||||
|
||||
th.Store.EXPECT().GetMembersForUser(userID).Return([]*model.BoardMember{
|
||||
{
|
||||
BoardID: boardID,
|
||||
UserID: userID,
|
||||
SchemeEditor: true,
|
||||
},
|
||||
}, nil).Times(3)
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(nil, nil)
|
||||
t.Run("-base case", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForUser(userID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
assert.False(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
board := &model.Board{
|
||||
ID: boardID,
|
||||
TeamID: teamID,
|
||||
}
|
||||
th.Store.EXPECT().GetBoard(boardID).Return(board, nil).Times(2)
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
t.Run("-team check false ", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForUser(userID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
|
||||
assert.False(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1)
|
||||
t.Run("-team check true", func(t *testing.T) {
|
||||
members, err := th.App.GetMembersForUser(userID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, members)
|
||||
|
||||
assert.True(t, members[0].SchemeAdmin)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ func TestGetUserCategoryBoards(t *testing.T) {
|
|||
Synthetic: false,
|
||||
},
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil)
|
||||
|
||||
categoryBoards, err := th.App.GetUserCategoryBoards("user_id", "team_id")
|
||||
|
@ -151,6 +152,7 @@ func TestCreateBoardsCategory(t *testing.T) {
|
|||
Synthetic: true,
|
||||
},
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3)
|
||||
|
||||
existingCategoryBoards := []model.CategoryBoards{}
|
||||
boardsCategory, err := th.App.createBoardsCategory("user_id", "team_id", existingCategoryBoards)
|
||||
|
@ -195,6 +197,7 @@ func TestCreateBoardsCategory(t *testing.T) {
|
|||
Synthetic: false,
|
||||
},
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1", "board_id_2", "board_id_3"}).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
|
||||
|
@ -244,6 +247,7 @@ func TestCreateBoardsCategory(t *testing.T) {
|
|||
Synthetic: true,
|
||||
},
|
||||
}, nil)
|
||||
th.Store.EXPECT().GetBoard(utils.Anything).Return(nil, nil).Times(3)
|
||||
th.Store.EXPECT().AddUpdateCategoryBoard("user_id", "boards_category_id", []string{"board_id_1"}).Return(nil)
|
||||
|
||||
th.Store.EXPECT().GetUserCategoryBoards("user_id", "team_id").Return([]model.CategoryBoards{
|
||||
|
|
|
@ -10,6 +10,9 @@ import (
|
|||
"github.com/mattermost/focalboard/server/auth"
|
||||
"github.com/mattermost/focalboard/server/services/config"
|
||||
"github.com/mattermost/focalboard/server/services/metrics"
|
||||
"github.com/mattermost/focalboard/server/services/permissions/mmpermissions"
|
||||
mmpermissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mmpermissions/mocks"
|
||||
permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks"
|
||||
"github.com/mattermost/focalboard/server/services/store/mockstore"
|
||||
"github.com/mattermost/focalboard/server/services/webhook"
|
||||
"github.com/mattermost/focalboard/server/ws"
|
||||
|
@ -23,6 +26,7 @@ type TestHelper struct {
|
|||
Store *mockstore.MockStore
|
||||
FilesBackend *mocks.FileBackend
|
||||
logger mlog.LoggerIFace
|
||||
API *mmpermissionsMocks.MockAPI
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
|
@ -37,6 +41,10 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
|||
webhook := webhook.NewClient(&cfg, logger)
|
||||
metricsService := metrics.NewMetrics(metrics.InstanceInfo{})
|
||||
|
||||
mockStore := permissionsMocks.NewMockStore(ctrl)
|
||||
mockAPI := mmpermissionsMocks.NewMockAPI(ctrl)
|
||||
permissions := mmpermissions.New(mockStore, mockAPI, mlog.CreateConsoleTestLogger(true, mlog.LvlError))
|
||||
|
||||
appServices := Services{
|
||||
Auth: auth,
|
||||
Store: store,
|
||||
|
@ -45,6 +53,7 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
|||
Metrics: metricsService,
|
||||
Logger: logger,
|
||||
SkipTemplateInit: true,
|
||||
Permissions: permissions,
|
||||
}
|
||||
app2 := New(&cfg, wsserver, appServices)
|
||||
|
||||
|
@ -60,5 +69,6 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
|||
Store: store,
|
||||
FilesBackend: filesBackend,
|
||||
logger: logger,
|
||||
API: mockAPI,
|
||||
}, tearDown
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestPrepareOnboardingTour(t *testing.T) {
|
|||
nil, nil)
|
||||
th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil).Times(2)
|
||||
th.Store.EXPECT().GetMembersForBoard("board_id_2").Return([]*model.BoardMember{}, nil).Times(1)
|
||||
th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(1)
|
||||
th.Store.EXPECT().GetBoard(welcomeBoard.ID).Return(&welcomeBoard, nil).Times(2)
|
||||
th.Store.EXPECT().GetBoard("board_id_2").Return(&welcomeBoard, nil).Times(1)
|
||||
th.Store.EXPECT().GetUsersByTeam("0", "", false, false).Return([]*model.User{}, nil)
|
||||
|
||||
|
|
|
@ -10,7 +10,20 @@ func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, erro
|
|||
}
|
||||
|
||||
func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string, excludeBots bool) ([]*model.User, error) {
|
||||
return a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName)
|
||||
users, err := a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID, excludeBots, a.config.ShowEmailAddress, a.config.ShowFullName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, u := range users {
|
||||
if a.permissions.HasPermissionToTeam(u.ID, teamID, model.PermissionManageTeam) {
|
||||
users[i].Permissions = append(users[i].Permissions, model.PermissionManageTeam.Id)
|
||||
}
|
||||
if a.permissions.HasPermissionTo(u.ID, model.PermissionManageSystem) {
|
||||
users[i].Permissions = append(users[i].Permissions, model.PermissionManageSystem.Id)
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateUserConfig(userID string, patch model.UserPreferencesPatch) ([]mmModel.Preference, error) {
|
||||
|
|
64
server/app/user_test.go
Normal file
64
server/app/user_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSearchUsers(t *testing.T) {
|
||||
th, tearDown := SetupTestHelper(t)
|
||||
defer tearDown()
|
||||
th.App.config.ShowEmailAddress = false
|
||||
th.App.config.ShowFullName = false
|
||||
|
||||
teamID := "team-id-1"
|
||||
userID := "user-id-1"
|
||||
|
||||
t.Run("return empty users", func(t *testing.T) {
|
||||
th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{}, nil)
|
||||
|
||||
users, err := th.App.SearchTeamUsers(teamID, "", "", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(users))
|
||||
})
|
||||
|
||||
t.Run("return user", func(t *testing.T) {
|
||||
th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(false).Times(1)
|
||||
th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1)
|
||||
|
||||
users, err := th.App.SearchTeamUsers(teamID, "", "", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
assert.Equal(t, 0, len(users[0].Permissions))
|
||||
})
|
||||
|
||||
t.Run("return team admin", func(t *testing.T) {
|
||||
th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
th.App.config.ShowEmailAddress = false
|
||||
th.App.config.ShowFullName = false
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1)
|
||||
th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(false).Times(1)
|
||||
|
||||
users, err := th.App.SearchTeamUsers(teamID, "", "", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id)
|
||||
})
|
||||
|
||||
t.Run("return system admin", func(t *testing.T) {
|
||||
th.Store.EXPECT().SearchUsersByTeam(teamID, "", "", true, false, false).Return([]*model.User{{ID: userID}}, nil)
|
||||
th.App.config.ShowEmailAddress = false
|
||||
th.App.config.ShowFullName = false
|
||||
th.API.EXPECT().HasPermissionToTeam(userID, teamID, model.PermissionManageTeam).Return(true).Times(1)
|
||||
th.API.EXPECT().HasPermissionTo(userID, model.PermissionManageSystem).Return(true).Times(1)
|
||||
|
||||
users, err := th.App.SearchTeamUsers(teamID, "", "", true)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
assert.Equal(t, users[0].Permissions[0], model.PermissionManageTeam.Id)
|
||||
assert.Equal(t, users[0].Permissions[1], model.PermissionManageSystem.Id)
|
||||
})
|
||||
}
|
|
@ -78,6 +78,9 @@ func (*FakePermissionPluginAPI) HasPermissionTo(userID string, permission *mmMod
|
|||
}
|
||||
|
||||
func (*FakePermissionPluginAPI) HasPermissionToTeam(userID string, teamID string, permission *mmModel.Permission) bool {
|
||||
if permission.Id == model.PermissionManageTeam.Id {
|
||||
return false
|
||||
}
|
||||
if userID == userNoTeamMember {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
|
||||
var (
|
||||
PermissionViewTeam = mmModel.PermissionViewTeam
|
||||
PermissionManageTeam = mmModel.PermissionManageTeam
|
||||
PermissionManageSystem = mmModel.PermissionManageSystem
|
||||
PermissionReadChannel = mmModel.PermissionReadChannel
|
||||
PermissionViewMembers = mmModel.PermissionViewMembers
|
||||
PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel
|
||||
|
|
|
@ -66,6 +66,9 @@ type User struct {
|
|||
// required: true
|
||||
IsGuest bool `json:"is_guest"`
|
||||
|
||||
// Special Permissions the user may have
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
|
||||
Roles string `json:"roles"`
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,9 @@ func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel
|
|||
if userID == "" || teamID == "" || permission == nil {
|
||||
return false
|
||||
}
|
||||
if permission.Id == model.PermissionManageTeam.Id {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,11 @@ func TestHasPermissionToTeam(t *testing.T) {
|
|||
hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
|
||||
t.Run("no users have PermissionManageTeam on teams", func(t *testing.T) {
|
||||
hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageTeam)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHasPermissionToBoard(t *testing.T) {
|
||||
|
@ -141,4 +146,27 @@ func TestHasPermissionToBoard(t *testing.T) {
|
|||
|
||||
th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
|
||||
t.Run("Manage Team Permission ", func(t *testing.T) {
|
||||
member := &model.BoardMember{
|
||||
UserID: "user-id",
|
||||
BoardID: "board-id",
|
||||
SchemeViewer: true,
|
||||
}
|
||||
|
||||
hasPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionViewBoard,
|
||||
}
|
||||
|
||||
hasNotPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardType,
|
||||
model.PermissionDeleteBoard,
|
||||
model.PermissionManageBoardRoles,
|
||||
model.PermissionShareBoard,
|
||||
model.PermissionManageBoardCards,
|
||||
model.PermissionManageBoardProperties,
|
||||
}
|
||||
|
||||
th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -58,6 +58,13 @@ func (th *TestHelper) checkBoardPermissions(roleName string, member *model.Board
|
|||
Return(member, nil).
|
||||
Times(1)
|
||||
|
||||
if !member.SchemeAdmin {
|
||||
th.api.EXPECT().
|
||||
HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam).
|
||||
Return(roleName == "elevated-admin").
|
||||
Times(1)
|
||||
}
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
|
@ -80,6 +87,13 @@ func (th *TestHelper) checkBoardPermissions(roleName string, member *model.Board
|
|||
Return(member, nil).
|
||||
Times(1)
|
||||
|
||||
if !member.SchemeAdmin {
|
||||
th.api.EXPECT().
|
||||
HasPermissionToTeam(member.UserID, teamID, model.PermissionManageTeam).
|
||||
Return(roleName == "elevated-admin").
|
||||
Times(1)
|
||||
}
|
||||
|
||||
hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
|
|
|
@ -82,7 +82,6 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
|||
if !s.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
return false
|
||||
}
|
||||
|
||||
member, err := s.store.GetMemberForBoard(boardID, userID)
|
||||
if model.IsErrNotFound(err) {
|
||||
return false
|
||||
|
@ -107,6 +106,13 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
|||
member.SchemeViewer = true
|
||||
}
|
||||
|
||||
// Admins become member of boards, but get minimal role
|
||||
// if they are a System/Team Admin (model.PermissionManageTeam)
|
||||
// elevate their permissions
|
||||
if !member.SchemeAdmin && s.HasPermissionToTeam(userID, board.TeamID, model.PermissionManageTeam) {
|
||||
return true
|
||||
}
|
||||
|
||||
switch permission {
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
|
||||
return member.SchemeAdmin
|
||||
|
|
|
@ -219,4 +219,25 @@ func TestHasPermissionToBoard(t *testing.T) {
|
|||
|
||||
th.checkBoardPermissions("viewer", member, teamID, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
|
||||
t.Run("elevate board viewer permissions", func(t *testing.T) {
|
||||
member := &model.BoardMember{
|
||||
UserID: userID,
|
||||
BoardID: boardID,
|
||||
SchemeViewer: true,
|
||||
}
|
||||
|
||||
hasPermissionTo := []*mmModel.Permission{
|
||||
model.PermissionManageBoardType,
|
||||
model.PermissionDeleteBoard,
|
||||
model.PermissionManageBoardRoles,
|
||||
model.PermissionShareBoard,
|
||||
model.PermissionManageBoardCards,
|
||||
model.PermissionViewBoard,
|
||||
model.PermissionManageBoardProperties,
|
||||
}
|
||||
|
||||
hasNotPermissionTo := []*mmModel.Permission{}
|
||||
th.checkBoardPermissions("elevated-admin", member, teamID, hasPermissionTo, hasNotPermissionTo)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1991,6 +1991,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
|
|||
>
|
||||
@username_1
|
||||
</strong>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Team Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2017,6 +2026,15 @@ exports[`src/components/shareBoard/shareBoard return shareBoard and click Select
|
|||
>
|
||||
@username_2
|
||||
</strong>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -728,3 +728,263 @@ exports[`src/components/shareBoard/userPermissionsRow should match snapshot in t
|
|||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`src/components/shareBoard/userPermissionsRow should match snapshot-admin 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
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
aria-label="menuwrapper"
|
||||
class="MenuWrapper override menuOpened"
|
||||
role="button"
|
||||
>
|
||||
<button
|
||||
class="user-item__button"
|
||||
>
|
||||
Admin
|
||||
<i
|
||||
class="CompassIcon icon-chevron-down CompassIcon"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left "
|
||||
style="top: 40px;"
|
||||
>
|
||||
<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="menu-option__icon"
|
||||
>
|
||||
<div
|
||||
class="empty-icon"
|
||||
/>
|
||||
</div>
|
||||
</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="menu-option__icon"
|
||||
>
|
||||
<div
|
||||
class="empty-icon"
|
||||
/>
|
||||
</div>
|
||||
</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="menu-option__icon"
|
||||
>
|
||||
<div
|
||||
class="empty-icon"
|
||||
/>
|
||||
</div>
|
||||
</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"
|
||||
>
|
||||
<div
|
||||
class="menu-option__icon"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
`;
|
||||
|
|
|
@ -548,8 +548,8 @@ describe('src/components/shareBoard/shareBoard', () => {
|
|||
}
|
||||
mockedOctoClient.getSharing.mockResolvedValue(sharing)
|
||||
const users: IUser[] = [
|
||||
{id: 'userid1', username: 'username_1'} as IUser,
|
||||
{id: 'userid2', username: 'username_2'} as IUser,
|
||||
{id: 'userid1', username: 'username_1', permissions: ['manage_team']} as IUser,
|
||||
{id: 'userid2', username: 'username_2', permissions: ['manage_system']} as IUser,
|
||||
{id: 'userid3', username: 'username_3'} as IUser,
|
||||
{id: 'userid4', username: 'username_4'} as IUser,
|
||||
]
|
||||
|
|
|
@ -32,6 +32,7 @@ import Button from '../../widgets/buttons/button'
|
|||
import {sendFlashMessage} from '../flashMessages'
|
||||
import {Permission} from '../../constants'
|
||||
import GuestBadge from '../../widgets/guestBadge'
|
||||
import AdminBadge from '../../widgets/adminBadge/adminBadge'
|
||||
|
||||
import TelemetryClient, {TelemetryActions, TelemetryCategory} from '../../telemetry/telemetryClient'
|
||||
|
||||
|
@ -310,6 +311,7 @@ export default function ShareBoardDialog(props: Props): JSX.Element {
|
|||
<strong>{Utils.getUserDisplayName(user, clientConfig.teammateNameDisplay)}</strong>
|
||||
<strong className='ml-2 text-light'>{`@${user.username}`}</strong>
|
||||
<GuestBadge show={Boolean(user?.is_guest)}/>
|
||||
<AdminBadge permissions={user.permissions}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -104,6 +104,38 @@ describe('src/components/shareBoard/userPermissionsRow', () => {
|
|||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match snapshot-admin', async () => {
|
||||
let container: Element | undefined
|
||||
mockedUtils.isFocalboardPlugin.mockReturnValue(false)
|
||||
const store = mockStateStore([thunk], state)
|
||||
|
||||
const newMe = Object.assign({}, me)
|
||||
newMe.permissions = ['manage_system']
|
||||
await act(async () => {
|
||||
const result = render(
|
||||
wrapDNDIntl(
|
||||
<ReduxProvider store={store}>
|
||||
<UserPermissionsRow
|
||||
user={newMe}
|
||||
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)
|
||||
|
|
|
@ -15,6 +15,7 @@ import {IUser} from '../../user'
|
|||
import {Utils} from '../../utils'
|
||||
import {Permission} from '../../constants'
|
||||
import GuestBadge from '../../widgets/guestBadge'
|
||||
import AdminBadge from '../../widgets/adminBadge/adminBadge'
|
||||
import {useAppSelector} from '../../store/hooks'
|
||||
import {getCurrentBoard} from '../../store/boards'
|
||||
|
||||
|
@ -65,6 +66,7 @@ const UserPermissionsRow = (props: Props): JSX.Element => {
|
|||
<strong className='ml-2 text-light'>{`@${user.username}`}</strong>
|
||||
{isMe && <strong className='ml-2 text-light'>{intl.formatMessage({id: 'ShareBoard.userPermissionsYouText', defaultMessage: '(You)'})}</strong>}
|
||||
<GuestBadge show={user.is_guest}/>
|
||||
<AdminBadge permissions={user.permissions}/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -160,7 +160,10 @@ class OctoClient {
|
|||
}
|
||||
|
||||
async getMe(): Promise<IUser | undefined> {
|
||||
const path = '/api/v2/users/me'
|
||||
let path = '/api/v2/users/me'
|
||||
if (this.teamId !== Constants.globalTeamId) {
|
||||
path += `?teamID=${this.teamId}`
|
||||
}
|
||||
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
|
||||
if (response.status !== 200) {
|
||||
return undefined
|
||||
|
@ -467,12 +470,15 @@ class OctoClient {
|
|||
return this.getJson<BoardMember>(response, {} as BoardMember)
|
||||
}
|
||||
|
||||
async joinBoard(boardId: string): Promise<BoardMember|undefined> {
|
||||
async joinBoard(boardId: string, allowAdmin: boolean): Promise<BoardMember|undefined> {
|
||||
Utils.log(`joinBoard: board ${boardId}`)
|
||||
|
||||
const response = await fetch(this.getBaseURL() + `/api/v2/boards/${boardId}/join`, {
|
||||
method: 'POST',
|
||||
let path = `/api/v2/boards/${boardId}/join`
|
||||
if (allowAdmin) {
|
||||
path += '?allow_admin'
|
||||
}
|
||||
const response = await fetch(this.getBaseURL() + path, {
|
||||
headers: this.headers(),
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
|
@ -680,6 +686,22 @@ class OctoClient {
|
|||
return (await this.getJson(response, [])) as IUser[]
|
||||
}
|
||||
|
||||
async getTeamUsersList(userIds: string[], teamId: string): Promise<IUser[] | []> {
|
||||
const path = this.teamPath(teamId) + '/users'
|
||||
const body = JSON.stringify(userIds)
|
||||
const response = await fetch(this.getBaseURL() + path, {
|
||||
headers: this.headers(),
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
return []
|
||||
}
|
||||
|
||||
return (await this.getJson(response, [])) as IUser[]
|
||||
}
|
||||
|
||||
async searchTeamUsers(searchQuery: string, excludeBots?: boolean): Promise<IUser[]> {
|
||||
let path = this.teamPath() + `/users?search=${searchQuery}`
|
||||
if (excludeBots) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import React, {useEffect, useState, useMemo, useCallback} from 'react'
|
||||
import {batch} from 'react-redux'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
import {useRouteMatch} from 'react-router-dom'
|
||||
import {useRouteMatch, useHistory} from 'react-router-dom'
|
||||
|
||||
import Workspace from '../../components/workspace'
|
||||
import CloudMessage from '../../components/messages/cloudMessage'
|
||||
|
@ -29,6 +29,7 @@ import {
|
|||
addMyBoardMemberships,
|
||||
} from '../../store/boards'
|
||||
import {getCurrentViewId, setCurrent as setCurrentView, updateViews} from '../../store/views'
|
||||
import ConfirmationDialog from '../../components/confirmationDialogBox'
|
||||
import {initialLoad, initialReadOnlyLoad, loadBoardData} from '../../store/initialLoad'
|
||||
import {useAppSelector, useAppDispatch} from '../../store/hooks'
|
||||
import {setTeam} from '../../store/teams'
|
||||
|
@ -79,6 +80,8 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||
const me = useAppSelector<IUser|null>(getMe)
|
||||
const hiddenBoardIDs = useAppSelector(getHiddenBoardIDs)
|
||||
const category = useAppSelector(getCategoryOfBoard(activeBoardId))
|
||||
const [showJoinBoardDialog, setShowJoinBoardDialog] = useState<boolean>(false)
|
||||
const history = useHistory()
|
||||
|
||||
// if we're in a legacy route and not showing a shared board,
|
||||
// redirect to the new URL schema equivalent
|
||||
|
@ -177,18 +180,40 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||
}
|
||||
}, [me?.id, activeBoardId])
|
||||
|
||||
const loadOrJoinBoard = useCallback(async (userId: string, boardTeamId: string, boardId: string) => {
|
||||
// and fetch its data
|
||||
const result: any = await dispatch(loadBoardData(boardId))
|
||||
if (result.payload.blocks.length === 0 && userId) {
|
||||
const member = await octoClient.joinBoard(boardId)
|
||||
if (!member) {
|
||||
UserSettings.setLastBoardID(boardTeamId, null)
|
||||
UserSettings.setLastViewId(boardId, null)
|
||||
dispatch(setGlobalError('board-not-found'))
|
||||
const onConfirmJoin = async () => {
|
||||
if (me) {
|
||||
joinBoard(me, teamId, match.params.boardId, true)
|
||||
setShowJoinBoardDialog(false)
|
||||
}
|
||||
}
|
||||
|
||||
const joinBoard = async (myUser: IUser, boardTeamId: string, boardId: string, allowAdmin: boolean) => {
|
||||
const member = await octoClient.joinBoard(boardId, allowAdmin)
|
||||
if (!member) {
|
||||
if (myUser.permissions?.find((s) => s === 'manage_system' || s === 'manage_team')) {
|
||||
setShowJoinBoardDialog(true)
|
||||
return
|
||||
}
|
||||
await dispatch(loadBoardData(boardId))
|
||||
UserSettings.setLastBoardID(boardTeamId, null)
|
||||
UserSettings.setLastViewId(boardId, null)
|
||||
dispatch(setGlobalError('board-not-found'))
|
||||
return
|
||||
}
|
||||
const result: any = await dispatch(loadBoardData(boardId))
|
||||
if (result.payload.blocks.length > 0 && myUser.id) {
|
||||
// set board as most recently viewed board
|
||||
UserSettings.setLastBoardID(boardTeamId, boardId)
|
||||
}
|
||||
}
|
||||
|
||||
const loadOrJoinBoard = useCallback(async (myUser: IUser, boardTeamId: string, boardId: string) => {
|
||||
// and fetch its data
|
||||
const result: any = await dispatch(loadBoardData(boardId))
|
||||
if (result.payload.blocks.length === 0 && myUser.id) {
|
||||
joinBoard(myUser, boardTeamId, boardId, false)
|
||||
} else {
|
||||
// set board as most recently viewed board
|
||||
UserSettings.setLastBoardID(boardTeamId, boardId)
|
||||
}
|
||||
|
||||
dispatch(fetchBoardMembers({
|
||||
|
@ -204,9 +229,6 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||
// set the active board
|
||||
dispatch(setCurrentBoard(match.params.boardId))
|
||||
|
||||
// and set it as most recently viewed board
|
||||
UserSettings.setLastBoardID(teamId, match.params.boardId)
|
||||
|
||||
if (viewId !== Constants.globalTeamId) {
|
||||
// reset current, even if empty string
|
||||
dispatch(setCurrentView(viewId))
|
||||
|
@ -220,7 +242,7 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||
|
||||
useEffect(() => {
|
||||
if (match.params.boardId && !props.readonly && me) {
|
||||
loadOrJoinBoard(me.id, teamId, match.params.boardId)
|
||||
loadOrJoinBoard(me, teamId, match.params.boardId)
|
||||
}
|
||||
}, [teamId, match.params.boardId, me?.id])
|
||||
|
||||
|
@ -251,49 +273,71 @@ const BoardPage = (props: Props): JSX.Element => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='BoardPage'>
|
||||
{!props.new && <TeamToBoardAndViewRedirect/>}
|
||||
<BackwardCompatibilityQueryParamsRedirect/>
|
||||
<SetWindowTitleAndIcon/>
|
||||
<UndoRedoHotKeys/>
|
||||
<WebsocketConnection/>
|
||||
<CloudMessage/>
|
||||
<VersionMessage/>
|
||||
<>
|
||||
{showJoinBoardDialog &&
|
||||
<ConfirmationDialog
|
||||
dialogBox={{
|
||||
heading: intl.formatMessage({id: 'boardPage.confirm-join-title', defaultMessage: 'Join private board'}),
|
||||
subText: intl.formatMessage({
|
||||
id: 'boardPage.confirm-join-text',
|
||||
defaultMessage: 'You are about to join a private board without explicitly being added by the board admin. Are you sure you wish to join this private board?',
|
||||
}),
|
||||
confirmButtonText: intl.formatMessage({id: 'boardPage.confirm-join-button', defaultMessage: 'Join'}),
|
||||
destructive: true, //board.channelId !== '',
|
||||
|
||||
{!mobileWarningClosed &&
|
||||
<div className='mobileWarning'>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='Error.mobileweb'
|
||||
defaultMessage='Mobile web support is currently in early beta. Not all functionality may be present.'
|
||||
onConfirm: onConfirmJoin,
|
||||
onClose: () => {
|
||||
setShowJoinBoardDialog(false)
|
||||
history.goBack()
|
||||
},
|
||||
}}
|
||||
/>}
|
||||
|
||||
{!showJoinBoardDialog &&
|
||||
<div className='BoardPage'>
|
||||
{!props.new && <TeamToBoardAndViewRedirect/>}
|
||||
<BackwardCompatibilityQueryParamsRedirect/>
|
||||
<SetWindowTitleAndIcon/>
|
||||
<UndoRedoHotKeys/>
|
||||
<WebsocketConnection/>
|
||||
<CloudMessage/>
|
||||
<VersionMessage/>
|
||||
|
||||
{!mobileWarningClosed &&
|
||||
<div className='mobileWarning'>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='Error.mobileweb'
|
||||
defaultMessage='Mobile web support is currently in early beta. Not all functionality may be present.'
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
UserSettings.mobileWarningClosed = true
|
||||
setMobileWarningClosed(true)
|
||||
}}
|
||||
icon={<CloseIcon/>}
|
||||
title='Close'
|
||||
className='margin-right'
|
||||
/>
|
||||
</div>}
|
||||
|
||||
{props.readonly && activeBoardId === undefined &&
|
||||
<div className='error'>
|
||||
{intl.formatMessage({id: 'BoardPage.syncFailed', defaultMessage: 'Board may be deleted or access revoked.'})}
|
||||
</div>}
|
||||
{
|
||||
|
||||
// Don't display Templates page
|
||||
// if readonly mode and no board defined.
|
||||
(!props.readonly || activeBoardId !== undefined) &&
|
||||
<Workspace
|
||||
readonly={props.readonly || false}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
UserSettings.mobileWarningClosed = true
|
||||
setMobileWarningClosed(true)
|
||||
}}
|
||||
icon={<CloseIcon/>}
|
||||
title='Close'
|
||||
className='margin-right'
|
||||
/>
|
||||
</div>}
|
||||
|
||||
{props.readonly && activeBoardId === undefined &&
|
||||
<div className='error'>
|
||||
{intl.formatMessage({id: 'BoardPage.syncFailed', defaultMessage: 'Board may be deleted or access revoked.'})}
|
||||
</div>}
|
||||
|
||||
{
|
||||
|
||||
// Don't display Templates page
|
||||
// if readonly mode and no board defined.
|
||||
(!props.readonly || activeBoardId !== undefined) &&
|
||||
<Workspace
|
||||
readonly={props.readonly || false}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ export const fetchBoardMembers = createAsyncThunk(
|
|||
const users = [] as IUser[]
|
||||
const userIDs = members.map((member) => member.userId)
|
||||
|
||||
const usersData = await client.getUsersList(userIDs)
|
||||
const usersData = await client.getTeamUsersList(userIDs, teamId)
|
||||
users.push(...usersData)
|
||||
|
||||
thunkAPI.dispatch(setBoardUsers(users))
|
||||
|
@ -85,9 +85,13 @@ export const updateMembersEnsuringBoardsAndUsers = createAsyncThunk(
|
|||
if (boardUsers[m.userId]) {
|
||||
return
|
||||
}
|
||||
const user = await client.getUser(m.userId)
|
||||
if (user) {
|
||||
thunkAPI.dispatch(addBoardUsers([user]))
|
||||
|
||||
const board = await client.getBoard(m.boardId)
|
||||
if (board) {
|
||||
const user = await client.getTeamUsersList([m.userId], board.teamId)
|
||||
if (user) {
|
||||
thunkAPI.dispatch(addBoardUsers(user))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ interface IUser {
|
|||
update_at: number
|
||||
is_bot: boolean
|
||||
is_guest: boolean
|
||||
permissions?: string[]
|
||||
roles: string
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`widgets/adminBadge should match the snapshot for Admin 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`widgets/adminBadge should match the snapshot for TeamAdmin 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="AdminBadge"
|
||||
>
|
||||
<div
|
||||
class="AdminBadge__box"
|
||||
>
|
||||
Team Admin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
16
webapp/src/widgets/adminBadge/adminBadge.scss
Normal file
16
webapp/src/widgets/adminBadge/adminBadge.scss
Normal file
|
@ -0,0 +1,16 @@
|
|||
.AdminBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0 10px 0 4px;
|
||||
}
|
||||
|
||||
.AdminBadge__box {
|
||||
padding: 2px 4px;
|
||||
border: 0;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: 2px;
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
32
webapp/src/widgets/adminBadge/adminBadge.test.tsx
Normal file
32
webapp/src/widgets/adminBadge/adminBadge.test.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {render} from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import {wrapIntl} from '../../testUtils'
|
||||
|
||||
import AdminBadge from './adminBadge'
|
||||
|
||||
describe('widgets/adminBadge', () => {
|
||||
test('should match the snapshot for TeamAdmin', () => {
|
||||
const {container} = render(wrapIntl(<AdminBadge permissions={['manage_team']}/>))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match the snapshot for Admin', () => {
|
||||
const {container} = render(wrapIntl(<AdminBadge permissions={['manage_team', 'manage_system']}/>))
|
||||
expect(container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('should match the snapshot for empty', () => {
|
||||
const {container} = render(wrapIntl(<AdminBadge permissions={[]}/>))
|
||||
expect(container).toMatchInlineSnapshot('<div />')
|
||||
})
|
||||
|
||||
test('should match the snapshot for invalid permission', () => {
|
||||
const {container} = render(wrapIntl(<AdminBadge permissions={['invalid_permission']}/>))
|
||||
expect(container).toMatchInlineSnapshot('<div />')
|
||||
})
|
||||
})
|
36
webapp/src/widgets/adminBadge/adminBadge.tsx
Normal file
36
webapp/src/widgets/adminBadge/adminBadge.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react'
|
||||
import {useIntl} from 'react-intl'
|
||||
|
||||
import './adminBadge.scss'
|
||||
|
||||
type Props = {
|
||||
permissions?: string[]
|
||||
}
|
||||
|
||||
const AdminBadge = (props: Props) => {
|
||||
const intl = useIntl()
|
||||
|
||||
if (!props.permissions) {
|
||||
return null
|
||||
}
|
||||
let text = ''
|
||||
if (props.permissions?.find((s) => s === 'manage_system')) {
|
||||
text = intl.formatMessage({id: 'AdminBadge.SystemAdmin', defaultMessage: 'Admin'})
|
||||
} else if (props.permissions?.find((s) => s === 'manage_team')) {
|
||||
text = intl.formatMessage({id: 'AdminBadge.TeamAdmin', defaultMessage: 'Team Admin'})
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className='AdminBadge'>
|
||||
<div className='AdminBadge__box'>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AdminBadge)
|
Loading…
Reference in a new issue