From 07a39f4b42b1286949ad3cb7f80942c004287b79 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 30 May 2025 15:19:42 +0530 Subject: [PATCH] feat: Enforce role permissions on filtered page (#11638) Fixes: https://github.com/chatwoot/chatwoot/issues/11610 Demo: https://www.loom.com/share/c9181b42619044379ba01e0ac913801d?sid=e306fe30-ce80-47ac-83e5-92132a99f464 --- .../store/modules/conversations/getters.js | 33 +- .../specs/conversations/getters.spec.js | 304 ++++++++++++++++++ 2 files changed, 331 insertions(+), 6 deletions(-) diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index f5b83e546..9f5744fbb 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -18,13 +18,34 @@ const getters = { getAllConversations: ({ allConversations, chatSortFilter: sortKey }) => { return allConversations.sort((a, b) => sortComparator(a, b, sortKey)); }, - getFilteredConversations: ({ - allConversations, - chatSortFilter, - appliedFilters, - }) => { + getFilteredConversations: ( + { allConversations, chatSortFilter, appliedFilters }, + _, + __, + rootGetters + ) => { + const currentUser = rootGetters.getCurrentUser; + const currentUserId = rootGetters.getCurrentUser.id; + const currentAccountId = rootGetters.getCurrentAccountId; + + const permissions = getUserPermissions(currentUser, currentAccountId); + const userRole = getUserRole(currentUser, currentAccountId); + return allConversations - .filter(conversation => matchesFilters(conversation, appliedFilters)) + .filter(conversation => { + const matchesFilterResult = matchesFilters( + conversation, + appliedFilters + ); + const allowedForRole = applyRoleFilter( + conversation, + userRole, + permissions, + currentUserId + ); + + return matchesFilterResult && allowedForRole; + }) .sort((a, b) => sortComparator(a, b, chatSortFilter)); }, getSelectedChat: ({ selectedChatId, allConversations }) => { diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index 8ac89f49a..7b6c38456 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -325,4 +325,308 @@ describe('#getters', () => { }); }); }); + + describe('#getFilteredConversations', () => { + const mockConversations = [ + { + id: 1, + status: 'open', + meta: { assignee: { id: 1 } }, + last_activity_at: 1000, + }, + { + id: 2, + status: 'open', + meta: {}, + last_activity_at: 2000, + }, + { + id: 3, + status: 'resolved', + meta: { assignee: { id: 2 } }, + last_activity_at: 3000, + }, + ]; + + const mockRootGetters = { + getCurrentUser: { + id: 1, + accounts: [{ id: 1, role: 'agent', permissions: [] }], + }, + getCurrentAccountId: 1, + }; + + it('filters conversations based on role permissions for administrator', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_desc', + appliedFilters: [], + }; + + const rootGetters = { + ...mockRootGetters, + getCurrentUser: { + ...mockRootGetters.getCurrentUser, + accounts: [{ id: 1, role: 'administrator', permissions: [] }], + }, + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + rootGetters + ); + + expect(result).toEqual([ + mockConversations[2], + mockConversations[1], + mockConversations[0], + ]); + }); + + it('filters conversations based on role permissions for agent', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_desc', + appliedFilters: [], + }; + + const rootGetters = { + ...mockRootGetters, + getCurrentUser: { + ...mockRootGetters.getCurrentUser, + accounts: [{ id: 1, role: 'agent', permissions: [] }], + }, + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + rootGetters + ); + + expect(result).toEqual([ + mockConversations[2], + mockConversations[1], + mockConversations[0], + ]); + }); + + it('filters conversations for custom role with conversation_manage permission', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_desc', + appliedFilters: [], + }; + + const rootGetters = { + ...mockRootGetters, + getCurrentUser: { + ...mockRootGetters.getCurrentUser, + accounts: [ + { + id: 1, + custom_role_id: 5, + permissions: ['conversation_manage'], + }, + ], + }, + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + rootGetters + ); + + expect(result).toEqual([ + mockConversations[2], + mockConversations[1], + mockConversations[0], + ]); + }); + + it('filters conversations for custom role with conversation_unassigned_manage permission', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_desc', + appliedFilters: [], + }; + + const rootGetters = { + ...mockRootGetters, + getCurrentUser: { + ...mockRootGetters.getCurrentUser, + accounts: [ + { + id: 1, + custom_role_id: 5, + permissions: ['conversation_unassigned_manage'], + }, + ], + }, + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + rootGetters + ); + + // Should include conversation assigned to user (id: 1) and unassigned conversation + expect(result).toEqual([mockConversations[1], mockConversations[0]]); + }); + + it('filters conversations for custom role with conversation_participating_manage permission', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_desc', + appliedFilters: [], + }; + + const rootGetters = { + ...mockRootGetters, + getCurrentUser: { + ...mockRootGetters.getCurrentUser, + accounts: [ + { + id: 1, + custom_role_id: 5, + permissions: ['conversation_participating_manage'], + }, + ], + }, + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + rootGetters + ); + + // Should only include conversation assigned to user (id: 1) + expect(result).toEqual([mockConversations[0]]); + }); + + it('filters conversations for custom role with no permissions', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_desc', + appliedFilters: [], + }; + + const rootGetters = { + ...mockRootGetters, + getCurrentUser: { + ...mockRootGetters.getCurrentUser, + accounts: [ + { + id: 1, + custom_role_id: 5, + permissions: [], + }, + ], + }, + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + rootGetters + ); + + // Should return empty array as user has no permissions + expect(result).toEqual([]); + }); + + it('applies filters and role permissions together', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_desc', + appliedFilters: [ + { + attribute_key: 'status', + filter_operator: 'equal_to', + values: ['open'], + query_operator: 'and', + }, + ], + }; + + const rootGetters = { + ...mockRootGetters, + getCurrentUser: { + ...mockRootGetters.getCurrentUser, + accounts: [ + { + id: 1, + custom_role_id: 5, + permissions: ['conversation_participating_manage'], + }, + ], + }, + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + rootGetters + ); + + // Should only include open conversation assigned to user (id: 1) + expect(result).toEqual([mockConversations[0]]); + }); + + it('returns empty array when no conversations match filters', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_desc', + appliedFilters: [ + { + attribute_key: 'status', + filter_operator: 'equal_to', + values: ['pending'], + query_operator: 'and', + }, + ], + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + mockRootGetters + ); + + expect(result).toEqual([]); + }); + + it('sorts filtered conversations according to chatSortFilter', () => { + const state = { + allConversations: mockConversations, + chatSortFilter: 'last_activity_at_asc', + appliedFilters: [], + }; + + const result = getters.getFilteredConversations( + state, + {}, + {}, + mockRootGetters + ); + + expect(result).toEqual([ + mockConversations[0], + mockConversations[1], + mockConversations[2], + ]); + }); + }); });