From c6c45c4c49be81a86240b7a4462b7d502be4257d Mon Sep 17 00:00:00 2001 From: sirux88 Date: Wed, 25 Jan 2023 08:06:21 +0100 Subject: [PATCH] working implementation --- src/api/core/organizations.rs | 232 ++++++++++++++++++ src/config.rs | 1 + src/mail.rs | 13 + .../templates/email/admin_reset_password.hbs | 6 + .../email/admin_reset_password.html.hbs | 11 + 5 files changed, 263 insertions(+) create mode 100644 src/static/templates/email/admin_reset_password.hbs create mode 100644 src/static/templates/email/admin_reset_password.html.hbs diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index c0af8f6e..4b0605f8 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -62,6 +62,7 @@ pub fn routes() -> Vec { get_plans_tax_rates, import, post_org_keys, + get_organization_keys, bulk_public_keys, deactivate_organization_user, bulk_deactivate_organization_user, @@ -86,6 +87,9 @@ pub fn routes() -> Vec { put_user_groups, delete_group_user, post_delete_group_user, + put_reset_password_enrollment, + get_reset_password_details, + put_reset_password, get_org_export ] } @@ -707,6 +711,10 @@ 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; @@ -721,6 +729,10 @@ 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?; } @@ -736,6 +748,10 @@ 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 } @@ -882,6 +898,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co #[allow(non_snake_case)] struct AcceptData { Token: String, + ResetPasswordKey: Option, } #[post("/organizations//users/<_org_user_id>/accept", data = "")] @@ -909,6 +926,11 @@ async fn accept_invite( err!("User already accepted the invitation") } + let master_password_required = OrgPolicy::org_is_reset_password_auto_enroll(org, &mut conn).await; + if data.ResetPasswordKey.is_none() && master_password_required { + err!("Reset password key is required, but not provided."); + } + // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type // It returns different error messages per function. if user_org.atype < UserOrgType::Admin { @@ -924,6 +946,11 @@ async fn accept_invite( } user_org.status = UserOrgStatus::Accepted as i32; + + if master_password_required { + user_org.reset_password_key = data.ResetPasswordKey; + } + user_org.save(&mut conn).await?; } } @@ -1570,6 +1597,19 @@ 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") + } + } + } + let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await { Some(p) => p, None => OrgPolicy::new(org_id.clone(), pol_type_enum, "{}".to_string()), @@ -2460,6 +2500,198 @@ async fn delete_group_user( GroupUser::delete_by_group_id_and_user_id(&group_id, &org_user_id, &mut conn).await } +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct OrganizationUserResetPasswordEnrollmentRequest { + ResetPasswordKey: Option, +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct OrganizationUserResetPasswordRequest { + NewMasterPasswordHash: String, + Key: String, +} + +#[get("/organizations//keys")] +async fn get_organization_keys(org_id: String, mut conn: DbConn) -> JsonResult { + let org = match Organization::find_by_uuid(&org_id, &mut conn).await { + Some(organization) => organization, + None => err!("Organization not found"), + }; + + Ok(Json(json!({ + "Object": "organizationKeys", + "PublicKey": org.public_key, + "PrivateKey": org.private_key, + }))) +} + +#[put("/organizations//users//reset-password", data = "")] +async fn put_reset_password( + org_id: String, + org_user_id: String, + headers: AdminHeaders, + data: JsonUpcase, + mut conn: DbConn, + ip: ClientIp, + nt: Notify<'_>, +) -> EmptyResult { + let org = match Organization::find_by_uuid(&org_id, &mut conn).await { + Some(org) => org, + 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"), + }; + + if org_user.reset_password_key.is_none() { + err!("Password reset not or not corretly 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"); + } + + 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_and_key(reset_request.NewMasterPasswordHash.as_str(), reset_request.Key.as_str(), 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?; + } + + log_event( + EventType::OrganizationUserAdminResetPassword as i32, + &org_user_id, + org.uuid.clone(), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + &mut conn, + ) + .await; + + Ok(()) +} + +#[get("/organizations//users//reset-password-details")] +async fn get_reset_password_details( + org_id: String, + org_user_id: String, + _headers: AdminHeaders, + mut conn: DbConn, +) -> JsonResult { + let org = match Organization::find_by_uuid(&org_id, &mut conn).await { + Some(org) => org, + 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"), + }; + + let user = match User::find_by_uuid(&org_user.user_uuid, &mut conn).await { + Some(user) => user, + None => err!("User not found"), + }; + + Ok(Json(json!({ + "Object": "organizationUserResetPasswordDetails", + "Kdf":user.client_kdf_type, + "KdfIterations":user.client_kdf_iter, + "ResetPasswordKey":org_user.reset_password_key, + "EncryptedPrivateKey":org.private_key , + + }))) +} + +#[put("/organizations//users//reset-password-enrollment", data = "")] +async fn put_reset_password_enrollment( + org_id: String, + org_user_id: String, + headers: Headers, + data: JsonUpcase, + mut conn: DbConn, + ip: ClientIp, +) -> EmptyResult { + 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 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"), + }; + + let reset_request = data.into_inner().data; + + if reset_request.ResetPasswordKey.is_none() + && OrgPolicy::org_is_reset_password_auto_enroll(&org_id, &mut conn).await + { + err!("Reset password can't be withdrawed due to an enterprise policy"); + } + + org_user.reset_password_key = reset_request.ResetPasswordKey; + org_user.save(&mut conn).await?; + + let log_id = if org_user.reset_password_key.is_some() { + EventType::OrganizationUserResetPasswordEnroll as i32 + } else { + EventType::OrganizationUserResetPasswordWithdraw as i32 + }; + + log_event(log_id, &org_user_id, org_id, headers.user.uuid.clone(), headers.device.atype, &ip.ip, &mut conn).await; + + Ok(()) +} + // This is a new function active since the v2022.9.x clients. // It combines the previous two calls done before. // We call those two functions here and combine them our selfs. diff --git a/src/config.rs b/src/config.rs index 46deed54..68a6811c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1136,6 +1136,7 @@ where reg!("email/email_footer"); reg!("email/email_footer_text"); + reg!("email/admin_reset_password", ".html"); reg!("email/change_email", ".html"); reg!("email/delete_account", ".html"); reg!("email/emergency_access_invite_accepted", ".html"); diff --git a/src/mail.rs b/src/mail.rs index 8ecb11c6..cffa65fb 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -496,6 +496,19 @@ pub async fn send_test(address: &str) -> EmptyResult { send_email(address, &subject, body_html, body_text).await } +pub async fn send_admin_reset_password(address: &str, user_name: &str, org_name: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/admin_reset_password", + json!({ + "url": CONFIG.domain(), + "img_src": CONFIG._smtp_img_src(), + "user_name": user_name, + "org_name": org_name, + }), + )?; + send_email(address, &subject, body_html, body_text).await +} + async fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult { let smtp_from = &CONFIG.smtp_from(); diff --git a/src/static/templates/email/admin_reset_password.hbs b/src/static/templates/email/admin_reset_password.hbs new file mode 100644 index 00000000..8d381772 --- /dev/null +++ b/src/static/templates/email/admin_reset_password.hbs @@ -0,0 +1,6 @@ +Master Password Has Been Changed + +The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately. + +=== +Github: https://github.com/dani-garcia/vaultwarden diff --git a/src/static/templates/email/admin_reset_password.html.hbs b/src/static/templates/email/admin_reset_password.html.hbs new file mode 100644 index 00000000..d9749d22 --- /dev/null +++ b/src/static/templates/email/admin_reset_password.html.hbs @@ -0,0 +1,11 @@ +Master Password Has Been Changed + +{{> email/email_header }} + + + + +
+ The master password for {{user_name}} has been changed by an administrator in your {{org_name}} organization. If you did not initiate this request, please reach out to your administrator immediately. +
+{{> email/email_footer }}