diff --git a/playwright/tests/organization.smtp.spec.ts b/playwright/tests/organization.smtp.spec.ts index 35dfcdb1..2be5fec1 100644 --- a/playwright/tests/organization.smtp.spec.ts +++ b/playwright/tests/organization.smtp.spec.ts @@ -40,6 +40,16 @@ test('Invite users', async ({ page }) => { await createAccount(test, page, users.user1, mail1Buffer); await orgs.create(test, page, 'Test'); + + await test.step(`Set account recovery`, async () => { + await orgs.policies(test, page, 'Test'); + await page.getByRole('button', { name: 'Account recovery' }).click(); + await page.getByRole('checkbox', { name: 'Turn on' }).check(); + await page.getByRole('checkbox', { name: 'Require new members' }).check(); + await page.getByRole('button', { name: 'Save' }).click(); + await utils.checkNotification(page, 'Edited policy Account recovery'); + }); + await orgs.members(test, page, 'Test'); await orgs.invite(test, page, 'Test', users.user2.email); await orgs.invite(test, page, 'Test', users.user3.email, { @@ -117,3 +127,27 @@ test('Organization is visible', async ({ page }) => { await page.getByRole('button', { name: 'vault: Test', exact: true }).click(); await expect(page.getByLabel('Filter: Default collection')).toBeVisible(); }); + +test('Recover user password', async ({ page }) => { + await logUser(test, page, users.user1, mail1Buffer); + + let newPassword = "TotoNewPassword"; + + await orgs.members(test, page, 'Test'); + await test.step(`Rrcover ${users.user2.email}`, async () => { + await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible(); + await page.getByRole('row').filter({hasText: users.user2.email}).getByLabel('Options').click(); + await page.getByRole('menuitem', { name: 'Recover account' }).click(); + await page.getByRole('textbox', { name: 'New master password (required)', exact: true }).fill(newPassword); + await page.getByRole('textbox', { name: 'Confirm new master password (' }).fill(newPassword); + await page.getByRole('button', { name: 'Save' }).click(); + await utils.checkNotification(page, 'Password reset success'); + }); + + let user2 = { + email: users.user2.email, + name: users.user2.name, + password: newPassword, + }; + await logUser(test, page, user2, mail2Buffer); +}); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index dd68cd5b..a0b3a06f 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -96,6 +96,7 @@ pub fn routes() -> Vec { put_reset_password_enrollment, get_reset_password_details, put_reset_password, + put_recover_account, get_org_export, post_api_key, rotate_api_key, @@ -2875,9 +2876,11 @@ struct OrganizationUserResetPasswordEnrollmentRequest { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct OrganizationUserResetPasswordRequest { +struct OrganizationUserRecoverAccountRequest { new_master_password_hash: String, key: String, + reset_master_password: bool, + reset_two_factor: bool, } // Upstream reports this is the renamed endpoint instead of `/keys` @@ -2905,12 +2908,43 @@ async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders get_organization_public_key(org_id, headers, conn).await } +// Will allow to reset 2FA too +// https://github.com/bitwarden/clients/blob/web-v2026.4.2/libs/admin-console/src/common/organization-user/models/requests/organization-user-reset-password.request.ts +#[put("/organizations//users//recover-account", data = "")] +async fn put_recover_account( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + data: Json, + conn: DbConn, + nt: Notify<'_>, +) -> EmptyResult { + let req = data.into_inner(); + if req.reset_master_password && !req.reset_two_factor { + recover_account(org_id, member_id, headers, req, conn, nt).await + } else { + err!("Unsupported operation") + } +} + +// Deprecated since `v2026.4.2` #[put("/organizations//users//reset-password", data = "")] async fn put_reset_password( org_id: OrganizationId, member_id: MembershipId, headers: AdminHeaders, - data: Json, + data: Json, + conn: DbConn, + nt: Notify<'_>, +) -> EmptyResult { + recover_account(org_id, member_id, headers, data.into_inner(), conn, nt).await +} + +async fn recover_account( + org_id: OrganizationId, + member_id: MembershipId, + headers: AdminHeaders, + reset_request: OrganizationUserRecoverAccountRequest, conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { @@ -2944,8 +2978,6 @@ async fn put_reset_password( err!(format!("Error sending user reset password email: {e:#?}")); } - let reset_request = data.into_inner(); - let mut user = user; user.set_password(reset_request.new_master_password_hash.as_str(), Some(reset_request.key), true, None, &conn) .await?;