From 95494083f2b09417fef916f4d315cb5a38a78128 Mon Sep 17 00:00:00 2001 From: sirux88 Date: Wed, 25 Jan 2023 08:06:21 +0100 Subject: [PATCH 1/7] added database migration --- .../down.sql | 0 .../up.sql | 2 ++ .../down.sql | 0 .../up.sql | 2 ++ .../down.sql | 0 .../up.sql | 2 ++ src/db/models/event.rs | 6 ++--- src/db/models/org_policy.rs | 23 ++++++++++++++++++- src/db/models/organization.rs | 8 +++++-- src/db/models/user.rs | 21 +++++++++++++++++ src/db/schemas/mysql/schema.rs | 1 + src/db/schemas/postgresql/schema.rs | 1 + src/db/schemas/sqlite/schema.rs | 1 + 13 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 migrations/mysql/2023-01-06-151600_add_reset_password_support/down.sql create mode 100644 migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql create mode 100644 migrations/postgresql/2023-01-06-151600_add_reset_password_support/down.sql create mode 100644 migrations/postgresql/2023-01-06-151600_add_reset_password_support/up.sql create mode 100644 migrations/sqlite/2023-01-06-151600_add_reset_password_support/down.sql create mode 100644 migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql diff --git a/migrations/mysql/2023-01-06-151600_add_reset_password_support/down.sql b/migrations/mysql/2023-01-06-151600_add_reset_password_support/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql b/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql new file mode 100644 index 00000000..d8173af4 --- /dev/null +++ b/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users_organizations +ADD COLUMN reset_password_key VARCHAR(255); diff --git a/migrations/postgresql/2023-01-06-151600_add_reset_password_support/down.sql b/migrations/postgresql/2023-01-06-151600_add_reset_password_support/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/postgresql/2023-01-06-151600_add_reset_password_support/up.sql b/migrations/postgresql/2023-01-06-151600_add_reset_password_support/up.sql new file mode 100644 index 00000000..326b3106 --- /dev/null +++ b/migrations/postgresql/2023-01-06-151600_add_reset_password_support/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users_organizations +ADD COLUMN reset_password_key TEXT; diff --git a/migrations/sqlite/2023-01-06-151600_add_reset_password_support/down.sql b/migrations/sqlite/2023-01-06-151600_add_reset_password_support/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql b/migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql new file mode 100644 index 00000000..326b3106 --- /dev/null +++ b/migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE users_organizations +ADD COLUMN reset_password_key TEXT; diff --git a/src/db/models/event.rs b/src/db/models/event.rs index 9196b8a8..64312273 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -87,9 +87,9 @@ pub enum EventType { OrganizationUserRemoved = 1503, OrganizationUserUpdatedGroups = 1504, // OrganizationUserUnlinkedSso = 1505, // Not supported - // OrganizationUserResetPasswordEnroll = 1506, // Not supported - // OrganizationUserResetPasswordWithdraw = 1507, // Not supported - // OrganizationUserAdminResetPassword = 1508, // Not supported + OrganizationUserResetPasswordEnroll = 1506, + OrganizationUserResetPasswordWithdraw = 1507, + OrganizationUserAdminResetPassword = 1508, // OrganizationUserResetSsoLink = 1509, // Not supported // OrganizationUserFirstSsoLogin = 1510, // Not supported OrganizationUserRevoked = 1511, diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index caa3335f..c9fd5c34 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -32,7 +32,7 @@ pub enum OrgPolicyType { PersonalOwnership = 5, DisableSend = 6, SendOptions = 7, - // ResetPassword = 8, // Not supported + ResetPassword = 8, // MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed) // DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed) } @@ -44,6 +44,13 @@ pub struct SendOptionsPolicyData { pub DisableHideEmail: bool, } +// https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs +#[derive(Deserialize)] +#[allow(non_snake_case)] +pub struct ResetPasswordDataModel { + pub AutoEnrollEnabled: bool, +} + pub type OrgPolicyResult = Result<(), OrgPolicyErr>; #[derive(Debug)] @@ -298,6 +305,20 @@ impl OrgPolicy { Ok(()) } + pub async fn org_is_reset_password_auto_enroll(org_uuid: &str, conn: &mut DbConn) -> bool { + match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await { + Some(policy) => match serde_json::from_str::>(&policy.data) { + Ok(opts) => { + return opts.data.AutoEnrollEnabled; + } + _ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data), + }, + None => return false, + } + + false + } + /// Returns true if the user belongs to an org that has enabled the `DisableHideEmail` /// option of the `Send Options` policy, and the user is not an owner or admin of that org. pub async fn is_hide_email_disabled(user_uuid: &str, conn: &mut DbConn) -> bool { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 331e1007..1de321bd 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -29,6 +29,7 @@ db_object! { pub akey: String, pub status: i32, pub atype: i32, + pub reset_password_key: Option, } } @@ -158,7 +159,7 @@ impl Organization { "SelfHost": true, "UseApi": false, // Not supported "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), - "UseResetPassword": false, // Not supported + "UseResetPassword": true, "BusinessName": null, "BusinessAddress1": null, @@ -194,6 +195,7 @@ impl UserOrganization { akey: String::new(), status: UserOrgStatus::Accepted as i32, atype: UserOrgType::User as i32, + reset_password_key: None, } } @@ -311,7 +313,8 @@ impl UserOrganization { "UseApi": false, // Not supported "SelfHost": true, "HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(), - "ResetPasswordEnrolled": false, // Not supported + "ResetPasswordEnrolled": self.reset_password_key.is_some(), + "UseResetPassword": true, "SsoBound": false, // Not supported "UseSso": false, // Not supported "ProviderId": null, @@ -377,6 +380,7 @@ impl UserOrganization { "Type": self.atype, "AccessAll": self.access_all, "TwoFactorEnabled": twofactor_enabled, + "ResetPasswordEnrolled":self.reset_password_key.is_some(), "Object": "organizationUserUserDetails", }) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 5ce87e14..2ca770b5 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -178,6 +178,27 @@ impl User { self.security_stamp = crate::util::get_uuid(); } + /// Set the password hash generated + /// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different. + /// + /// # Arguments + /// + /// * `new_password_hash` - A str which contains a hashed version of the users master password. + /// * `new_key` - A String which contains the new aKey value of the users master password. + /// * `allow_next_route` - A Option> with the function names of the next allowed (rocket) routes. + /// These routes are able to use the previous stamp id for the next 2 minutes. + /// After these 2 minutes this stamp will expire. + /// + pub fn set_password_and_key( + &mut self, + new_password_hash: &str, + new_key: &str, + allow_next_route: Option>, + ) { + self.set_password(new_password_hash, allow_next_route); + self.akey = String::from(new_key); + } + /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp. /// /// # Arguments diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index 27cd24c3..cdb3e059 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -222,6 +222,7 @@ table! { akey -> Text, status -> Integer, atype -> Integer, + reset_password_key -> Nullable, } } diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 0233e0c9..6ec8a979 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -222,6 +222,7 @@ table! { akey -> Text, status -> Integer, atype -> Integer, + reset_password_key -> Nullable, } } diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 391e6700..faaf6fae 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -222,6 +222,7 @@ table! { akey -> Text, status -> Integer, atype -> Integer, + reset_password_key -> Nullable, } } From c6c45c4c49be81a86240b7a4462b7d502be4257d Mon Sep 17 00:00:00 2001 From: sirux88 Date: Wed, 25 Jan 2023 08:06:21 +0100 Subject: [PATCH 2/7] 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 }} From adaefc8628423dd7aebb39e76ce35ee00ce618c5 Mon Sep 17 00:00:00 2001 From: sirux88 Date: Wed, 25 Jan 2023 08:09:26 +0100 Subject: [PATCH 3/7] fixes for current upstream main --- src/api/core/organizations.rs | 2 +- src/db/models/user.rs | 21 --------------------- 2 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 4b0605f8..964d4c4d 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2583,7 +2583,7 @@ async fn put_reset_password( let reset_request = data.into_inner().data; - user.set_password_and_key(reset_request.NewMasterPasswordHash.as_str(), reset_request.Key.as_str(), None); + 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; diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 2ca770b5..5ce87e14 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -178,27 +178,6 @@ impl User { self.security_stamp = crate::util::get_uuid(); } - /// Set the password hash generated - /// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different. - /// - /// # Arguments - /// - /// * `new_password_hash` - A str which contains a hashed version of the users master password. - /// * `new_key` - A String which contains the new aKey value of the users master password. - /// * `allow_next_route` - A Option> with the function names of the next allowed (rocket) routes. - /// These routes are able to use the previous stamp id for the next 2 minutes. - /// After these 2 minutes this stamp will expire. - /// - pub fn set_password_and_key( - &mut self, - new_password_hash: &str, - new_key: &str, - allow_next_route: Option>, - ) { - self.set_password(new_password_hash, allow_next_route); - self.akey = String::from(new_key); - } - /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp. /// /// # Arguments From 26cd5d96434d497cc0a7ca11cacc5ea9845230c0 Mon Sep 17 00:00:00 2001 From: sirux88 Date: Sat, 4 Feb 2023 09:23:13 +0100 Subject: [PATCH 4/7] Replaced wrong mysql column type --- .../mysql/2023-01-06-151600_add_reset_password_support/up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql b/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql index d8173af4..326b3106 100644 --- a/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql +++ b/migrations/mysql/2023-01-06-151600_add_reset_password_support/up.sql @@ -1,2 +1,2 @@ ALTER TABLE users_organizations -ADD COLUMN reset_password_key VARCHAR(255); +ADD COLUMN reset_password_key TEXT; From 62dfeb80f211f9ec283b46d8158f86d715f8e6b5 Mon Sep 17 00:00:00 2001 From: sirux88 Date: Sat, 4 Feb 2023 13:29:57 +0100 Subject: [PATCH 5/7] improved security, disabling policy usage on email-disabled clients and some refactoring --- src/api/core/organizations.rs | 143 ++++++++++++++++------------------ 1 file changed, 68 insertions(+), 75 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 964d4c4d..6224c18b 100644 --- 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"), }; - 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"), }; + 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 correctly enrolled"); + } + if org_user.status != (UserOrgStatus::Confirmed as i32) { + err!("Organization user must be confirmed for password reset functionality"); + } + + // 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 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,6 +2601,52 @@ async fn get_reset_password_details( }))) } +async fn check_reset_password_applicable_and_permissions( + org_id: &str, + org_user_id: &str, + headers: &AdminHeaders, + conn: &mut DbConn, +) -> EmptyResult { + 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"), + }; + + if !policy.enabled { + err!("Reset password policy not enabled"); + } + + Ok(()) +} + #[put("/organizations//users//reset-password-enrollment", data = "")] async fn put_reset_password_enrollment( org_id: String, @@ -2656,20 +2656,13 @@ async fn put_reset_password_enrollment( 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"), }; + check_reset_password_applicable(&org_id, &mut conn).await?; + let reset_request = data.into_inner().data; if reset_request.ResetPasswordKey.is_none() From a6558f55488c86c1aa702e9a5e7875afbd2e7490 Mon Sep 17 00:00:00 2001 From: sirux88 Date: Sun, 5 Feb 2023 16:34:48 +0100 Subject: [PATCH 6/7] rust lang specific improvements --- src/api/core/organizations.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 6224c18b..3b0d5fad 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2542,7 +2542,7 @@ async fn put_reset_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 { + if let Err(e) = mail::send_admin_reset_password(&user.email, &user.name, &org.name).await { error!("Error sending user reset password email: {:#?}", e); } @@ -2615,19 +2615,11 @@ async fn check_reset_password_applicable_and_permissions( }; // 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; + match headers.org_user_type { + UserOrgType::Owner => Ok(()), + UserOrgType::Admin if target_user.atype <= UserOrgType::Admin => Ok(()), + _ => err!("No permission to reset this user's password"), } - 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 { From 0d1753ac747c43b9f48310c4f7be284fdc6ee669 Mon Sep 17 00:00:00 2001 From: sirux88 Date: Sun, 5 Feb 2023 16:47:23 +0100 Subject: [PATCH 7/7] completly hide reset password policy on email disabled instances --- src/api/core/organizations.rs | 4 ---- src/db/models/organization.rs | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 3b0d5fad..9250f929 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1585,10 +1585,6 @@ async fn put_policy( } } - if pol_type_enum == OrgPolicyType::ResetPassword && data.enabled && !CONFIG.mail_enabled() { - 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 { Some(p) => p, None => OrgPolicy::new(org_id.clone(), pol_type_enum, "{}".to_string()), diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 1de321bd..a6e4be21 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -159,7 +159,7 @@ impl Organization { "SelfHost": true, "UseApi": false, // Not supported "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), - "UseResetPassword": true, + "UseResetPassword": CONFIG.mail_enabled(), "BusinessName": null, "BusinessAddress1": null, @@ -314,7 +314,7 @@ impl UserOrganization { "SelfHost": true, "HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(), "ResetPasswordEnrolled": self.reset_password_key.is_some(), - "UseResetPassword": true, + "UseResetPassword": CONFIG.mail_enabled(), "SsoBound": false, // Not supported "UseSso": false, // Not supported "ProviderId": null,