From 0df95088934e3e2c72cc765244b4a90da6206218 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:00:30 +0530 Subject: [PATCH] fix(security): keep inbox access filtering on the participating scope (#15207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An agent who is removed from an inbox could still see that inbox's conversations under the **Participating** filter. Removing an agent from an inbox does not delete the conversation participant records they already had, and the participating filter was ignoring inbox access entirely — so those conversations stayed visible indefinitely. The filter now respects inbox access like every other conversation filter. ## Linear Ticket - https://linear.app/chatwoot/issue/CW-6923 ## How to reproduce 1. Add an agent to two inboxes, A and B. 2. As that agent, become a participant on a conversation in inbox B (open it, or get added as a participant). 3. Remove the agent from inbox B in Settings → Inboxes → Collaborators. 4. Log in as the agent and open Conversations → Participating. 5. The inbox B conversation is still listed, and is openable. ## What changed `ConversationFinder#filter_by_conversation_type` **replaced** `@conversations` with `current_user.participating_conversations` for the `participating` type, discarding the inbox/permission-filtered scope built up by `Conversations::PermissionFilterService` immediately before it. It now narrows the existing scope by participating ids instead, so permission filtering survives. --- app/finders/conversation_finder.rb | 3 ++- spec/finders/conversation_finder_spec.rb | 30 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb index 74bf903f5..871c55591 100644 --- a/app/finders/conversation_finder.rb +++ b/app/finders/conversation_finder.rb @@ -142,7 +142,8 @@ class ConversationFinder conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id) @conversations = @conversations.where(id: conversation_ids) when 'participating' - @conversations = current_user.participating_conversations.where(account_id: current_account.id) + participant_conversation_ids = ConversationParticipant.where(account_id: current_account.id, user_id: current_user.id).select(:conversation_id) + @conversations = @conversations.where(id: participant_conversation_ids) when 'unattended' @conversations = @conversations.unattended end diff --git a/spec/finders/conversation_finder_spec.rb b/spec/finders/conversation_finder_spec.rb index 64f134fe2..c502336c0 100644 --- a/spec/finders/conversation_finder_spec.rb +++ b/spec/finders/conversation_finder_spec.rb @@ -290,5 +290,35 @@ describe ConversationFinder do expect(result[:conversations].length).to be 2 end end + + context 'with participating' do + let(:params) { { status: 'open', assignee_type: 'all', conversation_type: 'participating' } } + + it 'excludes participating conversations from inboxes the user no longer has access to' do + accessible_conversation = create(:conversation, account: account, inbox: inbox) + revoked_conversation = create(:conversation, account: account, inbox: restricted_inbox) + revoked_membership = create(:inbox_member, user: user_1, inbox: restricted_inbox) + create(:conversation_participant, user: user_1, conversation: accessible_conversation, account: account) + create(:conversation_participant, user: user_1, conversation: revoked_conversation, account: account) + revoked_membership.destroy! + + result = conversation_finder.perform + + expect(result[:conversations].map(&:id)).to contain_exactly(accessible_conversation.id) + end + + it 'excludes the inaccessible conversation from the meta counts too' do + accessible_conversation = create(:conversation, account: account, inbox: inbox) + revoked_conversation = create(:conversation, account: account, inbox: restricted_inbox) + revoked_membership = create(:inbox_member, user: user_1, inbox: restricted_inbox) + create(:conversation_participant, user: user_1, conversation: accessible_conversation, account: account) + create(:conversation_participant, user: user_1, conversation: revoked_conversation, account: account) + revoked_membership.destroy! + + result = conversation_finder.perform_meta_only + + expect(result[:count][:all_count]).to eq 1 + end + end end end