Skip to content

Commit 1206b61

Browse files
authored
fix(federation): error on first user interaction (#41689)
1 parent 92d0ffe commit 1206b61

10 files changed

Lines changed: 547 additions & 73 deletions

File tree

apps/meteor/server/services/room/service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,8 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
326326
...(inviter && { inviter: { _id: inviter._id, username: inviter.username!, ...(inviter.name && { name: inviter.name }) } }),
327327
...autoTranslateConfig,
328328
...getDefaultSubscriptionPref(userToBeAdded),
329-
...(room.t === 'd' && inviter && { fname: inviter.name, name: inviter.username }),
329+
// `name` is optional for users, so fall back to the username like `getNameForDMs` does
330+
...(room.t === 'd' && inviter && { fname: inviter.name || inviter.username, name: inviter.username }),
330331
});
331332

332333
if (insertedId) {

ee/packages/federation-matrix/src/api/_matrix/client/account.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ClientRouter } from './_shared';
55
import { isMatrixErrorProps, license, tags } from './_shared';
66
import { createOrUpdateFederatedUser } from '../../../helpers/createOrUpdateFederatedUser';
77
import { decodeXmppUserId, isFullXmppUserId, parseXmppUserId } from '../../../helpers/parseXmppUserId';
8+
import { validateFederatedUsername } from '../../../helpers/validateFederatedUsername';
89
import { logger } from '../../logger';
910
import { isAppServiceAuthenticatedMiddleware } from '../../middlewares/isAppServiceAuthenticated';
1011

@@ -93,7 +94,18 @@ export const addAccountRoutes = (router: ClientRouter) => {
9394
// The spec defines `username` as the desired localpart; normalize either form to the
9495
// fully-qualified MXID, which is what gets stored and what `user_id` must carry.
9596
const withSigil = body.username.startsWith('@') ? body.username : `@${body.username}`;
96-
const userId = withSigil.includes(':') ? withSigil : `${withSigil}:${serverName}`;
97+
const userId: string = withSigil.includes(':') ? withSigil : `${withSigil}:${serverName}`;
98+
99+
if (!validateFederatedUsername(userId)) {
100+
logger.warn({ msg: 'Malformed user id during AS registration', username: body.username });
101+
return {
102+
statusCode: 400,
103+
body: {
104+
errcode: 'M_INVALID_USERNAME',
105+
error: 'Could not derive a username from the provided user id',
106+
},
107+
};
108+
}
97109

98110
if (isReservedByAnotherAppService(userId)) {
99111
return {
@@ -146,6 +158,17 @@ export const addAccountRoutes = (router: ClientRouter) => {
146158

147159
const username = `@${decodedUsername.resource}:${serverName}`;
148160

161+
if (!validateFederatedUsername(username)) {
162+
logger.warn({ msg: 'Malformed user id derived from XMPP user id during AS registration', username: body.username });
163+
return {
164+
statusCode: 400,
165+
body: {
166+
errcode: 'M_INVALID_USERNAME',
167+
error: 'Could not derive a username from the provided XMPP user id',
168+
},
169+
};
170+
}
171+
149172
if (isReservedByAnotherAppService(username)) {
150173
return {
151174
statusCode: 400,

ee/packages/federation-matrix/src/events/member.ts

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { Room, Upload } from '@rocket.chat/core-services';
2-
import { isBannedSubscription, isRegisterUser } from '@rocket.chat/core-typings';
2+
import { isBannedSubscription } from '@rocket.chat/core-typings';
33
import type { IRoomNativeFederated, IRoom, IUser, RoomType } from '@rocket.chat/core-typings';
44
import { federationSDK, type HomeserverEventSignatures, type PduForType } from '@rocket.chat/federation-sdk';
55
import { Logger } from '@rocket.chat/logger';
66
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';
77
import debounce from 'lodash.debounce';
88
import mem from 'mem';
99

10-
import { createOrUpdateFederatedUser } from '../helpers/createOrUpdateFederatedUser';
1110
import { extractDomainFromMatrixUserId } from '../helpers/extractDomainFromMatrixUserId';
1211
import { getFederatedRoomName } from '../helpers/getFederatedRoomName';
12+
import { getOrCreateFederatedUser } from '../helpers/getOrCreateFederatedUser';
1313
import { getUsernameServername } from '../helpers/getUsernameServername';
1414
import { MatrixMediaService } from '../services/MatrixMediaService';
1515

@@ -74,40 +74,6 @@ async function downloadAndSetAvatar(user: IUser, avatarUrl: string | null): Prom
7474
}
7575
}
7676

77-
async function getOrCreateFederatedUser(userId: string): Promise<IUser> {
78-
try {
79-
const serverName = federationSDK.getConfig('serverName');
80-
const [username, userServerName, isLocal] = getUsernameServername(userId, serverName);
81-
82-
const user = await Users.findOneByUsername(username);
83-
if (user) {
84-
return user;
85-
}
86-
87-
const as = federationSDK.getAppServiceForUser(userId);
88-
if (as) {
89-
const user = await Users.findOneByUsername(userId);
90-
if (!user) {
91-
throw new Error('AppService user not found for creating user');
92-
}
93-
return user;
94-
}
95-
96-
if (isLocal) {
97-
throw new Error(`Local user ${username} not found for Matrix ID: ${userId}`);
98-
}
99-
100-
return createOrUpdateFederatedUser({
101-
username: userId,
102-
name: userId,
103-
origin: userServerName,
104-
});
105-
} catch (err) {
106-
logger.error({ msg: 'Error getting or creating federated user', err, userId });
107-
throw new Error(`Error getting or creating federated user ${userId}`);
108-
}
109-
}
110-
11177
async function getOrCreateFederatedRoom({
11278
matrixRoomId,
11379
roomName,
@@ -200,18 +166,7 @@ async function handleInvite({
200166
unsigned,
201167
}: HomeserverEventSignatures['homeserver.matrix.membership']['event']): Promise<void> {
202168
const inviterUser = await getOrCreateFederatedUser(senderId);
203-
if (!inviterUser) {
204-
throw new Error(`Failed to get or create inviter user: ${senderId}`);
205-
}
206-
207-
if (!isRegisterUser(inviterUser)) {
208-
throw new Error('Inviter user is not registered');
209-
}
210-
211169
const inviteeUser = await getOrCreateFederatedUser(userId);
212-
if (!inviteeUser) {
213-
throw new Error(`Failed to get or create invitee user: ${userId}`);
214-
}
215170

216171
const strippedState = unsigned.invite_room_state;
217172

@@ -298,9 +253,6 @@ async function handleJoin({
298253
content,
299254
}: HomeserverEventSignatures['homeserver.matrix.membership']['event']): Promise<void> {
300255
const joiningUser = await getOrCreateFederatedUser(userId);
301-
if (!joiningUser?.username) {
302-
throw new Error(`Failed to get or create joining user: ${userId}`);
303-
}
304256

305257
const room = await Rooms.findOneFederatedByMrid(roomId);
306258
if (!room) {
@@ -343,7 +295,7 @@ async function handleJoin({
343295
await Room.updateDirectMessageRoomName(
344296
room,
345297
[subscription._id],
346-
[{ _id: joiningUser._id, name: content.displayname || joiningUser.name || joiningUser.username, username: joiningUser.username }],
298+
[{ _id: joiningUser._id, name: content.displayname || joiningUser.name, username: joiningUser.username }],
347299
);
348300
}
349301

ee/packages/federation-matrix/src/helpers/createOrUpdateFederatedUser.spec.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,20 @@ jest.mock('@rocket.chat/models', () => ({
1111

1212
const mockFindOneAndUpdate = Users.findOneAndUpdate as jest.MockedFunction<typeof Users.findOneAndUpdate>;
1313

14+
const fakeUser = {
15+
_id: 'user123',
16+
username: '@alice:example.com',
17+
name: '@alice:example.com',
18+
federated: true,
19+
federation: { version: 1, mui: '@alice:example.com', origin: 'example.com' },
20+
};
21+
1422
describe('createOrUpdateFederatedUser', () => {
1523
beforeEach(() => {
1624
jest.clearAllMocks();
1725
});
1826

1927
it('should assign the "federated" role to the new user', async () => {
20-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
2128
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
2229

2330
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
@@ -28,7 +35,6 @@ describe('createOrUpdateFederatedUser', () => {
2835
});
2936

3037
it('should not assign the "user" role to federated users', async () => {
31-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
3238
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
3339

3440
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
@@ -38,7 +44,6 @@ describe('createOrUpdateFederatedUser', () => {
3844
});
3945

4046
it('should set federated=true on the created/updated user', async () => {
41-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
4247
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
4348

4449
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
@@ -48,7 +53,6 @@ describe('createOrUpdateFederatedUser', () => {
4853
});
4954

5055
it('should use the provided name when supplied', async () => {
51-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
5256
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
5357

5458
await createOrUpdateFederatedUser({ username: '@alice:example.com', name: 'Alice', origin: 'example.com' });
@@ -58,7 +62,6 @@ describe('createOrUpdateFederatedUser', () => {
5862
});
5963

6064
it('should default name to username when name is not provided', async () => {
61-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
6265
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
6366

6467
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
@@ -68,7 +71,6 @@ describe('createOrUpdateFederatedUser', () => {
6871
});
6972

7073
it('should set initial status to OFFLINE', async () => {
71-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
7274
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
7375

7476
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
@@ -78,7 +80,6 @@ describe('createOrUpdateFederatedUser', () => {
7880
});
7981

8082
it('should store the origin server in the federation object', async () => {
81-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
8283
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
8384

8485
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
@@ -88,7 +89,6 @@ describe('createOrUpdateFederatedUser', () => {
8889
});
8990

9091
it('should use upsert so the user is created if not found', async () => {
91-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
9292
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
9393

9494
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
@@ -105,8 +105,36 @@ describe('createOrUpdateFederatedUser', () => {
105105
);
106106
});
107107

108+
it('should throw when the returned document is not a native federated user', async () => {
109+
mockFindOneAndUpdate.mockResolvedValueOnce({ _id: 'user123', username: '@alice:example.com', name: 'Alice' } as any);
110+
111+
await expect(createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' })).rejects.toThrow(
112+
'Failed to create or update federated user: @alice:example.com',
113+
);
114+
});
115+
116+
// callers subscribe the returned document to a room, so a projection here silently hands them a
117+
// user missing every field but the two projected ones
118+
it('should not project fields away, so the full user document is returned', async () => {
119+
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
120+
121+
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
122+
123+
const [, , options] = mockFindOneAndUpdate.mock.calls[0];
124+
expect(options).not.toHaveProperty('projection');
125+
});
126+
127+
// the pre-image of an upsert that inserted is null, which would fail every first contact
128+
it('should ask for the document as it looks after the upsert', async () => {
129+
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
130+
131+
await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });
132+
133+
const [, , options] = mockFindOneAndUpdate.mock.calls[0];
134+
expect((options as any).returnDocument).toBe('after');
135+
});
136+
108137
it('should return the user returned by findOneAndUpdate', async () => {
109-
const fakeUser = { _id: 'user123', username: '@alice:example.com' };
110138
mockFindOneAndUpdate.mockResolvedValueOnce(fakeUser as any);
111139

112140
const result = await createOrUpdateFederatedUser({ username: '@alice:example.com', origin: 'example.com' });

ee/packages/federation-matrix/src/helpers/createOrUpdateFederatedUser.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { type IUser, UserStatus } from '@rocket.chat/core-typings';
1+
import {
2+
type IRegisterUser,
3+
type IUserNativeFederated,
4+
isRegisterUser,
5+
isUserNativeFederated,
6+
UserStatus,
7+
} from '@rocket.chat/core-typings';
8+
import type { UserID } from '@rocket.chat/federation-sdk';
29
import { Users } from '@rocket.chat/models';
310

411
/**
@@ -9,11 +16,11 @@ import { Users } from '@rocket.chat/models';
916
*/
1017

1118
export async function createOrUpdateFederatedUser(options: {
12-
username: string;
19+
username: UserID;
1320
name?: string;
1421
origin: string;
1522
asId?: string;
16-
}): Promise<IUser> {
23+
}): Promise<IUserNativeFederated & IRegisterUser> {
1724
const { username, name = username, origin, asId } = options;
1825

1926
// TODO: Have a specific method to handle this upsert
@@ -45,12 +52,12 @@ export async function createOrUpdateFederatedUser(options: {
4552
},
4653
{
4754
upsert: true,
48-
projection: { _id: 1, username: 1 },
4955
returnDocument: 'after',
5056
},
5157
);
5258

53-
if (!user) {
59+
// the upsert above writes every field these guards check, so they should never reject a returned document
60+
if (!user || !isUserNativeFederated(user) || !isRegisterUser(user)) {
5461
throw new Error(`Failed to create or update federated user: ${username}`);
5562
}
5663

0 commit comments

Comments
 (0)