diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 19f22ab6e7ac4..b936d30145069 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -143,6 +143,13 @@ export const createRoom = async ( return member.username?.includes(':') && member.username?.includes('@'); }); + // Prevent adding federated users to rooms that are not marked as federated explicitly + if (hasFederatedMembers && optionalExtraData.federated !== true) { + throw new Meteor.Error('error-federated-users-in-non-federated-rooms', 'Cannot add federated users to non-federated rooms', { + method: 'createRoom', + }); + } + const extraData = { ...optionalExtraData, ...((hasFederatedMembers || optionalExtraData.federated) && { diff --git a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts index 30830385b1dcd..c5fd718d6911f 100644 --- a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts +++ b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts @@ -1,7 +1,8 @@ import { api } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; -import { isRoomNativeFederated, isUserNativeFederated } from '@rocket.chat/core-typings'; +import { isRoomNativeFederated } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; +import { validateFederatedUsername } from '@rocket.chat/federation-matrix'; import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -88,15 +89,18 @@ export const addUsersToRoomMethod = async (userId: string, data: { rid: string; await Promise.all( data.users.map(async (username) => { - const newUser = await Users.findOneByUsernameIgnoringCase(sanitizeUsername(username)); - if (!newUser) { - throw new Meteor.Error('error-user-not-found', 'User not found', { + const sanitizedUsername = sanitizeUsername(username); + + // If it's a federated username format and the room is not federated, throw error immediately + if (validateFederatedUsername(sanitizedUsername) && !isRoomNativeFederated(room)) { + throw new Meteor.Error('error-federated-users-in-non-federated-rooms', 'Cannot add federated users to non-federated rooms', { method: 'addUsersToRoom', }); } - if (isUserNativeFederated(newUser) && !isRoomNativeFederated(room)) { - throw new Meteor.Error('error-federated-users-in-non-federated-rooms', 'Cannot add federated users to non-federated rooms', { + const newUser = await Users.findOneByUsernameIgnoringCase(sanitizedUsername); + if (!newUser) { + throw new Meteor.Error('error-user-not-found', 'User not found', { method: 'addUsersToRoom', }); } diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts index 24ad0484fddcc..cc4e116226665 100644 --- a/apps/meteor/app/slashcommands-invite/server/server.ts +++ b/apps/meteor/app/slashcommands-invite/server/server.ts @@ -1,9 +1,11 @@ -import { api } from '@rocket.chat/core-services'; +import { api, FederationMatrix } from '@rocket.chat/core-services'; import type { IUser, SlashCommandCallbackParams } from '@rocket.chat/core-typings'; -import { Subscriptions, Users } from '@rocket.chat/models'; +import { validateFederatedUsername } from '@rocket.chat/federation-matrix'; +import { Subscriptions, Users, Rooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { i18n } from '../../../server/lib/i18n'; +import { FederationActions } from '../../../server/services/room/hooks/BeforeFederationActions'; import { addUsersToRoomMethod, sanitizeUsername } from '../../lib/server/methods/addUsersToRoom'; import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/server/slashCommand'; @@ -15,13 +17,42 @@ import { slashCommands } from '../../utils/server/slashCommand'; slashCommands.add({ command: 'invite', callback: async ({ params, message, userId }: SlashCommandCallbackParams<'invite'>): Promise => { - const usernames = params + let usernames = params .split(/[\s,]/) .map((username) => sanitizeUsername(username)) .filter((a) => a !== ''); if (usernames.length === 0) { return; } + + // Get room information for federation check + 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; + } + + // Ensure federated users exist locally before looking them up + const federatedUsernames = usernames.filter((u) => validateFederatedUsername(u)) as string[]; + if (federatedUsernames.length > 0) { + if (FederationActions.shouldPerformFederationAction(room)) { + await FederationMatrix.ensureFederatedUsersExistLocally(federatedUsernames); + } else { + void api.broadcast('notify.ephemeralMessage', userId, message.rid, { + msg: i18n.t('You_cannot_add_external_users_to_non_federated_room', { lng: settings.get('Language') || 'en' }), + }); + // These federated users shouldn't be invited and we already broadcasted the error message + usernames = usernames.filter((username) => { + return !federatedUsernames.includes(username); + }); + if (usernames.length === 0) { + return; + } + } + } + const users = await Users.findByUsernames(usernames).toArray(); if (users.length === 0) { void api.broadcast('notify.ephemeralMessage', userId, message.rid, { diff --git a/ee/packages/federation-matrix/src/index.ts b/ee/packages/federation-matrix/src/index.ts index e7777aabbad28..b56729b4524ba 100644 --- a/ee/packages/federation-matrix/src/index.ts +++ b/ee/packages/federation-matrix/src/index.ts @@ -1,6 +1,6 @@ import 'reflect-metadata'; -export { FederationMatrix } from './FederationMatrix'; +export { FederationMatrix, validateFederatedUsername } from './FederationMatrix'; export { generateEd25519RandomSecretKey } from '@rocket.chat/federation-sdk';