Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions apps/meteor/app/lib/server/functions/createRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { beforeAddUserToRoom } from '../../../../server/lib/callbacks/beforeAddU
import { beforeCreateRoomCallback, prepareCreateRoomCallback } from '../../../../server/lib/callbacks/beforeCreateRoomCallback';
import { getSubscriptionAutotranslateDefaultConfig } from '../../../../server/lib/getSubscriptionAutotranslateDefaultConfig';
import { syncRoomRolePriorityForUserAndRoom } from '../../../../server/lib/roles/syncRoomRolePriority';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { getDefaultSubscriptionPref } from '../../../utils/lib/getDefaultSubscriptionPref';
import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName';
import { notifyOnRoomChanged, notifyOnSubscriptionChangedById } from '../lib/notifyListener';
Expand Down Expand Up @@ -184,12 +183,7 @@ export const createRoom = async <T extends RoomType>(

const shouldBeHandledByFederation = extraData.federated === true;

if (
shouldBeHandledByFederation &&
owner &&
!isUserNativeFederated(owner) &&
!(await hasPermissionAsync(owner._id, 'access-federation'))
) {
if (shouldBeHandledByFederation && owner && !isUserNativeFederated(owner) && !(await FederationMatrix.canUserAccessFederation(owner))) {
throw new Meteor.Error('error-not-authorized-federation', 'Not authorized to access federation', {
method: 'createRoom',
});
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/ee/server/hooks/federation/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FederationMatrix, Authorization, MeteorError, Room } from '@rocket.chat/core-services';
import { FederationMatrix, MeteorError, Room } from '@rocket.chat/core-services';
import { isEditedMessage, isRoomNativeFederated, isUserNativeFederated } from '@rocket.chat/core-typings';
import type { IRoomNativeFederated, IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import { validateFederatedUsername } from '@rocket.chat/federation-matrix';
Expand Down Expand Up @@ -112,7 +112,7 @@ beforeAddUserToRoom.add(
return;
}

if (!isUserNativeFederated(user) && !(await Authorization.hasPermission(user._id, 'access-federation'))) {
if (!isUserNativeFederated(user) && !(await FederationMatrix.canUserAccessFederation(user))) {
throw new MeteorError('error-not-authorized-federation', 'Not authorized to access federation');
}

Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/server/services/room/service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ServiceClassInternal, Authorization, Message, MeteorError } from '@rocket.chat/core-services';
import { ServiceClassInternal, Authorization, Message, MeteorError, FederationMatrix } from '@rocket.chat/core-services';
import type { ICreateRoomParams, IRoomService } from '@rocket.chat/core-services';
import {
type AtLeast,
Expand Down Expand Up @@ -149,7 +149,7 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
/**
* Method called by users to join a room.
*/
async join({ room, user, joinCode }: { room: IRoom; user: Pick<IUser, '_id' | 'federated' | 'federation'>; joinCode?: string }) {
async join({ room, user, joinCode }: { room: IRoom; user: IUser; joinCode?: string }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the RoomService.join method to verify the current signature
find apps/meteor -name "*.ts" -type f | grep -i room | grep -i service

Repository: RocketChat/Rocket.Chat

Length of output: 622


🏁 Script executed:

# Let's check the exact file and method signature
cat -n apps/meteor/server/services/room/service.ts | head -200 | tail -100

Repository: RocketChat/Rocket.Chat

Length of output: 4100


🏁 Script executed:

# Search for the join method definition and surrounding code
rg -nP --type=ts "async join\s*\(" apps/meteor/server/services/room/service.ts -A 10 -B 2

Repository: RocketChat/Rocket.Chat

Length of output: 743


🏁 Script executed:

# Find all calls to .join( method in the codebase
rg -nP --type=ts "\.join\s*\(\s*\{" apps/meteor ee/packages | grep -v "node_modules" | head -50

Repository: RocketChat/Rocket.Chat

Length of output: 595


🏁 Script executed:

# Find canUserAccessFederation method definition
rg -nP --type=ts "canUserAccessFederation" apps/meteor ee/packages -B 2 -A 10 | head -100

Repository: RocketChat/Rocket.Chat

Length of output: 5921


🏁 Script executed:

# Check all the call sites in detail - start with joinRoom.ts
cat -n apps/meteor/app/lib/server/methods/joinRoom.ts | head -50

Repository: RocketChat/Rocket.Chat

Length of output: 1253


🏁 Script executed:

# Check the channels.ts call site
cat -n apps/meteor/app/api/server/v1/channels.ts | sed -n '220,240p'

Repository: RocketChat/Rocket.Chat

Length of output: 646


🏁 Script executed:

# Check the slashcommands-join call site
cat -n apps/meteor/app/slashcommands-join/server/server.ts | sed -n '40,60p'

Repository: RocketChat/Rocket.Chat

Length of output: 729


🏁 Script executed:

# Check getRoomByNameOrIdWithOptionToJoin.ts
cat -n apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts | sed -n '80,95p'

Repository: RocketChat/Rocket.Chat

Length of output: 542


🏁 Script executed:

# Check the function signature
rg -nP --type=ts "export.*getRoomByNameOrIdWithOptionToJoin" apps/meteor -B 5 -A 15 | head -40

Repository: RocketChat/Rocket.Chat

Length of output: 2276


🏁 Script executed:

# Get more context on where canUserAccessFederation accesses emails
cat -n ee/packages/federation-matrix/src/FederationMatrix.ts | sed -n '811,830p'

Repository: RocketChat/Rocket.Chat

Length of output: 577


🏁 Script executed:

# Verify what properties the slashcommands-join actually fetches for user
cat -n apps/meteor/app/slashcommands-join/server/server.ts | sed -n '40,55p'

Repository: RocketChat/Rocket.Chat

Length of output: 642


🏁 Script executed:

# Check if there are any other calls to canUserAccessFederation and what user objects they pass
rg -nP --type=ts "canUserAccessFederation" apps/meteor ee/packages -B 5 | grep -E "(const user|user:)" | head -20

Repository: RocketChat/Rocket.Chat

Length of output: 187


🏁 Script executed:

# Let's look for git history or type definition changes - check current IUser type
rg -nP --type=ts "interface IUser" apps/meteor ee/packages | head -5

Repository: RocketChat/Rocket.Chat

Length of output: 311


Fix caller types to pass complete user objects to RoomService.join.

The user parameter expects full IUser type for canUserAccessFederation to access user.emails for domain validation. Two call sites have issues:

  1. apps/meteor/app/slashcommands-join/server/server.ts:46 — fetches user with only { federated: 1, federation: 1 } projection, excluding emails. Include emails in the projection before passing to Room.join.

  2. apps/meteor/app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.ts:17 — function signature accepts Pick<IUser, '_id' | 'username' | 'federated' | 'federation'> but passes to Room.join expecting full IUser. Update the parameter type or ensure callers pass complete user objects.

🤖 Prompt for AI Agents
In `@apps/meteor/server/services/room/service.ts` at line 152, RoomService.join
expects a full IUser because canUserAccessFederation reads user.emails; fix the
two call sites so they pass a complete user object: in the slashcommands-join
caller (server.ts) include emails in the Mongo projection when fetching the user
(add "emails" to the projection before calling RoomService.join), and in
getRoomByNameOrIdWithOptionToJoin adjust the parameter type (or the caller) so
it returns/passes a full IUser rather than Pick<IUser, '_id' | 'username' |
'federated' | 'federation'>; reference RoomService.join and
canUserAccessFederation when updating types/fetch logic to ensure user.emails is
present.

if (!(await roomCoordinator.getRoomDirectives(room.t)?.allowMemberAction(room, RoomMemberActions.JOIN, user._id))) {
throw new MeteorError('error-not-allowed', 'Not allowed', { method: 'joinRoom' });
}
Expand All @@ -165,7 +165,7 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
if (
FederationActions.shouldPerformFederationAction(room) &&
!isUserNativeFederated(user) &&
!(await Authorization.hasPermission(user._id, 'access-federation'))
!(await FederationMatrix.canUserAccessFederation(user))
) {
throw new MeteorError('error-not-authorized-federation', 'Not authorized to access federation', { method: 'joinRoom' });
}
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/server/settings/federation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,13 @@ export const createFederationServiceSettings = async (): Promise<void> => {
modules: ['federation'],
invalidValue: false,
});

await this.add('Federation_Service_Validate_User_Domain', false, {
type: 'boolean',
public: false,
enterprise: true,
modules: ['federation'],
invalidValue: false,
});
});
};
24 changes: 23 additions & 1 deletion ee/packages/federation-matrix/src/FederationMatrix.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type IFederationMatrixService, Room, ServiceClass } from '@rocket.chat/core-services';
import { Authorization, type IFederationMatrixService, Room, ServiceClass } from '@rocket.chat/core-services';
import {
isDeletedMessage,
isMessageFromMatrixFederation,
Expand Down Expand Up @@ -36,6 +36,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS

private processEDUPresence: boolean;

private validateUserDomain: boolean;

private readonly logger = new Logger(this.name);

override async created(): Promise<void> {
Expand All @@ -52,6 +54,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
this.processEDUTyping = value;
} else if (_id === 'Federation_Service_EDU_Process_Presence' && typeof value === 'boolean') {
this.processEDUPresence = value;
} else if (_id === 'Federation_Service_Validate_User_Domain' && typeof value === 'boolean') {
this.validateUserDomain = value;
}
});

Expand Down Expand Up @@ -98,6 +102,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
this.serverName = (await Settings.getValueById<string>('Federation_Service_Domain')) || '';
this.processEDUTyping = (await Settings.getValueById<boolean>('Federation_Service_EDU_Process_Typing')) || false;
this.processEDUPresence = (await Settings.getValueById<boolean>('Federation_Service_EDU_Process_Presence')) || false;
this.validateUserDomain = (await Settings.getValueById<boolean>('Federation_Service_Validate_User_Domain')) || false;
}

async createRoom(room: IRoom, owner: IUser): Promise<{ room_id: string; event_id: string }> {
Expand Down Expand Up @@ -802,4 +807,21 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
}
}
}

async canUserAccessFederation(user: IUser): Promise<boolean> {
if (!(await Authorization.hasPermission(user._id, 'access-federation'))) {
return false;
}

if (!this.validateUserDomain) {
return true;
}

return (
user.emails?.some((email) => {
const domain = email.address.split('@')[1];
return domain === this.serverName && email.verified;
}) ?? false
Comment on lines +820 to +824
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Email domain comparison should be case-insensitive.

Per RFC 5321, domain names in email addresses are case-insensitive. If a user's email was stored as [email protected] but serverName is example.com, they would incorrectly fail validation.

🔧 Suggested fix
 return (
     user.emails?.some((email) => {
-        const domain = email.address.split('@')[1];
-        return domain === this.serverName && email.verified;
+        const domain = email.address.split('@')[1]?.toLowerCase();
+        return domain === this.serverName.toLowerCase() && email.verified;
     }) ?? false
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
user.emails?.some((email) => {
const domain = email.address.split('@')[1];
return domain === this.serverName && email.verified;
}) ?? false
return (
user.emails?.some((email) => {
const domain = email.address.split('@')[1]?.toLowerCase();
return domain === this.serverName.toLowerCase() && email.verified;
}) ?? false
🤖 Prompt for AI Agents
In `@ee/packages/federation-matrix/src/FederationMatrix.ts` around lines 820 -
824, The domain comparison in the user email check is case-sensitive; update the
logic in the block using user.emails (the anonymous callback that does
email.address.split('@')[1]) to normalize both sides to the same case before
comparing: extract the domain safely from email.address, call .toLowerCase() (or
.toLocaleLowerCase()) on the extracted domain and on this.serverName, then
compare those lowercased values and keep the existing email.verified
requirement; ensure the code handles malformed addresses (no '@') gracefully
when extracting the domain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we care about this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KevLehman Yes, you should care about this. According to RFC 5321, domain names (the part after @) are not case-sensitive, following DNS rules.

The practical issue:
If a user's email is stored as [email protected] but your serverName setting is configured as example.com, the current code would incorrectly deny them federation access even though the domains are actually the same.

Why it matters:

  • Email addresses can be stored with various casing depending on how users enter them or how external systems provide them
  • This could lead to support issues where users with valid emails matching your domain are unexpectedly blocked from federation
  • The fix is simple and follows standard practices for domain comparison

Recommendation:
Implement the suggested change to normalize both domains to lowercase before comparison:

const domain = email.address.split('@')[1]?.toLowerCase();
return domain === this.serverName.toLowerCase() && email.verified;

This ensures reliable domain matching regardless of how the email address was originally stored.


🧠 Learnings used
Learnt from: ricardogarim
Repo: RocketChat/Rocket.Chat PR: 37377
File: apps/meteor/ee/server/hooks/federation/index.ts:86-88
Timestamp: 2025-11-04T16:49:19.107Z
Learning: In Rocket.Chat's federation system (apps/meteor/ee/server/hooks/federation/), permission checks follow two distinct patterns: (1) User-initiated federation actions (creating rooms, adding users to federated rooms, joining from invites) should throw MeteorError to inform users they lack 'access-federation' permission. (2) Remote server-initiated federation events should silently skip/ignore when users lack permission. The beforeAddUserToRoom hook only executes for local user-initiated actions, so throwing an error there is correct. Remote federation events are handled separately by the federation Matrix package with silent skipping logic.

Learnt from: rodrigok
Repo: RocketChat/Rocket.Chat PR: 36991
File: apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts:219-221
Timestamp: 2025-09-19T15:15:04.642Z
Learning: The Federation_Matrix_homeserver_domain setting in apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/Settings.ts is part of the old federation system and is being deprecated/removed, so configuration issues with this setting should not be flagged for improvement.

Learnt from: sampaiodiego
Repo: RocketChat/Rocket.Chat PR: 37357
File: ee/packages/federation-matrix/src/setup.ts:103-120
Timestamp: 2025-11-05T21:04:35.787Z
Learning: In Rocket.Chat's federation-matrix setup (ee/packages/federation-matrix/src/setup.ts and apps/meteor/ee/server/startup/federation.ts), configureFederationMatrixSettings does not need to be called before setupFederationMatrix. The SDK's init() establishes infrastructure (database, event handlers, APIs) first, and the configuration can be applied later via settings watchers before actual federation events are processed. The config only matters when events actually occur, at which point all infrastructure is already configured.

Learnt from: ricardogarim
Repo: RocketChat/Rocket.Chat PR: 37205
File: ee/packages/federation-matrix/src/FederationMatrix.ts:296-301
Timestamp: 2025-10-28T16:53:42.761Z
Learning: In the Rocket.Chat federation-matrix integration (ee/packages/federation-matrix/), the createRoom method from rocket.chat/federation-sdk will support a 4-argument signature (userId, roomName, visibility, displayName) in newer versions. Code using this 4-argument call is forward-compatible with planned library updates and should not be flagged as an error.

Learnt from: ricardogarim
Repo: RocketChat/Rocket.Chat PR: 37205
File: ee/packages/federation-matrix/src/FederationMatrix.ts:296-301
Timestamp: 2025-10-28T16:53:42.761Z
Learning: In the Rocket.Chat federation-matrix integration (ee/packages/federation-matrix/), the createRoom method from rocket.chat/federation-sdk will support a 4-argument signature (userId, roomName, visibility, displayName) in newer versions. Code using this 4-argument call is forward-compatible with planned library updates and should not be flagged as an error.

);
}
}
4 changes: 2 additions & 2 deletions ee/packages/federation-matrix/src/api/_matrix/invite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Authorization } from '@rocket.chat/core-services';
import { FederationMatrix } from '@rocket.chat/core-services';
import { NotAllowedError, federationSDK } from '@rocket.chat/federation-sdk';
import { Router } from '@rocket.chat/http-router';
import { Logger } from '@rocket.chat/logger';
Expand Down Expand Up @@ -175,7 +175,7 @@ export const getMatrixInviteRoutes = () => {
}

// check federation permission before processing the invite
if (!(await Authorization.hasPermission(ourUser._id, 'access-federation'))) {
if (!(await FederationMatrix.canUserAccessFederation(ourUser))) {
logger.info({ msg: 'User denied federation access, rejecting invite to room', userId: userToCheck, roomId });

return {
Expand Down
Loading
Loading