From cdff61dd65176a6ab84031febdadd204a9c5fd31 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 31 Mar 2026 16:28:15 -0300 Subject: [PATCH 01/11] fix: missing room type restrictions in ban/unban/kick slash commands --- .../lib/server/functions/banUserFromRoom.ts | 2 +- .../meteor/app/slashcommands-ban/server/ban.ts | 18 +++++++++++++++++- .../app/slashcommands-ban/server/unban.ts | 18 +++++++++++++++++- apps/meteor/server/lib/unbanUserFromRoom.ts | 6 ++++++ packages/i18n/src/locales/en.i18n.json | 1 + 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/banUserFromRoom.ts b/apps/meteor/app/lib/server/functions/banUserFromRoom.ts index 1fbea41d40081..98aca97ef313d 100644 --- a/apps/meteor/app/lib/server/functions/banUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/banUserFromRoom.ts @@ -72,5 +72,5 @@ export const banUserFromRoom = async function (rid: string, user: IUser, byUser: await performUserBan(room, user, byUser); - void afterBanFromRoomCallback.run({ bannedUser: user, userWhoBanned: byUser }, room); + await afterBanFromRoomCallback.run({ bannedUser: user, userWhoBanned: byUser }, room); }; diff --git a/apps/meteor/app/slashcommands-ban/server/ban.ts b/apps/meteor/app/slashcommands-ban/server/ban.ts index 3ca43a3b5f4db..4722e347b026f 100644 --- a/apps/meteor/app/slashcommands-ban/server/ban.ts +++ b/apps/meteor/app/slashcommands-ban/server/ban.ts @@ -1,6 +1,7 @@ import { api } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; +import { isRoomNativeFederated } from '@rocket.chat/core-typings'; +import { Rooms, Users } from '@rocket.chat/models'; import { banUserFromRoomMethod } from '../../../server/lib/banUserFromRoom'; import { i18n } from '../../../server/lib/i18n'; @@ -17,6 +18,21 @@ slashCommands.add({ return; } + const room = await Rooms.findOneById(message.rid); + if (!room) { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('error-invalid-room', { lng: settings.get('Language') || 'en' }), + }); + return; + } + + if (!isRoomNativeFederated(room)) { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('error-ban-not-supported', { lng: settings.get('Language') || 'en' }), + }); + return; + } + const user = await Users.findOneByUsernameIgnoringCase(username); if (!user) { void api.broadcast('notify.ephemeralMessage', userId, message.rid, { diff --git a/apps/meteor/app/slashcommands-ban/server/unban.ts b/apps/meteor/app/slashcommands-ban/server/unban.ts index 4cb52047b651f..b1fdd95dee0af 100644 --- a/apps/meteor/app/slashcommands-ban/server/unban.ts +++ b/apps/meteor/app/slashcommands-ban/server/unban.ts @@ -1,6 +1,7 @@ import { api } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; +import { isRoomNativeFederated } from '@rocket.chat/core-typings'; +import { Rooms, Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { unbanUserFromRoom } from '../../../server/lib/unbanUserFromRoom'; @@ -17,6 +18,21 @@ slashCommands.add({ return; } + const room = await Rooms.findOneById(message.rid); + if (!room) { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('error-invalid-room', { lng: settings.get('Language') || 'en' }), + }); + return; + } + + if (!isRoomNativeFederated(room)) { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('error-ban-not-supported', { lng: settings.get('Language') || 'en' }), + }); + return; + } + const user = await Users.findOneByUsernameIgnoringCase(username); if (!user) { void api.broadcast('notify.ephemeralMessage', userId, message.rid, { diff --git a/apps/meteor/server/lib/unbanUserFromRoom.ts b/apps/meteor/server/lib/unbanUserFromRoom.ts index 488a90239cb3b..51bb3567081e6 100644 --- a/apps/meteor/server/lib/unbanUserFromRoom.ts +++ b/apps/meteor/server/lib/unbanUserFromRoom.ts @@ -1,8 +1,10 @@ import { Rooms, Users } from '@rocket.chat/models'; +import { roomCoordinator } from './rooms/roomCoordinator'; import { canAccessRoomAsync } from '../../app/authorization/server'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { executeUnbanUserFromRoom } from '../../app/lib/server/functions/executeUnbanUserFromRoom'; +import { RoomMemberActions } from '../../definition/IRoomTypeConfig'; export const unbanUserFromRoom = async (fromId: string, data: { rid: string; username: string }): Promise => { if (!(await hasPermissionAsync(fromId, 'ban-user', data.rid))) { @@ -14,6 +16,10 @@ export const unbanUserFromRoom = async (fromId: string, data: { rid: string; use throw new Error('Invalid room'); } + if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { + throw new Error('Not allowed'); + } + const fromUser = await Users.findOneById(fromId); if (!fromUser) { throw new Error('Invalid user'); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index c6bb52defeaa6..40da45e7f1ed6 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6210,6 +6210,7 @@ "error-cannot-delete-app-user": "Deleting app user is not allowed, uninstall the corresponding app to remove it.", "error-cannot-place-chat-on-hold": "You cannot place chat on-hold", "error-cant-add-federated-users": "Can't add federated users to a non-federated room", + "error-ban-not-supported": "Unable to ban. This action is not supported for this room type.", "error-cant-invite-for-direct-room": "Can't invite user to direct rooms", "error-channels-setdefault-is-same": "The channel default setting is the same as what it would be changed to.", "error-channels-setdefault-missing-default-param": "The bodyParam 'default' is required", From 72800fdf5310283939f83270d5f9bea4e68b5db3 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Tue, 31 Mar 2026 16:59:21 -0300 Subject: [PATCH 02/11] refactor: action-neutral error message --- apps/meteor/app/slashcommands-ban/server/ban.ts | 2 +- apps/meteor/app/slashcommands-ban/server/unban.ts | 2 +- packages/i18n/src/locales/en.i18n.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/slashcommands-ban/server/ban.ts b/apps/meteor/app/slashcommands-ban/server/ban.ts index 4722e347b026f..443c5d29e13d5 100644 --- a/apps/meteor/app/slashcommands-ban/server/ban.ts +++ b/apps/meteor/app/slashcommands-ban/server/ban.ts @@ -28,7 +28,7 @@ slashCommands.add({ if (!isRoomNativeFederated(room)) { void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('error-ban-not-supported', { lng: settings.get('Language') || 'en' }), + msg: i18n.t('error-room-action-not-supported', { lng: settings.get('Language') || 'en' }), }); return; } diff --git a/apps/meteor/app/slashcommands-ban/server/unban.ts b/apps/meteor/app/slashcommands-ban/server/unban.ts index b1fdd95dee0af..25e0e18e45328 100644 --- a/apps/meteor/app/slashcommands-ban/server/unban.ts +++ b/apps/meteor/app/slashcommands-ban/server/unban.ts @@ -28,7 +28,7 @@ slashCommands.add({ if (!isRoomNativeFederated(room)) { void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('error-ban-not-supported', { lng: settings.get('Language') || 'en' }), + msg: i18n.t('error-room-action-not-supported', { lng: settings.get('Language') || 'en' }), }); return; } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 40da45e7f1ed6..7be47c2e71efb 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6210,7 +6210,7 @@ "error-cannot-delete-app-user": "Deleting app user is not allowed, uninstall the corresponding app to remove it.", "error-cannot-place-chat-on-hold": "You cannot place chat on-hold", "error-cant-add-federated-users": "Can't add federated users to a non-federated room", - "error-ban-not-supported": "Unable to ban. This action is not supported for this room type.", + "error-room-action-not-supported": "This action is not supported for this room type.", "error-cant-invite-for-direct-room": "Can't invite user to direct rooms", "error-channels-setdefault-is-same": "The channel default setting is the same as what it would be changed to.", "error-channels-setdefault-missing-default-param": "The bodyParam 'default' is required", From 54fa3933492e58659258a50f6a94070520dd7f78 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 1 Apr 2026 10:00:24 -0300 Subject: [PATCH 03/11] refactor: removed await --- apps/meteor/app/lib/server/functions/banUserFromRoom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/functions/banUserFromRoom.ts b/apps/meteor/app/lib/server/functions/banUserFromRoom.ts index 98aca97ef313d..1fbea41d40081 100644 --- a/apps/meteor/app/lib/server/functions/banUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/banUserFromRoom.ts @@ -72,5 +72,5 @@ export const banUserFromRoom = async function (rid: string, user: IUser, byUser: await performUserBan(room, user, byUser); - await afterBanFromRoomCallback.run({ bannedUser: user, userWhoBanned: byUser }, room); + void afterBanFromRoomCallback.run({ bannedUser: user, userWhoBanned: byUser }, room); }; From 565e970305e83b96af225af9915d72a70ce4b7e7 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 1 Apr 2026 10:01:47 -0300 Subject: [PATCH 04/11] refactor: change error message --- apps/meteor/server/lib/unbanUserFromRoom.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/lib/unbanUserFromRoom.ts b/apps/meteor/server/lib/unbanUserFromRoom.ts index 51bb3567081e6..129c58c7b2328 100644 --- a/apps/meteor/server/lib/unbanUserFromRoom.ts +++ b/apps/meteor/server/lib/unbanUserFromRoom.ts @@ -17,7 +17,7 @@ export const unbanUserFromRoom = async (fromId: string, data: { rid: string; use } if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { - throw new Error('Not allowed'); + throw new Error('error-not-allowed'); } const fromUser = await Users.findOneById(fromId); From 39517358e728dd72a81827b65bcc5767df9c37af Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 2 Apr 2026 17:16:40 -0300 Subject: [PATCH 05/11] refactor: moved room type validations from slashcommands to ban/unban methods --- .../app/slashcommands-ban/server/ban.ts | 18 +----------- .../app/slashcommands-ban/server/unban.ts | 18 +----------- apps/meteor/server/lib/banUserFromRoom.ts | 8 +++-- apps/meteor/server/lib/unbanUserFromRoom.ts | 5 ++-- .../server/methods/banUserFromRoom.spec.ts | 29 ++++++++++++------- 5 files changed, 30 insertions(+), 48 deletions(-) diff --git a/apps/meteor/app/slashcommands-ban/server/ban.ts b/apps/meteor/app/slashcommands-ban/server/ban.ts index 443c5d29e13d5..3ca43a3b5f4db 100644 --- a/apps/meteor/app/slashcommands-ban/server/ban.ts +++ b/apps/meteor/app/slashcommands-ban/server/ban.ts @@ -1,7 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { isRoomNativeFederated } from '@rocket.chat/core-typings'; -import { Rooms, Users } from '@rocket.chat/models'; +import { Users } from '@rocket.chat/models'; import { banUserFromRoomMethod } from '../../../server/lib/banUserFromRoom'; import { i18n } from '../../../server/lib/i18n'; @@ -18,21 +17,6 @@ slashCommands.add({ return; } - const room = await Rooms.findOneById(message.rid); - if (!room) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('error-invalid-room', { lng: settings.get('Language') || 'en' }), - }); - return; - } - - if (!isRoomNativeFederated(room)) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('error-room-action-not-supported', { lng: settings.get('Language') || 'en' }), - }); - return; - } - const user = await Users.findOneByUsernameIgnoringCase(username); if (!user) { void api.broadcast('notify.ephemeralMessage', userId, message.rid, { diff --git a/apps/meteor/app/slashcommands-ban/server/unban.ts b/apps/meteor/app/slashcommands-ban/server/unban.ts index 25e0e18e45328..4cb52047b651f 100644 --- a/apps/meteor/app/slashcommands-ban/server/unban.ts +++ b/apps/meteor/app/slashcommands-ban/server/unban.ts @@ -1,7 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { isRoomNativeFederated } from '@rocket.chat/core-typings'; -import { Rooms, Users } from '@rocket.chat/models'; +import { Users } from '@rocket.chat/models'; import { i18n } from '../../../server/lib/i18n'; import { unbanUserFromRoom } from '../../../server/lib/unbanUserFromRoom'; @@ -18,21 +17,6 @@ slashCommands.add({ return; } - const room = await Rooms.findOneById(message.rid); - if (!room) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('error-invalid-room', { lng: settings.get('Language') || 'en' }), - }); - return; - } - - if (!isRoomNativeFederated(room)) { - void api.broadcast('notify.ephemeralMessage', userId, message.rid, { - msg: i18n.t('error-room-action-not-supported', { lng: settings.get('Language') || 'en' }), - }); - return; - } - const user = await Users.findOneByUsernameIgnoringCase(username); if (!user) { void api.broadcast('notify.ephemeralMessage', userId, message.rid, { diff --git a/apps/meteor/server/lib/banUserFromRoom.ts b/apps/meteor/server/lib/banUserFromRoom.ts index e61c6d810bf18..abb7f3056a272 100644 --- a/apps/meteor/server/lib/banUserFromRoom.ts +++ b/apps/meteor/server/lib/banUserFromRoom.ts @@ -1,4 +1,4 @@ -import { isBannedSubscription } from '@rocket.chat/core-typings'; +import { isBannedSubscription, isRoomNativeFederated } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users, Roles } from '@rocket.chat/models'; import { roomCoordinator } from './rooms/roomCoordinator'; @@ -15,7 +15,11 @@ export const banUserFromRoomMethod = async (fromId: string, data: { rid: string; const room = await Rooms.findOneById(data.rid); - if (!room || !(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { + if (!room || !isRoomNativeFederated(room)) { + throw new Error('Invalid room'); + } + + if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { throw new Error('Not allowed'); } diff --git a/apps/meteor/server/lib/unbanUserFromRoom.ts b/apps/meteor/server/lib/unbanUserFromRoom.ts index 129c58c7b2328..7175090b56ec9 100644 --- a/apps/meteor/server/lib/unbanUserFromRoom.ts +++ b/apps/meteor/server/lib/unbanUserFromRoom.ts @@ -1,3 +1,4 @@ +import { isRoomNativeFederated } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { roomCoordinator } from './rooms/roomCoordinator'; @@ -12,12 +13,12 @@ export const unbanUserFromRoom = async (fromId: string, data: { rid: string; use } const room = await Rooms.findOneById(data.rid); - if (!room) { + if (!room || !isRoomNativeFederated(room)) { throw new Error('Invalid room'); } if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { - throw new Error('error-not-allowed'); + throw new Error('Not allowed'); } const fromUser = await Users.findOneById(fromId); diff --git a/apps/meteor/tests/unit/server/methods/banUserFromRoom.spec.ts b/apps/meteor/tests/unit/server/methods/banUserFromRoom.spec.ts index ed99b32b8f608..583fe46e0068e 100644 --- a/apps/meteor/tests/unit/server/methods/banUserFromRoom.spec.ts +++ b/apps/meteor/tests/unit/server/methods/banUserFromRoom.spec.ts @@ -35,6 +35,8 @@ const RoomMemberActions = { const { banUserFromRoomMethod } = p.noCallThru().load('../../../../server/lib/banUserFromRoom.ts', { '@rocket.chat/core-typings': { isBannedSubscription: (sub: { status?: string } | null) => sub?.status === 'BANNED', + isRoomNativeFederated: (room: { federated?: boolean; federation?: Record } | null) => + room?.federated === true && room?.federation !== undefined, }, '@rocket.chat/models': modelsMock, '../../app/authorization/server': { canAccessRoomAsync: canAccessRoomAsyncMock }, @@ -69,12 +71,19 @@ describe('banUserFromRoomMethod', () => { hasPermissionAsyncMock.resolves(true); modelsMock.Rooms.findOneById.resolves(null); - await expect(banUserFromRoomMethod('fromUserId', { rid: 'room1', username: 'testuser' })).to.be.rejectedWith('Not allowed'); + await expect(banUserFromRoomMethod('fromUserId', { rid: 'room1', username: 'testuser' })).to.be.rejectedWith('Invalid room'); }); - it('should throw if BAN action is not allowed for the room type', async () => { + it('should throw if room is not natively federated', async () => { hasPermissionAsyncMock.resolves(true); modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + + await expect(banUserFromRoomMethod('fromUserId', { rid: 'room1', username: 'testuser' })).to.be.rejectedWith('Invalid room'); + }); + + it('should throw if BAN action is not allowed for the room type', async () => { + hasPermissionAsyncMock.resolves(true); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(false), }); @@ -84,7 +93,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if fromUser does not exist', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -95,7 +104,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if fromUser cannot access the room', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -109,7 +118,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if the user to ban does not exist', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -122,7 +131,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if the user to ban is not in the room', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -138,7 +147,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if user is already banned', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -154,7 +163,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if user is the last owner', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -170,7 +179,7 @@ describe('banUserFromRoomMethod', () => { it('should allow banning if user is owner but not the last one', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -190,7 +199,7 @@ describe('banUserFromRoomMethod', () => { it('should successfully ban a user and return true', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); From 6af5b12f3237692f41c8147ef8312487214f459b Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 2 Apr 2026 19:39:28 -0300 Subject: [PATCH 06/11] show ban/unban UI elements for non-federated rooms --- .../hooks/roomActions/useBannedUsersRoomAction.ts | 14 ++++---------- .../actions/useBanUserAction.tsx | 11 +++-------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/apps/meteor/client/hooks/roomActions/useBannedUsersRoomAction.ts b/apps/meteor/client/hooks/roomActions/useBannedUsersRoomAction.ts index d158489a9d4ff..e4bbbd9a371f3 100644 --- a/apps/meteor/client/hooks/roomActions/useBannedUsersRoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useBannedUsersRoomAction.ts @@ -1,24 +1,18 @@ -import { isRoomNativeFederated } from '@rocket.chat/core-typings'; import type { RoomToolboxActionConfig } from '@rocket.chat/ui-contexts'; -import { usePermission, useUser } from '@rocket.chat/ui-contexts'; +import { usePermission } from '@rocket.chat/ui-contexts'; import { lazy, useMemo } from 'react'; -import * as Federation from '../../lib/federation/Federation'; -import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; +import { useRoom } from '../../views/room/contexts/RoomContext'; const BannedUsers = lazy(() => import('../../views/room/contextualBar/BannedUsers')); export const useBannedUsersRoomAction = () => { const room = useRoom(); - const user = useUser(); - const subscription = useRoomSubscription(); const hasPermissionToBan = usePermission('ban-user', room._id); - const federationCanBan = isRoomNativeFederated(room) && Federation.isEditableByTheUser(user || undefined, room, subscription); - return useMemo((): RoomToolboxActionConfig | undefined => { - if (!hasPermissionToBan || !federationCanBan) { + if (!hasPermissionToBan) { return undefined; } @@ -31,5 +25,5 @@ export const useBannedUsersRoomAction = () => { order: 13, type: 'moderation', }; - }, [federationCanBan, hasPermissionToBan]); + }, [hasPermissionToBan]); }; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useBanUserAction.tsx b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useBanUserAction.tsx index 43dfac319a3a5..8bc56c262b73e 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useBanUserAction.tsx +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useBanUserAction.tsx @@ -1,10 +1,8 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; -import { isRoomNativeFederated } from '@rocket.chat/core-typings'; -import { usePermission, useUser, useUserRoom, useUserSubscription } from '@rocket.chat/ui-contexts'; +import { usePermission, useUserRoom, useUserSubscription } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import * as Federation from '../../../../../lib/federation/Federation'; import { getRoomDirectives } from '../../../lib/getRoomDirectives'; import { useBanUser } from '../../useBanUser'; import type { UserInfoAction } from '../useUserInfoActions'; @@ -12,7 +10,6 @@ import type { UserInfoAction } from '../useUserInfoActions'; export const useBanUserAction = (user: Pick, roomId: IRoom['_id']): UserInfoAction | undefined => { const { t } = useTranslation(); - const currentUser = useUser(); const room = useUserRoom(roomId); const subscription = useUserSubscription(roomId); @@ -27,14 +24,12 @@ export const useBanUserAction = (user: Pick, roomId: const { _id: uid, username } = user; const hasPermissionToBan = usePermission('ban-user', roomId); - const federationCanBan = isRoomNativeFederated(room) && Federation.isEditableByTheUser(currentUser || undefined, room, subscription); - const { roomCanBan } = getRoomDirectives({ room, showingUserId: uid, userSubscription: subscription }); const handleBan = useBanUser({ roomId }); return useMemo(() => { - if (!hasPermissionToBan || !federationCanBan || !roomCanBan) { + if (!hasPermissionToBan || !roomCanBan) { return undefined; } @@ -45,5 +40,5 @@ export const useBanUserAction = (user: Pick, roomId: type: 'moderation' as const, variant: 'danger' as const, }; - }, [handleBan, roomCanBan, federationCanBan, hasPermissionToBan, t, username]); + }, [handleBan, roomCanBan, hasPermissionToBan, t, username]); }; From f3f723d43fd968a5eb9a3e22a03ee3cf9f1babb2 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 2 Apr 2026 19:39:58 -0300 Subject: [PATCH 07/11] remove backend validations for federated rooms --- apps/meteor/server/lib/banUserFromRoom.ts | 8 ++------ apps/meteor/server/lib/unbanUserFromRoom.ts | 7 +------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/apps/meteor/server/lib/banUserFromRoom.ts b/apps/meteor/server/lib/banUserFromRoom.ts index abb7f3056a272..e61c6d810bf18 100644 --- a/apps/meteor/server/lib/banUserFromRoom.ts +++ b/apps/meteor/server/lib/banUserFromRoom.ts @@ -1,4 +1,4 @@ -import { isBannedSubscription, isRoomNativeFederated } from '@rocket.chat/core-typings'; +import { isBannedSubscription } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users, Roles } from '@rocket.chat/models'; import { roomCoordinator } from './rooms/roomCoordinator'; @@ -15,11 +15,7 @@ export const banUserFromRoomMethod = async (fromId: string, data: { rid: string; const room = await Rooms.findOneById(data.rid); - if (!room || !isRoomNativeFederated(room)) { - throw new Error('Invalid room'); - } - - if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { + if (!room || !(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { throw new Error('Not allowed'); } diff --git a/apps/meteor/server/lib/unbanUserFromRoom.ts b/apps/meteor/server/lib/unbanUserFromRoom.ts index 7175090b56ec9..65916b399a1aa 100644 --- a/apps/meteor/server/lib/unbanUserFromRoom.ts +++ b/apps/meteor/server/lib/unbanUserFromRoom.ts @@ -1,4 +1,3 @@ -import { isRoomNativeFederated } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { roomCoordinator } from './rooms/roomCoordinator'; @@ -13,11 +12,7 @@ export const unbanUserFromRoom = async (fromId: string, data: { rid: string; use } const room = await Rooms.findOneById(data.rid); - if (!room || !isRoomNativeFederated(room)) { - throw new Error('Invalid room'); - } - - if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { + if (!room || !(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BAN, fromId))) { throw new Error('Not allowed'); } From 871bc96a1d982bbcb5f5921116c372b61d166911 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 2 Apr 2026 19:40:11 -0300 Subject: [PATCH 08/11] use correct unban function on rooms.unbanUser endpoint --- apps/meteor/app/api/server/v1/rooms.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index bb4278bbb70fb..ae02a578ab05c 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -34,6 +34,7 @@ import { eraseRoom } from '../../../../server/lib/eraseRoom'; import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOfRoomOrderedByRole'; import { openRoom } from '../../../../server/lib/openRoom'; import type { RoomRoles } from '../../../../server/lib/roles/getRoomRoles'; +import { unbanUserFromRoom } from '../../../../server/lib/unbanUserFromRoom'; import { hideRoomMethod } from '../../../../server/methods/hideRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; import { toggleFavoriteMethod } from '../../../../server/methods/toggleFavorite'; @@ -45,7 +46,6 @@ import { saveRoomSettings } from '../../../channel-settings/server/methods/saveR import { createDiscussion } from '../../../discussion/server/methods/createDiscussion'; import { FileUpload } from '../../../file-upload/server'; import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; -import { executeUnbanUserFromRoom } from '../../../lib/server/functions/executeUnbanUserFromRoom'; import { syncRolePrioritiesForRoomIfRequired } from '../../../lib/server/functions/syncRolePrioritiesForRoomIfRequired'; import { executeArchiveRoom } from '../../../lib/server/methods/archiveRoom'; import { cleanRoomHistoryMethod } from '../../../lib/server/methods/cleanRoomHistory'; @@ -1333,7 +1333,7 @@ export const roomEndpoints = API.v1 return API.v1.failure('Invalid user'); } - await executeUnbanUserFromRoom(this.bodyParams.roomId, user, this.user); + await unbanUserFromRoom(this.userId, { rid: this.bodyParams.roomId, username: user.username }); return API.v1.success(); }, From 05b0d935d9d16d3d58a8feac856d8ab95eea6e66 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 2 Apr 2026 19:46:06 -0300 Subject: [PATCH 09/11] revert test changes --- .../server/methods/banUserFromRoom.spec.ts | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/apps/meteor/tests/unit/server/methods/banUserFromRoom.spec.ts b/apps/meteor/tests/unit/server/methods/banUserFromRoom.spec.ts index 583fe46e0068e..ed99b32b8f608 100644 --- a/apps/meteor/tests/unit/server/methods/banUserFromRoom.spec.ts +++ b/apps/meteor/tests/unit/server/methods/banUserFromRoom.spec.ts @@ -35,8 +35,6 @@ const RoomMemberActions = { const { banUserFromRoomMethod } = p.noCallThru().load('../../../../server/lib/banUserFromRoom.ts', { '@rocket.chat/core-typings': { isBannedSubscription: (sub: { status?: string } | null) => sub?.status === 'BANNED', - isRoomNativeFederated: (room: { federated?: boolean; federation?: Record } | null) => - room?.federated === true && room?.federation !== undefined, }, '@rocket.chat/models': modelsMock, '../../app/authorization/server': { canAccessRoomAsync: canAccessRoomAsyncMock }, @@ -71,19 +69,12 @@ describe('banUserFromRoomMethod', () => { hasPermissionAsyncMock.resolves(true); modelsMock.Rooms.findOneById.resolves(null); - await expect(banUserFromRoomMethod('fromUserId', { rid: 'room1', username: 'testuser' })).to.be.rejectedWith('Invalid room'); - }); - - it('should throw if room is not natively federated', async () => { - hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); - - await expect(banUserFromRoomMethod('fromUserId', { rid: 'room1', username: 'testuser' })).to.be.rejectedWith('Invalid room'); + await expect(banUserFromRoomMethod('fromUserId', { rid: 'room1', username: 'testuser' })).to.be.rejectedWith('Not allowed'); }); it('should throw if BAN action is not allowed for the room type', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(false), }); @@ -93,7 +84,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if fromUser does not exist', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -104,7 +95,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if fromUser cannot access the room', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -118,7 +109,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if the user to ban does not exist', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -131,7 +122,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if the user to ban is not in the room', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -147,7 +138,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if user is already banned', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -163,7 +154,7 @@ describe('banUserFromRoomMethod', () => { it('should throw if user is the last owner', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -179,7 +170,7 @@ describe('banUserFromRoomMethod', () => { it('should allow banning if user is owner but not the last one', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); @@ -199,7 +190,7 @@ describe('banUserFromRoomMethod', () => { it('should successfully ban a user and return true', async () => { hasPermissionAsyncMock.resolves(true); - modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c', federated: true, federation: {} }); + modelsMock.Rooms.findOneById.resolves({ _id: 'room1', t: 'c' }); roomCoordinatorMock.getRoomDirectives.returns({ allowMemberAction: sinon.stub().resolves(true), }); From 4be38c0378d866fd993ae3dc2aedd6c7e2ea51ae Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 2 Apr 2026 19:46:16 -0300 Subject: [PATCH 10/11] remove unused i18n string --- packages/i18n/src/locales/en.i18n.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 7be47c2e71efb..c6bb52defeaa6 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6210,7 +6210,6 @@ "error-cannot-delete-app-user": "Deleting app user is not allowed, uninstall the corresponding app to remove it.", "error-cannot-place-chat-on-hold": "You cannot place chat on-hold", "error-cant-add-federated-users": "Can't add federated users to a non-federated room", - "error-room-action-not-supported": "This action is not supported for this room type.", "error-cant-invite-for-direct-room": "Can't invite user to direct rooms", "error-channels-setdefault-is-same": "The channel default setting is the same as what it would be changed to.", "error-channels-setdefault-missing-default-param": "The bodyParam 'default' is required", From 5a135ab029c18e877fe39f2d06c58561ebc3b0d9 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 2 Apr 2026 20:28:00 -0300 Subject: [PATCH 11/11] docs: update changeset to reflect new state --- .changeset/thick-nails-exist.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/thick-nails-exist.md b/.changeset/thick-nails-exist.md index 9150fe12335e3..985c251a2044e 100644 --- a/.changeset/thick-nails-exist.md +++ b/.changeset/thick-nails-exist.md @@ -1,5 +1,5 @@ --- -"@rocket.chat/meteor": patch +'@rocket.chat/meteor': minor --- -Adds support for ban management in federated rooms, enabling authorized users to ban and unban members via UI and slash commands. +Adds support for ban management in rooms, enabling authorized users to ban and unban members via UI and slash commands.