diff --git a/app/federation/server/functions/helpers.js b/app/federation/server/functions/helpers.js index adbe2ac2162..ceb01edd9b2 100644 --- a/app/federation/server/functions/helpers.js +++ b/app/federation/server/functions/helpers.js @@ -11,7 +11,8 @@ export function updateEnabled(enabled) { Settings.updateValueById('FEDERATION_Enabled', enabled); } -export const isFederated = (resource) => !!resource.federation; +export const checkRoomType = (room) => room.t === 'p' || room.t === 'd'; +export const checkRoomDomainsLength = (domains) => domains.length <= 10; export const hasExternalDomain = ({ federation }) => { // same test as isFederated(room) @@ -34,7 +35,7 @@ export const getFederatedRoomData = (room) => { if (room.t === 'd') { // Check if there is a federated user on this room - hasFederatedUser = room.usernames.find((u) => u.indexOf('@') !== -1); + hasFederatedUser = room.usernames.some(isFullyQualified); } else { // Find all subscriptions of this room subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch(); @@ -51,7 +52,7 @@ export const getFederatedRoomData = (room) => { users = Users.findUsersWithUsernameByIds(userIds).fetch(); // Check if there is a federated user on this room - hasFederatedUser = users.find((u) => u.username.indexOf('@') !== -1); + hasFederatedUser = users.some((u) => isFullyQualified(u.username)); } return { diff --git a/app/federation/server/hooks/afterAddedToRoom.js b/app/federation/server/hooks/afterAddedToRoom.js index 14f50d2c083..900ce0cc499 100644 --- a/app/federation/server/hooks/afterAddedToRoom.js +++ b/app/federation/server/hooks/afterAddedToRoom.js @@ -1,11 +1,12 @@ import { logger } from '../lib/logger'; -import { getFederatedRoomData, hasExternalDomain, isLocalUser } from '../functions/helpers'; +import { getFederatedRoomData, hasExternalDomain, isLocalUser, checkRoomType, checkRoomDomainsLength } from '../functions/helpers'; import { FederationRoomEvents, Subscriptions } from '../../../models/server'; import { normalizers } from '../normalizers'; import { doAfterCreateRoom } from './afterCreateRoom'; import { getFederationDomain } from '../lib/getFederationDomain'; import { dispatchEvent } from '../handler'; + async function afterAddedToRoom(involvedUsers, room) { const { user: addedUser } = involvedUsers; @@ -24,6 +25,11 @@ async function afterAddedToRoom(involvedUsers, room) { const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, addedUser._id); try { + // If the room is not on the allowed types, ignore + if (!checkRoomType(room)) { + throw new Error('Channels cannot be federated'); + } + // // Check if the room is already federated, if it is not, create the genesis event // @@ -41,6 +47,11 @@ async function afterAddedToRoom(involvedUsers, room) { // Get the users domains const domainsAfterAdd = users.map((u) => u.federation.origin); + // Check if the number of domains is allowed + if (!checkRoomDomainsLength(room.federation.domains)) { + throw new Error('Cannot federate rooms with more than 10 domains'); + } + // // Create the user add event // @@ -57,7 +68,7 @@ async function afterAddedToRoom(involvedUsers, room) { // Remove the user subscription from the room Subscriptions.remove({ _id: subscription._id }); - logger.client.error(() => `afterAddedToRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } => Could not add user: ${ err }`); + logger.client.error('afterAddedToRoom => Could not add user:', err); } return involvedUsers; diff --git a/app/federation/server/hooks/afterCreateDirectRoom.js b/app/federation/server/hooks/afterCreateDirectRoom.js index 78f1e303627..b32601b106c 100644 --- a/app/federation/server/hooks/afterCreateDirectRoom.js +++ b/app/federation/server/hooks/afterCreateDirectRoom.js @@ -4,16 +4,16 @@ import { normalizers } from '../normalizers'; import { deleteRoom } from '../../../lib/server/functions'; import { getFederationDomain } from '../lib/getFederationDomain'; import { dispatchEvents } from '../handler'; -import { hasExternalDomain } from '../functions/helpers'; +import { isFullyQualified } from '../functions/helpers'; async function afterCreateDirectRoom(room, extras) { logger.client.debug(() => `afterCreateDirectRoom => room=${ JSON.stringify(room, null, 2) } extras=${ JSON.stringify(extras, null, 2) }`); // If the room is federated, ignore - if (!hasExternalDomain(room)) { return room; } + if (room.federation) { return room; } // Check if there is a federated user on this direct room - const hasFederatedUser = room.usernames.find((u) => u.indexOf('@') !== -1); + const hasFederatedUser = room.usernames.some(isFullyQualified); // If there are not federated users on this room, ignore it if (!hasFederatedUser) { return room; } @@ -62,7 +62,7 @@ async function afterCreateDirectRoom(room, extras) { } catch (err) { Promise.await(deleteRoom(room._id)); - logger.client.error(() => `afterCreateDirectRoom => room=${ JSON.stringify(room, null, 2) } => Could not create federated room: ${ err }`); + logger.client.error('afterCreateDirectRoom => Could not create federated room:', err); } return room; diff --git a/app/federation/server/hooks/afterCreateRoom.js b/app/federation/server/hooks/afterCreateRoom.js index 83043c0062e..7b55a0cc506 100644 --- a/app/federation/server/hooks/afterCreateRoom.js +++ b/app/federation/server/hooks/afterCreateRoom.js @@ -4,6 +4,7 @@ import { normalizers } from '../normalizers'; import { deleteRoom } from '../../../lib/server/functions'; import { getFederationDomain } from '../lib/getFederationDomain'; import { dispatchEvents } from '../handler'; +import { checkRoomType, checkRoomDomainsLength } from '../functions/helpers'; export async function doAfterCreateRoom(room, users, subscriptions) { const normalizedUsers = []; @@ -37,6 +38,11 @@ export async function doAfterCreateRoom(room, users, subscriptions) { // Normalize room const normalizedRoom = normalizers.normalizeRoom(room, normalizedUsers); + // Check if the number of domains is allowed + if (!checkRoomDomainsLength(normalizedRoom.federation.domains)) { + throw new Error('Cannot federate rooms with more than 10 domains'); + } + // Ensure a genesis event for this room const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom); @@ -66,16 +72,21 @@ async function afterCreateRoom(roomOwner, room) { const hasFederatedUser = users.find((u) => u.username.indexOf('@') !== -1); // If there are not federated users on this room, ignore it - if (!hasFederatedUser) { return; } - - logger.client.debug(() => `afterCreateRoom => roomOwner=${ JSON.stringify(roomOwner, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + if (!hasFederatedUser) { return roomOwner; } try { + // If the room is not on the allowed types, ignore + if (!checkRoomType(room)) { + throw new Error('Channels cannot be federated'); + } + + logger.client.debug(() => `afterCreateRoom => roomOwner=${ JSON.stringify(roomOwner, null, 2) } room=${ JSON.stringify(room, null, 2) }`); + await doAfterCreateRoom(room, users, subscriptions); } catch (err) { deleteRoom(room._id); - logger.client.error(() => `afterCreateRoom => room=${ JSON.stringify(room, null, 2) } => Could not create federated room: ${ err }`); + logger.client.error('afterCreateRoom => Could not create federated room:', err); } return room; diff --git a/app/federation/server/hooks/afterRemoveFromRoom.js b/app/federation/server/hooks/afterRemoveFromRoom.js index 36ec2cebe17..ad876ae3657 100644 --- a/app/federation/server/hooks/afterRemoveFromRoom.js +++ b/app/federation/server/hooks/afterRemoveFromRoom.js @@ -42,7 +42,7 @@ async function afterRemoveFromRoom(involvedUsers, room) { // Dispatch the events dispatchEvent(domainsBeforeRemoval, removeUserEvent); } catch (err) { - logger.client.error(() => `afterRemoveFromRoom => involvedUsers=${ JSON.stringify(involvedUsers, null, 2) } => Could not add user: ${ err }`); + logger.client.error('afterRemoveFromRoom => Could not add user:', err); } return involvedUsers; diff --git a/app/federation/server/hooks/beforeDeleteRoom.js b/app/federation/server/hooks/beforeDeleteRoom.js index 44005d9059d..b1cf1c8b7c1 100644 --- a/app/federation/server/hooks/beforeDeleteRoom.js +++ b/app/federation/server/hooks/beforeDeleteRoom.js @@ -21,7 +21,7 @@ async function beforeDeleteRoom(roomId) { // Dispatch event (async) dispatchEvent(room.federation.domains, event); } catch (err) { - logger.client.error(() => `beforeDeleteRoom => room=${ JSON.stringify(room, null, 2) } => Could not remove room: ${ err }`); + logger.client.error('beforeDeleteRoom => Could not remove room:', err); throw err; } diff --git a/app/federation/server/normalizers/user.js b/app/federation/server/normalizers/user.js index 8dbc50a1682..e513ff57359 100644 --- a/app/federation/server/normalizers/user.js +++ b/app/federation/server/normalizers/user.js @@ -7,9 +7,12 @@ import { getFederationDomain } from '../lib/getFederationDomain'; const denormalizeUser = (originalResource) => { const resource = { ...originalResource }; - resource.emails = [{ - address: resource.federation.originalInfo.email, - }]; + // Only denormalize local emails + if (resource.federation && resource.federation.origin === getFederationDomain()) { + resource.emails = [{ + address: resource.federation.originalInfo.email, + }]; + } const [username, domain] = getNameAndDomain(resource.username); diff --git a/server/startup/migrations/index.js b/server/startup/migrations/index.js index 5ade91ac489..8a815d6232d 100644 --- a/server/startup/migrations/index.js +++ b/server/startup/migrations/index.js @@ -153,4 +153,5 @@ import './v152'; import './v153'; import './v154'; import './v155'; +import './v156'; import './xrun'; diff --git a/server/startup/migrations/v156.js b/server/startup/migrations/v156.js new file mode 100644 index 00000000000..87ddb31951a --- /dev/null +++ b/server/startup/migrations/v156.js @@ -0,0 +1,99 @@ +import { Mongo } from 'meteor/mongo'; + +import { Migrations } from '../../../app/migrations/server'; +import { + Messages, + Rooms, + Subscriptions, + Users, + Uploads, + Settings, +} from '../../../app/models/server'; + +const validSettings = [ + 'FEDERATION_Enabled', + 'FEDERATION_Domain', + 'FEDERATION_Public_Key', + 'FEDERATION_Discovery_Method', + 'FEDERATION_Test_Setup', +]; + +const federationEventsCollection = new Mongo.Collection('rocketchat_federation_events'); + +Migrations.add({ + version: 156, + up() { + try { + // Delete all old, deprecated tables + console.log('Migration: drop collection'); + Promise.await(federationEventsCollection.rawCollection().drop()); + } catch (err) { + // Ignore if the collection does not exist + if (!err.code || err.code !== 26) { + throw err; + } + } + + // Make sure we keep only the valid settings + console.log('Migration: remove old settings'); + Settings.remove({ $and: [{ _id: /FEDERATION/ }, { _id: { $nin: validSettings } }] }); + + // Normalize the federation property on all collections + + // Update rooms + console.log('Migration: update rooms'); + Promise.await(Rooms.model.rawCollection().updateMany({ + federation: { $exists: true }, + }, { + $unset: { federation: 1 }, + })); + + // Update all subscriptions + console.log('Migration: update subscriptions'); + Promise.await(Subscriptions.model.rawCollection().updateMany({ + federation: { $exists: true }, + }, { + $unset: { federation: 1 }, + })); + + // Update all users + console.log('Migration: update remote users'); + Users.find({ isRemote: true }).forEach((u) => { + const [name, domain] = u.username.split('@'); + + const username = `${ name }@old-${ domain }`; + + Users.update({ _id: u._id }, { + $unset: { federation: 1, emails: 1, isRemote: 1 }, + $set: { username, active: false, status: 'offline' }, + }); + }); + + console.log('Migration: update local users'); + Promise.await(Users.model.rawCollection().updateMany({ + federation: { $exists: true }, + }, { + $unset: { federation: 1 }, + })); + + // Update all messages + // We will not update the mentions and channels here + console.log('Migration: update messages'); + Promise.await(Messages.model.rawCollection().updateMany({ + federation: { $exists: true }, + }, { + $unset: { federation: 1 }, + })); + + // Update all uploads + console.log('Migration: update uploads'); + Promise.await(Uploads.model.rawCollection().updateMany({ + federation: { $exists: true }, + }, { + $unset: { federation: 1 }, + })); + }, + down() { + // Down migration does not apply in this case + }, +});