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
Этот коммит содержится в:
@@ -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()
|
||||
|
||||
|
||||
Ссылка в новой задаче
Block a user