-
Notifications
You must be signed in to change notification settings - Fork 13k
fix(federation): block username change of users in federated rooms #37210
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
Changes from all commits
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+174
to
+190
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. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
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
fullUseris not provided (which line 107 suggests is possible with the fallbackfullUser || await Users.findOneById(...)), callingisUserNativeFederated(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
isUserInFederatedRoomsfallback 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
🤖 Prompt for AI Agents