fix(security): keep inbox access filtering on the participating scope (#15207)

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.
This commit is contained in:
Tanmay Deep Sharma
2026-08-06 19:00:30 +05:30
committed by GitHub
parent e3e35ab7e1
commit 0df9508893
2 changed files with 32 additions and 1 deletions

View File

@@ -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