diff --git a/api4/team.go b/api4/team.go index 0319d0b566..ba3e9e8d2c 100644 --- a/api4/team.go +++ b/api4/team.go @@ -6,11 +6,14 @@ package api4 import ( "bytes" "encoding/base64" + "encoding/json" "fmt" "io" "io/ioutil" "net/http" + "regexp" "strconv" + "strings" "github.com/mattermost/mattermost-server/model" ) @@ -18,8 +21,15 @@ import ( const ( MAX_ADD_MEMBERS_BATCH = 20 MAXIMUM_BULK_IMPORT_SIZE = 10 * 1024 * 1024 + groupIDsParamPattern = "[^a-zA-Z0-9,]*" ) +var groupIDsQueryParamRegex *regexp.Regexp + +func init() { + groupIDsQueryParamRegex = regexp.MustCompile(groupIDsParamPattern) +} + func (api *API) InitTeam() { api.BaseRoutes.Teams.Handle("", api.ApiSessionRequired(createTeam)).Methods("POST") api.BaseRoutes.Teams.Handle("", api.ApiSessionRequired(getAllTeams)).Methods("GET") @@ -58,6 +68,8 @@ func (api *API) InitTeam() { api.BaseRoutes.Team.Handle("/invite/email", api.ApiSessionRequired(inviteUsersToTeam)).Methods("POST") api.BaseRoutes.Teams.Handle("/invites/email", api.ApiSessionRequired(invalidateAllEmailInvites)).Methods("DELETE") api.BaseRoutes.Teams.Handle("/invite/{invite_id:[A-Za-z0-9]+}", api.ApiHandler(getInviteInfo)).Methods("GET") + + api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/members_minus_group_members", api.ApiSessionRequired(teamMembersMinusGroupMembers)).Methods("GET") } func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1082,3 +1094,53 @@ func updateTeamScheme(c *Context, w http.ResponseWriter, r *http.Request) { ReturnStatusOK(w) } + +func teamMembersMinusGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTeamId() + if c.Err != nil { + return + } + + groupIDsParam := groupIDsQueryParamRegex.ReplaceAllString(c.Params.GroupIDs, "") + + if len(groupIDsParam) < 26 { + c.SetInvalidParam("group_ids") + return + } + + groupIDs := []string{} + for _, gid := range strings.Split(c.Params.GroupIDs, ",") { + if len(gid) != 26 { + c.SetInvalidParam("group_ids") + return + } + groupIDs = append(groupIDs, gid) + } + + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + users, totalCount, err := c.App.TeamMembersMinusGroupMembers( + c.Params.TeamId, + groupIDs, + c.Params.Page, + c.Params.PerPage, + ) + if err != nil { + c.Err = err + return + } + + b, marshalErr := json.Marshal(&model.UsersWithGroupsAndCount{ + Users: users, + Count: totalCount, + }) + if marshalErr != nil { + c.Err = model.NewAppError("Api4.teamMembersMinusGroupMembers", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError) + return + } + + w.Write(b) +} diff --git a/api4/team_test.go b/api4/team_test.go index 6616098b53..6ae57144e6 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2488,3 +2488,96 @@ func TestUpdateTeamScheme(t *testing.T) { _, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, teamScheme.Id) CheckUnauthorizedStatus(t, resp) } + +func TestTeamMembersMinusGroupMembers(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user1 := th.BasicUser + user2 := th.BasicUser2 + + team := th.CreateTeam() + team.GroupConstrained = model.NewBool(true) + team, err := th.App.UpdateTeam(team) + require.Nil(t, err) + + _, err = th.App.AddTeamMember(team.Id, user1.Id) + require.Nil(t, err) + _, err = th.App.AddTeamMember(team.Id, user2.Id) + require.Nil(t, err) + + group1 := th.CreateGroup() + group2 := th.CreateGroup() + + _, err = th.App.CreateOrRestoreGroupMember(group1.Id, user1.Id) + require.Nil(t, err) + _, err = th.App.CreateOrRestoreGroupMember(group2.Id, user2.Id) + require.Nil(t, err) + + // No permissions + _, _, res := th.Client.TeamMembersMinusGroupMembers(team.Id, []string{group1.Id, group2.Id}, 0, 100, "") + require.Equal(t, "api.context.permissions.app_error", res.Error.Id) + + testCases := map[string]struct { + groupIDs []string + page int + perPage int + length int + count int + otherAssertions func([]*model.UserWithGroups) + }{ + "All groups, expect no users removed": { + groupIDs: []string{group1.Id, group2.Id}, + page: 0, + perPage: 100, + length: 0, + count: 0, + }, + "Some nonexistent group, page 0": { + groupIDs: []string{model.NewId()}, + page: 0, + perPage: 1, + length: 1, + count: 2, + }, + "Some nonexistent group, page 1": { + groupIDs: []string{model.NewId()}, + page: 1, + perPage: 1, + length: 1, + count: 2, + }, + "One group, expect one user removed": { + groupIDs: []string{group1.Id}, + page: 0, + perPage: 100, + length: 1, + count: 1, + otherAssertions: func(uwg []*model.UserWithGroups) { + require.Equal(t, uwg[0].Id, user2.Id) + }, + }, + "Other group, expect other user removed": { + groupIDs: []string{group2.Id}, + page: 0, + perPage: 100, + length: 1, + count: 1, + otherAssertions: func(uwg []*model.UserWithGroups) { + require.Equal(t, uwg[0].Id, user1.Id) + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + uwg, count, res := th.SystemAdminClient.TeamMembersMinusGroupMembers(team.Id, tc.groupIDs, tc.page, tc.perPage, "") + require.Nil(t, res.Error) + require.Len(t, uwg, tc.length) + require.Equal(t, tc.count, int(count)) + if tc.otherAssertions != nil { + tc.otherAssertions(uwg) + } + }) + } +} diff --git a/app/group.go b/app/group.go index 52ac8d4ea0..836f00f34b 100644 --- a/app/group.go +++ b/app/group.go @@ -4,6 +4,8 @@ package app import ( + "strings" + "github.com/mattermost/mattermost-server/model" ) @@ -160,3 +162,62 @@ func (a *App) GetGroupsByTeam(teamId string, opts model.GroupSearchOpts) ([]*mod func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) { return a.Srv.Store.Group().GetGroups(page, perPage, opts) } + +// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given +// groups. +// +// The result can be used, for example, to determine the set of users who would be removed from a team if the team +// were group-constrained with the given groups. +func (a *App) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) { + users, err := a.Srv.Store.Group().TeamMembersMinusGroupMembers(teamID, groupIDs, page, perPage) + if err != nil { + return nil, 0, err + } + + // parse all group ids of all users + allUsersGroupIDMap := map[string]bool{} + for _, user := range users { + for _, groupID := range strings.Split(user.GroupIDs, ",") { + allUsersGroupIDMap[groupID] = true + } + } + + // create a slice of distinct group ids + var allUsersGroupIDSlice []string + for key := range allUsersGroupIDMap { + allUsersGroupIDSlice = append(allUsersGroupIDSlice, key) + } + + // retrieve groups from DB + groups, err := a.GetGroupsByIDs(allUsersGroupIDSlice) + if err != nil { + return nil, 0, err + } + + // map groups by id + groupMap := map[string]*model.Group{} + for _, group := range groups { + groupMap[group.Id] = group + } + + // populate each instance's groups field + for _, user := range users { + user.Groups = []*model.Group{} + for _, groupID := range strings.Split(user.GroupIDs, ",") { + group, ok := groupMap[groupID] + if ok { + user.Groups = append(user.Groups, group) + } + } + } + + totalCount, err := a.Srv.Store.Group().CountTeamMembersMinusGroupMembers(teamID, groupIDs) + if err != nil { + return nil, 0, err + } + return users, totalCount, nil +} + +func (a *App) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) { + return a.Srv.Store.Group().GetByIDs(groupIDs) +} diff --git a/model/client4.go b/model/client4.go index d90ec78638..d12f717a53 100644 --- a/model/client4.go +++ b/model/client4.go @@ -4412,3 +4412,15 @@ func (c *Client4) PatchGroupSyncable(groupID, syncableID string, syncableType Gr defer closeBody(r) return GroupSyncableFromJson(r.Body), BuildResponse(r) } + +func (c *Client4) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response) { + groupIDStr := strings.Join(groupIDs, ",") + query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage) + r, err := c.DoApiGet(c.GetTeamRoute(teamID)+"/members_minus_group_members"+query, etag) + if err != nil { + return nil, 0, BuildErrorResponse(r, err) + } + defer closeBody(r) + ugc := UsersWithGroupsAndCountFromJson(r.Body) + return ugc.Users, ugc.Count, BuildResponse(r) +} diff --git a/model/user.go b/model/user.go index 09cf369713..48052f5902 100644 --- a/model/user.go +++ b/model/user.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "net/http" "regexp" "sort" @@ -768,3 +769,24 @@ func IsValidLocale(locale string) bool { return true } + +type UserWithGroups struct { + User + GroupIDs string `json:"-"` + Groups []*Group `json:"groups"` + SchemeGuest bool `json:"scheme_guest"` + SchemeUser bool `json:"scheme_user"` + SchemeAdmin bool `json:"scheme_admin"` +} + +type UsersWithGroupsAndCount struct { + Users []*UserWithGroups `json:"users"` + Count int64 `json:"total_count"` +} + +func UsersWithGroupsAndCountFromJson(data io.Reader) *UsersWithGroupsAndCount { + uwg := &UsersWithGroupsAndCount{} + bodyBytes, _ := ioutil.ReadAll(data) + json.Unmarshal(bodyBytes, uwg) + return uwg +} diff --git a/store/sqlstore/group_supplier.go b/store/sqlstore/group_store.go similarity index 90% rename from store/sqlstore/group_supplier.go rename to store/sqlstore/group_store.go index 961f3dc7ad..6cc25c1b3e 100644 --- a/store/sqlstore/group_supplier.go +++ b/store/sqlstore/group_store.go @@ -7,8 +7,10 @@ import ( "database/sql" "fmt" "net/http" + "strings" "github.com/Masterminds/squirrel" + sq "github.com/Masterminds/squirrel" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/store" @@ -132,6 +134,19 @@ func (s *SqlGroupStore) Get(groupId string) store.StoreChannel { }) } +func (s *SqlGroupStore) GetByIDs(groupIDs []string) ([]*model.Group, *model.AppError) { + var groups []*model.Group + query := s.getQueryBuilder().Select("*").From("UserGroups").Where(sq.Eq{"Id": groupIDs}) + queryString, args, err := query.ToSql() + if err != nil { + return nil, model.NewAppError("SqlGroupStore.GetByIDs", "store.sql_group.app_error", nil, err.Error(), http.StatusInternalServerError) + } + if _, err := s.GetReplica().Select(&groups, queryString, args...); err != nil { + return nil, model.NewAppError("SqlGroupStore.GetByIDs", "store.select_error", nil, err.Error(), http.StatusInternalServerError) + } + return groups, nil +} + func (s *SqlGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) store.StoreChannel { return store.Do(func(result *store.StoreResult) { @@ -1035,3 +1050,81 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts) return groups, nil } + +func (s *SqlGroupStore) teamMembersMinusGroupMembersQuery(teamID string, groupIDs []string, isCount bool) squirrel.SelectBuilder { + var selectStr string + + if isCount { + selectStr = "count(DISTINCT Users.Id)" + } else { + tmpl := "Users.*, TeamMembers.SchemeGuest, TeamMembers.SchemeAdmin, TeamMembers.SchemeUser, %s AS GroupIDs" + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + selectStr = fmt.Sprintf(tmpl, "group_concat(UserGroups.Id)") + } else { + selectStr = fmt.Sprintf(tmpl, "string_agg(UserGroups.Id, ',')") + } + } + + subQuery := s.getQueryBuilder().Select("GroupMembers.UserId"). + From("GroupMembers"). + Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId"). + Where("GroupMembers.DeleteAt = 0"). + Where(fmt.Sprintf("GroupMembers.GroupId IN ('%s')", strings.Join(groupIDs, "', '"))) + + sql, _ := subQuery.MustSql() + + query := s.getQueryBuilder().Select(selectStr). + From("TeamMembers"). + Join("Teams ON Teams.Id = TeamMembers.TeamId"). + Join("Users ON Users.Id = TeamMembers.UserId"). + LeftJoin("Bots ON Bots.UserId = TeamMembers.UserId"). + Join("GroupMembers ON GroupMembers.UserId = Users.Id"). + Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId"). + Where("TeamMembers.DeleteAt = 0"). + Where("Teams.DeleteAt = 0"). + Where("Users.DeleteAt = 0"). + Where("Bots.UserId IS NULL"). + Where("Teams.Id = ?", teamID). + Where(fmt.Sprintf("Users.Id NOT IN (%s)", sql)) + + if !isCount { + query = query.GroupBy("Users.Id, TeamMembers.SchemeGuest, TeamMembers.SchemeAdmin, TeamMembers.SchemeUser") + } + + return query +} + +// TeamMembersMinusGroupMembers returns the set of users on the given team minus the set of users in the given +// groups. +func (s *SqlGroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, *model.AppError) { + query := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, false) + query = query.OrderBy("Users.Id").Limit(uint64(perPage)).Offset(uint64(page * perPage)) + + queryString, args, err := query.ToSql() + if err != nil { + return nil, model.NewAppError("SqlGroupStore.TeamMembersMinusGroupMembers", "store.sql_group.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var users []*model.UserWithGroups + if _, err = s.GetReplica().Select(&users, queryString, args...); err != nil { + return nil, model.NewAppError("SqlGroupStore.TeamMembersMinusGroupMembers", "store.select_error", nil, err.Error(), http.StatusInternalServerError) + } + + return users, nil +} + +// CountTeamMembersMinusGroupMembers returns the count of the set of users on the given team minus the set of users +// in the given groups. +func (s *SqlGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, *model.AppError) { + queryString, args, err := s.teamMembersMinusGroupMembersQuery(teamID, groupIDs, true).ToSql() + if err != nil { + return 0, model.NewAppError("SqlGroupStore.CountTeamMembersMinusGroupMembers", "store.sql_group.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + var count int64 + if count, err = s.GetReplica().SelectInt(queryString, args...); err != nil { + return 0, model.NewAppError("SqlGroupStore.CountTeamMembersMinusGroupMembers", "store.select_error", nil, err.Error(), http.StatusInternalServerError) + } + + return count, nil +} diff --git a/store/store.go b/store/store.go index b79ea001c2..75a21f94dd 100644 --- a/store/store.go +++ b/store/store.go @@ -571,6 +571,7 @@ type UserTermsOfServiceStore interface { type GroupStore interface { Create(group *model.Group) StoreChannel Get(groupID string) StoreChannel + GetByIDs(groupIDs []string) ([]*model.Group, *model.AppError) GetByRemoteID(remoteID string, groupSource model.GroupSource) StoreChannel GetAllBySource(groupSource model.GroupSource) StoreChannel Update(group *model.Group) StoreChannel @@ -601,6 +602,9 @@ type GroupStore interface { CountGroupsByTeam(teamId string, opts model.GroupSearchOpts) (int64, *model.AppError) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) + + TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, *model.AppError) + CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, *model.AppError) } type LinkMetadataStore interface { diff --git a/store/storetest/group_supplier.go b/store/storetest/group_store.go similarity index 92% rename from store/storetest/group_supplier.go rename to store/storetest/group_store.go index a5a5c3272f..9510a1761e 100644 --- a/store/storetest/group_supplier.go +++ b/store/storetest/group_store.go @@ -4,6 +4,9 @@ package storetest import ( + "fmt" + "math" + "sort" "strings" "testing" @@ -15,6 +18,7 @@ import ( func TestGroupStore(t *testing.T, ss store.Store) { t.Run("Create", func(t *testing.T) { testGroupStoreCreate(t, ss) }) t.Run("Get", func(t *testing.T) { testGroupStoreGet(t, ss) }) + t.Run("GetByIDs", func(t *testing.T) { testGroupStoreGetByIDs(t, ss) }) t.Run("GetByRemoteID", func(t *testing.T) { testGroupStoreGetByRemoteID(t, ss) }) t.Run("GetAllBySource", func(t *testing.T) { testGroupStoreGetAllByType(t, ss) }) t.Run("Update", func(t *testing.T) { testGroupStoreUpdate(t, ss) }) @@ -41,6 +45,8 @@ func TestGroupStore(t *testing.T, ss store.Store) { t.Run("GetGroupsByTeam", func(t *testing.T) { testGetGroupsByTeam(t, ss) }) t.Run("GetGroups", func(t *testing.T) { testGetGroups(t, ss) }) + + t.Run("TeamMembersMinusGroupMembers", func(t *testing.T) { testTeamMembersMinusGroupMembers(t, ss) }) } func testGroupStoreCreate(t *testing.T, ss store.Store) { @@ -173,6 +179,40 @@ func testGroupStoreGet(t *testing.T, ss store.Store) { require.Equal(t, res3.Err.Id, "store.sql_group.no_rows") } +func testGroupStoreGetByIDs(t *testing.T, ss store.Store) { + var group1 *model.Group + var group2 *model.Group + + for i := 0; i < 2; i++ { + group := &model.Group{ + Name: model.NewId(), + DisplayName: model.NewId(), + Description: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewId(), + } + res := <-ss.Group().Create(group) + require.Nil(t, res.Err) + group = res.Data.(*model.Group) + switch i { + case 0: + group1 = group + case 1: + group2 = group + } + } + + groups, err := ss.Group().GetByIDs([]string{group1.Id, group2.Id}) + require.Nil(t, err) + require.Len(t, groups, 2) + + for i := 0; i < 2; i++ { + require.True(t, (groups[i].Id == group1.Id || groups[i].Id == group2.Id)) + } + + require.True(t, groups[0].Id != groups[1].Id) +} + func testGroupStoreGetByRemoteID(t *testing.T, ss store.Store) { // Create a group g1 := &model.Group{ @@ -2200,3 +2240,155 @@ func testGetGroups(t *testing.T, ss store.Store) { }) } } + +func testTeamMembersMinusGroupMembers(t *testing.T, ss store.Store) { + const numberOfGroups = 3 + const numberOfUsers = 4 + + groups := []*model.Group{} + users := []*model.User{} + + team := &model.Team{ + DisplayName: model.NewId(), + Description: model.NewId(), + CompanyName: model.NewId(), + AllowOpenInvite: false, + InviteId: model.NewId(), + Name: model.NewId(), + Email: model.NewId() + "@simulator.amazonses.com", + Type: model.TEAM_OPEN, + GroupConstrained: model.NewBool(true), + } + team, err := ss.Team().Save(team) + require.Nil(t, err) + + for i := 0; i < numberOfUsers; i++ { + user := &model.User{ + Email: MakeEmail(), + Username: model.NewId(), + } + res := <-ss.User().Save(user) + require.Nil(t, res.Err) + user = res.Data.(*model.User) + users = append(users, user) + + trueOrFalse := int(math.Mod(float64(i), 2)) == 0 + res = <-ss.Team().SaveMember(&model.TeamMember{TeamId: team.Id, UserId: user.Id, SchemeUser: trueOrFalse, SchemeAdmin: !trueOrFalse}, 999) + require.Nil(t, res.Err) + } + + for i := 0; i < numberOfGroups; i++ { + group := &model.Group{ + Name: fmt.Sprintf("n_%d_%s", i, model.NewId()), + DisplayName: model.NewId(), + Source: model.GroupSourceLdap, + Description: model.NewId(), + RemoteId: model.NewId(), + } + res := <-ss.Group().Create(group) + require.Nil(t, res.Err) + group = res.Data.(*model.Group) + groups = append(groups, group) + } + + sort.Slice(users, func(i, j int) bool { + return users[i].Id < users[j].Id + }) + + // Add even users to even group, and the inverse + for i := 0; i < numberOfUsers; i++ { + groupIndex := int(math.Mod(float64(i), 2)) + res := <-ss.Group().CreateOrRestoreMember(groups[groupIndex].Id, users[i].Id) + require.Nil(t, res.Err) + + // Add everyone to group 2 + res = <-ss.Group().CreateOrRestoreMember(groups[numberOfGroups-1].Id, users[i].Id) + require.Nil(t, res.Err) + } + + testCases := map[string]struct { + expectedUserIDs []string + expectedTotalCount int64 + groupIDs []string + page int + perPage int + setup func() + teardown func() + }{ + "No group IDs, all members": { + expectedUserIDs: []string{users[0].Id, users[1].Id, users[2].Id, users[3].Id}, + expectedTotalCount: numberOfUsers, + groupIDs: []string{}, + page: 0, + perPage: 100, + }, + "All members, page 1": { + expectedUserIDs: []string{users[0].Id, users[1].Id}, + expectedTotalCount: numberOfUsers, + groupIDs: []string{}, + page: 0, + perPage: 2, + }, + "All members, page 2": { + expectedUserIDs: []string{users[2].Id, users[3].Id}, + expectedTotalCount: numberOfUsers, + groupIDs: []string{}, + page: 1, + perPage: 2, + }, + "Group 1, even users would be removed": { + expectedUserIDs: []string{users[0].Id, users[2].Id}, + expectedTotalCount: 2, + groupIDs: []string{groups[1].Id}, + page: 0, + perPage: 100, + }, + "Group 0, odd users would be removed": { + expectedUserIDs: []string{users[1].Id, users[3].Id}, + expectedTotalCount: 2, + groupIDs: []string{groups[0].Id}, + page: 0, + perPage: 100, + }, + "All groups, no users would be removed": { + expectedUserIDs: []string{}, + expectedTotalCount: 0, + groupIDs: []string{groups[0].Id, groups[1].Id}, + page: 0, + perPage: 100, + }, + } + + mapUserIDs := func(users []*model.UserWithGroups) []string { + ids := []string{} + for _, user := range users { + ids = append(ids, user.Id) + } + return ids + } + + for tcName, tc := range testCases { + t.Run(tcName, func(t *testing.T) { + if tc.setup != nil { + tc.setup() + } + + if tc.teardown != nil { + defer tc.teardown() + } + + actual, err := ss.Group().TeamMembersMinusGroupMembers(team.Id, tc.groupIDs, tc.page, tc.perPage) + require.Nil(t, err) + require.ElementsMatch(t, tc.expectedUserIDs, mapUserIDs(actual)) + + for _, user := range actual { + require.NotNil(t, user.GroupIDs) + require.True(t, (user.SchemeAdmin || user.SchemeUser)) + } + + actualCount, err := ss.Group().CountTeamMembersMinusGroupMembers(team.Id, tc.groupIDs) + require.Nil(t, err) + require.Equal(t, tc.expectedTotalCount, actualCount) + }) + } +} diff --git a/store/storetest/mocks/GroupStore.go b/store/storetest/mocks/GroupStore.go index f4b01d7eaa..e6533cadb4 100644 --- a/store/storetest/mocks/GroupStore.go +++ b/store/storetest/mocks/GroupStore.go @@ -109,6 +109,29 @@ func (_m *GroupStore) CountGroupsByTeam(teamId string, opts model.GroupSearchOpt return r0, r1 } +// CountTeamMembersMinusGroupMembers provides a mock function with given fields: teamID, groupIDs +func (_m *GroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, *model.AppError) { + ret := _m.Called(teamID, groupIDs) + + var r0 int64 + if rf, ok := ret.Get(0).(func(string, []string) int64); ok { + r0 = rf(teamID, groupIDs) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, []string) *model.AppError); ok { + r1 = rf(teamID, groupIDs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // Create provides a mock function with given fields: group func (_m *GroupStore) Create(group *model.Group) store.StoreChannel { ret := _m.Called(group) @@ -280,6 +303,31 @@ func (_m *GroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType return r0, r1 } +// GetByIDs provides a mock function with given fields: groupIDs +func (_m *GroupStore) GetByIDs(groupIDs []string) ([]*model.Group, *model.AppError) { + ret := _m.Called(groupIDs) + + var r0 []*model.Group + if rf, ok := ret.Get(0).(func([]string) []*model.Group); ok { + r0 = rf(groupIDs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Group) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func([]string) *model.AppError); ok { + r1 = rf(groupIDs) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // GetByRemoteID provides a mock function with given fields: remoteID, groupSource func (_m *GroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) store.StoreChannel { ret := _m.Called(remoteID, groupSource) @@ -444,6 +492,31 @@ func (_m *GroupStore) GetMemberUsersPage(groupID string, offset int, limit int) return r0 } +// TeamMembersMinusGroupMembers provides a mock function with given fields: teamID, groupIDs, page, perPage +func (_m *GroupStore) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, *model.AppError) { + ret := _m.Called(teamID, groupIDs, page, perPage) + + var r0 []*model.UserWithGroups + if rf, ok := ret.Get(0).(func(string, []string, int, int) []*model.UserWithGroups); ok { + r0 = rf(teamID, groupIDs, page, perPage) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.UserWithGroups) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, []string, int, int) *model.AppError); ok { + r1 = rf(teamID, groupIDs, page, perPage) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // TeamMembersToAdd provides a mock function with given fields: since func (_m *GroupStore) TeamMembersToAdd(since int64) ([]*model.UserTeamIDPair, *model.AppError) { ret := _m.Called(since) diff --git a/web/params.go b/web/params.go index 7fafe4bbbf..6a7dac0079 100644 --- a/web/params.go +++ b/web/params.go @@ -68,6 +68,7 @@ type Params struct { IncludeMemberCount bool NotAssociatedToGroup string ExcludeDefaultChannels bool + GroupIDs string } func ParamsFromRequest(r *http.Request) *Params { @@ -268,5 +269,7 @@ func ParamsFromRequest(r *http.Request) *Params { params.ExcludeDefaultChannels = val } + params.GroupIDs = query.Get("group_ids") + return params }