MM-16258: Adds new API endpoint + (App & Client & Store) to retrieve … (#11176)
* MM-16258: Adds new API endpoint + (App & Client & Store) to retrieve Users who would be removed from a list of hypothetical group IDs representing the synced groups. * MM-16258: Adds roles to JSON response. * MM-16258: Updates GetByIDs to use Squirrel. * MM-16258: Puts as much as possible into Squirrel. * MM-16258: Changes names of methods, functions, and route. * MM-16258: Updates some comments. * MM-16258: Extra validation of group_ids parameter. * MM-16258: Changes validation of group_ids query param. * MM-16258: Rename a variable and a constant. * MM-16258: Fix test.
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
5ee1f6e2e0
Коммит
9d41c7a583
62
api4/team.go
62
api4/team.go
@@ -6,11 +6,14 @@ package api4
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
)
|
)
|
||||||
@@ -18,8 +21,15 @@ import (
|
|||||||
const (
|
const (
|
||||||
MAX_ADD_MEMBERS_BATCH = 20
|
MAX_ADD_MEMBERS_BATCH = 20
|
||||||
MAXIMUM_BULK_IMPORT_SIZE = 10 * 1024 * 1024
|
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() {
|
func (api *API) InitTeam() {
|
||||||
api.BaseRoutes.Teams.Handle("", api.ApiSessionRequired(createTeam)).Methods("POST")
|
api.BaseRoutes.Teams.Handle("", api.ApiSessionRequired(createTeam)).Methods("POST")
|
||||||
api.BaseRoutes.Teams.Handle("", api.ApiSessionRequired(getAllTeams)).Methods("GET")
|
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.Team.Handle("/invite/email", api.ApiSessionRequired(inviteUsersToTeam)).Methods("POST")
|
||||||
api.BaseRoutes.Teams.Handle("/invites/email", api.ApiSessionRequired(invalidateAllEmailInvites)).Methods("DELETE")
|
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("/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) {
|
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)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2488,3 +2488,96 @@ func TestUpdateTeamScheme(t *testing.T) {
|
|||||||
_, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, teamScheme.Id)
|
_, resp = th.SystemAdminClient.UpdateTeamScheme(team.Id, teamScheme.Id)
|
||||||
CheckUnauthorizedStatus(t, resp)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
61
app/group.go
61
app/group.go
@@ -4,6 +4,8 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"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) {
|
func (a *App) GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *model.AppError) {
|
||||||
return a.Srv.Store.Group().GetGroups(page, perPage, opts)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4412,3 +4412,15 @@ func (c *Client4) PatchGroupSyncable(groupID, syncableID string, syncableType Gr
|
|||||||
defer closeBody(r)
|
defer closeBody(r)
|
||||||
return GroupSyncableFromJson(r.Body), BuildResponse(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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -768,3 +769,24 @@ func IsValidLocale(locale string) bool {
|
|||||||
|
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Masterminds/squirrel"
|
"github.com/Masterminds/squirrel"
|
||||||
|
sq "github.com/Masterminds/squirrel"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/model"
|
"github.com/mattermost/mattermost-server/model"
|
||||||
"github.com/mattermost/mattermost-server/store"
|
"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 {
|
func (s *SqlGroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) store.StoreChannel {
|
||||||
return store.Do(func(result *store.StoreResult) {
|
return store.Do(func(result *store.StoreResult) {
|
||||||
|
|
||||||
@@ -1035,3 +1050,81 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts)
|
|||||||
|
|
||||||
return groups, nil
|
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
|
||||||
|
}
|
||||||
@@ -571,6 +571,7 @@ type UserTermsOfServiceStore interface {
|
|||||||
type GroupStore interface {
|
type GroupStore interface {
|
||||||
Create(group *model.Group) StoreChannel
|
Create(group *model.Group) StoreChannel
|
||||||
Get(groupID string) StoreChannel
|
Get(groupID string) StoreChannel
|
||||||
|
GetByIDs(groupIDs []string) ([]*model.Group, *model.AppError)
|
||||||
GetByRemoteID(remoteID string, groupSource model.GroupSource) StoreChannel
|
GetByRemoteID(remoteID string, groupSource model.GroupSource) StoreChannel
|
||||||
GetAllBySource(groupSource model.GroupSource) StoreChannel
|
GetAllBySource(groupSource model.GroupSource) StoreChannel
|
||||||
Update(group *model.Group) StoreChannel
|
Update(group *model.Group) StoreChannel
|
||||||
@@ -601,6 +602,9 @@ type GroupStore interface {
|
|||||||
CountGroupsByTeam(teamId string, opts model.GroupSearchOpts) (int64, *model.AppError)
|
CountGroupsByTeam(teamId string, opts model.GroupSearchOpts) (int64, *model.AppError)
|
||||||
|
|
||||||
GetGroups(page, perPage int, opts model.GroupSearchOpts) ([]*model.Group, *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 {
|
type LinkMetadataStore interface {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
package storetest
|
package storetest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -15,6 +18,7 @@ import (
|
|||||||
func TestGroupStore(t *testing.T, ss store.Store) {
|
func TestGroupStore(t *testing.T, ss store.Store) {
|
||||||
t.Run("Create", func(t *testing.T) { testGroupStoreCreate(t, ss) })
|
t.Run("Create", func(t *testing.T) { testGroupStoreCreate(t, ss) })
|
||||||
t.Run("Get", func(t *testing.T) { testGroupStoreGet(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("GetByRemoteID", func(t *testing.T) { testGroupStoreGetByRemoteID(t, ss) })
|
||||||
t.Run("GetAllBySource", func(t *testing.T) { testGroupStoreGetAllByType(t, ss) })
|
t.Run("GetAllBySource", func(t *testing.T) { testGroupStoreGetAllByType(t, ss) })
|
||||||
t.Run("Update", func(t *testing.T) { testGroupStoreUpdate(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("GetGroupsByTeam", func(t *testing.T) { testGetGroupsByTeam(t, ss) })
|
||||||
|
|
||||||
t.Run("GetGroups", func(t *testing.T) { testGetGroups(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) {
|
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")
|
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) {
|
func testGroupStoreGetByRemoteID(t *testing.T, ss store.Store) {
|
||||||
// Create a group
|
// Create a group
|
||||||
g1 := &model.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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,6 +109,29 @@ func (_m *GroupStore) CountGroupsByTeam(teamId string, opts model.GroupSearchOpt
|
|||||||
return r0, r1
|
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
|
// Create provides a mock function with given fields: group
|
||||||
func (_m *GroupStore) Create(group *model.Group) store.StoreChannel {
|
func (_m *GroupStore) Create(group *model.Group) store.StoreChannel {
|
||||||
ret := _m.Called(group)
|
ret := _m.Called(group)
|
||||||
@@ -280,6 +303,31 @@ func (_m *GroupStore) GetAllGroupSyncablesByGroupId(groupID string, syncableType
|
|||||||
return r0, r1
|
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
|
// GetByRemoteID provides a mock function with given fields: remoteID, groupSource
|
||||||
func (_m *GroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) store.StoreChannel {
|
func (_m *GroupStore) GetByRemoteID(remoteID string, groupSource model.GroupSource) store.StoreChannel {
|
||||||
ret := _m.Called(remoteID, groupSource)
|
ret := _m.Called(remoteID, groupSource)
|
||||||
@@ -444,6 +492,31 @@ func (_m *GroupStore) GetMemberUsersPage(groupID string, offset int, limit int)
|
|||||||
return r0
|
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
|
// TeamMembersToAdd provides a mock function with given fields: since
|
||||||
func (_m *GroupStore) TeamMembersToAdd(since int64) ([]*model.UserTeamIDPair, *model.AppError) {
|
func (_m *GroupStore) TeamMembersToAdd(since int64) ([]*model.UserTeamIDPair, *model.AppError) {
|
||||||
ret := _m.Called(since)
|
ret := _m.Called(since)
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ type Params struct {
|
|||||||
IncludeMemberCount bool
|
IncludeMemberCount bool
|
||||||
NotAssociatedToGroup string
|
NotAssociatedToGroup string
|
||||||
ExcludeDefaultChannels bool
|
ExcludeDefaultChannels bool
|
||||||
|
GroupIDs string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParamsFromRequest(r *http.Request) *Params {
|
func ParamsFromRequest(r *http.Request) *Params {
|
||||||
@@ -268,5 +269,7 @@ func ParamsFromRequest(r *http.Request) *Params {
|
|||||||
params.ExcludeDefaultChannels = val
|
params.ExcludeDefaultChannels = val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.GroupIDs = query.Get("group_ids")
|
||||||
|
|
||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user