diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index 1a635af5e7207..761549ed10287 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -1,7 +1,8 @@ import { api } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; +import { isUserNativeFederated } from '@rocket.chat/core-typings'; import type { Updater } from '@rocket.chat/models'; -import { Invites, Users } from '@rocket.chat/models'; +import { Invites, Users, Subscriptions } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import type { ClientSession } from 'mongodb'; @@ -20,6 +21,13 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { notifyOnUserChange } from '../lib/notifyListener'; +const isUserInFederatedRooms = async (userId: string): Promise => { + const cursor = Subscriptions.findUserFederatedRoomIds(userId); + const hasAny = await cursor.hasNext(); + await cursor.close(); + return hasAny; +}; + export const setUsernameWithValidation = async (userId: string, username: string, joinDefaultChannelsSilenced?: boolean): Promise => { if (!username) { throw new Meteor.Error('error-invalid-username', 'Invalid username', { method: 'setUsername' }); @@ -31,6 +39,12 @@ export const setUsernameWithValidation = async (userId: string, username: string throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setUsername' }); } + if (isUserNativeFederated(user) || (await isUserInFederatedRooms(userId))) { + throw new Meteor.Error('error-not-allowed', 'Cannot change username for federated users or users in federated rooms', { + method: 'setUsername', + }); + } + if (user.username && !settings.get('Accounts_AllowUsernameChange')) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } @@ -84,6 +98,12 @@ export const _setUsername = async function ( return false; } + if (isUserNativeFederated(fullUser) || (await isUserInFederatedRooms(userId))) { + throw new Meteor.Error('error-not-allowed', 'Cannot change username for federated users or users in federated rooms', { + method: 'setUsername', + }); + } + const user = fullUser || (await Users.findOneById(userId, { session })); // User already has desired username, return if (user.username === username) { diff --git a/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts b/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts index 02bad9d841dee..77f47fa3c7dde 100644 --- a/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts +++ b/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts @@ -11,6 +11,9 @@ describe('setUsername', () => { findOneById: sinon.stub(), setUsername: sinon.stub(), }, + Subscriptions: { + findUserFederatedRoomIds: sinon.stub(), + }, Accounts: { sendEnrollmentEmail: sinon.stub(), }, @@ -49,7 +52,7 @@ describe('setUsername', () => { '../../../../server/database/utils': { onceTransactionCommitedSuccessfully: async (cb: any, _sess: any) => cb() }, 'meteor/meteor': { Meteor: { Error } }, '@rocket.chat/core-services': { api: stubs.api }, - '@rocket.chat/models': { Users: stubs.Users, Invites: stubs.Invites }, + '@rocket.chat/models': { Users: stubs.Users, Invites: stubs.Invites, Subscriptions: stubs.Subscriptions }, 'meteor/accounts-base': { Accounts: stubs.Accounts }, 'underscore': stubs.underscore, '../../../settings/server': { settings: stubs.settings }, @@ -65,9 +68,17 @@ describe('setUsername', () => { '../../../../server/lib/logger/system': { SystemLogger: stubs.SystemLogger }, }); + beforeEach(() => { + stubs.Subscriptions.findUserFederatedRoomIds.returns({ + hasNext: sinon.stub().resolves(false), + close: sinon.stub().resolves(), + }); + }); + afterEach(() => { stubs.Users.findOneById.reset(); stubs.Users.setUsername.reset(); + stubs.Subscriptions.findUserFederatedRoomIds.reset(); stubs.Accounts.sendEnrollmentEmail.reset(); stubs.settings.get.reset(); stubs.api.broadcast.reset(); @@ -143,6 +154,41 @@ describe('setUsername', () => { } }); + it('should throw an error if local user is in federated rooms', async () => { + stubs.Users.findOneById.resolves({ _id: userId, username: null }); + stubs.validateUsername.returns(true); + stubs.checkUsernameAvailability.resolves(true); + stubs.Subscriptions.findUserFederatedRoomIds.returns({ + hasNext: sinon.stub().resolves(true), + close: sinon.stub().resolves(), + }); + + try { + await setUsernameWithValidation(userId, 'newUsername'); + } catch (error: any) { + expect(stubs.Subscriptions.findUserFederatedRoomIds.calledOnce).to.be.true; + expect(error.message).to.equal('error-not-allowed'); + } + }); + + it('should throw an error if user is federated', async () => { + stubs.Users.findOneById.resolves({ + _id: userId, + username: null, + federated: true, + federation: { version: 1, mui: '@user:origin', origin: 'origin' }, + }); + stubs.validateUsername.returns(true); + stubs.checkUsernameAvailability.resolves(true); + + try { + await setUsernameWithValidation(userId, 'newUsername'); + } catch (error: any) { + expect(stubs.Subscriptions.findUserFederatedRoomIds.notCalled).to.be.true; + expect(error.message).to.equal('error-not-allowed'); + } + }); + it('should save the user identity when valid username is set', async () => { stubs.Users.findOneById.resolves({ _id: userId, username: null }); stubs.settings.get.withArgs('Accounts_AllowUsernameChange').returns(true);