mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-30 17:06:34 +08:00
MM-70240: Adjust post and thread payload sanitization (#37920)
* MM-70216: Adjust post and thread payload sanitization * MM-70216: Preserve MM blocks actions in thread test fixture Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
bf0f9de078
commit
d0be8f408e
@@ -3342,8 +3342,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(rctx request.CTX, postID
|
||||
if mErr != nil {
|
||||
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
|
||||
}
|
||||
a.sanitizeProfiles(thread.Participants, false)
|
||||
thread.Post.SanitizeProps()
|
||||
a.sanitizeThreadResponse(thread)
|
||||
|
||||
if a.IsCRTEnabledForUser(rctx, userID) {
|
||||
payload, jsonErr := json.Marshal(thread)
|
||||
|
||||
@@ -858,8 +858,7 @@ func (a *App) SendNotifications(rctx request.CTX, post *model.Post, team *model.
|
||||
userThread.UnreadMentions = 0
|
||||
userThread.UnreadReplies = 0
|
||||
}
|
||||
a.sanitizeProfiles(userThread.Participants, false)
|
||||
userThread.Post.SanitizeProps()
|
||||
a.sanitizeThreadResponse(userThread)
|
||||
|
||||
sanitizedPost, isMemberForPreview, err := a.SanitizePostMetadataForUser(rctx, userThread.Post, uid)
|
||||
if err != nil {
|
||||
@@ -1026,8 +1025,7 @@ func (a *App) RemoveNotifications(rctx request.CTX, post *model.Post, channel *m
|
||||
previousUnreadMentions := int64(0)
|
||||
previousUnreadReplies := int64(0)
|
||||
|
||||
a.sanitizeProfiles(userThread.Participants, false)
|
||||
userThread.Post.SanitizeProps()
|
||||
a.sanitizeThreadResponse(userThread)
|
||||
|
||||
sanitizedPost, isMemberForPreview, err1 := a.SanitizePostMetadataForUser(rctx, userThread.Post, userID)
|
||||
if err1 != nil {
|
||||
|
||||
@@ -3370,8 +3370,13 @@ func (a *App) CleanUpAfterPostDeletion(rctx request.CTX, post *model.Post, delet
|
||||
return model.NewAppError("DeletePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
sanitizedPostJSON, jsonErr := post.ToJSON()
|
||||
if jsonErr != nil {
|
||||
return model.NewAppError("DeletePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||
}
|
||||
|
||||
userMessage := model.NewWebSocketEvent(model.WebsocketEventPostDeleted, "", post.ChannelId, "", nil, "")
|
||||
userMessage.Add("post", string(postJSON))
|
||||
userMessage.Add("post", sanitizedPostJSON)
|
||||
userMessage.GetBroadcast().ContainsSanitizedData = true
|
||||
a.Publish(userMessage)
|
||||
|
||||
|
||||
@@ -5450,6 +5450,110 @@ func TestPermanentDeletePost(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCleanUpAfterPostDeletion(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
appErr := th.App.JoinChannel(th.Context, th.BasicChannel, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
appErr = th.App.JoinChannel(th.Context, th.BasicChannel, th.BasicUser2.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
waitForPostDeleted := func(t *testing.T, messages chan *model.WebSocketEvent) *model.WebSocketEvent {
|
||||
t.Helper()
|
||||
|
||||
select {
|
||||
case received := <-messages:
|
||||
return received
|
||||
case <-time.After(10 * time.Second):
|
||||
require.Fail(t, "Did not receive websocket message in time")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("post_deleted broadcast to channel members strips action integrations", func(t *testing.T) {
|
||||
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "interactive message",
|
||||
Props: model.StringInterface{
|
||||
model.PostPropsAttachments: []*model.MessageAttachment{
|
||||
{
|
||||
Text: "hello",
|
||||
Actions: []*model.PostAction{
|
||||
{
|
||||
Type: model.PostActionTypeButton,
|
||||
Name: "action",
|
||||
Integration: &model.PostActionIntegration{
|
||||
URL: "http://localhost:8065/secret-endpoint",
|
||||
Context: map[string]any{"secret_marker": "s3cr3t"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
memberMessages, closeMemberWS := connectFakeWebSocket(t, th, th.BasicUser2.Id, "", []model.WebsocketEventType{model.WebsocketEventPostDeleted})
|
||||
defer closeMemberWS()
|
||||
|
||||
_, appErr = th.App.DeletePost(th.Context, post.Id, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
memberEvent := waitForPostDeleted(t, memberMessages)
|
||||
memberPostJSON, ok := memberEvent.GetData()["post"].(string)
|
||||
require.True(t, ok)
|
||||
assert.NotContains(t, memberPostJSON, "secret-endpoint")
|
||||
assert.NotContains(t, memberPostJSON, "secret_marker")
|
||||
assert.Nil(t, memberEvent.GetData()["delete_by"])
|
||||
|
||||
var memberPost model.Post
|
||||
require.NoError(t, json.Unmarshal([]byte(memberPostJSON), &memberPost))
|
||||
require.Equal(t, post.Id, memberPost.Id)
|
||||
memberAttachments := memberPost.Attachments()
|
||||
require.Len(t, memberAttachments, 1)
|
||||
require.Len(t, memberAttachments[0].Actions, 1)
|
||||
assert.Equal(t, "action", memberAttachments[0].Actions[0].Name, "non-secret attachment data must be preserved")
|
||||
assert.Nil(t, memberAttachments[0].Actions[0].Integration)
|
||||
})
|
||||
|
||||
t.Run("post_deleted broadcast to channel members strips mm_blocks_actions secrets", func(t *testing.T) {
|
||||
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "blocks message",
|
||||
}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
post.AddProp(model.PostPropsMmBlocksActions, map[string]any{
|
||||
"mm_blocks_act": map[string]any{
|
||||
"type": model.MmBlocksActionTypeExternal,
|
||||
"url": "http://localhost:8065/secret-endpoint",
|
||||
"context": map[string]any{"secret_marker": "s3cr3t"},
|
||||
},
|
||||
})
|
||||
|
||||
memberMessages, closeMemberWS := connectFakeWebSocket(t, th, th.BasicUser2.Id, "", []model.WebsocketEventType{model.WebsocketEventPostDeleted})
|
||||
defer closeMemberWS()
|
||||
|
||||
appErr = th.App.CleanUpAfterPostDeletion(th.Context, post, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
memberEvent := waitForPostDeleted(t, memberMessages)
|
||||
memberPostJSON, ok := memberEvent.GetData()["post"].(string)
|
||||
require.True(t, ok)
|
||||
assert.NotContains(t, memberPostJSON, "secret-endpoint")
|
||||
assert.NotContains(t, memberPostJSON, "secret_marker")
|
||||
|
||||
var memberPost model.Post
|
||||
require.NoError(t, json.Unmarshal([]byte(memberPostJSON), &memberPost))
|
||||
assert.Equal(t, post.Id, memberPost.Id)
|
||||
assert.Nil(t, memberPost.GetProp(model.PostPropsMmBlocksActions))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendTestMessage(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
@@ -3062,8 +3062,7 @@ func (a *App) GetThreadsForUser(rctx request.CTX, userID, teamID string, options
|
||||
Posts: make(map[string]*model.Post, len(result.Threads)),
|
||||
}
|
||||
for _, thread := range result.Threads {
|
||||
a.sanitizeProfiles(thread.Participants, false)
|
||||
thread.Post.SanitizeProps()
|
||||
a.sanitizeThreadResponse(thread)
|
||||
list.AddPost(thread.Post)
|
||||
}
|
||||
|
||||
@@ -3098,12 +3097,20 @@ func (a *App) GetThreadForUser(rctx request.CTX, threadMembership *model.ThreadM
|
||||
}
|
||||
}
|
||||
|
||||
a.sanitizeProfiles(thread.Participants, false)
|
||||
thread.Post.SanitizeProps()
|
||||
a.sanitizeThreadResponse(thread)
|
||||
a.populatePostListTranslations(rctx, &model.PostList{Posts: map[string]*model.Post{thread.Post.Id: thread.Post}})
|
||||
return thread, nil
|
||||
}
|
||||
|
||||
// sanitizeThreadResponse removes server-only data from a thread response before it is sent to clients.
|
||||
func (a *App) sanitizeThreadResponse(thread *model.ThreadResponse) {
|
||||
a.sanitizeProfiles(thread.Participants, false)
|
||||
if thread.Post != nil {
|
||||
thread.Post.SanitizeProps()
|
||||
thread.Post.StripActionIntegrations()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) UpdateThreadsReadForUser(userID, teamID string) *model.AppError {
|
||||
nErr := a.Srv().Store().Thread().MarkAllAsReadByTeam(userID, teamID)
|
||||
if nErr != nil {
|
||||
@@ -3182,8 +3189,7 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(rctx request.CTX, userID,
|
||||
}
|
||||
return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
a.sanitizeProfiles(userThread.Participants, false)
|
||||
userThread.Post.SanitizeProps()
|
||||
a.sanitizeThreadResponse(userThread)
|
||||
sanitizedPost, isMemberForPreviews, appErr := a.SanitizePostMetadataForUser(rctx, userThread.Post, userID)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
|
||||
@@ -2536,6 +2536,94 @@ func TestUpdateThreadReadForUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetThreadsForUserSanitizesRootPost(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.ThreadAutoFollow = true
|
||||
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
|
||||
})
|
||||
|
||||
appErr := th.App.JoinChannel(th.Context, th.BasicChannel, th.BasicUser.Id)
|
||||
require.Nil(t, appErr)
|
||||
appErr = th.App.JoinChannel(th.Context, th.BasicChannel, th.BasicUser2.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
assertSanitized := func(t *testing.T, post *model.Post) {
|
||||
t.Helper()
|
||||
|
||||
require.NotNil(t, post)
|
||||
attachments := post.Attachments()
|
||||
require.Len(t, attachments, 1)
|
||||
require.Len(t, attachments[0].Actions, 1)
|
||||
assert.Equal(t, "action", attachments[0].Actions[0].Name, "non-secret attachment data must be preserved")
|
||||
assert.Nil(t, attachments[0].Actions[0].Integration)
|
||||
assert.Nil(t, post.GetProp(model.PostPropsMmBlocksActions))
|
||||
|
||||
postJSON, err := json.Marshal(post)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(postJSON), "secret-endpoint")
|
||||
assert.NotContains(t, string(postJSON), "secret_marker")
|
||||
}
|
||||
|
||||
rootPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "interactive root",
|
||||
Props: model.StringInterface{
|
||||
model.PostPropsAttachments: []*model.MessageAttachment{
|
||||
{
|
||||
Text: "hello",
|
||||
Actions: []*model.PostAction{
|
||||
{
|
||||
Type: model.PostActionTypeButton,
|
||||
Name: "action",
|
||||
Integration: &model.PostActionIntegration{
|
||||
URL: "http://localhost:8065/secret-endpoint",
|
||||
Context: map[string]any{"secret_marker": "s3cr3t"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
model.PostPropsMmBlocksActions: map[string]any{
|
||||
"mm_blocks_act": map[string]any{
|
||||
"type": model.MmBlocksActionTypeExternal,
|
||||
"url": "http://localhost:8065/secret-endpoint",
|
||||
"context": map[string]any{"secret_marker": "s3cr3t"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, th.BasicChannel, model.CreatePostFlags{AllowMmBlocksActions: true})
|
||||
require.Nil(t, appErr)
|
||||
require.NotNil(t, rootPost.GetProp(model.PostPropsMmBlocksActions))
|
||||
|
||||
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser2.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
RootId: rootPost.Id,
|
||||
Message: "reply",
|
||||
}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
t.Run("GetThreadForUser strips action integrations from the root post", func(t *testing.T) {
|
||||
threadMembership, appErr := th.App.GetThreadMembershipForUser(th.BasicUser2.Id, rootPost.Id)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
thread, appErr := th.App.GetThreadForUser(th.Context, threadMembership, false)
|
||||
require.Nil(t, appErr)
|
||||
assertSanitized(t, thread.Post)
|
||||
})
|
||||
|
||||
t.Run("GetThreadsForUser strips action integrations from the root post", func(t *testing.T) {
|
||||
threads, appErr := th.App.GetThreadsForUser(th.Context, th.BasicUser2.Id, th.BasicTeam.Id, model.GetUserThreadsOpts{PageSize: 10})
|
||||
require.Nil(t, appErr)
|
||||
require.Len(t, threads.Threads, 1)
|
||||
assertSanitized(t, threads.Threads[0].Post)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateUserWithInitialPreferences(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
Reference in New Issue
Block a user