FIX: prevents user to restore message deleted by staff (#22571)

It could only occur on message created by the user itself and deleted while the user was looking at the channel.

It more generally fix the trash service which was not correctly setting the author of the delete.
This commit is contained in:
Joffrey JAFFEUX 2023-07-13 10:16:15 +02:00 committed by GitHub
parent c996e5502f
commit 32e32f43b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 178 additions and 47 deletions

View File

@ -174,6 +174,7 @@ module Chat
type: "delete", type: "delete",
deleted_id: chat_message.id, deleted_id: chat_message.id,
deleted_at: chat_message.deleted_at, deleted_at: chat_message.deleted_at,
deleted_by_id: chat_message.deleted_by_id,
latest_not_deleted_message_id: latest_not_deleted_message_id, latest_not_deleted_message_id: latest_not_deleted_message_id,
}, },
) )

View File

@ -50,8 +50,8 @@ module Chat
guardian.can_delete_chat?(message, message.chat_channel.chatable) guardian.can_delete_chat?(message, message.chat_channel.chatable)
end end
def trash_message(message:, **) def trash_message(message:, guardian:, **)
message.trash! message.trash!(guardian.user)
end end
def destroy_notifications(message:, **) def destroy_notifications(message:, **)

View File

@ -96,8 +96,10 @@ export default class ChatMessageInteractor {
get canRestoreMessage() { get canRestoreMessage() {
return ( return (
this.canDelete &&
this.message?.deletedAt && this.message?.deletedAt &&
(this.currentUser.staff ||
(this.message?.user?.id === this.currentUser.id &&
this.message?.deletedById === this.currentUser.id)) &&
this.message.channel?.canModifyMessages?.(this.currentUser) this.message.channel?.canModifyMessages?.(this.currentUser)
); );
} }
@ -118,7 +120,7 @@ export default class ChatMessageInteractor {
get canFlagMessage() { get canFlagMessage() {
return ( return (
this.currentUser?.id !== this.message?.user?.id && this.currentUser.id !== this.message?.user?.id &&
this.message?.userFlagStatus === undefined && this.message?.userFlagStatus === undefined &&
this.message.channel?.canFlag && this.message.channel?.canFlag &&
!this.message?.chatWebhookEvent && !this.message?.chatWebhookEvent &&
@ -128,7 +130,7 @@ export default class ChatMessageInteractor {
get canRebakeMessage() { get canRebakeMessage() {
return ( return (
this.currentUser?.staff && this.currentUser.staff &&
this.message.channel?.canModifyMessages?.(this.currentUser) this.message.channel?.canModifyMessages?.(this.currentUser)
); );
} }
@ -142,7 +144,7 @@ export default class ChatMessageInteractor {
} }
get canDelete() { get canDelete() {
return this.currentUser?.id === this.message.user.id return this.currentUser.id === this.message.user.id
? this.message.channel?.canDeleteSelf ? this.message.channel?.canDeleteSelf
: this.message.channel?.canDeleteOthers; : this.message.channel?.canDeleteOthers;
} }

View File

@ -52,6 +52,7 @@ export default class ChatMessage {
@tracked thread; @tracked thread;
@tracked manager; @tracked manager;
@tracked threadTitle; @tracked threadTitle;
@tracked deletedById;
@tracked _deletedAt; @tracked _deletedAt;
@tracked _cooked; @tracked _cooked;
@ -69,6 +70,7 @@ export default class ChatMessage {
this.hidden = args.hidden || false; this.hidden = args.hidden || false;
this.chatWebhookEvent = args.chatWebhookEvent || args.chat_webhook_event; this.chatWebhookEvent = args.chatWebhookEvent || args.chat_webhook_event;
this.createdAt = args.createdAt || args.created_at; this.createdAt = args.createdAt || args.created_at;
this.deletedById = args.deletedById || args.deleted_by_id;
this._deletedAt = args.deletedAt || args.deleted_at; this._deletedAt = args.deletedAt || args.deleted_at;
this.expanded = this.expanded =
this.hidden || this._deletedAt ? false : args.expanded || true; this.hidden || this._deletedAt ? false : args.expanded || true;

View File

@ -177,6 +177,7 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
if (this.currentUser.staff || this.currentUser.id === targetMsg.user.id) { if (this.currentUser.staff || this.currentUser.id === targetMsg.user.id) {
targetMsg.deletedAt = data.deleted_at; targetMsg.deletedAt = data.deleted_at;
targetMsg.deletedById = data.deleted_by_id;
targetMsg.expanded = false; targetMsg.expanded = false;
} else { } else {
this.messagesManager.removeMessage(targetMsg); this.messagesManager.removeMessage(targetMsg);

View File

@ -172,9 +172,9 @@ module Chat
if message.user_id == current_user.id if message.user_id == current_user.id
case chatable case chatable
when Category when Category
return can_see_category?(chatable) return message.deleted_by_id == current_user.id || can_see_category?(chatable)
when Chat::DirectMessage when Chat::DirectMessage
return true return message.deleted_by_id == current_user.id || is_staff?
end end
end end

View File

@ -236,10 +236,16 @@ RSpec.describe Chat::GuardianExtensions do
context "when chatable is a direct message" do context "when chatable is a direct message" do
fab!(:chatable) { Chat::DirectMessage.create! } fab!(:chatable) { Chat::DirectMessage.create! }
it "allows owner to restore" do it "allows owner to restore when deleted by owner" do
message.trash!(guardian.user)
expect(guardian.can_restore_chat?(message, chatable)).to eq(true) expect(guardian.can_restore_chat?(message, chatable)).to eq(true)
end end
it "disallow owner to restore when deleted by staff" do
message.trash!(staff_guardian.user)
expect(guardian.can_restore_chat?(message, chatable)).to eq(false)
end
it "allows staff to restore" do it "allows staff to restore" do
expect(staff_guardian.can_restore_chat?(message, chatable)).to eq(true) expect(staff_guardian.can_restore_chat?(message, chatable)).to eq(true)
end end
@ -323,9 +329,15 @@ RSpec.describe Chat::GuardianExtensions do
expect(staff_guardian.can_restore_chat?(message, chatable)).to eq(true) expect(staff_guardian.can_restore_chat?(message, chatable)).to eq(true)
end end
it "allows owner to restore" do it "allows owner to restore when deleted by owner" do
message.trash!(guardian.user)
expect(guardian.can_restore_chat?(message, chatable)).to eq(true) expect(guardian.can_restore_chat?(message, chatable)).to eq(true)
end end
it "disallow owner to restore when deleted by staff" do
message.trash!(staff_guardian.user)
expect(guardian.can_restore_chat?(message, chatable)).to eq(false)
end
end end
end end
end end

View File

@ -110,91 +110,91 @@ RSpec.describe Chat::Api::ChannelMessagesController do
describe "#restore" do describe "#restore" do
RSpec.shared_examples "chat_message_restoration" do RSpec.shared_examples "chat_message_restoration" do
it "doesn't allow a user to restore another user's message" do it "doesn't allow a user to restore another user's message" do
sign_in(other_user) another_user = Fabricate(:user)
message = Fabricate(:chat_message, chat_channel: chat_channel, user: another_user)
message.trash!(another_user)
put "/chat/api/channels/#{chat_channel.id}/messages/#{deleted_message.id}/restore.json" sign_in(current_user)
put "/chat/api/channels/#{chat_channel.id}/messages/#{message.id}/restore.json"
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
it "allows a user to restore their own messages" do it "allows a user to restore their own messages" do
message = Fabricate(:chat_message, chat_channel: chat_channel, user: current_user)
message.trash!(current_user)
sign_in(current_user) sign_in(current_user)
put "/chat/api/channels/#{chat_channel.id}/messages/#{deleted_message.id}/restore.json" put "/chat/api/channels/#{chat_channel.id}/messages/#{message.id}/restore.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(deleted_message.reload.deleted_at).to be_nil expect(message.reload.deleted_at).to be_nil
end end
it "allows admin to restore others' messages" do it "allows admin to restore others' messages" do
message = Fabricate(:chat_message, chat_channel: chat_channel, user: current_user)
message.trash!(current_user)
sign_in(admin) sign_in(admin)
put "/chat/api/channels/#{chat_channel.id}/messages/#{deleted_message.id}/restore.json" put "/chat/api/channels/#{chat_channel.id}/messages/#{message.id}/restore.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(deleted_message.reload.deleted_at).to be_nil expect(message.reload.deleted_at).to be_nil
end end
it "does not allow message restore when channel is read_only" do it "does not allow message restore when channel is read_only" do
message = Fabricate(:chat_message, chat_channel: chat_channel, user: current_user)
message.trash!(current_user)
sign_in(current_user) sign_in(current_user)
chat_channel.update!(status: :read_only) chat_channel.update!(status: :read_only)
put "/chat/api/channels/#{chat_channel.id}/messages/#{deleted_message.id}/restore.json" put "/chat/api/channels/#{chat_channel.id}/messages/#{message.id}/restore.json"
expect(response.status).to eq(403) expect(response.status).to eq(403)
expect(deleted_message.reload.deleted_at).not_to be_nil expect(message.reload.deleted_at).not_to be_nil
sign_in(admin) sign_in(admin)
put "/chat/api/channels/#{chat_channel.id}/messages/#{deleted_message.id}/restore.json" put "/chat/api/channels/#{chat_channel.id}/messages/#{message.id}/restore.json"
expect(response.status).to eq(403) expect(response.status).to eq(403)
end end
it "only allows admin to restore when channel is closed" do it "only allows admin to restore when channel is closed" do
message = Fabricate(:chat_message, chat_channel: chat_channel, user: current_user)
message.trash!(current_user)
sign_in(admin) sign_in(admin)
chat_channel.update!(status: :read_only) chat_channel.update!(status: :read_only)
put "/chat/api/channels/#{chat_channel.id}/messages/#{deleted_message.id}/restore.json" put "/chat/api/channels/#{chat_channel.id}/messages/#{message.id}/restore.json"
expect(response.status).to eq(403) expect(response.status).to eq(403)
expect(deleted_message.reload.deleted_at).not_to be_nil expect(message.reload.deleted_at).not_to be_nil
chat_channel.update!(status: :closed) chat_channel.update!(status: :closed)
put "/chat/api/channels/#{chat_channel.id}/messages/#{deleted_message.id}/restore.json" put "/chat/api/channels/#{chat_channel.id}/messages/#{message.id}/restore.json"
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(deleted_message.reload.deleted_at).to be_nil expect(message.reload.deleted_at).to be_nil
end end
end end
fab!(:admin) { Fabricate(:admin) } fab!(:admin) { Fabricate(:admin) }
fab!(:second_user) { Fabricate(:user) } fab!(:another_user) { Fabricate(:user) }
before do
message =
Chat::Message.create(
user: current_user,
message: "this is a message",
chat_channel: chat_channel,
)
message.trash!
end
let(:deleted_message) do
Chat::Message.unscoped.where(user: current_user, chat_channel: chat_channel).last
end
describe "for category" do describe "for category" do
fab!(:category) { Fabricate(:category) } fab!(:category) { Fabricate(:category) }
fab!(:chat_channel) { Fabricate(:category_channel, chatable: category) } fab!(:chat_channel) { Fabricate(:category_channel, chatable: category) }
it_behaves_like "chat_message_restoration" do it_behaves_like "chat_message_restoration"
let(:other_user) { second_user }
end
end end
describe "for dm channel" do describe "for dm channel" do
fab!(:user_2) { Fabricate(:user) } fab!(:user_2) { Fabricate(:user) }
fab!(:chat_channel) { Fabricate(:direct_message_channel, users: [current_user, user_2]) } fab!(:chat_channel) do
Fabricate(:direct_message_channel, users: [current_user, another_user])
it_behaves_like "chat_message_restoration" do
let(:other_user) { user_2 }
end end
it_behaves_like "chat_message_restoration"
end end
end end
end end

View File

@ -15,6 +15,7 @@ describe Chat::Publisher do
MessageBus.track_publish { described_class.publish_delete!(channel, message_2) }[0].data MessageBus.track_publish { described_class.publish_delete!(channel, message_2) }[0].data
expect(data["deleted_at"]).to eq(message_2.deleted_at.iso8601(3)) expect(data["deleted_at"]).to eq(message_2.deleted_at.iso8601(3))
expect(data["deleted_by_id"]).to eq(message_2.deleted_by_id)
expect(data["deleted_id"]).to eq(message_2.id) expect(data["deleted_id"]).to eq(message_2.id)
expect(data["latest_not_deleted_message_id"]).to eq(message_1.id) expect(data["latest_not_deleted_message_id"]).to eq(message_1.id)
expect(data["type"]).to eq("delete") expect(data["type"]).to eq("delete")

View File

@ -41,6 +41,10 @@ RSpec.describe Chat::TrashMessage do
it "trashes the message" do it "trashes the message" do
result result
expect(Chat::Message.find_by(id: message.id)).to be_nil expect(Chat::Message.find_by(id: message.id)).to be_nil
deleted_message = Chat::Message.unscoped.find_by(id: message.id)
expect(deleted_message.deleted_by_id).to eq(current_user.id)
expect(deleted_message.deleted_at).to be_within(1.minute).of(Time.zone.now)
end end
it "destroys notifications for mentions" do it "destroys notifications for mentions" do
@ -65,6 +69,7 @@ RSpec.describe Chat::TrashMessage do
{ {
"type" => "delete", "type" => "delete",
"deleted_id" => message.id, "deleted_id" => message.id,
"deleted_by_id" => current_user.id,
"deleted_at" => message.reload.deleted_at.iso8601(3), "deleted_at" => message.reload.deleted_at.iso8601(3),
"latest_not_deleted_message_id" => nil, "latest_not_deleted_message_id" => nil,
}, },

View File

@ -120,6 +120,12 @@ module PageObjects
find("[data-value='delete']").click find("[data-value='delete']").click
end end
def restore_message(message)
hover_message(message)
click_more_button
find("[data-value='restore']").click
end
def open_edit_message(message) def open_edit_message(message)
hover_message(message) hover_message(message)
click_more_button click_more_button

View File

@ -18,7 +18,16 @@ module PageObjects
end end
def hover def hover
message_by_id(message.id).hover component.hover
end
def open_more_menu
hover
click_more_button
end
def expand
component.find(".chat-message-expand").click
end end
def select(shift: false) def select(shift: false)
@ -37,7 +46,7 @@ module PageObjects
component.click(delay: 0.6) component.click(delay: 0.6)
page.find(".chat-message-actions [data-id=\"select\"]").click page.find(".chat-message-actions [data-id=\"select\"]").click
else else
component.hover hover
click_more_button click_more_button
page.find("[data-value='select']").click page.find("[data-value='select']").click
end end

View File

@ -16,6 +16,22 @@ module PageObjects
page.find(context) page.find(context)
end end
def has_action?(action, **args)
message = find(args)
message.open_more_menu
page.has_css?("[data-value='#{action}']")
end
def has_no_action?(action, **args)
message = find(args)
message.open_more_menu
page.has_no_css?("[data-value='#{action}']")
end
def expand(**args)
find(args).expand
end
def select(args) def select(args)
find(args).select find(args).select
end end

View File

@ -0,0 +1,76 @@
# frozen_string_literal: true
RSpec.describe "Restore message", type: :system do
fab!(:admin_user) { Fabricate(:admin) }
fab!(:regular_user) { Fabricate(:user) }
fab!(:another_user) { Fabricate(:user) }
fab!(:channel_1) { Fabricate(:category_channel) }
let(:chat_page) { PageObjects::Pages::Chat.new }
let(:chat_channel_page) { PageObjects::Pages::ChatChannel.new }
before do
chat_system_bootstrap
channel_1.add(admin_user)
channel_1.add(regular_user)
channel_1.add(another_user)
end
context "when user deletes its own message" do
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1, user: regular_user) }
it "can be restored by the owner" do
sign_in(regular_user)
chat_page.visit_channel(channel_1)
chat_channel_page.delete_message(message_1)
expect(chat_channel_page.messages).to have_deleted_message(message_1, count: 1)
expect(chat_channel_page.messages).to have_action("restore", id: message_1.id)
end
it "can't be restored by another user" do
using_session(:another_user) do
sign_in(another_user)
chat_page.visit_channel(channel_1)
end
using_session(:regular_user) do |session|
sign_in(regular_user)
chat_page.visit_channel(channel_1)
chat_channel_page.delete_message(message_1)
session.quit
end
using_session(:another_user) do |session|
expect(chat_channel_page.messages).to have_no_message(id: message_1.id)
session.quit
end
end
end
context "when staff deletes user message" do
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1, user: regular_user) }
it "can't be restored by owner" do
using_session(:regular_user) do
sign_in(regular_user)
chat_page.visit_channel(channel_1)
end
using_session(:admin_user) do |session|
sign_in(admin_user)
chat_page.visit_channel(channel_1)
chat_channel_page.delete_message(message_1)
session.quit
end
using_session(:regular_user) do |session|
expect(chat_channel_page.messages).to have_deleted_message(message_1, count: 1)
chat_channel_page.messages.expand(id: message_1.id)
expect(chat_channel_page.messages).to have_no_action("restore", id: message_1.id)
session.quit
end
end
end
end