mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-05-18 09:33:56 +00:00
Merge branch 'main' into issue-3166
This commit is contained in:
commit
bc49d1f90d
46 changed files with 641 additions and 355 deletions
|
@ -123,7 +123,9 @@ async fn post_emergency_access(
|
|||
|
||||
emergency_access.atype = new_type;
|
||||
emergency_access.wait_time_days = data.WaitTimeDays;
|
||||
emergency_access.key_encrypted = data.KeyEncrypted;
|
||||
if data.KeyEncrypted.is_some() {
|
||||
emergency_access.key_encrypted = data.KeyEncrypted;
|
||||
}
|
||||
|
||||
emergency_access.save(&mut conn).await?;
|
||||
Ok(Json(emergency_access.to_json()))
|
||||
|
|
|
@ -62,6 +62,7 @@ pub fn routes() -> Vec<Route> {
|
|||
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<Route> {
|
|||
put_user_groups,
|
||||
delete_group_user,
|
||||
post_delete_group_user,
|
||||
put_reset_password_enrollment,
|
||||
get_reset_password_details,
|
||||
put_reset_password,
|
||||
get_org_export
|
||||
]
|
||||
}
|
||||
|
@ -882,6 +886,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<String>,
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/users/<_org_user_id>/accept", data = "<data>")]
|
||||
|
@ -909,6 +914,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 +934,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?;
|
||||
}
|
||||
}
|
||||
|
@ -2460,6 +2475,204 @@ 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<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 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 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, &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_logout(&user, None).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 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"),
|
||||
};
|
||||
|
||||
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,
|
||||
"KdfIterations":user.client_kdf_iter,
|
||||
"ResetPasswordKey":org_user.reset_password_key,
|
||||
"EncryptedPrivateKey":org.private_key ,
|
||||
|
||||
})))
|
||||
}
|
||||
|
||||
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
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
||||
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/<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()
|
||||
&& 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.
|
||||
|
|
|
@ -79,7 +79,7 @@ async fn icon_redirect(domain: &str, template: &str) -> Option<Redirect> {
|
|||
return None;
|
||||
}
|
||||
|
||||
if is_domain_blacklisted(domain).await {
|
||||
if check_domain_blacklist_reason(domain).await.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
@ -258,9 +258,15 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum DomainBlacklistReason {
|
||||
Regex,
|
||||
IP,
|
||||
}
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
#[cached(key = "String", convert = r#"{ domain.to_string() }"#, size = 16, time = 60)]
|
||||
async fn is_domain_blacklisted(domain: &str) -> bool {
|
||||
async fn check_domain_blacklist_reason(domain: &str) -> Option<DomainBlacklistReason> {
|
||||
// First check the blacklist regex if there is a match.
|
||||
// This prevents the blocked domain(s) from being leaked via a DNS lookup.
|
||||
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
|
||||
|
@ -284,7 +290,7 @@ async fn is_domain_blacklisted(domain: &str) -> bool {
|
|||
|
||||
if is_match {
|
||||
debug!("Blacklisted domain: {} matched ICON_BLACKLIST_REGEX", domain);
|
||||
return true;
|
||||
return Some(DomainBlacklistReason::Regex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,13 +299,13 @@ async fn is_domain_blacklisted(domain: &str) -> bool {
|
|||
for addr in s {
|
||||
if !is_global(addr.ip()) {
|
||||
debug!("IP {} for domain '{}' is not a global IP!", addr.ip(), domain);
|
||||
return true;
|
||||
return Some(DomainBlacklistReason::IP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
None
|
||||
}
|
||||
|
||||
async fn get_icon(domain: &str) -> Option<(Vec<u8>, String)> {
|
||||
|
@ -564,8 +570,10 @@ async fn get_page(url: &str) -> Result<Response, Error> {
|
|||
}
|
||||
|
||||
async fn get_page_with_referer(url: &str, referer: &str) -> Result<Response, Error> {
|
||||
if is_domain_blacklisted(url::Url::parse(url).unwrap().host_str().unwrap_or_default()).await {
|
||||
warn!("Favicon '{}' resolves to a blacklisted domain or IP!", url);
|
||||
match check_domain_blacklist_reason(url::Url::parse(url).unwrap().host_str().unwrap_or_default()).await {
|
||||
Some(DomainBlacklistReason::Regex) => warn!("Favicon '{}' is from a blacklisted domain!", url),
|
||||
Some(DomainBlacklistReason::IP) => warn!("Favicon '{}' is hosted on a non-global IP!", url),
|
||||
None => (),
|
||||
}
|
||||
|
||||
let mut client = CLIENT.get(url);
|
||||
|
@ -659,8 +667,10 @@ fn parse_sizes(sizes: &str) -> (u16, u16) {
|
|||
}
|
||||
|
||||
async fn download_icon(domain: &str) -> Result<(Bytes, Option<&str>), Error> {
|
||||
if is_domain_blacklisted(domain).await {
|
||||
err_silent!("Domain is blacklisted", domain)
|
||||
match check_domain_blacklist_reason(domain).await {
|
||||
Some(DomainBlacklistReason::Regex) => err_silent!("Domain is blacklisted", domain),
|
||||
Some(DomainBlacklistReason::IP) => err_silent!("Host resolves to a non-global IP", domain),
|
||||
None => (),
|
||||
}
|
||||
|
||||
let icon_result = get_icon_url(domain).await?;
|
||||
|
|
|
@ -141,6 +141,8 @@ macro_rules! make_config {
|
|||
)+)+
|
||||
config.domain_set = _domain_set;
|
||||
|
||||
config.domain = config.domain.trim_end_matches('/').to_string();
|
||||
|
||||
config.signups_domains_whitelist = config.signups_domains_whitelist.trim().to_lowercase();
|
||||
config.org_creation_users = config.org_creation_users.trim().to_lowercase();
|
||||
|
||||
|
@ -1136,6 +1138,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");
|
||||
|
|
|
@ -287,47 +287,95 @@ impl Collection {
|
|||
}
|
||||
|
||||
pub async fn is_writable_by_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
match UserOrganization::find_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
|
||||
None => false, // Not in Org
|
||||
Some(user_org) => {
|
||||
if user_org.has_full_access() {
|
||||
return true;
|
||||
}
|
||||
|
||||
db_run! { conn: {
|
||||
users_collections::table
|
||||
.filter(users_collections::collection_uuid.eq(&self.uuid))
|
||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||
.filter(users_collections::read_only.eq(false))
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.ok()
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
}
|
||||
let user_uuid = user_uuid.to_string();
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(collections::uuid).and(
|
||||
users_collections::user_uuid.eq(user_uuid.clone())
|
||||
)
|
||||
))
|
||||
.left_join(users_organizations::table.on(
|
||||
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
||||
users_organizations::user_uuid.eq(user_uuid)
|
||||
)
|
||||
))
|
||||
.left_join(groups_users::table.on(
|
||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||
))
|
||||
.left_join(groups::table.on(
|
||||
groups::uuid.eq(groups_users::groups_uuid)
|
||||
))
|
||||
.left_join(collections_groups::table.on(
|
||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
||||
collections_groups::collections_uuid.eq(collections::uuid)
|
||||
)
|
||||
))
|
||||
.filter(collections::uuid.eq(&self.uuid))
|
||||
.filter(
|
||||
users_collections::collection_uuid.eq(&self.uuid).and(users_collections::read_only.eq(false)).or(// Directly accessed collection
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
||||
)).or(
|
||||
groups::access_all.eq(true) // access_all in groups
|
||||
).or( // access via groups
|
||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(
|
||||
collections_groups::collections_uuid.is_not_null().and(
|
||||
collections_groups::read_only.eq(false))
|
||||
)
|
||||
)
|
||||
)
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.ok()
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
|
||||
pub async fn hide_passwords_for_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
|
||||
match UserOrganization::find_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
|
||||
None => true, // Not in Org
|
||||
Some(user_org) => {
|
||||
if user_org.has_full_access() {
|
||||
return false;
|
||||
}
|
||||
|
||||
db_run! { conn: {
|
||||
users_collections::table
|
||||
.filter(users_collections::collection_uuid.eq(&self.uuid))
|
||||
.filter(users_collections::user_uuid.eq(user_uuid))
|
||||
.filter(users_collections::hide_passwords.eq(true))
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.ok()
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
}
|
||||
let user_uuid = user_uuid.to_string();
|
||||
db_run! { conn: {
|
||||
collections::table
|
||||
.left_join(users_collections::table.on(
|
||||
users_collections::collection_uuid.eq(collections::uuid).and(
|
||||
users_collections::user_uuid.eq(user_uuid.clone())
|
||||
)
|
||||
))
|
||||
.left_join(users_organizations::table.on(
|
||||
collections::org_uuid.eq(users_organizations::org_uuid).and(
|
||||
users_organizations::user_uuid.eq(user_uuid)
|
||||
)
|
||||
))
|
||||
.left_join(groups_users::table.on(
|
||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
|
||||
))
|
||||
.left_join(groups::table.on(
|
||||
groups::uuid.eq(groups_users::groups_uuid)
|
||||
))
|
||||
.left_join(collections_groups::table.on(
|
||||
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and(
|
||||
collections_groups::collections_uuid.eq(collections::uuid)
|
||||
)
|
||||
))
|
||||
.filter(collections::uuid.eq(&self.uuid))
|
||||
.filter(
|
||||
users_collections::collection_uuid.eq(&self.uuid).and(users_collections::hide_passwords.eq(true)).or(// Directly accessed collection
|
||||
users_organizations::access_all.eq(true).or( // access_all in Organization
|
||||
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
|
||||
)).or(
|
||||
groups::access_all.eq(true) // access_all in groups
|
||||
).or( // access via groups
|
||||
groups_users::users_organizations_uuid.eq(users_organizations::uuid).and(
|
||||
collections_groups::collections_uuid.is_not_null().and(
|
||||
collections_groups::hide_passwords.eq(true))
|
||||
)
|
||||
)
|
||||
)
|
||||
.count()
|
||||
.first::<i64>(conn)
|
||||
.ok()
|
||||
.unwrap_or(0) != 0
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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::<UpCase<ResetPasswordDataModel>>(&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 {
|
||||
|
|
|
@ -29,6 +29,7 @@ db_object! {
|
|||
pub akey: String,
|
||||
pub status: i32,
|
||||
pub atype: i32,
|
||||
pub reset_password_key: Option<String>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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": CONFIG.mail_enabled(),
|
||||
|
||||
"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": CONFIG.mail_enabled(),
|
||||
"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",
|
||||
})
|
||||
|
|
|
@ -222,6 +222,7 @@ table! {
|
|||
akey -> Text,
|
||||
status -> Integer,
|
||||
atype -> Integer,
|
||||
reset_password_key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -222,6 +222,7 @@ table! {
|
|||
akey -> Text,
|
||||
status -> Integer,
|
||||
atype -> Integer,
|
||||
reset_password_key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -222,6 +222,7 @@ table! {
|
|||
akey -> Text,
|
||||
status -> Integer,
|
||||
atype -> Integer,
|
||||
reset_password_key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
<div class="row my-2 align-items-center pt-3 border-top" title="Send a test email to given email address">
|
||||
<label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label>
|
||||
<div class="col-sm-8 input-group">
|
||||
<input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email" required>
|
||||
<input class="form-control" id="smtp-test-email" type="email" placeholder="Enter test email" required spellcheck="false">
|
||||
<button type="button" class="btn btn-outline-primary input-group-text" id="smtpTest">Send test email</button>
|
||||
<div class="invalid-tooltip">Please provide a valid email address</div>
|
||||
</div>
|
||||
|
@ -85,7 +85,7 @@
|
|||
<input readonly class="form-control" id="input_{{name}}" type="password" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
|
||||
<button class="btn btn-outline-secondary" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button>
|
||||
{{else}}
|
||||
<input readonly class="form-control" id="input_{{name}}" type="{{type}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}}>
|
||||
<input readonly class="form-control" id="input_{{name}}" type="{{type}}" value="{{value}}" {{#if default}} placeholder="Default: {{default}}" {{/if}} spellcheck="false">
|
||||
{{#case type "password"}}
|
||||
<button class="btn btn-outline-secondary" type="button" data-vw-pw-toggle="input_{{name}}">Show/hide</button>
|
||||
{{/case}}
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
<small>Email:</small>
|
||||
|
||||
<form class="form-inline input-group w-50" id="inviteUserForm">
|
||||
<input type="email" class="form-control me-2" id="inviteEmail" placeholder="Enter email" required>
|
||||
<input type="email" class="form-control me-2" id="inviteEmail" placeholder="Enter email" required spellcheck="false">
|
||||
<button type="submit" class="btn btn-primary">Invite</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
6
src/static/templates/email/admin_reset_password.hbs
Normal file
6
src/static/templates/email/admin_reset_password.hbs
Normal file
|
@ -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
Normal file
11
src/static/templates/email/admin_reset_password.html.hbs
Normal file
|
@ -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 }}
|
Loading…
Add table
Add a link
Reference in a new issue