From 03787f9386c36a84bc8d46ff7c145fa379a3ecf6 Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Fri, 5 Jun 2026 14:29:47 +0200 Subject: [PATCH] Automated cherry pick of #36814 (#36894) Automatic Merge --- .../services/sharedchannel/sync_recv.go | 8 +++ .../services/sharedchannel/sync_recv_test.go | 65 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/server/platform/services/sharedchannel/sync_recv.go b/server/platform/services/sharedchannel/sync_recv.go index eb90303c64..8d9bb60de6 100644 --- a/server/platform/services/sharedchannel/sync_recv.go +++ b/server/platform/services/sharedchannel/sync_recv.go @@ -507,6 +507,10 @@ func (scs *Service) upsertSyncPost(post *model.Post, targetChannel *model.Channe ) } } else if post.DeleteAt > 0 { + // make sure the post being deleted is owned by the remote + if rpost.GetRemoteID() != rc.RemoteId { + return nil, fmt.Errorf("post sync failed: %w", ErrRemoteIDMismatch) + } // delete post rpost, appErr = scs.app.DeletePost(rctx, post.Id, post.UserId) if appErr == nil { @@ -516,6 +520,10 @@ func (scs *Service) upsertSyncPost(post *model.Post, targetChannel *model.Channe ) } } else if post.EditAt > rpost.EditAt || post.Message != rpost.Message || post.UpdateAt > rpost.UpdateAt || post.Metadata != nil { + // make sure the post being edited is owned by the remote + if rpost.GetRemoteID() != rc.RemoteId { + return nil, fmt.Errorf("post sync failed: %w", ErrRemoteIDMismatch) + } scs.transformMentionsOnReceive(rctx, post, targetChannel, rc, mentionTransforms) var priority *model.PostPriority var acknowledgements []*model.PostAcknowledgement diff --git a/server/platform/services/sharedchannel/sync_recv_test.go b/server/platform/services/sharedchannel/sync_recv_test.go index f3b2702bab..f84a2fd1ad 100644 --- a/server/platform/services/sharedchannel/sync_recv_test.go +++ b/server/platform/services/sharedchannel/sync_recv_test.go @@ -128,3 +128,68 @@ func TestUpsertSyncUserStatus(t *testing.T) { mockApp.AssertNotCalled(t, "SaveAndBroadcastStatus") }) } + +func TestUpsertSyncPost(t *testing.T) { + remoteID := model.NewId() + channelID := model.NewId() + channel := &model.Channel{Id: channelID, Type: model.ChannelTypeOpen} + rc := &model.RemoteCluster{RemoteId: remoteID, Name: "test-remote"} + + setup := func(t *testing.T, existing *model.Post) (*Service, *MockAppIface) { + mockPostStore := &mocks.PostStore{} + mockPostStore.On("GetSingle", mock.Anything, existing.Id, true).Return(existing, nil) + + mockStore := &mocks.Store{} + mockStore.On("Post").Return(mockPostStore) + + logger := mlog.CreateConsoleTestLogger(t) + mockServer := &MockServerIface{} + mockServer.On("GetStore").Return(mockStore) + mockServer.On("Log").Return(logger) + + mockApp := &MockAppIface{} + + return &Service{server: mockServer, app: mockApp}, mockApp + } + + t.Run("rejects edit of a post owned by a different remote", func(t *testing.T) { + otherRemoteID := model.NewId() + postID := model.NewId() + existing := &model.Post{Id: postID, ChannelId: channelID, Message: "original", RemoteId: model.NewPointer(otherRemoteID)} + + scs, mockApp := setup(t, existing) + + _, err := scs.upsertSyncPost(&model.Post{Id: postID, ChannelId: channelID, Message: "tampered"}, channel, rc, nil) + + require.Error(t, err) + assert.ErrorIs(t, err, ErrRemoteIDMismatch) + mockApp.AssertNotCalled(t, "UpdatePost", mock.Anything, mock.Anything, mock.Anything) + }) + + t.Run("rejects delete of a post owned by a local user", func(t *testing.T) { + postID := model.NewId() + existing := &model.Post{Id: postID, ChannelId: channelID, Message: "original", RemoteId: nil} + + scs, mockApp := setup(t, existing) + + _, err := scs.upsertSyncPost(&model.Post{Id: postID, ChannelId: channelID, DeleteAt: model.GetMillis()}, channel, rc, nil) + + require.Error(t, err) + assert.ErrorIs(t, err, ErrRemoteIDMismatch) + mockApp.AssertNotCalled(t, "DeletePost", mock.Anything, mock.Anything, mock.Anything) + }) + + t.Run("allows edit of a post owned by the sending remote", func(t *testing.T) { + postID := model.NewId() + existing := &model.Post{Id: postID, ChannelId: channelID, Message: "original", RemoteId: model.NewPointer(remoteID)} + + scs, mockApp := setup(t, existing) + updated := &model.Post{Id: postID, ChannelId: channelID, Message: "updated"} + mockApp.On("UpdatePost", mock.Anything, mock.Anything, mock.Anything).Return(updated, false, (*model.AppError)(nil)) + + _, err := scs.upsertSyncPost(&model.Post{Id: postID, ChannelId: channelID, Message: "updated"}, channel, rc, nil) + + require.NoError(t, err) + mockApp.AssertCalled(t, "UpdatePost", mock.Anything, mock.Anything, mock.Anything) + }) +}