diff --git a/.changeset/gold-cougars-stare.md b/.changeset/gold-cougars-stare.md new file mode 100644 index 0000000000000..f5393b41ec8ab --- /dev/null +++ b/.changeset/gold-cougars-stare.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue that allowed agents without the `preview-c-room` permission to join a closed livechat conversation, creating a livechat room that could not be closed or removed from the sidebar. diff --git a/apps/meteor/app/lib/server/methods/joinRoom.ts b/apps/meteor/app/lib/server/methods/joinRoom.ts index ef3f069ee6142..8960936d7c33d 100644 --- a/apps/meteor/app/lib/server/methods/joinRoom.ts +++ b/apps/meteor/app/lib/server/methods/joinRoom.ts @@ -1,5 +1,5 @@ import { Room } from '@rocket.chat/core-services'; -import type { IRoom } from '@rocket.chat/core-typings'; +import { type IRoom } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms } from '@rocket.chat/models'; import { check } from 'meteor/check'; diff --git a/apps/meteor/client/views/room/hooks/useOpenRoom.ts b/apps/meteor/client/views/room/hooks/useOpenRoom.ts index f215b7ed9ef90..8faad251dc8b0 100644 --- a/apps/meteor/client/views/room/hooks/useOpenRoom.ts +++ b/apps/meteor/client/views/room/hooks/useOpenRoom.ts @@ -1,4 +1,4 @@ -import type { IRoom, RoomType } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom, type IRoom, type RoomType } from '@rocket.chat/core-typings'; import { useMethod, usePermission, useRoute, useSetting, useUser } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; @@ -90,7 +90,7 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st const sub = Subscriptions.findOne({ rid: room._id }); // if user doesn't exist at this point, anonymous read is enabled, otherwise an error would have been thrown - if (user && !sub && !hasPreviewPermission) { + if (user && !sub && !hasPreviewPermission && !isOmnichannelRoom(room)) { throw new NotSubscribedToRoomError(undefined, { rid: room._id }); } diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index 8afc133f1cc59..cc83ffe66e88a 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -1,6 +1,6 @@ import { ServiceClassInternal, Authorization, MeteorError } from '@rocket.chat/core-services'; import type { ICreateRoomParams, IRoomService } from '@rocket.chat/core-services'; -import { type AtLeast, type IRoom, type IUser, isRoomWithJoinCode } from '@rocket.chat/core-typings'; +import { type AtLeast, type IRoom, type IUser, isOmnichannelRoom, isRoomWithJoinCode } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { FederationActions } from './hooks/BeforeFederationActions'; @@ -102,6 +102,10 @@ export class RoomService extends ServiceClassInternal implements IRoomService { throw new MeteorError('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); } + if (isOmnichannelRoom(room) && !room.open) { + throw new MeteorError('room-closed', 'Room is closed', { method: 'joinRoom' }); + } + if (!(await Authorization.canAccessRoom(room, user))) { throw new MeteorError('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); } diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts index e718cc0a5e442..232e76a2d8789 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-history.spec.ts @@ -31,9 +31,11 @@ test.describe('Omnichannel chat history', () => { await api.delete('/livechat/users/agent/user1'); await api.delete('/livechat/users/manager/user1'); await agent.page.close(); + + await api.post('/permissions.update', { permissions: [{ _id: 'preview-c-room', roles: ['admin', 'owner', 'moderator', 'user'] }] }); }); - test('Receiving a message from visitor', async ({ page }) => { + test('Receiving a message from visitor', async ({ page, api }) => { await test.step('Expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); @@ -72,5 +74,19 @@ test.describe('Omnichannel chat history', () => { await agent.poHomeOmnichannel.contacts.contactInfo.historyItem.click(); await expect(agent.poHomeOmnichannel.contacts.contactInfo.historyMessage).toBeVisible(); }); + + await api.post('/permissions.update', { permissions: [{ _id: 'preview-c-room', roles: [] }] }); + + await test.step('Expect agent to see conversation history, but not join room', async () => { + await agent.page.reload(); + + await agent.poHomeOmnichannel.contacts.contactInfo.historyItem.click(); + await agent.poHomeOmnichannel.contacts.contactInfo.historyMessage.click(); + await agent.poHomeOmnichannel.contacts.contactInfo.btnOpenChat.click(); + + // Should not show the NoSubscribedRoom.tsx component on livechat rooms + await expect(agent.page.locator('div >> text=This conversation is already closed.')).toBeVisible(); + await expect(agent.page.locator('div >> text="this_a_test_message_from_visitor"')).toBeVisible(); + }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts index f7e9cf94e506d..1aed34bed21d9 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts @@ -26,4 +26,8 @@ export class OmnichannelContactInfo extends OmnichannelManageContact { get historyMessage(): Locator { return this.dialogContactInfo.getByRole('listitem').first(); } + + get btnOpenChat(): Locator { + return this.dialogContactInfo.getByRole('button', { name: 'Open chat' }); + } } diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 2671e7f189c0f..0d6651f74ba9b 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -1,5 +1,5 @@ import type { Credentials } from '@rocket.chat/api-client'; -import type { IMessage, IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IOmnichannelRoom, IRoom, IThreadMessage, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; @@ -7,6 +7,7 @@ import { after, before, describe, it } from 'mocha'; import { api, credentials, getCredentials, methodCall, request } from '../../data/api-data'; import { sendSimpleMessage } from '../../data/chat.helper'; import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; +import { closeOmnichannelRoom, createAgent, createLivechatRoom, createVisitor } from '../../data/livechat/rooms'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; @@ -3927,4 +3928,46 @@ describe('Meteor.methods', () => { }); }); }); + + describe('[@joinRoom]', async () => { + let room: IOmnichannelRoom; + let user: TestUser; + let userCredentials: Credentials; + + before(async () => { + const visitor = await createVisitor(); + room = await createLivechatRoom(visitor.token); + await closeOmnichannelRoom(room._id); + + user = await createUser(); + await createAgent(user.username); + userCredentials = await login(user.username, password); + }); + + after(() => Promise.all([deleteUser(user)])); + + it('should not allow an agent to join a closed livechat room', async () => { + await request + .post(methodCall('joinRoom')) + .set(userCredentials) + .send({ + message: JSON.stringify({ + method: 'joinRoom', + params: [room._id], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error').that.is.an('object'); + expect(data.error).to.have.a.property('error', 'room-closed'); + }); + }); + }); });