MM-58577 Check remote ownership for posts and reactions (#27317)

* - ensure that posts and reactions can only be added via sync when coming from a remote that the target channel is shared with.
- ensure that posts and reactions are only modified/deleted by the remote that owns them.

* check that reaction belongs to post that belongs to channel that is shared with remote;  check that posts belong to channel shared with remote

* check for correct error type in unit test

* tweak unit test
Этот коммит содержится в:
Doug Lauder
2024-06-11 11:51:00 -04:00
коммит произвёл GitHub
родитель 6f8de3449a
Коммит 594ba6e665
10 изменённых файлов: 276 добавлений и 26 удалений

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

@@ -7684,6 +7684,24 @@ func (s *OpenTracingLayerReactionStore) GetForPostSince(postId string, since int
return result, err
}
func (s *OpenTracingLayerReactionStore) GetSingle(userID string, postID string, remoteID string, emojiName string) (*model.Reaction, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetSingle")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.ReactionStore.GetSingle(userID, postID, remoteID, emojiName)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerReactionStore) GetUniqueCountForPost(postId string) (int, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.GetUniqueCountForPost")

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

@@ -8741,6 +8741,27 @@ func (s *RetryLayerReactionStore) GetForPostSince(postId string, since int64, ex
}
func (s *RetryLayerReactionStore) GetSingle(userID string, postID string, remoteID string, emojiName string) (*model.Reaction, error) {
tries := 0
for {
result, err := s.ReactionStore.GetSingle(userID, postID, remoteID, emojiName)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerReactionStore) GetUniqueCountForPost(postId string) (int, error) {
tries := 0

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

@@ -5,6 +5,7 @@ package sqlstore
import (
"database/sql"
"fmt"
"time"
sq "github.com/mattermost/squirrel"
@@ -198,6 +199,33 @@ func (s *SqlReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction,
return reactions, nil
}
func (s *SqlReactionStore) GetSingle(userID, postID, remoteID, emojiName string) (*model.Reaction, error) {
query := s.getQueryBuilder().
Select("UserId", "PostId", "EmojiName", "CreateAt",
"COALESCE(UpdateAt, CreateAt) As UpdateAt", "COALESCE(DeleteAt, 0) As DeleteAt",
"RemoteId", "ChannelId").
From("Reactions").
Where(sq.Eq{"UserId": userID}).
Where(sq.Eq{"PostId": postID}).
Where(sq.Eq{"COALESCE(RemoteId, '')": remoteID}).
Where(sq.Eq{"EmojiName": emojiName})
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "reactions_getsingle_tosql")
}
var reactions []*model.Reaction
if err := s.GetReplicaX().Select(&reactions, queryString, args...); err != nil {
return nil, errors.Wrapf(err, "failed to find reaction")
}
if len(reactions) == 0 {
return nil, store.NewErrNotFound("Reaction", fmt.Sprintf("user_id=%s, post_id=%s, remote_id=%s, emoji_name=%s",
userID, postID, remoteID, emojiName))
}
return reactions[0], nil
}
func (s *SqlReactionStore) DeleteAllWithEmojiName(emojiName string) error {
var reactions []*model.Reaction
now := model.GetMillis()

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

@@ -740,6 +740,7 @@ type ReactionStore interface {
ExistsOnPost(postId string, emojiName string) (bool, error)
DeleteAllWithEmojiName(emojiName string) error
BulkGetForPosts(postIds []string) ([]*model.Reaction, error)
GetSingle(userID, postID, remoteID, emojiName string) (*model.Reaction, error)
DeleteOrphanedRowsByIds(r *model.RetentionIdsForDeletion) error
PermanentDeleteBatch(endTime int64, limit int64) (int64, error)
PermanentDeleteByUser(userID string) error

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

@@ -198,6 +198,36 @@ func (_m *ReactionStore) GetForPostSince(postId string, since int64, excludeRemo
return r0, r1
}
// GetSingle provides a mock function with given fields: userID, postID, remoteID, emojiName
func (_m *ReactionStore) GetSingle(userID string, postID string, remoteID string, emojiName string) (*model.Reaction, error) {
ret := _m.Called(userID, postID, remoteID, emojiName)
if len(ret) == 0 {
panic("no return value specified for GetSingle")
}
var r0 *model.Reaction
var r1 error
if rf, ok := ret.Get(0).(func(string, string, string, string) (*model.Reaction, error)); ok {
return rf(userID, postID, remoteID, emojiName)
}
if rf, ok := ret.Get(0).(func(string, string, string, string) *model.Reaction); ok {
r0 = rf(userID, postID, remoteID, emojiName)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Reaction)
}
}
if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok {
r1 = rf(userID, postID, remoteID, emojiName)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetUniqueCountForPost provides a mock function with given fields: postId
func (_m *ReactionStore) GetUniqueCountForPost(postId string) (int, error) {
ret := _m.Called(postId)

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

@@ -31,6 +31,7 @@ func TestReactionStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStor
t.Run("ReactionDeadlock", func(t *testing.T) { testReactionDeadlock(t, rctx, ss) })
t.Run("ExistsOnPost", func(t *testing.T) { testExistsOnPost(t, rctx, ss) })
t.Run("GetUniqueCountForPost", func(t *testing.T) { testGetUniqueCountForPost(t, rctx, ss) })
t.Run("ReactionGetSingle", func(t *testing.T) { testReactionGetSingle(t, rctx, ss) })
}
func testReactionSave(t *testing.T, rctx request.CTX, ss store.Store) {
@@ -938,3 +939,85 @@ func testGetUniqueCountForPost(t *testing.T, rctx request.CTX, ss store.Store) {
require.NoError(t, err)
require.Equal(t, 2, count)
}
func testReactionGetSingle(t *testing.T, rctx request.CTX, ss store.Store) {
var (
testUserID = model.NewId()
testEmojiName = "smile"
testRemoteID = model.NewId()
)
t.Run("get without remoteId", func(t *testing.T) {
post, err := ss.Post().Save(rctx, &model.Post{
ChannelId: model.NewId(),
UserId: testUserID,
})
require.NoError(t, err)
reaction := &model.Reaction{
UserId: testUserID,
PostId: post.Id,
EmojiName: testEmojiName,
}
_, nErr := ss.Reaction().Save(reaction)
require.NoError(t, nErr)
reactionFound, err := ss.Reaction().GetSingle(testUserID, post.Id, "", testEmojiName)
require.NoError(t, err)
assert.Equal(t, testUserID, reactionFound.UserId)
assert.Equal(t, post.Id, reactionFound.PostId)
assert.Equal(t, "", reactionFound.GetRemoteID())
assert.Equal(t, testEmojiName, reactionFound.EmojiName)
})
t.Run("get with remoteId", func(t *testing.T) {
post, err := ss.Post().Save(rctx, &model.Post{
ChannelId: model.NewId(),
UserId: testUserID,
})
require.NoError(t, err)
reaction := &model.Reaction{
UserId: testUserID,
PostId: post.Id,
EmojiName: testEmojiName,
RemoteId: model.NewString(testRemoteID),
}
_, nErr := ss.Reaction().Save(reaction)
require.NoError(t, nErr)
reactionFound, err := ss.Reaction().GetSingle(testUserID, post.Id, testRemoteID, testEmojiName)
require.NoError(t, err)
assert.Equal(t, testUserID, reactionFound.UserId)
assert.Equal(t, post.Id, reactionFound.PostId)
assert.Equal(t, testRemoteID, reactionFound.GetRemoteID())
assert.Equal(t, testEmojiName, reactionFound.EmojiName)
})
t.Run("not found - wrong remoteID", func(t *testing.T) {
post, err := ss.Post().Save(rctx, &model.Post{
ChannelId: model.NewId(),
UserId: testUserID,
})
require.NoError(t, err)
reaction := &model.Reaction{
UserId: testUserID,
PostId: post.Id,
EmojiName: testEmojiName,
RemoteId: model.NewString(testRemoteID),
}
_, nErr := ss.Reaction().Save(reaction)
require.NoError(t, nErr)
reactionFound, err := ss.Reaction().GetSingle(testUserID, post.Id, "bogus-remoteId", testEmojiName)
require.Error(t, err)
assert.Nil(t, reactionFound)
var errNotFound *store.ErrNotFound
assert.ErrorAs(t, err, &errNotFound)
})
}

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

@@ -6937,6 +6937,22 @@ func (s *TimerLayerReactionStore) GetForPostSince(postId string, since int64, ex
return result, err
}
func (s *TimerLayerReactionStore) GetSingle(userID string, postID string, remoteID string, emojiName string) (*model.Reaction, error) {
start := time.Now()
result, err := s.ReactionStore.GetSingle(userID, postID, remoteID, emojiName)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("ReactionStore.GetSingle", success, elapsed)
}
return result, err
}
func (s *TimerLayerReactionStore) GetUniqueCountForPost(postId string) (int, error) {
start := time.Now()