MM-14753: Verifies that user can join teams and channels in spite of group constraints. (#10529)
* MM-147753: Verifies that users are allowed to be members of a team or a channel, based on group constraints, prior to allowing the API to add them. * MM-14753: Allow methods to return meaningful results for deleted teams or channels. * MM-14753: Renames methods to differentiate from permissions and other team and channel restrictions. * MM-14753: Only check if users are team/channel members if team/channel is group constrained. * MM-14753: Updates test function names. * MM-14753: Changes a few method signatures. * MM-14753: Small refactor and adds missing returns. * MM-14753: Changes method names from Get* to Filter* name prefixes. * MM-14753: Renames error variables. * MM-14753: Updates method names for consistency with join table names. * MM-14753: Adds case for non AppError return. * Update i18n/en.json
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
43fa7e0548
Коммит
7bde0378cd
@@ -44,6 +44,7 @@ type TestHelper struct {
|
|||||||
BasicDeletedChannel *model.Channel
|
BasicDeletedChannel *model.Channel
|
||||||
BasicChannel2 *model.Channel
|
BasicChannel2 *model.Channel
|
||||||
BasicPost *model.Post
|
BasicPost *model.Post
|
||||||
|
Group *model.Group
|
||||||
|
|
||||||
SystemAdminClient *model.Client4
|
SystemAdminClient *model.Client4
|
||||||
SystemAdminUser *model.User
|
SystemAdminUser *model.User
|
||||||
@@ -210,6 +211,7 @@ func (me *TestHelper) InitBasic() *TestHelper {
|
|||||||
me.App.UpdateUserRoles(me.BasicUser.Id, model.SYSTEM_USER_ROLE_ID, false)
|
me.App.UpdateUserRoles(me.BasicUser.Id, model.SYSTEM_USER_ROLE_ID, false)
|
||||||
me.Client.DeleteChannel(me.BasicDeletedChannel.Id)
|
me.Client.DeleteChannel(me.BasicDeletedChannel.Id)
|
||||||
me.LoginBasic()
|
me.LoginBasic()
|
||||||
|
me.Group = me.CreateGroup()
|
||||||
|
|
||||||
return me
|
return me
|
||||||
}
|
}
|
||||||
@@ -509,6 +511,24 @@ func (me *TestHelper) GenerateTestEmail() string {
|
|||||||
return strings.ToLower(model.NewId() + "@dockerhost")
|
return strings.ToLower(model.NewId() + "@dockerhost")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (me *TestHelper) CreateGroup() *model.Group {
|
||||||
|
id := model.NewId()
|
||||||
|
group := &model.Group{
|
||||||
|
Name: "n-" + id,
|
||||||
|
DisplayName: "dn_" + id,
|
||||||
|
Source: model.GroupSourceLdap,
|
||||||
|
RemoteId: "ri_" + id,
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.DisableDebugLogForTest()
|
||||||
|
group, err := me.App.CreateGroup(group)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
utils.EnableDebugLogForTest()
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
|
||||||
func GenerateTestUsername() string {
|
func GenerateTestUsername() string {
|
||||||
return "fakeuser" + model.NewRandomString(10)
|
return "fakeuser" + model.NewRandomString(10)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1174,6 +1174,22 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if channel.GroupConstrained != nil && *channel.GroupConstrained {
|
||||||
|
nonMembers, err := c.App.FilterNonGroupChannelMembers([]string{member.UserId}, channel)
|
||||||
|
if err != nil {
|
||||||
|
if v, ok := err.(*model.AppError); ok {
|
||||||
|
c.Err = v
|
||||||
|
} else {
|
||||||
|
c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.error", nil, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(nonMembers) > 0 {
|
||||||
|
c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.user_denied", map[string]interface{}{"UserIDs": nonMembers}, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cm, err := c.App.AddChannelMember(member.UserId, channel, c.App.Session.UserId, postRootId, c.App.Session.Id)
|
cm, err := c.App.AddChannelMember(member.UserId, channel, c.App.Session.UserId, postRootId, c.App.Session.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
|
|||||||
@@ -2021,6 +2021,30 @@ func TestAddChannelMember(t *testing.T) {
|
|||||||
_, resp = Client.AddChannelMember(privateChannel.Id, user3.Id)
|
_, resp = Client.AddChannelMember(privateChannel.Id, user3.Id)
|
||||||
CheckNoError(t, resp)
|
CheckNoError(t, resp)
|
||||||
Client.Logout()
|
Client.Logout()
|
||||||
|
|
||||||
|
// Set a channel to group-constrained
|
||||||
|
privateChannel.GroupConstrained = model.NewBool(true)
|
||||||
|
_, appErr := th.App.UpdateChannel(privateChannel)
|
||||||
|
require.Nil(t, appErr)
|
||||||
|
|
||||||
|
// User is not in associated groups so shouldn't be allowed
|
||||||
|
_, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user.Id)
|
||||||
|
CheckErrorMessage(t, resp, "api.channel.add_members.user_denied")
|
||||||
|
|
||||||
|
// Associate group to team
|
||||||
|
_, appErr = th.App.CreateGroupSyncable(&model.GroupSyncable{
|
||||||
|
GroupId: th.Group.Id,
|
||||||
|
SyncableId: privateChannel.Id,
|
||||||
|
Type: model.GroupSyncableTypeChannel,
|
||||||
|
})
|
||||||
|
require.Nil(t, appErr)
|
||||||
|
|
||||||
|
// Add user to group
|
||||||
|
_, appErr = th.App.CreateOrRestoreGroupMember(th.Group.Id, user.Id)
|
||||||
|
require.Nil(t, appErr)
|
||||||
|
|
||||||
|
_, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user.Id)
|
||||||
|
CheckNoError(t, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddChannelMemberAddMyself(t *testing.T) {
|
func TestAddChannelMemberAddMyself(t *testing.T) {
|
||||||
|
|||||||
56
api4/team.go
56
api4/team.go
@@ -388,6 +388,28 @@ func addTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
team, err := c.App.GetTeam(member.TeamId)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if team.GroupConstrained != nil && *team.GroupConstrained {
|
||||||
|
nonMembers, err := c.App.FilterNonGroupTeamMembers([]string{member.UserId}, team)
|
||||||
|
if err != nil {
|
||||||
|
if v, ok := err.(*model.AppError); ok {
|
||||||
|
c.Err = v
|
||||||
|
} else {
|
||||||
|
c.Err = model.NewAppError("addTeamMember", "api.team.add_members.error", nil, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(nonMembers) > 0 {
|
||||||
|
c.Err = model.NewAppError("addTeamMember", "api.team.add_members.user_denied", map[string]interface{}{"UserIDs": nonMembers}, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
member, err = c.App.AddTeamMember(member.TeamId, member.UserId)
|
member, err = c.App.AddTeamMember(member.TeamId, member.UserId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -432,11 +454,43 @@ func addTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
var err *model.AppError
|
var err *model.AppError
|
||||||
members := model.TeamMembersFromJson(r.Body)
|
members := model.TeamMembersFromJson(r.Body)
|
||||||
|
|
||||||
if len(members) > MAX_ADD_MEMBERS_BATCH || len(members) == 0 {
|
if len(members) > MAX_ADD_MEMBERS_BATCH {
|
||||||
c.SetInvalidParam("too many members in batch")
|
c.SetInvalidParam("too many members in batch")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(members) == 0 {
|
||||||
|
c.SetInvalidParam("no members in batch")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberIDs []string
|
||||||
|
for _, member := range members {
|
||||||
|
memberIDs = append(memberIDs, member.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
team, err := c.App.GetTeam(c.Params.TeamId)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if team.GroupConstrained != nil && *team.GroupConstrained {
|
||||||
|
nonMembers, err := c.App.FilterNonGroupTeamMembers(memberIDs, team)
|
||||||
|
if err != nil {
|
||||||
|
if v, ok := err.(*model.AppError); ok {
|
||||||
|
c.Err = v
|
||||||
|
} else {
|
||||||
|
c.Err = model.NewAppError("addTeamMembers", "api.team.add_members.error", nil, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(nonMembers) > 0 {
|
||||||
|
c.Err = model.NewAppError("addTeamMembers", "api.team.add_members.user_denied", map[string]interface{}{"UserIDs": nonMembers}, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var userIds []string
|
var userIds []string
|
||||||
for _, member := range members {
|
for _, member := range members {
|
||||||
if member.TeamId != c.Params.TeamId {
|
if member.TeamId != c.Params.TeamId {
|
||||||
|
|||||||
@@ -1442,6 +1442,30 @@ func TestAddTeamMember(t *testing.T) {
|
|||||||
if tm != nil {
|
if tm != nil {
|
||||||
t.Fatal("should have not returned team member")
|
t.Fatal("should have not returned team member")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set a team to group-constrained
|
||||||
|
team.GroupConstrained = model.NewBool(true)
|
||||||
|
_, err := th.App.UpdateTeam(team)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// User is not in associated groups so shouldn't be allowed
|
||||||
|
_, resp = th.SystemAdminClient.AddTeamMember(team.Id, otherUser.Id)
|
||||||
|
CheckErrorMessage(t, resp, "api.team.add_members.user_denied")
|
||||||
|
|
||||||
|
// Associate group to team
|
||||||
|
_, err = th.App.CreateGroupSyncable(&model.GroupSyncable{
|
||||||
|
GroupId: th.Group.Id,
|
||||||
|
SyncableId: team.Id,
|
||||||
|
Type: model.GroupSyncableTypeTeam,
|
||||||
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// Add user to group
|
||||||
|
_, err = th.App.CreateOrRestoreGroupMember(th.Group.Id, otherUser.Id)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
_, resp = th.SystemAdminClient.AddTeamMember(team.Id, otherUser.Id)
|
||||||
|
CheckNoError(t, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddTeamMemberMyself(t *testing.T) {
|
func TestAddTeamMemberMyself(t *testing.T) {
|
||||||
@@ -1578,7 +1602,7 @@ func TestAddTeamMembers(t *testing.T) {
|
|||||||
CheckBadRequestStatus(t, resp)
|
CheckBadRequestStatus(t, resp)
|
||||||
|
|
||||||
_, resp = Client.AddTeamMembers(GenerateTestId(), userList)
|
_, resp = Client.AddTeamMembers(GenerateTestId(), userList)
|
||||||
CheckForbiddenStatus(t, resp)
|
CheckNotFoundStatus(t, resp)
|
||||||
|
|
||||||
testUserList := append(userList, GenerateTestId())
|
testUserList := append(userList, GenerateTestId())
|
||||||
_, resp = Client.AddTeamMembers(team.Id, testUserList)
|
_, resp = Client.AddTeamMembers(team.Id, testUserList)
|
||||||
@@ -1633,6 +1657,30 @@ func TestAddTeamMembers(t *testing.T) {
|
|||||||
// Should work as a regular user.
|
// Should work as a regular user.
|
||||||
_, resp = Client.AddTeamMembers(team.Id, userList)
|
_, resp = Client.AddTeamMembers(team.Id, userList)
|
||||||
CheckNoError(t, resp)
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
// Set a team to group-constrained
|
||||||
|
team.GroupConstrained = model.NewBool(true)
|
||||||
|
_, err := th.App.UpdateTeam(team)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// User is not in associated groups so shouldn't be allowed
|
||||||
|
_, resp = Client.AddTeamMembers(team.Id, userList)
|
||||||
|
CheckErrorMessage(t, resp, "api.team.add_members.user_denied")
|
||||||
|
|
||||||
|
// Associate group to team
|
||||||
|
_, err = th.App.CreateGroupSyncable(&model.GroupSyncable{
|
||||||
|
GroupId: th.Group.Id,
|
||||||
|
SyncableId: team.Id,
|
||||||
|
Type: model.GroupSyncableTypeTeam,
|
||||||
|
})
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// Add user to group
|
||||||
|
_, err = th.App.CreateOrRestoreGroupMember(th.Group.Id, userList[0])
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
_, resp = Client.AddTeamMembers(team.Id, userList)
|
||||||
|
CheckNoError(t, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoveTeamMember(t *testing.T) {
|
func TestRemoveTeamMember(t *testing.T) {
|
||||||
|
|||||||
84
app/user.go
84
app/user.go
@@ -596,6 +596,24 @@ func (a *App) GetUsersWithoutTeam(offset int, limit int) ([]*model.User, *model.
|
|||||||
return result.Data.([]*model.User), nil
|
return result.Data.([]*model.User), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTeamGroupUsers returns the users who are associated to the team via GroupTeams and GroupMembers.
|
||||||
|
func (a *App) GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) {
|
||||||
|
result := <-a.Srv.Store.User().GetTeamGroupUsers(teamID)
|
||||||
|
if result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
return result.Data.([]*model.User), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelGroupUsers returns the users who are associated to the channel via GroupChannels and GroupMembers.
|
||||||
|
func (a *App) GetChannelGroupUsers(channelID string) ([]*model.User, *model.AppError) {
|
||||||
|
result := <-a.Srv.Store.User().GetChannelGroupUsers(channelID)
|
||||||
|
if result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
return result.Data.([]*model.User), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) GetUsersByIds(userIds []string, asAdmin bool) ([]*model.User, *model.AppError) {
|
func (a *App) GetUsersByIds(userIds []string, asAdmin bool) ([]*model.User, *model.AppError) {
|
||||||
result := <-a.Srv.Store.User().GetProfileByIds(userIds, true)
|
result := <-a.Srv.Store.User().GetProfileByIds(userIds, true)
|
||||||
if result.Err != nil {
|
if result.Err != nil {
|
||||||
@@ -1853,3 +1871,69 @@ func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provide
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilterNonGroupTeamMembers returns the subset of the given user IDs of the users who are not members of groups
|
||||||
|
// associated to the team.
|
||||||
|
func (a *App) FilterNonGroupTeamMembers(userIDs []string, team *model.Team) ([]string, error) {
|
||||||
|
teamGroupUsers, err := a.GetTeamGroupUsers(team.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// possible if no groups associated or no group members in any of the associated groups
|
||||||
|
if len(teamGroupUsers) == 0 {
|
||||||
|
return userIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nonMemberIDs := []string{}
|
||||||
|
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
userIsMember := false
|
||||||
|
|
||||||
|
for _, pu := range teamGroupUsers {
|
||||||
|
if pu.Id == userID {
|
||||||
|
userIsMember = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userIsMember {
|
||||||
|
nonMemberIDs = append(nonMemberIDs, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonMemberIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterNonGroupChannelMembers returns the subset of the given user IDs of the users who are not members of groups
|
||||||
|
// associated to the channel.
|
||||||
|
func (a *App) FilterNonGroupChannelMembers(userIDs []string, channel *model.Channel) ([]string, error) {
|
||||||
|
channelGroupUsers, err := a.GetChannelGroupUsers(channel.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// possible if no groups associated or no group members in any of the associated groups
|
||||||
|
if len(channelGroupUsers) == 0 {
|
||||||
|
return userIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nonMemberIDs := []string{}
|
||||||
|
|
||||||
|
for _, userID := range userIDs {
|
||||||
|
userIsMember := false
|
||||||
|
|
||||||
|
for _, pu := range channelGroupUsers {
|
||||||
|
if pu.Id == userID {
|
||||||
|
userIsMember = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userIsMember {
|
||||||
|
nonMemberIDs = append(nonMemberIDs, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonMemberIDs, nil
|
||||||
|
}
|
||||||
|
|||||||
16
i18n/en.json
16
i18n/en.json
@@ -1906,6 +1906,22 @@
|
|||||||
"id": "api.team.update_team_scheme.scheme_scope.error",
|
"id": "api.team.update_team_scheme.scheme_scope.error",
|
||||||
"translation": "Unable to set the scheme to the team because the supplied scheme is not a team scheme."
|
"translation": "Unable to set the scheme to the team because the supplied scheme is not a team scheme."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.team.add_members.user_denied",
|
||||||
|
"translation": "Team membership denied to the following users because of group constraints: {{ .UserIDs }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.channel.add_members.user_denied",
|
||||||
|
"translation": "Channel membership denied to the following users because of group constraints: {{ .UserIDs }}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.team.add_members.error",
|
||||||
|
"translation": "Error adding team member(s)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.channel.add_members.error",
|
||||||
|
"translation": "Error adding channel member(s)."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.templates.deactivate_body.info",
|
"id": "api.templates.deactivate_body.info",
|
||||||
"translation": "You deactivated your account on {{ .SiteURL }}."
|
"translation": "You deactivated your account on {{ .SiteURL }}."
|
||||||
|
|||||||
@@ -1536,3 +1536,83 @@ func (us SqlUserStore) GetUsersBatchForIndexing(startTime, endTime int64, limit
|
|||||||
result.Data = usersForIndexing
|
result.Data = usersForIndexing
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (us SqlUserStore) GetTeamGroupUsers(teamID string) store.StoreChannel {
|
||||||
|
return store.Do(func(result *store.StoreResult) {
|
||||||
|
query := us.usersQuery.
|
||||||
|
Where(`Id IN (
|
||||||
|
SELECT
|
||||||
|
GroupMembers.UserId
|
||||||
|
FROM
|
||||||
|
Teams
|
||||||
|
JOIN GroupTeams ON GroupTeams.TeamId = Teams.Id
|
||||||
|
JOIN UserGroups ON UserGroups.Id = GroupTeams.GroupId
|
||||||
|
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
|
||||||
|
WHERE
|
||||||
|
Teams.Id = ?
|
||||||
|
AND GroupTeams.DeleteAt = 0
|
||||||
|
AND UserGroups.DeleteAt = 0
|
||||||
|
AND GroupMembers.DeleteAt = 0
|
||||||
|
GROUP BY
|
||||||
|
GroupMembers.UserId
|
||||||
|
)`, teamID)
|
||||||
|
|
||||||
|
queryString, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlUserStore.UsersPermittedToTeam", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []*model.User
|
||||||
|
if _, err := us.GetReplica().Select(&users, queryString, args...); err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlUserStore.UsersPermittedToTeam", "store.sql_user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range users {
|
||||||
|
u.Sanitize(map[string]bool{})
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Data = users
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (us SqlUserStore) GetChannelGroupUsers(channelID string) store.StoreChannel {
|
||||||
|
return store.Do(func(result *store.StoreResult) {
|
||||||
|
query := us.usersQuery.
|
||||||
|
Where(`Id IN (
|
||||||
|
SELECT
|
||||||
|
GroupMembers.UserId
|
||||||
|
FROM
|
||||||
|
Channels
|
||||||
|
JOIN GroupChannels ON GroupChannels.ChannelId = Channels.Id
|
||||||
|
JOIN UserGroups ON UserGroups.Id = GroupChannels.GroupId
|
||||||
|
JOIN GroupMembers ON GroupMembers.GroupId = UserGroups.Id
|
||||||
|
WHERE
|
||||||
|
Channels.Id = ?
|
||||||
|
AND GroupChannels.DeleteAt = 0
|
||||||
|
AND UserGroups.DeleteAt = 0
|
||||||
|
AND GroupMembers.DeleteAt = 0
|
||||||
|
GROUP BY
|
||||||
|
GroupMembers.UserId
|
||||||
|
)`, channelID)
|
||||||
|
|
||||||
|
queryString, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlUserStore.GetChannelGroupUsers", "store.sql_user.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []*model.User
|
||||||
|
if _, err := us.GetReplica().Select(&users, queryString, args...); err != nil {
|
||||||
|
result.Err = model.NewAppError("SqlUserStore.GetChannelGroupUsers", "store.sql_user.get_profiles.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, u := range users {
|
||||||
|
u.Sanitize(map[string]bool{})
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Data = users
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ type UserStore interface {
|
|||||||
GetAllAfter(limit int, afterId string) StoreChannel
|
GetAllAfter(limit int, afterId string) StoreChannel
|
||||||
GetUsersBatchForIndexing(startTime, endTime int64, limit int) StoreChannel
|
GetUsersBatchForIndexing(startTime, endTime int64, limit int) StoreChannel
|
||||||
Count(options model.UserCountOptions) StoreChannel
|
Count(options model.UserCountOptions) StoreChannel
|
||||||
|
GetTeamGroupUsers(teamID string) StoreChannel
|
||||||
|
GetChannelGroupUsers(channelID string) StoreChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
type BotStore interface {
|
type BotStore interface {
|
||||||
|
|||||||
@@ -848,3 +848,35 @@ func (_m *UserStore) VerifyEmail(userId string, email string) store.StoreChannel
|
|||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTeamGroupUsers provides a mock function with given fields: userId, email
|
||||||
|
func (_m *UserStore) GetTeamGroupUsers(teamID string) store.StoreChannel {
|
||||||
|
ret := _m.Called(teamID)
|
||||||
|
|
||||||
|
var r0 store.StoreChannel
|
||||||
|
if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
|
||||||
|
r0 = rf(teamID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(store.StoreChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannelGroupUsers provides a mock function with given fields: userId, email
|
||||||
|
func (_m *UserStore) GetChannelGroupUsers(teamID string) store.StoreChannel {
|
||||||
|
ret := _m.Called(teamID)
|
||||||
|
|
||||||
|
var r0 store.StoreChannel
|
||||||
|
if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
|
||||||
|
r0 = rf(teamID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(store.StoreChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ func TestUserStore(t *testing.T, ss store.Store) {
|
|||||||
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testUserStoreClearAllCustomRoleAssignments(t, ss) })
|
t.Run("ClearAllCustomRoleAssignments", func(t *testing.T) { testUserStoreClearAllCustomRoleAssignments(t, ss) })
|
||||||
t.Run("GetAllAfter", func(t *testing.T) { testUserStoreGetAllAfter(t, ss) })
|
t.Run("GetAllAfter", func(t *testing.T) { testUserStoreGetAllAfter(t, ss) })
|
||||||
t.Run("GetUsersBatchForIndexing", func(t *testing.T) { testUserStoreGetUsersBatchForIndexing(t, ss) })
|
t.Run("GetUsersBatchForIndexing", func(t *testing.T) { testUserStoreGetUsersBatchForIndexing(t, ss) })
|
||||||
|
t.Run("GetTeamGroupUsers", func(t *testing.T) { testUserStoreGetTeamGroupUsers(t, ss) })
|
||||||
|
t.Run("GetChannelGroupUsers", func(t *testing.T) { testUserStoreGetChannelGroupUsers(t, ss) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUserStoreSave(t *testing.T, ss store.Store) {
|
func testUserStoreSave(t *testing.T, ss store.Store) {
|
||||||
@@ -3430,3 +3432,252 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, ss store.Store) {
|
|||||||
assert.Equal(t, res4List[0].Username, u1.Username)
|
assert.Equal(t, res4List[0].Username, u1.Username)
|
||||||
assert.Equal(t, res4List[1].Username, u2.Username)
|
assert.Equal(t, res4List[1].Username, u2.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testUserStoreGetTeamGroupUsers(t *testing.T, ss store.Store) {
|
||||||
|
// create team
|
||||||
|
id := model.NewId()
|
||||||
|
res := <-ss.Team().Save(&model.Team{
|
||||||
|
DisplayName: "dn_" + id,
|
||||||
|
Name: "n-" + id,
|
||||||
|
Email: id + "@test.com",
|
||||||
|
Type: model.TEAM_INVITE,
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
team := res.Data.(*model.Team)
|
||||||
|
require.NotNil(t, team)
|
||||||
|
|
||||||
|
// create users
|
||||||
|
var testUsers []*model.User
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
id = model.NewId()
|
||||||
|
res = <-ss.User().Save(&model.User{
|
||||||
|
Email: id + "@test.com",
|
||||||
|
Username: "un_" + id,
|
||||||
|
Nickname: "nn_" + id,
|
||||||
|
FirstName: "f_" + id,
|
||||||
|
LastName: "l_" + id,
|
||||||
|
Password: "Password1",
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
user := res.Data.(*model.User)
|
||||||
|
require.NotNil(t, user)
|
||||||
|
testUsers = append(testUsers, user)
|
||||||
|
}
|
||||||
|
userGroupA := testUsers[0]
|
||||||
|
userGroupB := testUsers[1]
|
||||||
|
userNoGroup := testUsers[2]
|
||||||
|
|
||||||
|
// add non-group-member to the team (to prove that the query isn't just returning all members)
|
||||||
|
res = <-ss.Team().SaveMember(&model.TeamMember{
|
||||||
|
TeamId: team.Id,
|
||||||
|
UserId: userNoGroup.Id,
|
||||||
|
}, 999)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// create groups
|
||||||
|
var testGroups []*model.Group
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
id = model.NewId()
|
||||||
|
res = <-ss.Group().Create(&model.Group{
|
||||||
|
Name: "n_" + id,
|
||||||
|
DisplayName: "dn_" + id,
|
||||||
|
Source: model.GroupSourceLdap,
|
||||||
|
RemoteId: "ri_" + id,
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
group := res.Data.(*model.Group)
|
||||||
|
require.NotNil(t, group)
|
||||||
|
testGroups = append(testGroups, group)
|
||||||
|
}
|
||||||
|
groupA := testGroups[0]
|
||||||
|
groupB := testGroups[1]
|
||||||
|
|
||||||
|
// add members to groups
|
||||||
|
res = <-ss.Group().CreateOrRestoreMember(groupA.Id, userGroupA.Id)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
res = <-ss.Group().CreateOrRestoreMember(groupB.Id, userGroupB.Id)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// association one group to team
|
||||||
|
res = <-ss.Group().CreateGroupSyncable(&model.GroupSyncable{
|
||||||
|
GroupId: groupA.Id,
|
||||||
|
SyncableId: team.Id,
|
||||||
|
Type: model.GroupSyncableTypeTeam,
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
var users []*model.User
|
||||||
|
|
||||||
|
requireNUsers := func(n int) {
|
||||||
|
res = <-ss.User().GetTeamGroupUsers(team.Id)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
users = res.Data.([]*model.User)
|
||||||
|
require.NotNil(t, users)
|
||||||
|
require.Len(t, users, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// team not group constrained returns users
|
||||||
|
requireNUsers(1)
|
||||||
|
|
||||||
|
// update team to be group-constrained
|
||||||
|
team.GroupConstrained = model.NewBool(true)
|
||||||
|
res = <-ss.Team().Update(team)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// still returns user (being group-constrained has no effect)
|
||||||
|
requireNUsers(1)
|
||||||
|
|
||||||
|
// associate other group to team
|
||||||
|
res = <-ss.Group().CreateGroupSyncable(&model.GroupSyncable{
|
||||||
|
GroupId: groupB.Id,
|
||||||
|
SyncableId: team.Id,
|
||||||
|
Type: model.GroupSyncableTypeTeam,
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// should return users from all groups
|
||||||
|
// 2 users now that both groups have been associated to the team
|
||||||
|
requireNUsers(2)
|
||||||
|
|
||||||
|
// add team membership of allowed user
|
||||||
|
res = <-ss.Team().SaveMember(&model.TeamMember{
|
||||||
|
TeamId: team.Id,
|
||||||
|
UserId: userGroupA.Id,
|
||||||
|
}, 999)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// ensure allowed member still returned by query
|
||||||
|
requireNUsers(2)
|
||||||
|
|
||||||
|
// delete team membership of allowed user
|
||||||
|
res = <-ss.Team().RemoveMember(team.Id, userGroupA.Id)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// ensure removed allowed member still returned by query
|
||||||
|
requireNUsers(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUserStoreGetChannelGroupUsers(t *testing.T, ss store.Store) {
|
||||||
|
// create channel
|
||||||
|
id := model.NewId()
|
||||||
|
res := <-ss.Channel().Save(&model.Channel{
|
||||||
|
DisplayName: "dn_" + id,
|
||||||
|
Name: "n-" + id,
|
||||||
|
Type: model.CHANNEL_PRIVATE,
|
||||||
|
}, 999)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
channel := res.Data.(*model.Channel)
|
||||||
|
require.NotNil(t, channel)
|
||||||
|
|
||||||
|
// create users
|
||||||
|
var testUsers []*model.User
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
id = model.NewId()
|
||||||
|
res = <-ss.User().Save(&model.User{
|
||||||
|
Email: id + "@test.com",
|
||||||
|
Username: "un_" + id,
|
||||||
|
Nickname: "nn_" + id,
|
||||||
|
FirstName: "f_" + id,
|
||||||
|
LastName: "l_" + id,
|
||||||
|
Password: "Password1",
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
user := res.Data.(*model.User)
|
||||||
|
require.NotNil(t, user)
|
||||||
|
testUsers = append(testUsers, user)
|
||||||
|
}
|
||||||
|
userGroupA := testUsers[0]
|
||||||
|
userGroupB := testUsers[1]
|
||||||
|
userNoGroup := testUsers[2]
|
||||||
|
|
||||||
|
// add non-group-member to the channel (to prove that the query isn't just returning all members)
|
||||||
|
res = <-ss.Channel().SaveMember(&model.ChannelMember{
|
||||||
|
ChannelId: channel.Id,
|
||||||
|
UserId: userNoGroup.Id,
|
||||||
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// create groups
|
||||||
|
var testGroups []*model.Group
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
id = model.NewId()
|
||||||
|
res = <-ss.Group().Create(&model.Group{
|
||||||
|
Name: "n_" + id,
|
||||||
|
DisplayName: "dn_" + id,
|
||||||
|
Source: model.GroupSourceLdap,
|
||||||
|
RemoteId: "ri_" + id,
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
group := res.Data.(*model.Group)
|
||||||
|
require.NotNil(t, group)
|
||||||
|
testGroups = append(testGroups, group)
|
||||||
|
}
|
||||||
|
groupA := testGroups[0]
|
||||||
|
groupB := testGroups[1]
|
||||||
|
|
||||||
|
// add members to groups
|
||||||
|
res = <-ss.Group().CreateOrRestoreMember(groupA.Id, userGroupA.Id)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
res = <-ss.Group().CreateOrRestoreMember(groupB.Id, userGroupB.Id)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// association one group to channel
|
||||||
|
res = <-ss.Group().CreateGroupSyncable(&model.GroupSyncable{
|
||||||
|
GroupId: groupA.Id,
|
||||||
|
SyncableId: channel.Id,
|
||||||
|
Type: model.GroupSyncableTypeChannel,
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
var users []*model.User
|
||||||
|
|
||||||
|
requireNUsers := func(n int) {
|
||||||
|
res = <-ss.User().GetChannelGroupUsers(channel.Id)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
users = res.Data.([]*model.User)
|
||||||
|
require.NotNil(t, users)
|
||||||
|
require.Len(t, users, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel not group constrained returns users
|
||||||
|
requireNUsers(1)
|
||||||
|
|
||||||
|
// update team to be group-constrained
|
||||||
|
channel.GroupConstrained = model.NewBool(true)
|
||||||
|
res = <-ss.Channel().Update(channel)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// still returns user (being group-constrained has no effect)
|
||||||
|
requireNUsers(1)
|
||||||
|
|
||||||
|
// associate other group to team
|
||||||
|
res = <-ss.Group().CreateGroupSyncable(&model.GroupSyncable{
|
||||||
|
GroupId: groupB.Id,
|
||||||
|
SyncableId: channel.Id,
|
||||||
|
Type: model.GroupSyncableTypeChannel,
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// should return users from all groups
|
||||||
|
// 2 users now that both groups have been associated to the team
|
||||||
|
requireNUsers(2)
|
||||||
|
|
||||||
|
// add team membership of allowed user
|
||||||
|
res = <-ss.Channel().SaveMember(&model.ChannelMember{
|
||||||
|
ChannelId: channel.Id,
|
||||||
|
UserId: userGroupA.Id,
|
||||||
|
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||||
|
})
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// ensure allowed member still returned by query
|
||||||
|
requireNUsers(2)
|
||||||
|
|
||||||
|
// delete team membership of allowed user
|
||||||
|
res = <-ss.Channel().RemoveMember(channel.Id, userGroupA.Id)
|
||||||
|
require.Nil(t, res.Err)
|
||||||
|
|
||||||
|
// ensure removed allowed member still returned by query
|
||||||
|
requireNUsers(2)
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user