[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
Этот коммит содержится в:
@@ -49,7 +49,8 @@ describe('channel groups', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('limits the listed groups if the parent team is group-constrained', () => {
|
// This test is using broken functionality to verify that listed groups are limited. This needs to be changed so that a system admin is part of the contrained group
|
||||||
|
it.skip('limits the listed groups if the parent team is group-constrained', () => {
|
||||||
// # Visit a channel
|
// # Visit a channel
|
||||||
cy.visit(`/${testTeam.name}/channels/off-topic`);
|
cy.visit(`/${testTeam.name}/channels/off-topic`);
|
||||||
|
|
||||||
|
|||||||
@@ -391,6 +391,15 @@ func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
appErr = c.App.FillInChannelProps(c.AppContext, rchannel)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
c.Err = appErr
|
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) {
|
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.
|
// Test updating the header of someone else's GM channel.
|
||||||
user := th.CreateUser()
|
user := th.CreateUser()
|
||||||
|
|||||||
@@ -641,7 +641,7 @@ func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.App.Srv().Go(func() {
|
c.App.Srv().Go(func() {
|
||||||
c.App.SyncRolesAndMembership(c.AppContext, syncableID, syncableType, c.Params.GroupId)
|
c.App.RemoveMembershipsFromUnlinkedSyncable(c.AppContext, syncableID, syncableType)
|
||||||
})
|
})
|
||||||
|
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
|
|||||||
@@ -831,6 +831,168 @@ func TestUnlinkGroupChannel(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
CheckBadRequestStatus(t, response)
|
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) {
|
func TestGetGroupTeam(t *testing.T) {
|
||||||
|
|||||||
@@ -257,18 +257,27 @@ func patchTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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.AddEventPriorState(oldTeam)
|
||||||
auditRec.AddEventObjectType("team")
|
auditRec.AddEventObjectType("team")
|
||||||
}
|
}
|
||||||
|
|
||||||
patchedTeam, err := c.App.PatchTeam(c.Params.TeamId, &team)
|
patchedTeam, err := c.App.PatchTeam(c.Params.TeamId, &team)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
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)
|
c.App.SanitizeTeam(*c.AppContext.Session(), patchedTeam)
|
||||||
|
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -651,6 +652,149 @@ func TestPatchTeam(t *testing.T) {
|
|||||||
_, _, err3 = th.Client.PatchTeam(context.Background(), rteam2.Id, patch2)
|
_, _, err3 = th.Client.PatchTeam(context.Background(), rteam2.Id, patch2)
|
||||||
require.Error(t, err3)
|
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) {
|
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
|
// DeleteGroupConstrainedMemberships deletes team and channel memberships of users who aren't members of the allowed
|
||||||
// groups of all group-constrained teams and channels.
|
// groups of all group-constrained teams and channels.
|
||||||
func (a *App) DeleteGroupConstrainedMemberships(rctx request.CTX) error {
|
func (a *App) DeleteGroupConstrainedMemberships(rctx request.CTX) error {
|
||||||
err := a.deleteGroupConstrainedChannelMemberships(rctx, nil)
|
err := a.DeleteGroupConstrainedChannelMemberships(rctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.deleteGroupConstrainedTeamMemberships(rctx, nil)
|
err = a.DeleteGroupConstrainedTeamMemberships(rctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -155,10 +155,10 @@ func (a *App) DeleteGroupConstrainedMemberships(rctx request.CTX) error {
|
|||||||
return nil
|
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,
|
// 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.
|
// 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)
|
teamMembers, appErr := a.TeamMembersToRemove(teamID)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
return appErr
|
return appErr
|
||||||
@@ -183,10 +183,10 @@ func (a *App) deleteGroupConstrainedTeamMemberships(rctx request.CTX, teamID *st
|
|||||||
return multiErr.ErrorOrNil()
|
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,
|
// 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.
|
// 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)
|
channelMembers, appErr := a.ChannelMembersToRemove(channelID)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
return appErr
|
return appErr
|
||||||
@@ -301,16 +301,24 @@ func (a *App) SyncRolesAndMembership(rctx request.CTX, syncableID string, syncab
|
|||||||
if err := a.createDefaultTeamMemberships(rctx, params); err != nil {
|
if err := a.createDefaultTeamMemberships(rctx, params); err != nil {
|
||||||
rctx.Logger().Warn("Error creating default team memberships", mlog.Err(err))
|
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:
|
case model.GroupSyncableTypeChannel:
|
||||||
params.ScopedChannelID = &syncableID
|
params.ScopedChannelID = &syncableID
|
||||||
if err := a.createDefaultChannelMemberships(rctx, params); err != nil {
|
if err := a.createDefaultChannelMemberships(rctx, params); err != nil {
|
||||||
rctx.Logger().Warn("Error creating default channel memberships", mlog.Err(err))
|
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))
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,27 +424,9 @@ export default class ChannelDetails extends React.PureComponent<ChannelDetailsPr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = [];
|
let privacyChangePromise;
|
||||||
if (isPrivacyChanging) {
|
if (isPrivacyChanging) {
|
||||||
const convert = actions.updateChannelPrivacy(channel.id, isPublic ? Constants.OPEN_CHANNEL : Constants.PRIVATE_CHANNEL);
|
privacyChangePromise = actions.updateChannelPrivacy(channel.id, isPublic ? Constants.OPEN_CHANNEL : Constants.PRIVATE_CHANNEL);
|
||||||
promises.push(
|
|
||||||
convert.then((res: ActionResult) => {
|
|
||||||
if ('error' in res) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
return actions.patchChannel(channel.id, {
|
|
||||||
...channel,
|
|
||||||
group_constrained: isSynced,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
promises.push(
|
|
||||||
actions.patchChannel(channel.id, {
|
|
||||||
...channel,
|
|
||||||
group_constrained: isSynced,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchChannelSyncable = groups.
|
const patchChannelSyncable = groups.
|
||||||
@@ -453,57 +435,80 @@ export default class ChannelDetails extends React.PureComponent<ChannelDetailsPr
|
|||||||
}).
|
}).
|
||||||
map((g) => actions.patchGroupSyncable(g.id, channelID, SyncableType.Channel, {scheme_admin: g.scheme_admin}));
|
map((g) => actions.patchGroupSyncable(g.id, channelID, SyncableType.Channel, {scheme_admin: g.scheme_admin}));
|
||||||
|
|
||||||
const unlink = origGroups.
|
|
||||||
filter((g) => {
|
|
||||||
return !groups.some((group) => group.id === g.id);
|
|
||||||
}).
|
|
||||||
map((g) => actions.unlinkGroupSyncable(g.id, channelID, SyncableType.Channel));
|
|
||||||
|
|
||||||
const link = groups.
|
const link = groups.
|
||||||
filter((g) => {
|
filter((g) => {
|
||||||
return !origGroups.some((group) => group.id === g.id);
|
return !origGroups.some((group) => group.id === g.id);
|
||||||
}).
|
}).
|
||||||
map((g) => actions.linkGroupSyncable(g.id, channelID, SyncableType.Channel, {auto_add: true, scheme_admin: g.scheme_admin}));
|
map((g) => actions.linkGroupSyncable(g.id, channelID, SyncableType.Channel, {auto_add: true, scheme_admin: g.scheme_admin}));
|
||||||
|
|
||||||
const groupActions = [...promises, ...patchChannelSyncable, ...unlink, ...link];
|
// First execute link operations
|
||||||
if (groupActions.length > 0) {
|
const promisesToExecute = [...patchChannelSyncable, ...link];
|
||||||
const result = await Promise.all(groupActions);
|
if (privacyChangePromise) {
|
||||||
const resultWithError = result.find((r) => 'error' in r);
|
promisesToExecute.push(privacyChangePromise);
|
||||||
|
}
|
||||||
|
const linkResult = await Promise.all(promisesToExecute);
|
||||||
|
let resultWithError = linkResult.find((r) => 'error' in r);
|
||||||
|
if (resultWithError && 'error' in resultWithError) {
|
||||||
|
serverError = <FormError error={resultWithError.error.message}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then patch the channel
|
||||||
|
const patchResult = await actions.patchChannel(channel.id, {
|
||||||
|
...channel,
|
||||||
|
group_constrained: isSynced,
|
||||||
|
});
|
||||||
|
|
||||||
|
if ('error' in patchResult) {
|
||||||
|
serverError = <FormError error={patchResult.error.message}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlink = origGroups.
|
||||||
|
filter((g) => {
|
||||||
|
return !groups.some((group) => group.id === g.id);
|
||||||
|
}).
|
||||||
|
map((g) => actions.unlinkGroupSyncable(g.id, channelID, SyncableType.Channel));
|
||||||
|
|
||||||
|
// Finally execute unlink operations
|
||||||
|
if (unlink.length > 0) {
|
||||||
|
const unlinkResult = await Promise.all(unlink);
|
||||||
|
resultWithError = unlinkResult.find((r) => 'error' in r);
|
||||||
if (resultWithError && 'error' in resultWithError) {
|
if (resultWithError && 'error' in resultWithError) {
|
||||||
serverError = <FormError error={resultWithError.error.message}/>;
|
serverError = <FormError error={resultWithError.error.message}/>;
|
||||||
} else {
|
|
||||||
if (unlink.length > 0) {
|
|
||||||
trackEvent('admin_channel_config_page', 'groups_removed_from_channel', {count: unlink.length, channel_id: channelID});
|
|
||||||
}
|
|
||||||
if (link.length > 0) {
|
|
||||||
trackEvent('admin_channel_config_page', 'groups_added_to_channel', {count: link.length, channel_id: channelID});
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionsToAwait: any[] = [];
|
|
||||||
if (this.props.channelModerationEnabled) {
|
|
||||||
actionsToAwait.push(actions.getGroups(channelID));
|
|
||||||
}
|
|
||||||
if (isPrivacyChanging) {
|
|
||||||
// If the privacy is changing update the manage_members value for the channel moderation widget
|
|
||||||
if (this.props.channelModerationEnabled) {
|
|
||||||
actionsToAwait.push(
|
|
||||||
actions.getChannelModerations(channelID).then(() => {
|
|
||||||
const manageMembersIndex = channelPermissions.findIndex((element) => element.name === Permissions.CHANNEL_MODERATED_PERMISSIONS.MANAGE_MEMBERS);
|
|
||||||
if (channelPermissions) {
|
|
||||||
const updatedManageMembers = this.props.channelPermissions.find((element) => element.name === Permissions.CHANNEL_MODERATED_PERMISSIONS.MANAGE_MEMBERS);
|
|
||||||
channelPermissions[manageMembersIndex] = updatedManageMembers || channelPermissions[manageMembersIndex];
|
|
||||||
}
|
|
||||||
this.setState({channelPermissions});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (actionsToAwait.length > 0) {
|
|
||||||
await Promise.all(actionsToAwait);
|
|
||||||
}
|
|
||||||
await Promise.resolve();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(resultWithError && 'error' in resultWithError) && !('error' in patchResult)) {
|
||||||
|
if (unlink.length > 0) {
|
||||||
|
trackEvent('admin_channel_config_page', 'groups_removed_from_channel', {count: unlink.length, channel_id: channelID});
|
||||||
|
}
|
||||||
|
if (link.length > 0) {
|
||||||
|
trackEvent('admin_channel_config_page', 'groups_added_to_channel', {count: link.length, channel_id: channelID});
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionsToAwait: any[] = [];
|
||||||
|
if (this.props.channelModerationEnabled) {
|
||||||
|
actionsToAwait.push(actions.getGroups(channelID));
|
||||||
|
}
|
||||||
|
if (isPrivacyChanging) {
|
||||||
|
// If the privacy is changing update the manage_members value for the channel moderation widget
|
||||||
|
if (this.props.channelModerationEnabled) {
|
||||||
|
actionsToAwait.push(
|
||||||
|
actions.getChannelModerations(channelID).then(() => {
|
||||||
|
const manageMembersIndex = channelPermissions.findIndex((element) => element.name === Permissions.CHANNEL_MODERATED_PERMISSIONS.MANAGE_MEMBERS);
|
||||||
|
if (channelPermissions) {
|
||||||
|
const updatedManageMembers = this.props.channelPermissions.find((element) => element.name === Permissions.CHANNEL_MODERATED_PERMISSIONS.MANAGE_MEMBERS);
|
||||||
|
channelPermissions[manageMembersIndex] = updatedManageMembers || channelPermissions[manageMembersIndex];
|
||||||
|
}
|
||||||
|
this.setState({channelPermissions});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actionsToAwait.length > 0) {
|
||||||
|
await Promise.all(actionsToAwait);
|
||||||
|
}
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
if (this.props.channelModerationEnabled) {
|
if (this.props.channelModerationEnabled) {
|
||||||
const patchChannelPermissionsArray: ChannelModerationPatch[] = channelPermissions.map((p) => {
|
const patchChannelPermissionsArray: ChannelModerationPatch[] = channelPermissions.map((p) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -180,32 +180,55 @@ export default class TeamDetails extends React.PureComponent<Props, State> {
|
|||||||
serverError = <NeedGroupsError/>;
|
serverError = <NeedGroupsError/>;
|
||||||
saveNeeded = true;
|
saveNeeded = true;
|
||||||
} else {
|
} else {
|
||||||
const patchTeamPromise = actions.patchTeam({
|
|
||||||
...team,
|
|
||||||
group_constrained: syncChecked,
|
|
||||||
allowed_domains: allowedDomainsChecked ? allowedDomains : '',
|
|
||||||
allow_open_invite: allAllowedChecked,
|
|
||||||
});
|
|
||||||
const patchTeamSyncable = groups.
|
const patchTeamSyncable = groups.
|
||||||
filter((g) => {
|
filter((g) => {
|
||||||
return origGroups.some((group) => group.id === g.id && group.scheme_admin !== g.scheme_admin);
|
return origGroups.some((group) => group.id === g.id && group.scheme_admin !== g.scheme_admin);
|
||||||
}).
|
}).
|
||||||
map((g) => actions.patchGroupSyncable(g.id, teamID, SyncableType.Team, {scheme_admin: g.scheme_admin}));
|
map((g) => actions.patchGroupSyncable(g.id, teamID, SyncableType.Team, {scheme_admin: g.scheme_admin}));
|
||||||
const unlink = origGroups.
|
|
||||||
filter((g) => {
|
|
||||||
return !groups.some((group) => group.id === g.id);
|
|
||||||
}).
|
|
||||||
map((g) => actions.unlinkGroupSyncable(g.id, teamID, SyncableType.Team));
|
|
||||||
const link = groups.
|
const link = groups.
|
||||||
filter((g) => {
|
filter((g) => {
|
||||||
return !origGroups.some((group) => group.id === g.id);
|
return !origGroups.some((group) => group.id === g.id);
|
||||||
}).
|
}).
|
||||||
map((g) => actions.linkGroupSyncable(g.id, teamID, SyncableType.Team, {auto_add: true, scheme_admin: g.scheme_admin}));
|
map((g) => actions.linkGroupSyncable(g.id, teamID, SyncableType.Team, {auto_add: true, scheme_admin: g.scheme_admin}));
|
||||||
const result = await Promise.all([patchTeamPromise, ...patchTeamSyncable, ...unlink, ...link]);
|
|
||||||
const resultWithError = result.find((r) => r.error);
|
// First execute patch and link operations
|
||||||
if (resultWithError) {
|
const groupResult = await Promise.all([...patchTeamSyncable, ...link]);
|
||||||
serverError = <FormError error={resultWithError.error?.message}/>;
|
const groupResultWithError = groupResult.find((r) => r.error);
|
||||||
} else {
|
|
||||||
|
if (groupResultWithError) {
|
||||||
|
serverError = <FormError error={groupResultWithError.error?.message}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After group operations succeed, patch the team
|
||||||
|
const patchTeamResult = await actions.patchTeam({
|
||||||
|
...team,
|
||||||
|
group_constrained: syncChecked,
|
||||||
|
allowed_domains: allowedDomainsChecked ? allowedDomains : '',
|
||||||
|
allow_open_invite: allAllowedChecked,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (patchTeamResult.error) {
|
||||||
|
serverError = <FormError error={patchTeamResult.error?.message}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After patching the team, handle unlinking groups
|
||||||
|
const unlink = origGroups.
|
||||||
|
filter((g) => {
|
||||||
|
return !groups.some((group) => group.id === g.id);
|
||||||
|
}).
|
||||||
|
map((g) => actions.unlinkGroupSyncable(g.id, teamID, SyncableType.Team));
|
||||||
|
|
||||||
|
let unlinkResultWithError: ActionResult | undefined;
|
||||||
|
if (unlink.length > 0) {
|
||||||
|
const unlinkResult = await Promise.all(unlink);
|
||||||
|
unlinkResultWithError = unlinkResult.find((r) => r.error);
|
||||||
|
if (unlinkResultWithError) {
|
||||||
|
serverError = <FormError error={unlinkResultWithError.error?.message}/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patchTeamResult.error && !groupResultWithError && !unlinkResultWithError) {
|
||||||
if (unlink.length > 0) {
|
if (unlink.length > 0) {
|
||||||
trackEvent('admin_team_config_page', 'groups_removed_from_team', {count: unlink.length, team_id: teamID});
|
trackEvent('admin_team_config_page', 'groups_removed_from_team', {count: unlink.length, team_id: teamID});
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user