MM-16224: Adds new API endpoint + (App & Client & Store) to retrieve the difference between the set of channel members and given group members. (#11186)

Этот коммит содержится в:
Martin Kraft
2019-06-17 11:04:27 -04:00
коммит произвёл GitHub
родитель 4d223ba3a2
Коммит e15a75a2ec
8 изменённых файлов: 496 добавлений и 0 удалений

Просмотреть файл

@@ -4,7 +4,9 @@
package api4
import (
"encoding/json"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
@@ -37,6 +39,7 @@ func (api *API) InitChannel() {
api.BaseRoutes.Channel.Handle("/stats", api.ApiSessionRequired(getChannelStats)).Methods("GET")
api.BaseRoutes.Channel.Handle("/pinned", api.ApiSessionRequired(getPinnedPosts)).Methods("GET")
api.BaseRoutes.Channel.Handle("/timezones", api.ApiSessionRequired(getChannelMembersTimezones)).Methods("GET")
api.BaseRoutes.Channel.Handle("/members_minus_group_members", api.ApiSessionRequired(channelMembersMinusGroupMembers)).Methods("GET")
api.BaseRoutes.ChannelForUser.Handle("/unread", api.ApiSessionRequired(getChannelUnread)).Methods("GET")
api.BaseRoutes.ChannelByName.Handle("", api.ApiSessionRequired(getChannelByName)).Methods("GET")
@@ -1295,3 +1298,53 @@ func updateChannelScheme(c *Context, w http.ResponseWriter, r *http.Request) {
ReturnStatusOK(w)
}
func channelMembersMinusGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
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.ChannelMembersMinusGroupMembers(
c.Params.ChannelId,
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.channelMembersMinusGroupMembers", "api.marshal_error", nil, marshalErr.Error(), http.StatusInternalServerError)
return
}
w.Write(b)
}

Просмотреть файл

@@ -2637,3 +2637,97 @@ func TestGetChannelMembersTimezones(t *testing.T) {
}
}
func TestChannelMembersMinusGroupMembers(t *testing.T) {
th := Setup().InitBasic()
defer th.TearDown()
user1 := th.BasicUser
user2 := th.BasicUser2
channel := th.CreatePrivateChannel()
_, err := th.App.AddChannelMember(user1.Id, channel, "", "")
require.Nil(t, err)
_, err = th.App.AddChannelMember(user2.Id, channel, "", "")
require.Nil(t, err)
channel.GroupConstrained = model.NewBool(true)
channel, err = th.App.UpdateChannel(channel)
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.ChannelMembersMinusGroupMembers(channel.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.ChannelMembersMinusGroupMembers(channel.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)
}
})
}
}

Просмотреть файл

@@ -221,3 +221,58 @@ func (a *App) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, pag
func (a *App) GetGroupsByIDs(groupIDs []string) ([]*model.Group, *model.AppError) {
return a.Srv.Store.Group().GetByIDs(groupIDs)
}
// ChannelMembersMinusGroupMembers returns the set of users in the given channel 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 channel if the
// channel were group-constrained with the given groups.
func (a *App) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError) {
users, err := a.Srv.Store.Group().ChannelMembersMinusGroupMembers(channelID, 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().CountChannelMembersMinusGroupMembers(channelID, groupIDs)
if err != nil {
return nil, 0, err
}
return users, totalCount, nil
}

Просмотреть файл

@@ -4424,3 +4424,15 @@ func (c *Client4) TeamMembersMinusGroupMembers(teamID string, groupIDs []string,
ugc := UsersWithGroupsAndCountFromJson(r.Body)
return ugc.Users, ugc.Count, BuildResponse(r)
}
func (c *Client4) ChannelMembersMinusGroupMembers(channelID 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.GetChannelRoute(channelID)+"/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)
}

Просмотреть файл

@@ -1128,3 +1128,80 @@ func (s *SqlGroupStore) CountTeamMembersMinusGroupMembers(teamID string, groupID
return count, nil
}
func (s *SqlGroupStore) channelMembersMinusGroupMembersQuery(channelID string, groupIDs []string, isCount bool) squirrel.SelectBuilder {
var selectStr string
if isCount {
selectStr = "count(DISTINCT Users.Id)"
} else {
tmpl := "Users.*, ChannelMembers.SchemeGuest, ChannelMembers.SchemeAdmin, ChannelMembers.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("ChannelMembers").
Join("Channels ON Channels.Id = ChannelMembers.ChannelId").
Join("Users ON Users.Id = ChannelMembers.UserId").
LeftJoin("Bots ON Bots.UserId = ChannelMembers.UserId").
Join("GroupMembers ON GroupMembers.UserId = Users.Id").
Join("UserGroups ON UserGroups.Id = GroupMembers.GroupId").
Where("Channels.DeleteAt = 0").
Where("Users.DeleteAt = 0").
Where("Bots.UserId IS NULL").
Where("Channels.Id = ?", channelID).
Where(fmt.Sprintf("Users.Id NOT IN (%s)", sql))
if !isCount {
query = query.GroupBy("Users.Id, ChannelMembers.SchemeGuest, ChannelMembers.SchemeAdmin, ChannelMembers.SchemeUser")
}
return query
}
// ChannelMembersMinusGroupMembers returns the set of users in the given channel minus the set of users in the given
// groups.
func (s *SqlGroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, *model.AppError) {
query := s.channelMembersMinusGroupMembersQuery(channelID, 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.ChannelMembersMinusGroupMembers", "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.ChannelMembersMinusGroupMembers", "store.select_error", nil, err.Error(), http.StatusInternalServerError)
}
return users, nil
}
// CountChannelMembersMinusGroupMembers returns the count of the set of users in the given channel minus the set of users
// in the given groups.
func (s *SqlGroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, *model.AppError) {
queryString, args, err := s.channelMembersMinusGroupMembersQuery(channelID, groupIDs, true).ToSql()
if err != nil {
return 0, model.NewAppError("SqlGroupStore.CountChannelMembersMinusGroupMembers", "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.CountChannelMembersMinusGroupMembers", "store.select_error", nil, err.Error(), http.StatusInternalServerError)
}
return count, nil
}

Просмотреть файл

@@ -605,6 +605,8 @@ type GroupStore interface {
TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, *model.AppError)
CountTeamMembersMinusGroupMembers(teamID string, groupIDs []string) (int64, *model.AppError)
ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, *model.AppError)
CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, *model.AppError)
}
type LinkMetadataStore interface {

Просмотреть файл

@@ -47,6 +47,7 @@ func TestGroupStore(t *testing.T, ss store.Store) {
t.Run("GetGroups", func(t *testing.T) { testGetGroups(t, ss) })
t.Run("TeamMembersMinusGroupMembers", func(t *testing.T) { testTeamMembersMinusGroupMembers(t, ss) })
t.Run("ChannelMembersMinusGroupMembers", func(t *testing.T) { testChannelMembersMinusGroupMembers(t, ss) })
}
func testGroupStoreCreate(t *testing.T, ss store.Store) {
@@ -2392,3 +2393,157 @@ func testTeamMembersMinusGroupMembers(t *testing.T, ss store.Store) {
})
}
}
func testChannelMembersMinusGroupMembers(t *testing.T, ss store.Store) {
const numberOfGroups = 3
const numberOfUsers = 4
groups := []*model.Group{}
users := []*model.User{}
channel := &model.Channel{
TeamId: model.NewId(),
DisplayName: "A Name",
Name: model.NewId(),
Type: model.CHANNEL_PRIVATE,
GroupConstrained: model.NewBool(true),
}
channel, err := ss.Channel().Save(channel, 9999)
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.Channel().SaveMember(&model.ChannelMember{
ChannelId: channel.Id,
UserId: user.Id,
SchemeUser: trueOrFalse,
SchemeAdmin: !trueOrFalse,
NotifyProps: model.GetDefaultChannelNotifyProps(),
})
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().ChannelMembersMinusGroupMembers(channel.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().CountChannelMembersMinusGroupMembers(channel.Id, tc.groupIDs)
require.Nil(t, err)
require.Equal(t, tc.expectedTotalCount, actualCount)
})
}
}

Просмотреть файл

@@ -13,6 +13,31 @@ type GroupStore struct {
mock.Mock
}
// ChannelMembersMinusGroupMembers provides a mock function with given fields: channelID, groupIDs, page, perPage
func (_m *GroupStore) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page int, perPage int) ([]*model.UserWithGroups, *model.AppError) {
ret := _m.Called(channelID, groupIDs, page, perPage)
var r0 []*model.UserWithGroups
if rf, ok := ret.Get(0).(func(string, []string, int, int) []*model.UserWithGroups); ok {
r0 = rf(channelID, 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(channelID, groupIDs, page, perPage)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// ChannelMembersToAdd provides a mock function with given fields: since
func (_m *GroupStore) ChannelMembersToAdd(since int64) ([]*model.UserChannelIDPair, *model.AppError) {
ret := _m.Called(since)
@@ -63,6 +88,29 @@ func (_m *GroupStore) ChannelMembersToRemove() ([]*model.ChannelMember, *model.A
return r0, r1
}
// CountChannelMembersMinusGroupMembers provides a mock function with given fields: channelID, groupIDs
func (_m *GroupStore) CountChannelMembersMinusGroupMembers(channelID string, groupIDs []string) (int64, *model.AppError) {
ret := _m.Called(channelID, groupIDs)
var r0 int64
if rf, ok := ret.Get(0).(func(string, []string) int64); ok {
r0 = rf(channelID, 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(channelID, groupIDs)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*model.AppError)
}
}
return r0, r1
}
// CountGroupsByChannel provides a mock function with given fields: channelId, opts
func (_m *GroupStore) CountGroupsByChannel(channelId string, opts model.GroupSearchOpts) (int64, *model.AppError) {
ret := _m.Called(channelId, opts)