Skip to content

Conversation

@ricardogarim
Copy link
Contributor

@ricardogarim ricardogarim commented Oct 10, 2025

As per FDR-229, this PR introduces a block on username changes for users who are:

  • Federated users, or
  • Local users that are members of federated rooms.

According to the Matrix specification, username changes are not allowed — only display name updates are permitted.

This change prevents breaking the federation flow.

A follow-up PR will enable updating display names both from Rocket.Chat to remote nodes and vice versa.

Summary by CodeRabbit

  • Bug Fixes
    • Block username changes for users who are federated or belong to federated rooms. Attempts now show an error, preserving account consistency across federations. Existing validations (format, availability, first-run) remain unchanged.
  • Tests
    • Added unit tests covering federated scenarios, including users in federated rooms and fully federated users, to ensure the new restrictions behave as expected.

@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Oct 10, 2025

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link

changeset-bot bot commented Oct 10, 2025

⚠️ No Changeset found

Latest commit: 1778672

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 10, 2025

Walkthrough

Adds federation-aware guards to username change flow: imports federation helpers, checks if a user is federated or in any federated rooms, and blocks username changes accordingly. Unit tests were extended to mock Subscriptions.findUserFederatedRoomIds and cover both federated-user and federated-room scenarios.

Changes

Cohort / File(s) Summary
Federation guards in username change
apps/meteor/app/lib/server/functions/setUsername.ts
Adds isUserInFederatedRooms using Subscriptions.findUserFederatedRoomIds; imports isUserNativeFederated and Subscriptions. Adds guard checks in setUsernameWithValidation and _setUsername to block renames for federated users or users in federated rooms while keeping existing validations.
Unit tests for federated scenarios
apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts
Adds Subscriptions.findUserFederatedRoomIds stubs via proxyquire, configures hasNext/close behavior in beforeEach and reset in afterEach. Adds tests that assert errors when the user is federated and when a local user is in federated rooms.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller as Caller
  participant Service as setUsernameWithValidation
  participant Typing as isUserNativeFederated
  participant SubModel as Subscriptions.findUserFederatedRoomIds

  Caller->>Service: request username change (userId, newUsername)
  Service->>Typing: isUserNativeFederated(user)
  alt User is federated
    Service-->>Caller: throw error (federated user cannot rename)
  else Not federated
    Service->>SubModel: findUserFederatedRoomIds(userId)
    SubModel-->>Service: cursor (hasNext/close)
    alt User in federated rooms
      Service-->>Caller: throw error (user in federated rooms)
    else No federated rooms
      Service->>Service: run existing validations (format, availability, enroll/first-run)
      Service-->>Caller: proceed / complete username change
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • rodrigok
  • ggazzo

Poem

I twitch my ears at names that roam,
In federated fields I guard the home—
“No renames here,” I thump in time,
So messages hop true every line.
Carrot-signed, your watchful rabbit. 🐇

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly describes blocking username changes for users in federated rooms but does not mention that federated users themselves are also blocked, so it only partially covers the main change.
Linked Issues Check ✅ Passed The pull request adds the required guard logic in both username change functions and includes unit tests for federated and federated-room scenarios, directly satisfying the requirements of FDR-229.
Out of Scope Changes Check ✅ Passed All code and test updates focus exclusively on blocking username changes for federated users or users in federated rooms, and no unrelated or out-of-scope modifications are present.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/federation-block-username-change

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 19c19a6 and 1778672.

📒 Files selected for processing (1)
  • apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: 🔨 Test Storybook / Test Storybook
  • GitHub Check: 🔎 Code Check / Code Lint
  • GitHub Check: 🔨 Test Unit / Unit Tests
  • GitHub Check: 🔎 Code Check / TypeScript
  • GitHub Check: 📦 Meteor Build - coverage
  • GitHub Check: CodeQL-Build
  • GitHub Check: CodeQL-Build

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Oct 11, 2025

Codecov Report

❌ Patch coverage is 81.81818% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.36%. Comparing base (4bd409d) to head (1778672).
⚠️ Report is 11 commits behind head on release-7.11.0.

Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##           release-7.11.0   #37210      +/-   ##
==================================================
- Coverage           66.37%   66.36%   -0.01%     
==================================================
  Files                3386     3386              
  Lines              115619   115629      +10     
  Branches            21351    21359       +8     
==================================================
+ Hits                76739    76741       +2     
- Misses              36275    36280       +5     
- Partials             2605     2608       +3     
Flag Coverage Δ
e2e 57.25% <ø> (-0.03%) ⬇️
unit 71.23% <81.81%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ricardogarim ricardogarim marked this pull request as ready for review October 11, 2025 13:32
@ricardogarim ricardogarim requested a review from a team as a code owner October 11, 2025 13:32
@ricardogarim ricardogarim added this to the 7.11.0 milestone Oct 11, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd409d and 19c19a6.

📒 Files selected for processing (2)
  • apps/meteor/app/lib/server/functions/setUsername.ts (4 hunks)
  • apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
apps/meteor/app/lib/server/functions/setUsername.ts (1)
packages/core-typings/src/IUser.ts (1)
  • isUserNativeFederated (276-277)
apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts (1)
apps/meteor/app/lib/server/functions/setUsername.ts (1)
  • setUsernameWithValidation (31-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: 📦 Build Packages
🔇 Additional comments (8)
apps/meteor/app/lib/server/functions/setUsername.ts (4)

3-3: LGTM!

The import of isUserNativeFederated is appropriate for checking if a user is a federated user based on the user object structure.


5-5: LGTM!

The addition of Subscriptions to the imports enables checking if a user belongs to federated rooms.


24-29: LGTM!

The helper function properly checks for federated room membership and correctly handles cursor cleanup by calling close() after checking hasNext().


42-46: LGTM!

The federation guard is correctly placed after fetching and validating the user exists (lines 36-40), ensuring we have a valid user object before checking federation status.

apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts (4)

14-16: LGTM!

The Subscriptions stub is properly added to support federation-aware tests.


55-55: LGTM!

The Subscriptions model is correctly wired through proxyquire alongside other models.


71-76: LGTM!

The beforeEach hook correctly configures the default cursor mock using .returns() (not .resolves()), which matches the implementation's expectation that findUserFederatedRoomIds returns a cursor object synchronously.


81-81: LGTM!

The stub is properly reset in the afterEach hook.

Comment on lines +101 to +105
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',
});
}
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

Move federation check after user object is available.

The federation check at lines 101-105 happens before ensuring a valid user object is available. When fullUser is not provided (which line 107 suggests is possible with the fallback fullUser || await Users.findOneById(...)), calling isUserNativeFederated(fullUser) on an undefined/null value may not work correctly.

While the type guard might handle this gracefully by returning false, relying on this behavior is fragile. Additionally, a federated user might theoretically not be in any federated rooms, so the isUserInFederatedRooms fallback wouldn't catch this edge case.

Apply this diff to move the federation check after the user object is guaranteed to be available:

 	if (!validateUsername(username)) {
 		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 }));
+	
+	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',
+		});
+	}
+
 	// User already has desired username, return
 	if (user.username === username) {
 		return user;
📝 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
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',
});
}
if (!validateUsername(username)) {
return false;
}
const user = fullUser || (await Users.findOneById(userId, { session }));
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',
});
}
// User already has desired username, return
if (user.username === username) {
return user;
}
🤖 Prompt for AI Agents
In apps/meteor/app/lib/server/functions/setUsername.ts around lines 101 to 105,
the federation check runs before guaranteeing a valid user object; move the
federation check to after you resolve fullUser (i.e., keep the existing fallback
fullUser = fullUser || await Users.findOneById(userId) first), then perform the
checks using the now-guaranteed fullUser: call isUserNativeFederated(fullUser)
and, if needed, await isUserInFederatedRooms(userId) and throw the Meteor.Error
when either condition is true.

Comment on lines 157 to 172
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.resolves({
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');
}
});
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 | 🔴 Critical

Fix cursor mock and test assertion pattern.

This test has two issues:

  1. Line 161 uses .resolves() instead of .returns(): The implementation expects findUserFederatedRoomIds to return a cursor object synchronously, but .resolves() makes it return a Promise. This is inconsistent with the correct pattern used in beforeEach (line 72) and would cause the test to fail.

  2. Missing error assertion: The test uses try-catch without asserting that an error was actually thrown. If the code doesn't throw an error, the test passes silently, creating a false positive.

Apply this diff to fix both issues:

 		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.resolves({
+			stubs.Subscriptions.findUserFederatedRoomIds.returns({
 				hasNext: sinon.stub().resolves(true),
 				close: sinon.stub().resolves(),
 			});
 
+			let errorThrown = false;
 			try {
 				await setUsernameWithValidation(userId, 'newUsername');
 			} catch (error: any) {
+				errorThrown = true;
 				expect(stubs.Subscriptions.findUserFederatedRoomIds.calledOnce).to.be.true;
 				expect(error.message).to.equal('error-not-allowed');
 			}
+			expect(errorThrown).to.be.true;
 		});
📝 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
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.resolves({
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 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(),
});
let errorThrown = false;
try {
await setUsernameWithValidation(userId, 'newUsername');
} catch (error: any) {
errorThrown = true;
expect(stubs.Subscriptions.findUserFederatedRoomIds.calledOnce).to.be.true;
expect(error.message).to.equal('error-not-allowed');
}
expect(errorThrown).to.be.true;
});
🤖 Prompt for AI Agents
In apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts around
lines 157-172, change the mock for Subscriptions.findUserFederatedRoomIds to
return the cursor synchronously (use .returns(...) with the object containing
hasNext and close stubs) instead of .resolves(...), and replace the current
try/catch pattern with a proper assertion that the call rejects (e.g., await
expect(setUsernameWithValidation(userId,
'newUsername')).to.be.rejectedWith('error-not-allowed') or keep the try/catch
but add an explicit expect.fail() immediately after the await to ensure the test
fails if no error is thrown); also keep the assertion that
findUserFederatedRoomIds was called once inside the rejection assertion block or
after awaiting the rejected promise.

Comment on lines +174 to +190
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');
}
});
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

Add error assertion to prevent false positives.

The test uses try-catch without asserting that an error was thrown. If the code doesn't throw an error, the test passes silently, creating a false positive.

Apply this diff to add proper error assertion:

 		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);
 
+			let errorThrown = false;
 			try {
 				await setUsernameWithValidation(userId, 'newUsername');
 			} catch (error: any) {
+				errorThrown = true;
 				expect(stubs.Subscriptions.findUserFederatedRoomIds.notCalled).to.be.true;
 				expect(error.message).to.equal('error-not-allowed');
 			}
+			expect(errorThrown).to.be.true;
 		});
📝 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
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 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);
let errorThrown = false;
try {
await setUsernameWithValidation(userId, 'newUsername');
} catch (error: any) {
errorThrown = true;
expect(stubs.Subscriptions.findUserFederatedRoomIds.notCalled).to.be.true;
expect(error.message).to.equal('error-not-allowed');
}
expect(errorThrown).to.be.true;
});
🤖 Prompt for AI Agents
In apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts around
lines 174 to 190, the test uses a try/catch but doesn’t assert that an error was
actually thrown, allowing false positives; update the test to explicitly assert
a rejection (e.g., replace the try/catch with an assertion like await
expect(setUsernameWithValidation(userId,
'newUsername')).to.be.rejectedWith('error-not-allowed') and then assert
stubs.Subscriptions.findUserFederatedRoomIds was not called, or if keeping
try/catch, add an explicit fail() after the await to ensure the test fails when
no error is thrown and keep the existing checks inside the catch.

@ricardogarim ricardogarim force-pushed the fix/federation-block-username-change branch from 19c19a6 to 1778672 Compare October 11, 2025 15:12
@rodrigok rodrigok merged commit 9ef6efe into release-7.11.0 Oct 14, 2025
49 checks passed
@rodrigok rodrigok deleted the fix/federation-block-username-change branch October 14, 2025 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants