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:
Scott Bishel 2023-02-14 09:17:33 -07:00 committed by GitHub
parent 759a8bb76a
commit 098868387e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1031 additions and 80 deletions

View file

@ -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,

View file

@ -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()
}

View file

@ -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)

View file

@ -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

View file

@ -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)
})
}

View file

@ -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{

View file

@ -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
}

View file

@ -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)

View file

@ -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
View 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)
})
}

View file

@ -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
}

View file

@ -6,6 +6,8 @@ import (
var (
PermissionViewTeam = mmModel.PermissionViewTeam
PermissionManageTeam = mmModel.PermissionManageTeam
PermissionManageSystem = mmModel.PermissionManageSystem
PermissionReadChannel = mmModel.PermissionReadChannel
PermissionViewMembers = mmModel.PermissionViewMembers
PermissionCreatePublicChannel = mmModel.PermissionCreatePublicChannel

View file

@ -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"`
}

View file

@ -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
}

View file

@ -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)
})
}

View file

@ -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)
})

View file

@ -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

View file

@ -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)
})
}

View file

@ -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>

View file

@ -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>
`;

View file

@ -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,
]

View file

@ -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>
)

View file

@ -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)

View file

@ -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>

View file

@ -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) {

View file

@ -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>
</>
)
}

View file

@ -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))
}
}
})

View file

@ -14,6 +14,7 @@ interface IUser {
update_at: number
is_bot: boolean
is_guest: boolean
permissions?: string[]
roles: string
}

View file

@ -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>
`;

View 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;
}

View 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 />')
})
})

View 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)