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 удалений

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

@@ -18,7 +18,8 @@ import (
)
var (
ErrRemoteIDMismatch = errors.New("remoteID mismatch")
ErrRemoteIDMismatch = errors.New("remoteID mismatch")
ErrChannelIDMismatch = errors.New("channelID mismatch")
)
func (scs *Service) onReceiveSyncMessage(msg model.RemoteClusterMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
@@ -46,7 +47,7 @@ func (scs *Service) onReceiveSyncMessage(msg model.RemoteClusterMsg, rc *model.R
}
func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc *model.RemoteCluster, response *remotecluster.Response) error {
var channel *model.Channel
var targetChannel *model.Channel
var team *model.Team
var err error
@@ -65,14 +66,23 @@ func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc
mlog.Int("reaction_count", len(syncMsg.Reactions)),
)
if channel, err = scs.server.GetStore().Channel().Get(syncMsg.ChannelId, true); err != nil {
if targetChannel, err = scs.server.GetStore().Channel().Get(syncMsg.ChannelId, true); err != nil {
// if the channel doesn't exist then none of these sync items are going to work.
return fmt.Errorf("channel not found processing sync message: %w", err)
}
// make sure target channel is shared with the remote
exists, err := scs.server.GetStore().SharedChannel().HasRemote(targetChannel.Id, rc.RemoteId)
if err != nil {
return fmt.Errorf("cannot check channel share state for sync message: %w", err)
}
if !exists {
return fmt.Errorf("cannot process sync message; channel not shared with remote: %w", ErrRemoteIDMismatch)
}
// add/update users before posts
for _, user := range syncMsg.Users {
if userSaved, err := scs.upsertSyncUser(c, user, channel, rc); err != nil {
if userSaved, err := scs.upsertSyncUser(c, user, targetChannel, rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync user",
mlog.String("remote", rc.Name),
mlog.String("channel_id", syncMsg.ChannelId),
@@ -103,7 +113,7 @@ func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc
continue
}
if channel.Type != model.ChannelTypeDirect && team == nil {
if targetChannel.Type != model.ChannelTypeDirect && team == nil {
var err2 error
team, err2 = scs.server.GetStore().Channel().GetTeamForChannel(syncMsg.ChannelId)
if err2 != nil {
@@ -124,7 +134,7 @@ func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc
}
// add/update post
rpost, err := scs.upsertSyncPost(post, channel, rc)
rpost, err := scs.upsertSyncPost(post, targetChannel, rc)
if err != nil {
syncResp.PostErrors = append(syncResp.PostErrors, post.Id)
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync post",
@@ -140,7 +150,7 @@ func (scs *Service) processSyncMessage(c request.CTX, syncMsg *model.SyncMsg, rc
// add/remove reactions
for _, reaction := range syncMsg.Reactions {
if _, err := scs.upsertSyncReaction(reaction, rc); err != nil {
if _, err := scs.upsertSyncReaction(reaction, targetChannel, rc); err != nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "Error upserting sync reaction",
mlog.String("remote", rc.Name),
mlog.String("user_id", reaction.UserId),
@@ -189,12 +199,12 @@ func (scs *Service) upsertSyncUser(c request.CTX, user *model.User, channel *mod
}
} else {
// existing user. Make sure user belongs to the remote that issued the update
if SafeString(euser.RemoteId) != rc.RemoteId {
if euser.GetRemoteID() != rc.RemoteId {
scs.server.Log().Log(mlog.LvlSharedChannelServiceError, "RemoteID mismatch sync'ing user",
mlog.String("remote", rc.Name),
mlog.String("user_id", user.Id),
mlog.String("existing_user_remote_id", SafeString(euser.RemoteId)),
mlog.String("update_user_remote_id", SafeString(user.RemoteId)),
mlog.String("existing_user_remote_id", euser.GetRemoteID()),
mlog.String("update_user_remote_id", user.GetRemoteID()),
)
return nil, fmt.Errorf("error updating user: %w", ErrRemoteIDMismatch)
}
@@ -337,7 +347,7 @@ func (scs *Service) updateSyncUser(rctx request.CTX, patch *model.UserPatch, use
return nil, fmt.Errorf("error updating sync user %s: %w", user.Id, err)
}
func (scs *Service) upsertSyncPost(post *model.Post, channel *model.Channel, rc *model.RemoteCluster) (*model.Post, error) {
func (scs *Service) upsertSyncPost(post *model.Post, targetChannel *model.Channel, rc *model.RemoteCluster) (*model.Post, error) {
var appErr *model.AppError
post.RemoteId = model.NewString(rc.RemoteId)
@@ -350,9 +360,24 @@ func (scs *Service) upsertSyncPost(post *model.Post, channel *model.Channel, rc
}
}
// ensure the post is in the target channel. This ensures the post can only be associated with a channel
// that is shared with the remote.
if post.ChannelId != targetChannel.Id || (rpost != nil && rpost.ChannelId != targetChannel.Id) {
return nil, fmt.Errorf("post sync failed: %w", ErrChannelIDMismatch)
}
if rpost == nil {
// post doesn't exist; create new one
rpost, appErr = scs.app.CreatePost(rctx, post, channel, true, true)
// post doesn't exist; check that user belongs to remote and create post.
// user is not checked for edit/delete because admins can perform those actions
user, err := scs.server.GetStore().User().Get(context.TODO(), post.UserId)
if err != nil {
return nil, fmt.Errorf("error fetching user for post sync: %w", err)
}
if user.GetRemoteID() != rc.RemoteId {
return nil, fmt.Errorf("post sync failed: %w", ErrRemoteIDMismatch)
}
rpost, appErr = scs.app.CreatePost(rctx, post, targetChannel, true, true)
if appErr == nil {
scs.server.Log().Log(mlog.LvlSharedChannelServiceDebug, "Created sync post",
mlog.String("post_id", post.Id),
@@ -392,21 +417,49 @@ func (scs *Service) upsertSyncPost(post *model.Post, channel *model.Channel, rc
return rpost, rerr
}
func (scs *Service) upsertSyncReaction(reaction *model.Reaction, rc *model.RemoteCluster) (*model.Reaction, error) {
func (scs *Service) upsertSyncReaction(reaction *model.Reaction, targetChannel *model.Channel, rc *model.RemoteCluster) (*model.Reaction, error) {
savedReaction := reaction
var appErr *model.AppError
reaction.RemoteId = model.NewString(rc.RemoteId)
// check that the reaction's post is in the target channel. This ensures the reaction can only be associated with a post
// that is in a channel shared with the remote.
rctx := request.EmptyContext(scs.server.Log())
post, err := scs.server.GetStore().Post().GetSingle(rctx, reaction.PostId, true)
if err != nil {
return nil, fmt.Errorf("error fetching post for reaction sync: %w", err)
}
if post.ChannelId != targetChannel.Id {
return nil, fmt.Errorf("reaction sync failed: %w", ErrChannelIDMismatch)
}
if reaction.DeleteAt == 0 {
existingReaction, err := scs.server.GetStore().Reaction().GetSingle(reaction.UserId, reaction.PostId, rc.RemoteId, reaction.EmojiName)
if err != nil && !isNotFoundError(err) {
return nil, fmt.Errorf("error fetching reaction for sync: %w", err)
}
if existingReaction == nil {
// reaction does not exist; check that user belongs to remote and create reaction
// this is not done for delete since deletion can be done by admins on the remote
user, err := scs.server.GetStore().User().Get(context.TODO(), reaction.UserId)
if err != nil {
return nil, fmt.Errorf("error fetching user for reaction sync: %w", err)
}
if user.GetRemoteID() != rc.RemoteId {
return nil, fmt.Errorf("reaction sync failed: %w", ErrRemoteIDMismatch)
}
reaction.RemoteId = model.NewString(rc.RemoteId)
savedReaction, appErr = scs.app.SaveReactionForPost(request.EmptyContext(scs.server.Log()), reaction)
} else {
// make sure the reaction being deleted is owned by the remote
if existingReaction.GetRemoteID() != rc.RemoteId {
return nil, fmt.Errorf("reaction sync failed: %w", ErrRemoteIDMismatch)
}
appErr = scs.app.DeleteReactionForPost(request.EmptyContext(scs.server.Log()), reaction)
}
var err error
var retErr error
if appErr != nil {
err = errors.New(appErr.Error())
retErr = errors.New(appErr.Error())
}
return savedReaction, err
return savedReaction, retErr
}

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

@@ -148,10 +148,3 @@ func reducePostsSliceInCache(posts []*model.Post, cache map[string]*model.Post)
}
return reduced
}
func SafeString(p *string) string {
if p == nil {
return ""
}
return *p
}