Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-11-26 05:50:29 +01:00
working implementation
Dieser Commit ist enthalten in:
Ursprung
95494083f2
Commit
c6c45c4c49
5 geänderte Dateien mit 263 neuen und 0 gelöschten Zeilen
|
@ -62,6 +62,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
get_plans_tax_rates,
|
get_plans_tax_rates,
|
||||||
import,
|
import,
|
||||||
post_org_keys,
|
post_org_keys,
|
||||||
|
get_organization_keys,
|
||||||
bulk_public_keys,
|
bulk_public_keys,
|
||||||
deactivate_organization_user,
|
deactivate_organization_user,
|
||||||
bulk_deactivate_organization_user,
|
bulk_deactivate_organization_user,
|
||||||
|
@ -86,6 +87,9 @@ pub fn routes() -> Vec<Route> {
|
||||||
put_user_groups,
|
put_user_groups,
|
||||||
delete_group_user,
|
delete_group_user,
|
||||||
post_delete_group_user,
|
post_delete_group_user,
|
||||||
|
put_reset_password_enrollment,
|
||||||
|
get_reset_password_details,
|
||||||
|
put_reset_password,
|
||||||
get_org_export
|
get_org_export
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -707,6 +711,10 @@ async fn send_invite(
|
||||||
err!("Only Owners can invite Managers, Admins or Owners")
|
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() {
|
for email in data.Emails.iter() {
|
||||||
let email = email.to_lowercase();
|
let email = email.to_lowercase();
|
||||||
let mut user_org_status = UserOrgStatus::Invited as i32;
|
let mut user_org_status = UserOrgStatus::Invited as i32;
|
||||||
|
@ -721,6 +729,10 @@ async fn send_invite(
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CONFIG.mail_enabled() {
|
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);
|
let invitation = Invitation::new(&email);
|
||||||
invitation.save(&mut conn).await?;
|
invitation.save(&mut conn).await?;
|
||||||
}
|
}
|
||||||
|
@ -736,6 +748,10 @@ async fn send_invite(
|
||||||
// automatically accept existing users if mail is disabled
|
// automatically accept existing users if mail is disabled
|
||||||
if !CONFIG.mail_enabled() && !user.password_hash.is_empty() {
|
if !CONFIG.mail_enabled() && !user.password_hash.is_empty() {
|
||||||
user_org_status = UserOrgStatus::Accepted as i32;
|
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
|
user
|
||||||
}
|
}
|
||||||
|
@ -882,6 +898,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct AcceptData {
|
struct AcceptData {
|
||||||
Token: String,
|
Token: String,
|
||||||
|
ResetPasswordKey: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/organizations/<org_id>/users/<_org_user_id>/accept", data = "<data>")]
|
#[post("/organizations/<org_id>/users/<_org_user_id>/accept", data = "<data>")]
|
||||||
|
@ -909,6 +926,11 @@ async fn accept_invite(
|
||||||
err!("User already accepted the invitation")
|
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
|
// 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.
|
// It returns different error messages per function.
|
||||||
if user_org.atype < UserOrgType::Admin {
|
if user_org.atype < UserOrgType::Admin {
|
||||||
|
@ -924,6 +946,11 @@ async fn accept_invite(
|
||||||
}
|
}
|
||||||
|
|
||||||
user_org.status = UserOrgStatus::Accepted as i32;
|
user_org.status = UserOrgStatus::Accepted as i32;
|
||||||
|
|
||||||
|
if master_password_required {
|
||||||
|
user_org.reset_password_key = data.ResetPasswordKey;
|
||||||
|
}
|
||||||
|
|
||||||
user_org.save(&mut conn).await?;
|
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 {
|
let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &mut conn).await {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
None => OrgPolicy::new(org_id.clone(), pol_type_enum, "{}".to_string()),
|
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
|
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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct OrganizationUserResetPasswordRequest {
|
||||||
|
NewMasterPasswordHash: String,
|
||||||
|
Key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/organizations/<org_id>/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/<org_id>/users/<org_user_id>/reset-password", data = "<data>")]
|
||||||
|
async fn put_reset_password(
|
||||||
|
org_id: String,
|
||||||
|
org_user_id: String,
|
||||||
|
headers: AdminHeaders,
|
||||||
|
data: JsonUpcase<OrganizationUserResetPasswordRequest>,
|
||||||
|
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/<org_id>/users/<org_user_id>/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/<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 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.
|
// This is a new function active since the v2022.9.x clients.
|
||||||
// It combines the previous two calls done before.
|
// It combines the previous two calls done before.
|
||||||
// We call those two functions here and combine them our selfs.
|
// We call those two functions here and combine them our selfs.
|
||||||
|
|
|
@ -1136,6 +1136,7 @@ where
|
||||||
reg!("email/email_footer");
|
reg!("email/email_footer");
|
||||||
reg!("email/email_footer_text");
|
reg!("email/email_footer_text");
|
||||||
|
|
||||||
|
reg!("email/admin_reset_password", ".html");
|
||||||
reg!("email/change_email", ".html");
|
reg!("email/change_email", ".html");
|
||||||
reg!("email/delete_account", ".html");
|
reg!("email/delete_account", ".html");
|
||||||
reg!("email/emergency_access_invite_accepted", ".html");
|
reg!("email/emergency_access_invite_accepted", ".html");
|
||||||
|
|
13
src/mail.rs
13
src/mail.rs
|
@ -496,6 +496,19 @@ pub async fn send_test(address: &str) -> EmptyResult {
|
||||||
send_email(address, &subject, body_html, body_text).await
|
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 {
|
async fn send_email(address: &str, subject: &str, body_html: String, body_text: String) -> EmptyResult {
|
||||||
let smtp_from = &CONFIG.smtp_from();
|
let smtp_from = &CONFIG.smtp_from();
|
||||||
|
|
||||||
|
|
6
src/static/templates/email/admin_reset_password.hbs
Normale Datei
6
src/static/templates/email/admin_reset_password.hbs
Normale Datei
|
@ -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
|
11
src/static/templates/email/admin_reset_password.html.hbs
Normale Datei
11
src/static/templates/email/admin_reset_password.html.hbs
Normale Datei
|
@ -0,0 +1,11 @@
|
||||||
|
Master Password Has Been Changed
|
||||||
|
<!---------------->
|
||||||
|
{{> email/email_header }}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
The master password for <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{user_name}}</b> has been changed by an administrator in your <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{org_name}}</b> organization. If you did not initiate this request, please reach out to your administrator immediately.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{> email/email_footer }}
|
Laden …
In neuem Issue referenzieren