-
Notifications
You must be signed in to change notification settings - Fork 13k
feat(federation): add improved errors messaging #37581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
52b0bb2
036ac42
66999d3
92acb93
47be39e
f2673fa
8318ca6
e82b553
2e34a35
33dadae
68c9535
7f7928a
312ec65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,8 @@ import { Logger } from '@rocket.chat/logger'; | |
| import { Users, Subscriptions, Messages, Rooms, Settings } from '@rocket.chat/models'; | ||
| import emojione from 'emojione'; | ||
|
|
||
| import { isFederationDomainAllowed } from './api/middlewares/isFederationDomainAllowed'; | ||
| import { FederationValidationError } from './errors/FederationValidationError'; | ||
| import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './helpers/message.parsers'; | ||
| import { MatrixMediaService } from './services/MatrixMediaService'; | ||
|
|
||
|
|
@@ -206,6 +208,25 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS | |
| this.processEDUPresence = (await Settings.getValueById<boolean>('Federation_Service_EDU_Process_Presence')) || false; | ||
| } | ||
|
|
||
| async validateFederatedUsers(usernames: string[]): Promise<void> { | ||
| const hasInvalidFederatedUsername = usernames.some((username) => !validateFederatedUsername(username)); | ||
| if (hasInvalidFederatedUsername) { | ||
| throw new FederationValidationError( | ||
| 'POLICY_DENIED', | ||
| `Invalid federated username format: ${usernames.filter((username) => !validateFederatedUsername(username)).join(', ')}. Federated usernames must follow the format @username:domain.com`, | ||
| ); | ||
| } | ||
|
|
||
| const federatedUsers = usernames.filter(validateFederatedUsername); | ||
| if (federatedUsers.length === 0) { | ||
| return; | ||
| } | ||
|
|
||
| for await (const username of federatedUsers) { | ||
| await federationSDK.validateOutboundUser(userIdSchema.parse(username)); | ||
| } | ||
| } | ||
|
Comment on lines
+211
to
+228
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Logic inconsistency: method rejects local usernames but includes filtering for them. The method checks if any username is invalid federated format (line 212) and throws before reaching the filter (line 220). This creates confusion:
The past review comment flagged this issue as addressed in commit 82fa056, but the logic still rejects mixed inputs. For example, Recommended fix: Pre-filter to federated usernames at the start, matching the behavior of 🔎 Proposed refactor to handle mixed inputs async validateFederatedUsers(usernames: string[]): Promise<void> {
- const hasInvalidFederatedUsername = usernames.some((username) => !validateFederatedUsername(username));
- if (hasInvalidFederatedUsername) {
- throw new FederationValidationError(
- 'POLICY_DENIED',
- `Invalid federated username format: ${usernames.filter((username) => !validateFederatedUsername(username)).join(', ')}. Federated usernames must follow the format @username:domain.com`,
- );
- }
-
const federatedUsers = usernames.filter(validateFederatedUsername);
if (federatedUsers.length === 0) {
return;
}
for await (const username of federatedUsers) {
await federationSDK.validateOutboundUser(userIdSchema.parse(username));
}
}Alternatively, if the method should strictly validate that all inputs are federated format, update the method name (e.g., 🤖 Prompt for AI Agents |
||
|
|
||
| async createRoom(room: IRoom, owner: IUser): Promise<{ room_id: string; event_id: string }> { | ||
| if (room.t !== 'c' && room.t !== 'p') { | ||
| throw new Error('Room is not a public or private room'); | ||
|
|
@@ -215,11 +236,8 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS | |
| const matrixUserId = userIdSchema.parse(`@${owner.username}:${this.serverName}`); | ||
| const roomName = room.name || room.fname || 'Untitled Room'; | ||
|
|
||
| // canonical alias computed from name | ||
| const matrixRoomResult = await federationSDK.createRoom(matrixUserId, roomName, room.t === 'c' ? 'public' : 'invite'); | ||
|
|
||
| this.logger.debug('Matrix room created:', matrixRoomResult); | ||
|
|
||
| await Rooms.setAsFederated(room._id, { mrid: matrixRoomResult.room_id, origin: this.serverName }); | ||
|
|
||
| // Members are NOT invited here - invites are sent via beforeAddUserToRoom callback. | ||
|
|
@@ -863,24 +881,30 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS | |
| if (!homeserverUrl) { | ||
| return [matrixId, 'UNABLE_TO_VERIFY']; | ||
| } | ||
|
|
||
| try { | ||
| const result = await federationSDK.queryProfileRemote< | ||
| | { | ||
| avatar_url: string; | ||
| displayname: string; | ||
| } | ||
| | { | ||
| errcode: string; | ||
| error: string; | ||
| } | ||
| >({ homeserverUrl, userId: matrixId }); | ||
|
|
||
| if ('errcode' in result && result.errcode === 'M_NOT_FOUND') { | ||
| return [matrixId, 'UNVERIFIED']; | ||
| // check RC domain allowlist | ||
| if (!(await isFederationDomainAllowed([homeserverUrl]))) { | ||
| return [matrixId, 'POLICY_DENIED']; | ||
| } | ||
|
|
||
| // validate using homeserver (network + user existence) | ||
| await federationSDK.validateOutboundUser(userIdSchema.parse(matrixId)); | ||
|
|
||
| return [matrixId, 'VERIFIED']; | ||
| } catch (e) { | ||
| if (e && typeof e === 'object' && 'code' in e) { | ||
| const error = e as { code: string }; | ||
| if (error.code === 'CONNECTION_FAILED') { | ||
| return [matrixId, 'UNABLE_TO_VERIFY']; | ||
| } | ||
| if (error.code === 'USER_NOT_FOUND') { | ||
| return [matrixId, 'UNVERIFIED']; | ||
| } | ||
| if (error.code === 'POLICY_DENIED') { | ||
| return [matrixId, 'POLICY_DENIED']; | ||
| } | ||
| } | ||
| return [matrixId, 'UNABLE_TO_VERIFY']; | ||
| } | ||
| }), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| // Local copy to avoid broken import chain in homeserver's federation-sdk | ||
| export class FederationValidationError extends Error { | ||
| public error: string; | ||
|
|
||
| constructor( | ||
| public code: 'POLICY_DENIED' | 'CONNECTION_FAILED' | 'USER_NOT_FOUND', | ||
| public userMessage: string, | ||
| ) { | ||
| super(userMessage); | ||
| this.name = 'FederationValidationError'; | ||
| this.error = `federation-${code.toLowerCase().replace(/_/g, '-')}`; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent federated user detection logic and unnecessary type annotations.
Two issues:
Inconsistent detection criteria: Line 164 checks for both
:AND@to identify federated users, but line 209 only checks for:. This inconsistency could cause members to be incorrectly classified. Matrix user IDs follow the format@localpart:domain, so both characters should be required.Unnecessary complexity: Since this code is inside the
onlyUsernames(members)type guard,membersis guaranteed to bestring[]. The type annotations(string | IUser)and the runtime type checks (typeof member === 'string') are unnecessary.🔎 Proposed fix
🤖 Prompt for AI Agents