vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 62dfeb80f211f9ec283b46d8158f86d715f8e6b5
parent 26cd5d96434d497cc0a7ca11cacc5ea9845230c0
Author: sirux88 <sirux88@gmail.com>
Date:   Sat,  4 Feb 2023 13:29:57 +0100

improved security, disabling policy usage on
email-disabled clients and some refactoring

Diffstat:
Msrc/api/core/organizations.rs | 137++++++++++++++++++++++++++++++++++++++-----------------------------------------
1 file changed, 65 insertions(+), 72 deletions(-)

diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -711,10 +711,6 @@ async fn send_invite( err!("Only Owners can invite Managers, Admins or Owners") } - if !CONFIG.mail_enabled() && OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await { - err!("With mailing disabled and auto-enrollment-feature of reset-password-policy enabled it's not possible to invite users"); - } - for email in data.Emails.iter() { let email = email.to_lowercase(); let mut user_org_status = UserOrgStatus::Invited as i32; @@ -729,10 +725,6 @@ async fn send_invite( } if !CONFIG.mail_enabled() { - if OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await { - err!("With disabled mailing and enabled auto-enrollment-feature of reset-password-policy it's not possible to invite existing users"); - } - let invitation = Invitation::new(&email); invitation.save(&mut conn).await?; } @@ -748,10 +740,6 @@ async fn send_invite( // automatically accept existing users if mail is disabled if !CONFIG.mail_enabled() && !user.password_hash.is_empty() { user_org_status = UserOrgStatus::Accepted as i32; - - if OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await { - err!("With disabled mailing and enabled auto-enrollment-feature of reset-password-policy it's not possible to invite existing users"); - } } user } @@ -1597,17 +1585,8 @@ async fn put_policy( } } - // This check is required since invited users automatically get accepted if mailing is not enabled (this seems like a vaultwarden specific feature) - // As a result of this the necessary "/accepted"-endpoint doesn't get hit. - // But this endpoint is required for autoenrollment while invitation. - // Nevertheless reset password is fully fuctiontional in settings without mailing by manual enrollment - if pol_type_enum == OrgPolicyType::ResetPassword && data.enabled && !CONFIG.mail_enabled() { - if let Some(policy_data) = &data.data { - if policy_data["autoEnrollEnabled"].as_bool().unwrap_or(false) { - err!("Autoenroll can't be used since it requires enabled emailing") - } - } + err!("Due to potential security flaws and/or misuse reset password policy is disabled on mail disabled instances") } let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await { @@ -2542,55 +2521,37 @@ async fn put_reset_password( None => err!("Required organization not found"), }; - let policy = match OrgPolicy::find_by_org_and_type(&org.uuid, OrgPolicyType::ResetPassword, &mut conn).await { - Some(p) => p, - None => err!("Policy not found"), - }; - - if !policy.enabled { - err!("Reset password policy not enabled"); - } - let org_user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org.uuid, &mut conn).await { Some(user) => user, None => err!("User to reset isn't member of required organization"), }; + let mut user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { + Some(user) => user, + None => err!("User not found"), + }; + + check_reset_password_applicable_and_permissions(&org_id, &org_user_id, &headers, &mut conn).await?; + if org_user.reset_password_key.is_none() { - err!("Password reset not or not corretly enrolled"); + err!("Password reset not or not correctly enrolled"); } if org_user.status != (UserOrgStatus::Confirmed as i32) { err!("Organization user must be confirmed for password reset functionality"); } - //Resetting user must be higher/equal to user to reset - let mut reset_allowed = false; - if headers.org_user_type == UserOrgType::Owner { - reset_allowed = true; - } - if headers.org_user_type == UserOrgType::Admin { - reset_allowed = org_user.atype != (UserOrgType::Owner as i32); - } - - if !reset_allowed { - err!("No permission to reset this user's password"); + // Sending email before resetting password to ensure working email configuration and the resulting + // user notification. Also this might add some protection against security flaws and misuse + if let Err(e) = mail::send_admin_reset_password(&user.email.to_lowercase(), &user.name, &org.name).await { + error!("Error sending user reset password email: {:#?}", e); } - let mut user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { - Some(user) => user, - None => err!("User not found"), - }; - let reset_request = data.into_inner().data; user.set_password(reset_request.NewMasterPasswordHash.as_str(), Some(reset_request.Key), true, None); user.save(&mut conn).await?; - nt.send_user_update(UpdateType::LogOut, &user).await; - - if CONFIG.mail_enabled() { - mail::send_admin_reset_password(&user.email.to_lowercase(), &user.name, &org.name).await?; - } + nt.send_logout(&user, None).await; log_event( EventType::OrganizationUserAdminResetPassword as i32, @@ -2610,7 +2571,7 @@ async fn put_reset_password( async fn get_reset_password_details( org_id: String, org_user_id: String, - _headers: AdminHeaders, + headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { let org = match Organization::find_by_uuid(&org_id, &mut conn).await { @@ -2618,15 +2579,6 @@ async fn get_reset_password_details( None => err!("Required organization not found"), }; - let policy = match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::ResetPassword, &mut conn).await { - Some(p) => p, - None => err!("Policy not found"), - }; - - if !policy.enabled { - err!("Reset password policy not enabled"); - } - let org_user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &mut conn).await { Some(user) => user, None => err!("User to reset isn't member of required organization"), @@ -2637,6 +2589,8 @@ async fn get_reset_password_details( None => err!("User not found"), }; + check_reset_password_applicable_and_permissions(&org_id, &org_user_id, &headers, &mut conn).await?; + Ok(Json(json!({ "Object": "organizationUserResetPasswordDetails", "Kdf":user.client_kdf_type, @@ -2647,16 +2601,41 @@ async fn get_reset_password_details( }))) } -#[put("/organizations/<org_id>/users/<org_user_id>/reset-password-enrollment", data = "<data>")] -async fn put_reset_password_enrollment( - org_id: String, - org_user_id: String, - headers: Headers, - data: JsonUpcase<OrganizationUserResetPasswordEnrollmentRequest>, - mut conn: DbConn, - ip: ClientIp, +async fn check_reset_password_applicable_and_permissions( + org_id: &str, + org_user_id: &str, + headers: &AdminHeaders, + conn: &mut DbConn, ) -> EmptyResult { - let policy = match OrgPolicy::find_by_org_and_type(&org_id, OrgPolicyType::ResetPassword, &mut conn).await { + check_reset_password_applicable(org_id, conn).await?; + + let target_user = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { + Some(user) => user, + None => err!("Reset target user not found"), + }; + + // Resetting user must be higher/equal to user to reset + let mut reset_allowed = false; + if headers.org_user_type == UserOrgType::Owner { + reset_allowed = true; + } + if headers.org_user_type == UserOrgType::Admin { + reset_allowed = target_user.atype != (UserOrgType::Owner as i32); + } + + if !reset_allowed { + err!("No permission to reset this user's password"); + } + + Ok(()) +} + +async fn check_reset_password_applicable(org_id: &str, conn: &mut DbConn) -> EmptyResult { + if !CONFIG.mail_enabled() { + err!("Password reset is not supported on an email-disabled instance."); + } + + let policy = match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::ResetPassword, conn).await { Some(p) => p, None => err!("Policy not found"), }; @@ -2665,11 +2644,25 @@ async fn put_reset_password_enrollment( err!("Reset password policy not enabled"); } + Ok(()) +} + +#[put("/organizations/<org_id>/users/<org_user_id>/reset-password-enrollment", data = "<data>")] +async fn put_reset_password_enrollment( + org_id: String, + org_user_id: String, + headers: Headers, + data: JsonUpcase<OrganizationUserResetPasswordEnrollmentRequest>, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { let mut org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { Some(u) => u, None => err!("User to enroll isn't member of required organization"), }; + check_reset_password_applicable(&org_id, &mut conn).await?; + let reset_request = data.into_inner().data; if reset_request.ResetPasswordKey.is_none()