[MM-23719] api4/channel: add move channel to a team endpoint (#14246)

* api4: add move channel method

* api4: add tests for move channel, model: add move channel to client4.go

* add api.channel.move_channel.type.invalid message

* model/client4: remove a redundant line

* api4/channel: add tests for gm and private channel types

* app/channel: update move channel comment

* app/channel: add extra check if a users joins to channel during movement

* app/channel: log errors for post move channel

* app/channel: remove deactivated members by default while moving a ch.

* model/client: update move channel command

* fix vet errors

* app/channel: add missing webhook updates

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2020-06-22 16:57:49 +03:00
коммит произвёл GitHub
родитель 0e714f350a
Коммит 124014ad9c
9 изменённых файлов: 296 добавлений и 27 удалений

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

@@ -222,6 +222,9 @@ type AppIface interface {
MakeAuditRecord(event string, initialStatus string) *audit.Record
// MarkChanelAsUnreadFromPost will take a post and set the channel as unread from that one.
MarkChannelAsUnreadFromPost(postID string, userID string) (*model.ChannelUnreadAt, *model.AppError)
// MoveChannel method is prone to data races if someone joins to channel during the move process. However this
// function is only exposed to sysadmins and the possibility of this edge case is realtively small.
MoveChannel(team *model.Team, channel *model.Channel, user *model.User) *model.AppError
// NewWebConn returns a new WebConn instance.
NewWebConn(ws *websocket.Conn, session model.Session, t goi18n.TranslateFunc, locale string) *WebConn
// NewWebHub creates a new Hub.
@@ -292,9 +295,6 @@ type AppIface interface {
// The result can be used, for example, to determine the set of users who would be removed from a team if the team
// were group-constrained with the given groups.
TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int) ([]*model.UserWithGroups, int64, *model.AppError)
// This function is intended for use from the CLI. It is not robust against people joining the channel while the move
// is in progress, and therefore should not be used from the API without first fixing this potential race condition.
MoveChannel(team *model.Team, channel *model.Channel, user *model.User, removeDeactivatedMembers bool) *model.AppError
// This function migrates the default built in roles from code/config to the database.
DoAdvancedPermissionsMigration()
// This to be used for places we check the users password when they are already logged in
@@ -788,6 +788,7 @@ type AppIface interface {
RegenerateTeamInviteId(teamId string) (*model.Team, *model.AppError)
RegisterPluginCommand(pluginId string, command *model.Command) error
ReloadConfig() error
RemoveAllDeactivatedMembersFromChannel(channel *model.Channel) *model.AppError
RemoveConfigListener(id string)
RemoveFile(path string) *model.AppError
RemovePlugin(id string) *model.AppError

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

@@ -2344,15 +2344,13 @@ func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError {
return nil
}
// This function is intended for use from the CLI. It is not robust against people joining the channel while the move
// is in progress, and therefore should not be used from the API without first fixing this potential race condition.
func (a *App) MoveChannel(team *model.Team, channel *model.Channel, user *model.User, removeDeactivatedMembers bool) *model.AppError {
if removeDeactivatedMembers {
if err := a.Srv().Store.Channel().RemoveAllDeactivatedMembers(channel.Id); err != nil {
return err
}
}
func (a *App) RemoveAllDeactivatedMembersFromChannel(channel *model.Channel) *model.AppError {
return a.Srv().Store.Channel().RemoveAllDeactivatedMembers(channel.Id)
}
// MoveChannel method is prone to data races if someone joins to channel during the move process. However this
// function is only exposed to sysadmins and the possibility of this edge case is realtively small.
func (a *App) MoveChannel(team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
// Check that all channel members are in the destination team.
channelMembers, err := a.GetChannelMembersPage(channel.Id, 0, 10000000)
if err != nil {
@@ -2394,7 +2392,40 @@ func (a *App) MoveChannel(team *model.Team, channel *model.Channel, user *model.
return model.NewAppError("MoveChannel", "app.channel.update_channel.internal_error", nil, err.Error(), http.StatusInternalServerError)
}
}
a.postChannelMoveMessage(user, channel, previousTeam)
if incomingWebhooks, err := a.GetIncomingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
mlog.Warn("Failed to get incoming webhooks", mlog.Err(err))
} else {
for _, webhook := range incomingWebhooks {
if webhook.ChannelId == channel.Id {
webhook.TeamId = team.Id
if _, err := a.Srv().Store.Webhook().UpdateIncoming(webhook); err != nil {
mlog.Warn("Failed to move incoming webhook to new team", mlog.String("webhook id", webhook.Id))
}
}
}
}
if outgoingWebhooks, err := a.GetOutgoingWebhooksForTeamPage(previousTeam.Id, 0, 10000000); err != nil {
mlog.Warn("Failed to get outgoing webhooks", mlog.Err(err))
} else {
for _, webhook := range outgoingWebhooks {
if webhook.ChannelId == channel.Id {
webhook.TeamId = team.Id
if _, err := a.Srv().Store.Webhook().UpdateOutgoing(webhook); err != nil {
mlog.Warn("Failed to move outgoing webhook to new team.", mlog.String("webhook id", webhook.Id))
}
}
}
}
if err := a.removeUsersFromChannelNotMemberOfTeam(user, channel, team); err != nil {
mlog.Warn("error while removing non-team member users", mlog.Err(err))
}
if err := a.postChannelMoveMessage(user, channel, previousTeam); err != nil {
mlog.Warn("error while posting move channel message", mlog.Err(err))
}
return nil
}
@@ -2418,6 +2449,40 @@ func (a *App) postChannelMoveMessage(user *model.User, channel *model.Channel, p
return nil
}
func (a *App) removeUsersFromChannelNotMemberOfTeam(remover *model.User, channel *model.Channel, team *model.Team) *model.AppError {
channelMembers, err := a.GetChannelMembersPage(channel.Id, 0, 10000000)
if err != nil {
return err
}
channelMemberIds := []string{}
channelMemberMap := make(map[string]struct{})
for _, channelMember := range *channelMembers {
channelMemberMap[channelMember.UserId] = struct{}{}
channelMemberIds = append(channelMemberIds, channelMember.UserId)
}
if len(channelMemberIds) > 0 {
teamMembers, err := a.GetTeamMembersByIds(team.Id, channelMemberIds, nil)
if err != nil {
return err
}
if len(teamMembers) != len(*channelMembers) {
for _, teamMember := range teamMembers {
delete(channelMemberMap, teamMember.UserId)
}
for userId := range channelMemberMap {
if err := a.removeUserFromChannel(userId, remover.Id, channel); err != nil {
return err
}
}
}
}
return nil
}
func (a *App) GetPinnedPosts(channelId string) (*model.PostList, *model.AppError) {
return a.Srv().Store.Channel().GetPinnedPosts(channelId)
}

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

@@ -68,6 +68,40 @@ func TestPermanentDeleteChannel(t *testing.T) {
require.NotNil(t, err, "Outgoing webhook wasn't deleted")
}
func TestRemoveAllDeactivatedMembersFromChannel(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
var err *model.AppError
team := th.CreateTeam()
channel := th.CreateChannel(team)
defer func() {
th.App.PermanentDeleteChannel(channel)
th.App.PermanentDeleteTeam(team)
}()
_, err = th.App.AddUserToTeam(team.Id, th.BasicUser.Id, "")
require.Nil(t, err)
deacivatedUser := th.CreateUser()
_, err = th.App.AddUserToTeam(team.Id, deacivatedUser.Id, "")
require.Nil(t, err)
_, err = th.App.AddUserToChannel(deacivatedUser, channel)
require.Nil(t, err)
channelMembers, err := th.App.GetChannelMembersPage(channel.Id, 0, 10000000)
require.Nil(t, err)
require.Len(t, *channelMembers, 2)
_, err = th.App.UpdateActive(deacivatedUser, false)
require.Nil(t, err)
err = th.App.RemoveAllDeactivatedMembersFromChannel(channel)
require.Nil(t, err)
channelMembers, err = th.App.GetChannelMembersPage(channel.Id, 0, 10000000)
require.Nil(t, err)
require.Len(t, *channelMembers, 1)
}
func TestMoveChannel(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
@@ -97,13 +131,13 @@ func TestMoveChannel(t *testing.T) {
_, err = th.App.AddUserToChannel(th.BasicUser2, channel1)
require.Nil(t, err)
err = th.App.MoveChannel(targetTeam, channel1, th.BasicUser, false)
err = th.App.MoveChannel(targetTeam, channel1, th.BasicUser)
require.NotNil(t, err, "Should have failed due to mismatched members.")
_, err = th.App.AddUserToTeam(targetTeam.Id, th.BasicUser2.Id, "")
require.Nil(t, err)
err = th.App.MoveChannel(targetTeam, channel1, th.BasicUser, false)
err = th.App.MoveChannel(targetTeam, channel1, th.BasicUser)
require.Nil(t, err)
// Test moving a channel with a deactivated user who isn't in the destination team.
@@ -123,12 +157,9 @@ func TestMoveChannel(t *testing.T) {
_, err = th.App.UpdateActive(deacivatedUser, false)
require.Nil(t, err)
err = th.App.MoveChannel(targetTeam, channel2, th.BasicUser, false)
err = th.App.MoveChannel(targetTeam, channel2, th.BasicUser)
require.NotNil(t, err, "Should have failed due to mismatched deacivated member.")
err = th.App.MoveChannel(targetTeam, channel2, th.BasicUser, true)
require.Nil(t, err)
// Test moving a channel with no members.
channel3 := &model.Channel{
DisplayName: "dn_" + model.NewId(),
@@ -142,7 +173,7 @@ func TestMoveChannel(t *testing.T) {
require.Nil(t, err)
defer th.App.PermanentDeleteChannel(channel3)
err = th.App.MoveChannel(targetTeam, channel3, th.BasicUser, false)
err = th.App.MoveChannel(targetTeam, channel3, th.BasicUser)
assert.Nil(t, err)
}

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

@@ -9966,7 +9966,7 @@ func (a *OpenTracingAppLayer) MigrateFilenamesToFileInfos(post *model.Post) []*m
return resultVar0
}
func (a *OpenTracingAppLayer) MoveChannel(team *model.Team, channel *model.Channel, user *model.User, removeDeactivatedMembers bool) *model.AppError {
func (a *OpenTracingAppLayer) MoveChannel(team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.MoveChannel")
@@ -9978,7 +9978,7 @@ func (a *OpenTracingAppLayer) MoveChannel(team *model.Team, channel *model.Chann
}()
defer span.Finish()
resultVar0 := a.app.MoveChannel(team, channel, user, removeDeactivatedMembers)
resultVar0 := a.app.MoveChannel(team, channel, user)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
@@ -11029,6 +11029,28 @@ func (a *OpenTracingAppLayer) ReloadConfig() error {
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveAllDeactivatedMembersFromChannel(channel *model.Channel) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveAllDeactivatedMembersFromChannel")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.RemoveAllDeactivatedMembersFromChannel(channel)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RemoveConfigListener(id string) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RemoveConfigListener")