1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-07-03 11:05:07 +00:00

Merge branch 'main' into feature/kdf-options

This commit is contained in:
Daniel García 2023-02-12 19:23:14 +01:00 committed by GitHub
commit 5bcee24f88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 831 additions and 463 deletions

View file

@ -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()))

View file

@ -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.

View file

@ -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?;

View file

@ -118,8 +118,8 @@ pub fn static_files(filename: String) -> Result<(ContentType, &'static [u8]), Er
"jdenticon.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jdenticon.js"))),
"datatables.js" => Ok((ContentType::JavaScript, include_bytes!("../static/scripts/datatables.js"))),
"datatables.css" => Ok((ContentType::CSS, include_bytes!("../static/scripts/datatables.css"))),
"jquery-3.6.2.slim.js" => {
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.2.slim.js")))
"jquery-3.6.3.slim.js" => {
Ok((ContentType::JavaScript, include_bytes!("../static/scripts/jquery-3.6.3.slim.js")))
}
_ => err!(format!("Static file not found: {filename}")),
}

View file

@ -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");

View file

@ -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
}}
}
}

View file

@ -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,

View file

@ -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 {

View file

@ -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",
})

View file

@ -224,6 +224,7 @@ table! {
akey -> Text,
status -> Integer,
atype -> Integer,
reset_password_key -> Nullable<Text>,
}
}

View file

@ -224,6 +224,7 @@ table! {
akey -> Text,
status -> Integer,
atype -> Integer,
reset_password_key -> Nullable<Text>,
}
}

View file

@ -224,6 +224,7 @@ table! {
akey -> Text,
status -> Integer,
atype -> Integer,
reset_password_key -> Nullable<Text>,
}
}

View file

@ -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();

View file

@ -18,24 +18,31 @@ img {
border: var(--bs-alert-border);
}
#users-table .vw-account-details {
min-width: 250px;
}
#users-table .vw-created-at, #users-table .vw-last-active {
width: 85px;
min-width: 70px;
min-width: 85px;
max-width: 85px;
}
#users-table .vw-items {
width: 35px;
#users-table .vw-items, #orgs-table .vw-items, #orgs-table .vw-users {
min-width: 35px;
max-width: 40px;
}
#users-table .vw-organizations {
min-width: 120px;
#users-table .vw-attachments, #orgs-table .vw-attachments {
min-width: 100px;
max-width: 130px;
}
#users-table .vw-actions, #orgs-table .vw-actions {
width: 130px;
min-width: 130px;
max-width: 130px;
}
#users-table .vw-org-cell {
max-height: 120px;
}
#orgs-table .vw-org-details {
min-width: 285px;
}
#support-string {
height: 16rem;

View file

@ -1,4 +1,6 @@
"use strict";
/* eslint-env es2017, browser */
/* exported BASE_URL, _post */
function getBaseUrl() {
// If the base URL is `https://vaultwarden.example.com/base/path/`,
@ -26,6 +28,8 @@ function msg(text, reload_page = true) {
}
function _post(url, successMsg, errMsg, body, reload_page = true) {
let respStatus;
let respStatusText;
fetch(url, {
method: "POST",
body: body,
@ -33,22 +37,30 @@ function _post(url, successMsg, errMsg, body, reload_page = true) {
credentials: "same-origin",
headers: { "Content-Type": "application/json" }
}).then( resp => {
if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); }
const respStatus = resp.status;
const respStatusText = resp.statusText;
if (resp.ok) {
msg(successMsg, reload_page);
// Abuse the catch handler by setting error to false and continue
return Promise.reject({error: false});
}
respStatus = resp.status;
respStatusText = resp.statusText;
return resp.text();
}).then( respText => {
try {
const respJson = JSON.parse(respText);
return respJson ? respJson.ErrorModel.Message : "Unknown error";
if (respJson.ErrorModel && respJson.ErrorModel.Message) {
return respJson.ErrorModel.Message;
} else {
return Promise.reject({body:`${respStatus} - ${respStatusText}\n\nUnknown error`, error: true});
}
} catch (e) {
return Promise.reject({body:respStatus + " - " + respStatusText, error: true});
return Promise.reject({body:`${respStatus} - ${respStatusText}\n\n[Catch] ${e}`, error: true});
}
}).then( apiMsg => {
msg(errMsg + "\n" + apiMsg, reload_page);
msg(`${errMsg}\n${apiMsg}`, reload_page);
}).catch( e => {
if (e.error === false) { return true; }
else { msg(errMsg + "\n" + e.body, reload_page); }
else { msg(`${errMsg}\n${e.body}`, reload_page); }
});
}

View file

@ -1,4 +1,6 @@
"use strict";
/* eslint-env es2017, browser */
/* global BASE_URL:readable, BSN:readable */
var dnsCheck = false;
var timeCheck = false;
@ -65,7 +67,7 @@ function checkVersions(platform, installed, latest, commit=null) {
// ================================
// Generate support string to be pasted on github or the forum
async function generateSupportString(dj) {
async function generateSupportString(event, dj) {
event.preventDefault();
event.stopPropagation();
@ -114,7 +116,7 @@ async function generateSupportString(dj) {
document.getElementById("copy-support").classList.remove("d-none");
}
function copyToClipboard() {
function copyToClipboard(event) {
event.preventDefault();
event.stopPropagation();
@ -208,12 +210,18 @@ function init(dj) {
}
// onLoad events
document.addEventListener("DOMContentLoaded", (/*event*/) => {
document.addEventListener("DOMContentLoaded", (event) => {
const diag_json = JSON.parse(document.getElementById("diagnostics_json").innerText);
init(diag_json);
document.getElementById("gen-support").addEventListener("click", () => {
generateSupportString(diag_json);
});
document.getElementById("copy-support").addEventListener("click", copyToClipboard);
const btnGenSupport = document.getElementById("gen-support");
if (btnGenSupport) {
btnGenSupport.addEventListener("click", () => {
generateSupportString(event, diag_json);
});
}
const btnCopySupport = document.getElementById("copy-support");
if (btnCopySupport) {
btnCopySupport.addEventListener("click", copyToClipboard);
}
});

View file

@ -1,6 +1,8 @@
"use strict";
/* eslint-env es2017, browser, jquery */
/* global _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */
function deleteOrganization() {
function deleteOrganization(event) {
event.preventDefault();
event.stopPropagation();
const org_uuid = event.target.dataset.vwOrgUuid;
@ -28,9 +30,22 @@ function deleteOrganization() {
}
}
function initActions() {
document.querySelectorAll("button[vw-delete-organization]").forEach(btn => {
btn.addEventListener("click", deleteOrganization);
});
if (jdenticon) {
jdenticon();
}
}
// onLoad events
document.addEventListener("DOMContentLoaded", (/*event*/) => {
jQuery("#orgs-table").DataTable({
"drawCallback": function() {
initActions();
},
"stateSave": true,
"responsive": true,
"lengthMenu": [
@ -46,9 +61,10 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
});
// Add click events for organization actions
document.querySelectorAll("button[vw-delete-organization]").forEach(btn => {
btn.addEventListener("click", deleteOrganization);
});
initActions();
document.getElementById("reload").addEventListener("click", reload);
const btnReload = document.getElementById("reload");
if (btnReload) {
btnReload.addEventListener("click", reload);
}
});

View file

@ -1,6 +1,8 @@
"use strict";
/* eslint-env es2017, browser */
/* global _post:readable, BASE_URL:readable */
function smtpTest() {
function smtpTest(event) {
event.preventDefault();
event.stopPropagation();
if (formHasChanges(config_form)) {
@ -41,7 +43,7 @@ function getFormData() {
return data;
}
function saveConfig() {
function saveConfig(event) {
const data = JSON.stringify(getFormData());
_post(`${BASE_URL}/admin/config/`,
"Config saved correctly",
@ -51,7 +53,7 @@ function saveConfig() {
event.preventDefault();
}
function deleteConf() {
function deleteConf(event) {
event.preventDefault();
event.stopPropagation();
const input = prompt(
@ -68,7 +70,7 @@ function deleteConf() {
}
}
function backupDatabase() {
function backupDatabase(event) {
event.preventDefault();
event.stopPropagation();
_post(`${BASE_URL}/admin/config/backup_db`,
@ -94,24 +96,26 @@ function formHasChanges(form) {
// This function will prevent submitting a from when someone presses enter.
function preventFormSubmitOnEnter(form) {
form.onkeypress = function(e) {
const key = e.charCode || e.keyCode || 0;
if (key == 13) {
e.preventDefault();
}
};
if (form) {
form.addEventListener("keypress", (event) => {
if (event.key == "Enter") {
event.preventDefault();
}
});
}
}
// This function will hook into the smtp-test-email input field and will call the smtpTest() function when enter is pressed.
function submitTestEmailOnEnter() {
const smtp_test_email_input = document.getElementById("smtp-test-email");
smtp_test_email_input.onkeypress = function(e) {
const key = e.charCode || e.keyCode || 0;
if (key == 13) {
e.preventDefault();
smtpTest();
}
};
if (smtp_test_email_input) {
smtp_test_email_input.addEventListener("keypress", (event) => {
if (event.key == "Enter") {
event.preventDefault();
smtpTest(event);
}
});
}
}
// Colorize some settings which are high risk
@ -124,11 +128,11 @@ function colorRiskSettings() {
});
}
function toggleVis(evt) {
function toggleVis(event) {
event.preventDefault();
event.stopPropagation();
const elem = document.getElementById(evt.target.dataset.vwPwToggle);
const elem = document.getElementById(event.target.dataset.vwPwToggle);
const type = elem.getAttribute("type");
if (type === "text") {
elem.setAttribute("type", "password");
@ -146,9 +150,11 @@ function masterCheck(check_id, inputs_query) {
}
const checkbox = document.getElementById(check_id);
const onChange = onChanged(checkbox, inputs_query);
onChange(); // Trigger the event initially
checkbox.addEventListener("change", onChange);
if (checkbox) {
const onChange = onChanged(checkbox, inputs_query);
onChange(); // Trigger the event initially
checkbox.addEventListener("change", onChange);
}
}
const config_form = document.getElementById("config-form");
@ -172,9 +178,18 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
password_toggle_btn.addEventListener("click", toggleVis);
});
document.getElementById("backupDatabase").addEventListener("click", backupDatabase);
document.getElementById("deleteConf").addEventListener("click", deleteConf);
document.getElementById("smtpTest").addEventListener("click", smtpTest);
const btnBackupDatabase = document.getElementById("backupDatabase");
if (btnBackupDatabase) {
btnBackupDatabase.addEventListener("click", backupDatabase);
}
const btnDeleteConf = document.getElementById("deleteConf");
if (btnDeleteConf) {
btnDeleteConf.addEventListener("click", deleteConf);
}
const btnSmtpTest = document.getElementById("smtpTest");
if (btnSmtpTest) {
btnSmtpTest.addEventListener("click", smtpTest);
}
config_form.addEventListener("submit", saveConfig);
});

View file

@ -1,6 +1,8 @@
"use strict";
/* eslint-env es2017, browser, jquery */
/* global _post:readable, BASE_URL:readable, reload:readable, jdenticon:readable */
function deleteUser() {
function deleteUser(event) {
event.preventDefault();
event.stopPropagation();
const id = event.target.parentNode.dataset.vwUserUuid;
@ -22,7 +24,7 @@ function deleteUser() {
}
}
function remove2fa() {
function remove2fa(event) {
event.preventDefault();
event.stopPropagation();
const id = event.target.parentNode.dataset.vwUserUuid;
@ -36,7 +38,7 @@ function remove2fa() {
);
}
function deauthUser() {
function deauthUser(event) {
event.preventDefault();
event.stopPropagation();
const id = event.target.parentNode.dataset.vwUserUuid;
@ -50,7 +52,7 @@ function deauthUser() {
);
}
function disableUser() {
function disableUser(event) {
event.preventDefault();
event.stopPropagation();
const id = event.target.parentNode.dataset.vwUserUuid;
@ -68,7 +70,7 @@ function disableUser() {
}
}
function enableUser() {
function enableUser(event) {
event.preventDefault();
event.stopPropagation();
const id = event.target.parentNode.dataset.vwUserUuid;
@ -86,7 +88,7 @@ function enableUser() {
}
}
function updateRevisions() {
function updateRevisions(event) {
event.preventDefault();
event.stopPropagation();
_post(`${BASE_URL}/admin/users/update_revision`,
@ -95,7 +97,7 @@ function updateRevisions() {
);
}
function inviteUser() {
function inviteUser(event) {
event.preventDefault();
event.stopPropagation();
const email = document.getElementById("inviteEmail");
@ -182,7 +184,7 @@ userOrgTypeDialog.addEventListener("hide.bs.modal", function() {
document.getElementById("userOrgTypeOrgUuid").value = "";
}, false);
function updateUserOrgType() {
function updateUserOrgType(event) {
event.preventDefault();
event.stopPropagation();
@ -195,26 +197,7 @@ function updateUserOrgType() {
);
}
// onLoad events
document.addEventListener("DOMContentLoaded", (/*event*/) => {
jQuery("#users-table").DataTable({
"stateSave": true,
"responsive": true,
"lengthMenu": [
[-1, 5, 10, 25, 50],
["All", 5, 10, 25, 50]
],
"pageLength": -1, // Default show all
"columnDefs": [{
"targets": [1, 2],
"type": "date-iso"
}, {
"targets": 6,
"searchable": false,
"orderable": false
}]
});
function initUserTable() {
// Color all the org buttons per type
document.querySelectorAll("button[data-vw-org-type]").forEach(function(e) {
const orgType = ORG_TYPES[e.dataset.vwOrgType];
@ -222,7 +205,6 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
e.title = orgType.name;
});
// Add click events for user actions
document.querySelectorAll("button[vw-remove2fa]").forEach(btn => {
btn.addEventListener("click", remove2fa);
});
@ -239,8 +221,51 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
btn.addEventListener("click", enableUser);
});
document.getElementById("updateRevisions").addEventListener("click", updateRevisions);
document.getElementById("reload").addEventListener("click", reload);
document.getElementById("userOrgTypeForm").addEventListener("submit", updateUserOrgType);
document.getElementById("inviteUserForm").addEventListener("submit", inviteUser);
if (jdenticon) {
jdenticon();
}
}
// onLoad events
document.addEventListener("DOMContentLoaded", (/*event*/) => {
jQuery("#users-table").DataTable({
"drawCallback": function() {
initUserTable();
},
"stateSave": true,
"responsive": true,
"lengthMenu": [
[-1, 2, 5, 10, 25, 50],
["All", 2, 5, 10, 25, 50]
],
"pageLength": 2, // Default show all
"columnDefs": [{
"targets": [1, 2],
"type": "date-iso"
}, {
"targets": 6,
"searchable": false,
"orderable": false
}]
});
// Add click events for user actions
initUserTable();
const btnUpdateRevisions = document.getElementById("updateRevisions");
if (btnUpdateRevisions) {
btnUpdateRevisions.addEventListener("click", updateRevisions);
}
const btnReload = document.getElementById("reload");
if (btnReload) {
btnReload.addEventListener("click", reload);
}
const btnUserOrgTypeForm = document.getElementById("userOrgTypeForm");
if (btnUserOrgTypeForm) {
btnUserOrgTypeForm.addEventListener("submit", updateUserOrgType);
}
const btnInviteUserForm = document.getElementById("inviteUserForm");
if (btnInviteUserForm) {
btnInviteUserForm.addEventListener("submit", inviteUser);
}
});

View file

@ -1,5 +1,5 @@
/*!
* jQuery JavaScript Library v3.6.2 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector
* jQuery JavaScript Library v3.6.3 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector
* https://jquery.com/
*
* Includes Sizzle.js
@ -9,7 +9,7 @@
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2022-12-13T14:56Z
* Date: 2022-12-20T21:28Z
*/
( function( global, factory ) {
@ -151,7 +151,7 @@ function toType( obj ) {
var
version = "3.6.2 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",
version = "3.6.3 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/Tween,-effects/animatedSelector",
// Define a local copy of jQuery
jQuery = function( selector, context ) {
@ -522,14 +522,14 @@ function isArrayLike( obj ) {
}
var Sizzle =
/*!
* Sizzle CSS Selector Engine v2.3.8
* Sizzle CSS Selector Engine v2.3.9
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2022-11-16
* Date: 2022-12-19
*/
( function( window ) {
var i,
@ -890,7 +890,7 @@ function Sizzle( selector, context, results, seed ) {
if ( support.cssSupportsSelector &&
// eslint-disable-next-line no-undef
!CSS.supports( "selector(" + newSelector + ")" ) ) {
!CSS.supports( "selector(:is(" + newSelector + "))" ) ) {
// Support: IE 11+
// Throw to get to the same code path as an error directly in qSA.
@ -1492,9 +1492,8 @@ setDocument = Sizzle.setDocument = function( node ) {
// `:has()` uses a forgiving selector list as an argument so our regular
// `try-catch` mechanism fails to catch `:has()` with arguments not supported
// natively like `:has(:contains("Foo"))`. Where supported & spec-compliant,
// we now use `CSS.supports("selector(SELECTOR_TO_BE_TESTED)")` but outside
// that, let's mark `:has` as buggy to always use jQuery traversal for
// `:has()`.
// we now use `CSS.supports("selector(:is(SELECTOR_TO_BE_TESTED))")`, but
// outside that we mark `:has` as buggy.
rbuggyQSA.push( ":has" );
}

View file

@ -5,10 +5,10 @@
<table id="orgs-table" class="table table-sm table-striped table-hover">
<thead>
<tr>
<th>Organization</th>
<th>Users</th>
<th>Items</th>
<th>Attachments</th>
<th class="vw-org-details">Organization</th>
<th class="vw-users">Users</th>
<th class="vw-items">Items</th>
<th class="vw-attachments">Attachments</th>
<th class="vw-actions">Actions</th>
</tr>
</thead>
@ -38,7 +38,7 @@
{{/if}}
</td>
<td class="text-end px-0 small">
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button>
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-organization data-vw-org-uuid="{{jsesc Id no_quote}}" data-vw-org-name="{{jsesc Name no_quote}}" data-vw-billing-email="{{jsesc BillingEmail no_quote}}">Delete Organization</button>
</td>
</tr>
{{/each}}
@ -53,7 +53,7 @@
</main>
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
<script src="{{urlpath}}/vw_static/jquery-3.6.2.slim.js"></script>
<script src="{{urlpath}}/vw_static/jquery-3.6.3.slim.js"></script>
<script src="{{urlpath}}/vw_static/datatables.js"></script>
<script src="{{urlpath}}/vw_static/admin_organizations.js"></script>
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>

View file

@ -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}}

View file

@ -5,7 +5,7 @@
<table id="users-table" class="table table-sm table-striped table-hover">
<thead>
<tr>
<th>User</th>
<th class="vw-account-details">User</th>
<th class="vw-created-at">Created at</th>
<th class="vw-last-active">Last Active</th>
<th class="vw-items">Items</th>
@ -63,14 +63,14 @@
<td class="text-end px-0 small">
<span data-vw-user-uuid="{{jsesc Id no_quote}}" data-vw-user-email="{{jsesc Email no_quote}}">
{{#if TwoFactorEnabled}}
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-remove2fa>Remove all 2FA</button>
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-remove2fa>Remove all 2FA</button>
{{/if}}
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-deauth-user>Deauthorize sessions</button>
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-delete-user>Delete User</button>
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-deauth-user>Deauthorize sessions</button>
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-delete-user>Delete User</button>
{{#if user_enabled}}
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-disable-user>Disable User</button>
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-disable-user>Disable User</button>
{{else}}
<button type="button" class="btn btn-sm btn-link p-0 border-0" vw-enable-user>Enable User</button>
<button type="button" class="btn btn-sm btn-link p-0 border-0 float-right" vw-enable-user>Enable User</button>
{{/if}}
</span>
</td>
@ -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>
@ -137,7 +137,7 @@
</main>
<link rel="stylesheet" href="{{urlpath}}/vw_static/datatables.css" />
<script src="{{urlpath}}/vw_static/jquery-3.6.2.slim.js"></script>
<script src="{{urlpath}}/vw_static/jquery-3.6.3.slim.js"></script>
<script src="{{urlpath}}/vw_static/datatables.js"></script>
<script src="{{urlpath}}/vw_static/admin_users.js"></script>
<script src="{{urlpath}}/vw_static/jdenticon.js"></script>

View 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

View 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 }}