diff --git a/.changeset/chatty-kids-compare.md b/.changeset/chatty-kids-compare.md new file mode 100644 index 0000000000000..da889e416ddf5 --- /dev/null +++ b/.changeset/chatty-kids-compare.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where account security page was flashing sometimes for users with mandatory two factor configured. diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 03c9602e59f00..345aa01e688ea 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -40,7 +40,7 @@ Accounts.config({ * * we are removing the status here because meteor send 'offline' */ -Object.assign(Accounts._defaultPublishFields.projection, (({ status, ...rest }) => rest)(getBaseUserFields())); +Object.assign(Accounts._defaultPublishFields.projection, (({ status, ...rest }) => rest)(getBaseUserFields(true))); Meteor.startup(() => { settings.watchMultiple(['Accounts_LoginExpiration', 'Site_Name', 'From_Email'], () => { diff --git a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts index 5e2a3bf2b4d73..a366f95a2fe54 100644 --- a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts @@ -2,7 +2,7 @@ type UserFields = { [k: string]: number; }; -export const getBaseUserFields = (): UserFields => ({ +export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({ 'name': 1, 'username': 1, 'nickname': 1, @@ -31,4 +31,5 @@ export const getBaseUserFields = (): UserFields => ({ 'avatarETag': 1, 'extension': 1, 'openBusinessHours': 1, + ...(allowServiceKeys && { 'services.totp.enabled': 1, 'services.email2fa.enabled': 1 }), }); diff --git a/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts b/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts index 293eb8607342d..c9d7cb0fc8945 100644 --- a/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts @@ -5,10 +5,8 @@ type UserFields = { }; export const getDefaultUserFields = (): UserFields => ({ - ...getBaseUserFields(), + ...getBaseUserFields(true), 'services.github': 1, 'services.gitlab': 1, 'services.password.bcrypt': 1, - 'services.totp.enabled': 1, - 'services.email2fa.enabled': 1, }); diff --git a/apps/meteor/server/startup/initialData.js b/apps/meteor/server/startup/initialData.js index 30415e8b20224..5de4aad42aab5 100644 --- a/apps/meteor/server/startup/initialData.js +++ b/apps/meteor/server/startup/initialData.js @@ -217,7 +217,7 @@ Meteor.startup(async () => { emails: [ { address: 'rocketchat.internal.admin.test@rocket.chat', - verified: false, + verified: true, }, ], status: 'offline', diff --git a/apps/meteor/tests/e2e/enforce-2FA.spec.ts b/apps/meteor/tests/e2e/enforce-2FA.spec.ts new file mode 100644 index 0000000000000..cb5671864e3d5 --- /dev/null +++ b/apps/meteor/tests/e2e/enforce-2FA.spec.ts @@ -0,0 +1,63 @@ +import { IS_EE } from './config/constants'; +import { Users } from './fixtures/userStates'; +import { HomeChannel, AccountProfile } from './page-objects'; +import { createCustomRole, deleteCustomRole } from './utils/custom-role'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe('enforce two factor authentication', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poHomeChannel: HomeChannel; + let poAccountProfile: AccountProfile; + let customRoleId = ''; + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + poAccountProfile = new AccountProfile(page); + }); + + test.beforeAll(async ({ api }) => { + const roleResponse = await createCustomRole(api, { name: 'enforce-2FA', mandatory2fa: true }); + expect(roleResponse.status()).toBe(200); + const { role } = await roleResponse.json(); + customRoleId = role._id; + + const userUpdateRes = await api.post('/users.update', { + data: { roles: ['user', customRoleId, 'admin'] }, + userId: 'rocketchat.internal.admin.test', + }); + expect(userUpdateRes.status()).toBe(200); + + const disableEmail2FA = await api.post('/users.2fa.disableEmail', {}); + expect(disableEmail2FA.status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + const userUpdateRes = await api.post('/users.update', { + data: { roles: ['user', 'admin'] }, + userId: 'rocketchat.internal.admin.test', + }); + expect(userUpdateRes.status()).toBe(200); + + const deleteRole = await deleteCustomRole(api, 'enforce-2FA'); + expect(deleteRole.status()).toBe(200); + + const enableEmail2FA = await api.post('/users.2fa.enableEmail', {}); + expect(enableEmail2FA.status()).toBe(200); + }); + + test('should redirect to 2FA setup page and setup email 2FA', async ({ page }) => { + await page.goto('/home'); + await expect(poHomeChannel.sidenav.sidebarHomeAction).not.toBeVisible(); + await expect(poAccountProfile.securityHeader).toBeVisible(); + + await poAccountProfile.security2FASection.click(); + await expect(poAccountProfile.enableEmail2FAButton).toBeVisible(); + await poAccountProfile.enableEmail2FAButton.click(); + + await expect(poHomeChannel.toastSuccess).toBeVisible(); + await expect(poHomeChannel.sidenav.sidebarHomeAction).toBeVisible(); + await expect(poAccountProfile.securityHeader).not.toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 60c039cbe9530..bf2eefeb11a37 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -57,6 +57,10 @@ export class HomeSidenav { return this.page.getByRole('toolbar', { name: 'Sidebar actions' }); } + get sidebarHomeAction(): Locator { + return this.sidebarToolbar.getByRole('button', { name: 'Home' }); + } + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { await this.sidebarToolbar.getByRole('button', { name: 'Display', exact: true }).click(); await this.sidebarToolbar.getByRole('menuitemcheckbox', { name: mode }).click(); diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 1c880929650db..1a6a1046ae776 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -127,7 +127,38 @@ export class DDPStreamer extends ServiceClass { async function sendUserData(client: Client, userId: string) { // TODO figure out what fields to send. maybe to to export function getBaseUserFields to a package const loggedUser = await Users.findOneById(userId, { - projection: { name: 1, username: 1, settings: 1, roles: 1, active: 1, statusLivechat: 1, statusDefault: 1, status: 1 }, + projection: { + 'name': 1, + 'username': 1, + 'nickname': 1, + 'emails': 1, + 'status': 1, + 'statusDefault': 1, + 'statusText': 1, + 'statusConnection': 1, + 'bio': 1, + 'avatarOrigin': 1, + 'utcOffset': 1, + 'language': 1, + 'settings': 1, + 'enableAutoAway': 1, + 'idleTimeLimit': 1, + 'roles': 1, + 'active': 1, + 'defaultRoom': 1, + 'customFields': 1, + 'requirePasswordChange': 1, + 'requirePasswordChangeReason': 1, + 'statusLivechat': 1, + 'banners': 1, + 'oauth.authorizedClients': 1, + '_updatedAt': 1, + 'avatarETag': 1, + 'extension': 1, + 'openBusinessHours': 1, + 'services.totp.enabled': 1, + 'services.email2fa.enabled': 1, + }, }); if (!loggedUser) { return;