[MM-63504] When changing a channel to group synced, it doesn't always clear members (#30526)

* fix issue with channel and team membership after group constraints are enabled
Этот коммит содержится в:
Ben Cooke
2025-04-01 12:20:00 -04:00
коммит произвёл GitHub
родитель a47269cfe2
Коммит 2454de5b4a
10 изменённых файлов: 614 добавлений и 91 удалений

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

@@ -391,6 +391,15 @@ func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
// If the channel is now group constrained but wasn't previously, delete members that aren't part of the channel's groups
if patch.GroupConstrained != nil && *patch.GroupConstrained && (originalOldChannel.GroupConstrained == nil || !*originalOldChannel.GroupConstrained) {
c.App.Srv().Go(func() {
if err := c.App.DeleteGroupConstrainedChannelMemberships(c.AppContext, &rchannel.Id); err != nil {
c.Logger.Warn("Error deleting group-constrained channel memberships", mlog.Err(err))
}
})
}
appErr = c.App.FillInChannelProps(c.AppContext, rchannel)
if appErr != nil {
c.Err = appErr

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

@@ -479,6 +479,168 @@ func TestPatchChannel(t *testing.T) {
})
})
t.Run("Test GroupConstrained flag set to true and non group members are removed", func(t *testing.T) {
client.Logout(context.Background())
th.LoginBasic()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
defer func() {
th.App.Srv().RemoveLicense()
}()
// Create a test group
group := th.CreateGroup()
// Create a channel and set it as group-constrained
channel := th.CreatePrivateChannel()
// Add user to the channel
th.AddUserToChannel(th.BasicUser2, channel)
// Create a group user
groupUser := th.CreateUser()
th.LinkUserToTeam(groupUser, th.BasicTeam)
// Create a group member
_, appErr := th.App.UpsertGroupMember(group.Id, groupUser.Id)
require.Nil(t, appErr)
// Associate the group with the channel
autoAdd := true
schemeAdmin := true
_, r, err := th.SystemAdminClient.LinkGroupSyncable(context.Background(), group.Id, channel.Id, model.GroupSyncableTypeChannel, &model.GroupSyncablePatch{AutoAdd: &autoAdd, SchemeAdmin: &schemeAdmin})
require.NoError(t, err)
CheckCreatedStatus(t, r)
patch := &model.ChannelPatch{}
patch.GroupConstrained = model.NewPointer(true)
_, r, err = th.SystemAdminClient.PatchChannel(context.Background(), channel.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, r)
// Wait for the user to be removed from the channel by polling until they're gone
// or until we hit the timeout
timeout := time.After(3 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
userRemoved := false
for !userRemoved {
select {
case <-timeout:
require.Fail(t, "Timed out waiting for user to be removed from channel")
return
case <-ticker.C:
// Check if the user is still a member
_, r, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, th.BasicUser2.Id, "")
if err != nil && r.StatusCode == http.StatusNotFound {
// User has been removed, we can continue the test
userRemoved = true
}
}
}
// Verify the user is no longer a member of the channel
_, r, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, th.BasicUser2.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, r)
})
t.Run("Test GroupConstrained flag changed from true to false and non group members are not removed", func(t *testing.T) {
client.Logout(context.Background())
th.LoginBasic()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
defer func() {
th.App.Srv().RemoveLicense()
}()
// Create a test group
group := th.CreateGroup()
// Create a channel and set it as group-constrained
channel := th.CreatePrivateChannel()
// Create a group user
groupUser := th.CreateUser()
th.LinkUserToTeam(groupUser, th.BasicTeam)
// Create a group member
_, appErr := th.App.UpsertGroupMember(group.Id, groupUser.Id)
require.Nil(t, appErr)
// Associate the group with the channel
autoAdd := true
schemeAdmin := true
_, r, err := th.SystemAdminClient.LinkGroupSyncable(context.Background(), group.Id, channel.Id, model.GroupSyncableTypeChannel, &model.GroupSyncablePatch{AutoAdd: &autoAdd, SchemeAdmin: &schemeAdmin})
require.NoError(t, err)
CheckCreatedStatus(t, r)
// Wait for the user to be added to the channel by polling until you see them
// or until we hit the timeout
timeout := time.After(3 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
var cm *model.ChannelMember
userFound := false
for !userFound {
select {
case <-timeout:
require.Fail(t, "Timed out waiting for user to be added to the channel")
return
case <-ticker.C:
// Check if the user is now a member
cm, _, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
if err == nil && cm.UserId == groupUser.Id {
// User has been added, we can continue the test
userFound = true
}
}
}
patch := &model.ChannelPatch{}
patch.GroupConstrained = model.NewPointer(true)
_, r, err = th.SystemAdminClient.PatchChannel(context.Background(), channel.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, r)
// Change the GroupConstrained flag to false
patch.GroupConstrained = model.NewPointer(false)
_, r, err = th.SystemAdminClient.PatchChannel(context.Background(), channel.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, r)
// Unlink the group
r, err = th.SystemAdminClient.UnlinkGroupSyncable(context.Background(), group.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
CheckOKStatus(t, r)
// Wait for a reasonable amount of time to ensure the user is not removed because the channel is no longer group constrained
timeout = time.After(2 * time.Second)
ticker = time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
userStillPresent := true
for userStillPresent {
select {
case <-timeout:
// If we reach the timeout, the user is still present, which is what we want
// Verify the user is still a member of the channel
cm, r, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
require.NoError(t, err)
CheckOKStatus(t, r)
require.Equal(t, groupUser.Id, cm.UserId)
return
case <-ticker.C:
// Check if the user is still a member
_, r, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
if err != nil && r.StatusCode == http.StatusNotFound {
// User has been removed, which is not what we want
require.Fail(t, "User was incorrectly removed from the channel")
userStillPresent = false
}
}
}
})
t.Run("Test updating the header of someone else's GM channel", func(t *testing.T) {
// Test updating the header of someone else's GM channel.
user := th.CreateUser()

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

@@ -641,7 +641,7 @@ func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
}
c.App.Srv().Go(func() {
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId)
c.App.RemoveMembershipsFromUnlinkedSyncable(c.AppContext, syncableID, syncableType)
})
auditRec.Success()

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

@@ -831,6 +831,168 @@ func TestUnlinkGroupChannel(t *testing.T) {
require.Error(t, err)
CheckBadRequestStatus(t, response)
})
t.Run("Unlinking a group in a group constrained channel causes group members to be removed", func(t *testing.T) {
// Create a test group
group := th.CreateGroup()
// Create a channel and set it as group-constrained
channel := th.CreatePrivateChannel()
// Create a group user
groupUser := th.CreateUser()
th.LinkUserToTeam(groupUser, th.BasicTeam)
// Create a group member
_, appErr := th.App.UpsertGroupMember(group.Id, groupUser.Id)
require.Nil(t, appErr)
// Associate the group with the channel
autoAdd := true
schemeAdmin := true
_, r, err := th.SystemAdminClient.LinkGroupSyncable(context.Background(), group.Id, channel.Id, model.GroupSyncableTypeChannel, &model.GroupSyncablePatch{AutoAdd: &autoAdd, SchemeAdmin: &schemeAdmin})
require.NoError(t, err)
CheckCreatedStatus(t, r)
// Wait for the user to be added to the channel by polling until you see them
// or until we hit the timeout
timeout := time.After(5 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
var cm *model.ChannelMember
userFound := false
for !userFound {
select {
case <-timeout:
require.Fail(t, "Timed out waiting for user to be added to the channel")
return
case <-ticker.C:
// Check if the user is now a member
cm, _, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
if err == nil && cm.UserId == groupUser.Id {
// User has been added, we can continue the test
userFound = true
}
}
}
patch := &model.ChannelPatch{}
patch.GroupConstrained = model.NewPointer(true)
_, r, err = th.SystemAdminClient.PatchChannel(context.Background(), channel.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, r)
// Unlink the group
r, err = th.SystemAdminClient.UnlinkGroupSyncable(context.Background(), group.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
CheckOKStatus(t, r)
// Wait for the user to be removed from the channel by polling until they're gone
// or until we hit the timeout
timeout = time.After(3 * time.Second)
ticker = time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
userRemoved := false
for !userRemoved {
select {
case <-timeout:
require.Fail(t, "Timed out waiting for user to be removed from channel")
return
case <-ticker.C:
// Check if the user is still a member
_, r, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
if err != nil && r.StatusCode == http.StatusNotFound {
// User has been removed, we can continue the test
userRemoved = true
}
}
}
// Verify the user is no longer a member of the channel
_, r, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
require.Error(t, err)
CheckNotFoundStatus(t, r)
})
t.Run("Unlinking a group in a non group constrained channel does not remove group members from the channel", func(t *testing.T) {
// Create a test group
group := th.CreateGroup()
// Create a channel and set it as group-constrained
channel := th.CreatePrivateChannel()
// Create a group user
groupUser := th.CreateUser()
th.LinkUserToTeam(groupUser, th.BasicTeam)
// Create a group member
_, appErr := th.App.UpsertGroupMember(group.Id, groupUser.Id)
require.Nil(t, appErr)
// Associate the group with the channel
autoAdd := true
schemeAdmin := true
_, r, err := th.SystemAdminClient.LinkGroupSyncable(context.Background(), group.Id, channel.Id, model.GroupSyncableTypeChannel, &model.GroupSyncablePatch{AutoAdd: &autoAdd, SchemeAdmin: &schemeAdmin})
require.NoError(t, err)
CheckCreatedStatus(t, r)
// Wait for the user to be added to the channel by polling until you see them
// or until we hit the timeout
timeout := time.After(5 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
var cm *model.ChannelMember
userFound := false
for !userFound {
select {
case <-timeout:
require.Fail(t, "Timed out waiting for user to be added to the channel")
return
case <-ticker.C:
// Check if the user is now a member
cm, _, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
if err == nil && cm.UserId == groupUser.Id {
// User has been added, we can continue the test
userFound = true
}
}
}
// Unlink the group
r, err = th.SystemAdminClient.UnlinkGroupSyncable(context.Background(), group.Id, channel.Id, model.GroupSyncableTypeChannel)
require.NoError(t, err)
CheckOKStatus(t, r)
// Wait for a reasonable amount of time to ensure the user is not removed because the channel is not group constrained
timeout = time.After(2 * time.Second)
ticker = time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
userStillPresent := true
for userStillPresent {
select {
case <-timeout:
// If we reach the timeout, the user is still present, which is what we want
// Verify the user is still a member of the channel
cm, r, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
require.NoError(t, err)
CheckOKStatus(t, r)
require.Equal(t, groupUser.Id, cm.UserId)
return
case <-ticker.C:
// Check if the user is still a member
_, r, err = th.SystemAdminClient.GetChannelMember(context.Background(), channel.Id, groupUser.Id, "")
if err != nil && r.StatusCode == http.StatusNotFound {
// User has been removed, which is not what we want
require.Fail(t, "User was incorrectly removed from the channel")
userStillPresent = false
}
}
}
})
}
func TestGetGroupTeam(t *testing.T) {

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

@@ -257,18 +257,27 @@ func patchTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if oldTeam, err := c.App.GetTeam(c.Params.TeamId); err == nil {
oldTeam, err := c.App.GetTeam(c.Params.TeamId)
if err == nil {
auditRec.AddEventPriorState(oldTeam)
auditRec.AddEventObjectType("team")
}
patchedTeam, err := c.App.PatchTeam(c.Params.TeamId, &team)
if err != nil {
c.Err = err
return
}
// If the team is now group constrained but wasn't previously, delete members that aren't part of the team's groups
if patchedTeam.GroupConstrained != nil && *patchedTeam.GroupConstrained && (oldTeam.GroupConstrained == nil || !*oldTeam.GroupConstrained) {
c.App.Srv().Go(func() {
if err := c.App.DeleteGroupConstrainedTeamMemberships(c.AppContext, &c.Params.TeamId); err != nil {
c.Logger.Warn("Error deleting group-constrained team memberships", mlog.Err(err))
}
})
}
c.App.SanitizeTeam(*c.AppContext.Session(), patchedTeam)
auditRec.Success()

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

@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -651,6 +652,149 @@ func TestPatchTeam(t *testing.T) {
_, _, err3 = th.Client.PatchTeam(context.Background(), rteam2.Id, patch2)
require.Error(t, err3)
})
t.Run("GroupConstrained flag set to true and non group members are removed", func(t *testing.T) {
var appErr *model.AppError
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
defer func() {
appErr = th.App.Srv().RemoveLicense()
require.Nil(t, appErr)
}()
th.LoginTeamAdmin()
team2 := &model.Team{DisplayName: "Name", Name: GenerateTestTeamName(), Email: th.GenerateTestEmail(), Type: model.TeamOpen, AllowOpenInvite: false}
team2, _, _ = th.Client.CreateTeam(context.Background(), team2)
_, resp, err := th.SystemAdminClient.AddTeamMember(context.Background(), team2.Id, th.BasicUser2.Id)
require.NoError(t, err)
CheckCreatedStatus(t, resp)
// Create a test group
group := th.CreateGroup()
// Create a group user
groupUser := th.CreateUser()
th.LinkUserToTeam(groupUser, th.BasicTeam)
// Create a group member
_, appErr = th.App.UpsertGroupMember(group.Id, groupUser.Id)
require.Nil(t, appErr)
// Associate the group with the team
autoAdd := true
schemeAdmin := true
_, r, err := th.SystemAdminClient.LinkGroupSyncable(context.Background(), group.Id, team2.Id, model.GroupSyncableTypeTeam, &model.GroupSyncablePatch{AutoAdd: &autoAdd, SchemeAdmin: &schemeAdmin})
require.NoError(t, err)
CheckCreatedStatus(t, r)
patch := &model.TeamPatch{}
patch.GroupConstrained = model.NewPointer(true)
_, r, err = th.SystemAdminClient.PatchTeam(context.Background(), team2.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, r)
// Wait for the user to be removed from the team by polling until they're gone
// or until we hit the timeout
timeout := time.After(5 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
var tm *model.TeamMember
userRemoved := false
for !userRemoved {
select {
case <-timeout:
require.Fail(t, "Timed out waiting for user to be removed from team")
return
case <-ticker.C:
// Check if the user is still a member
tm, r, err = th.SystemAdminClient.GetTeamMember(context.Background(), team2.Id, th.BasicUser2.Id, "")
if err == nil && r.StatusCode == http.StatusOK && tm.DeleteAt != 0 {
// User has been removed, we can continue the test
userRemoved = true
}
}
}
tm, r, err = th.SystemAdminClient.GetTeamMember(context.Background(), team2.Id, th.BasicUser2.Id, "")
require.NoError(t, err)
CheckOKStatus(t, r)
require.Equal(t, tm.UserId, th.BasicUser2.Id)
require.NotEqual(t, tm.DeleteAt, int64(0))
})
t.Run("GroupConstrained flag changed from true to false and non group members are not removed", func(t *testing.T) {
var appErr *model.AppError
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
defer func() {
appErr = th.App.Srv().RemoveLicense()
require.Nil(t, appErr)
}()
th.LoginTeamAdmin()
team2 := &model.Team{DisplayName: "Name", Name: GenerateTestTeamName(), Email: th.GenerateTestEmail(), Type: model.TeamOpen, AllowOpenInvite: false}
team2, _, _ = th.Client.CreateTeam(context.Background(), team2)
// Create a test group
group := th.CreateGroup()
// Create a group user
groupUser := th.CreateUser()
th.LinkUserToTeam(groupUser, th.BasicTeam)
// Create a group member
_, appErr = th.App.UpsertGroupMember(group.Id, groupUser.Id)
require.Nil(t, appErr)
// Associate the group with the team
autoAdd := true
schemeAdmin := true
_, r, err := th.SystemAdminClient.LinkGroupSyncable(context.Background(), group.Id, team2.Id, model.GroupSyncableTypeTeam, &model.GroupSyncablePatch{AutoAdd: &autoAdd, SchemeAdmin: &schemeAdmin})
require.NoError(t, err)
CheckCreatedStatus(t, r)
patch := &model.TeamPatch{}
patch.GroupConstrained = model.NewPointer(true)
_, r, err = th.SystemAdminClient.PatchTeam(context.Background(), team2.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, r)
patch.GroupConstrained = model.NewPointer(false)
_, r, err = th.SystemAdminClient.PatchTeam(context.Background(), team2.Id, patch)
require.NoError(t, err)
CheckOKStatus(t, r)
// Unlink the group
r, err = th.SystemAdminClient.UnlinkGroupSyncable(context.Background(), group.Id, team2.Id, model.GroupSyncableTypeTeam)
require.NoError(t, err)
CheckOKStatus(t, r)
// Wait for a reasonable amount of time to ensure the user is not removed because the team is no longer group constrained
timeout := time.After(2 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
var tm *model.TeamMember
userStillPresent := true
for userStillPresent {
select {
case <-timeout:
// If we reach the timeout, the user is still present, which is what we want
// Verify the user is still a member of the team
tm, r, err = th.SystemAdminClient.GetTeamMember(context.Background(), team2.Id, groupUser.Id, "")
require.NoError(t, err)
CheckOKStatus(t, r)
require.Equal(t, tm.DeleteAt, int64(0))
return
case <-ticker.C:
// Check if the user is still a member
tm, r, err = th.SystemAdminClient.GetTeamMember(context.Background(), team2.Id, groupUser.Id, "")
if err == nil && r.StatusCode == http.StatusOK && tm.DeleteAt != 0 {
// User has been removed, which is not what we want
require.Fail(t, "User was incorrectly removed from the team")
userStillPresent = false
}
}
}
})
}
func TestRestoreTeam(t *testing.T) {

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

@@ -142,12 +142,12 @@ func (a *App) CreateDefaultMemberships(rctx request.CTX, params model.CreateDefa
// DeleteGroupConstrainedMemberships deletes team and channel memberships of users who aren't members of the allowed
// groups of all group-constrained teams and channels.
func (a *App) DeleteGroupConstrainedMemberships(rctx request.CTX) error {
err := a.deleteGroupConstrainedChannelMemberships(rctx, nil)
err := a.DeleteGroupConstrainedChannelMemberships(rctx, nil)
if err != nil {
return err
}
err = a.deleteGroupConstrainedTeamMemberships(rctx, nil)
err = a.DeleteGroupConstrainedTeamMemberships(rctx, nil)
if err != nil {
return err
}
@@ -155,10 +155,10 @@ func (a *App) DeleteGroupConstrainedMemberships(rctx request.CTX) error {
return nil
}
// deleteGroupConstrainedTeamMemberships deletes team memberships of users who aren't members of the allowed
// DeleteGroupConstrainedTeamMemberships deletes team memberships of users who aren't members of the allowed
// groups of the given group-constrained team. If a teamID is given then the procedure is scoped to the given team,
// if teamID is nil then the procedure affects all teams.
func (a *App) deleteGroupConstrainedTeamMemberships(rctx request.CTX, teamID *string) error {
func (a *App) DeleteGroupConstrainedTeamMemberships(rctx request.CTX, teamID *string) error {
teamMembers, appErr := a.TeamMembersToRemove(teamID)
if appErr != nil {
return appErr
@@ -183,10 +183,10 @@ func (a *App) deleteGroupConstrainedTeamMemberships(rctx request.CTX, teamID *st
return multiErr.ErrorOrNil()
}
// deleteGroupConstrainedChannelMemberships deletes channel memberships of users who aren't members of the allowed
// DeleteGroupConstrainedChannelMemberships deletes channel memberships of users who aren't members of the allowed
// groups of the given group-constrained channel. If a channelID is given then the procedure is scoped to the given team,
// if channelID is nil then the procedure affects all teams.
func (a *App) deleteGroupConstrainedChannelMemberships(rctx request.CTX, channelID *string) error {
func (a *App) DeleteGroupConstrainedChannelMemberships(rctx request.CTX, channelID *string) error {
channelMembers, appErr := a.ChannelMembersToRemove(channelID)
if appErr != nil {
return appErr
@@ -301,16 +301,24 @@ func (a *App) SyncRolesAndMembership(rctx request.CTX, syncableID string, syncab
if err := a.createDefaultTeamMemberships(rctx, params); err != nil {
rctx.Logger().Warn("Error creating default team memberships", mlog.Err(err))
}
if err := a.deleteGroupConstrainedTeamMemberships(rctx, &syncableID); err != nil {
rctx.Logger().Warn("Error deleting group constrained team memberships", mlog.Err(err))
}
case model.GroupSyncableTypeChannel:
params.ScopedChannelID = &syncableID
if err := a.createDefaultChannelMemberships(rctx, params); err != nil {
rctx.Logger().Warn("Error creating default channel memberships", mlog.Err(err))
}
if err := a.deleteGroupConstrainedChannelMemberships(rctx, &syncableID); err != nil {
}
}
// This method should be called when a syncable is unlinked from a group
func (a *App) RemoveMembershipsFromUnlinkedSyncable(rctx request.CTX, syncableID string, syncableType model.GroupSyncableType) {
switch syncableType {
case model.GroupSyncableTypeTeam:
if err := a.DeleteGroupConstrainedTeamMemberships(rctx, &syncableID); err != nil {
rctx.Logger().Warn("Error deleting group constrained team memberships", mlog.Err(err))
}
case model.GroupSyncableTypeChannel:
if err := a.DeleteGroupConstrainedChannelMemberships(rctx, &syncableID); err != nil {
rctx.Logger().Warn("Error deleting group constrained channel memberships", mlog.Err(err))
}
}
}