diff --git a/.env.template b/.env.template index 530a6a01..8c741d83 100644 --- a/.env.template +++ b/.env.template @@ -194,11 +194,13 @@ ## Name shown in the invitation emails that don't come from a specific organization # INVITATION_ORG_NAME=Vaultwarden -## Per-organization attachment limit (KB) -## Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more +## Per-organization attachment storage limit (KB) +## Max kilobytes of attachment storage allowed per organization. +## When this limit is reached, organization members will not be allowed to upload further attachments for ciphers owned by that organization. # ORG_ATTACHMENT_LIMIT= -## Per-user attachment limit (KB). -## Limit in kilobytes for a users attachments, once the limit is exceeded it won't be possible to upload more +## Per-user attachment storage limit (KB) +## Max kilobytes of attachment storage allowed per user. +## When this limit is reached, the user will not be allowed to upload further attachments. # USER_ATTACHMENT_LIMIT= ## Number of days to wait before auto-deleting a trashed item. @@ -210,8 +212,10 @@ ## The change only applies when the password is changed # PASSWORD_ITERATIONS=100000 -## Whether password hint should be sent into the error response when the client request it -# SHOW_PASSWORD_HINT=true +## Controls whether a password hint should be shown directly in the web page if +## SMTP service is not configured. Not recommended for publicly-accessible instances +## as this provides unauthenticated access to potentially sensitive data. +# SHOW_PASSWORD_HINT=false ## Domain settings ## The domain must match the address from where you access the server diff --git a/Cargo.lock b/Cargo.lock index ab9bd9fe..fc4327df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,9 +248,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" [[package]] name = "cfg-if" @@ -323,9 +323,9 @@ dependencies = [ [[package]] name = "cookie" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf8865bac3d9a3bde5bde9088ca431b11f5d37c7a578b8086af77248b76627" +checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d" dependencies = [ "percent-encoding 2.1.0", "time 0.2.27", @@ -354,7 +354,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55b4ac5559dd39f7bdc516f769cb412b151585d8886d216871a8435ed7f862cd" dependencies = [ - "cookie 0.15.0", + "cookie 0.15.1", "idna 0.2.3", "log 0.4.14", "publicsuffix 2.1.0", @@ -873,9 +873,9 @@ checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" [[package]] name = "handlebars" -version = "4.0.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2060119114dd8a8bc87facce6384751af8280a7adc8e203c023c95cbb11f5663" +checksum = "72a0ffab8c36d0436114310c7e10b59b3307e650ddfabf6d006028e29a70c6e6" dependencies = [ "log 0.4.14", "pest", @@ -886,12 +886,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" - [[package]] name = "hashbrown" version = "0.11.2" @@ -1008,9 +1002,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.9" +version = "0.14.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83" +checksum = "7728a72c4c7d72665fde02204bcbd93b247721025b222ef78606f14513e0fd03" dependencies = [ "bytes 1.0.1", "futures-channel", @@ -1049,7 +1043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes 1.0.1", - "hyper 0.14.9", + "hyper 0.14.10", "native-tls", "tokio", "tokio-native-tls", @@ -1079,19 +1073,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", - "hashbrown 0.9.1", + "hashbrown", ] [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", ] @@ -1201,9 +1195,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.97" +version = "0.2.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" [[package]] name = "libsqlite3-sys" @@ -1670,9 +1664,9 @@ dependencies = [ [[package]] name = "parity-ws" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e02a625dd75084c2a7024f07c575b61b782f729d18702dabb3cdbf31911dc61" +checksum = "322d72dfe461b8b9e367d057ceace105379d64d5b03907d23c481ccf3fbf8aa4" dependencies = [ "byteorder", "bytes 0.4.12", @@ -1882,9 +1876,9 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" [[package]] name = "pin-project-lite" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -1972,7 +1966,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ac055aef7cc7a1caefbc65144be879e862467dcd9b8a8d57b64a13e7dce15d" dependencies = [ "byteorder", - "hashbrown 0.11.2", + "hashbrown", "idna 0.2.3", "psl-types", ] @@ -2209,7 +2203,7 @@ dependencies = [ "futures-util", "http", "http-body", - "hyper 0.14.9", + "hyper 0.14.10", "hyper-tls", "ipnet", "js-sys", @@ -2730,9 +2724,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" @@ -2801,18 +2795,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", @@ -2894,9 +2888,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.7.1" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2" +checksum = "98c8b05dc14c75ea83d63dd391100353789f5f24b8b3866542a5e85c8be8e985" dependencies = [ "autocfg", "bytes 1.0.1", @@ -3149,7 +3143,7 @@ dependencies = [ "chashmap", "chrono", "chrono-tz", - "cookie 0.15.0", + "cookie 0.15.1", "cookie_store 0.15.0", "data-encoding", "data-url", diff --git a/Cargo.toml b/Cargo.toml index ba302901..e3f5263a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ rocket_contrib = "=0.5.0-dev" reqwest = { version = "0.11.4", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies"] } # Used for custom short lived cookie jar -cookie = "0.15.0" +cookie = "0.15.1" cookie_store = "0.15.0" bytes = "1.0.1" url = "2.2.2" @@ -93,7 +93,7 @@ jsonwebtoken = "7.2.0" # U2F library u2f = "0.2.0" -webauthn-rs = "0.3.0-alpha.7" +webauthn-rs = "=0.3.0-alpha.7" # Yubico Library yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false } @@ -113,7 +113,7 @@ tracing = { version = "0.1.26", features = ["log"] } # Needed to have lettre tra lettre = { version = "0.10.0-rc.3", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false } # Template library -handlebars = { version = "4.0.1", features = ["dir_source"] } +handlebars = { version = "4.1.0", features = ["dir_source"] } # For favicon extraction from main website html5ever = "0.25.1" @@ -122,7 +122,7 @@ regex = { version = "1.5.4", features = ["std", "perf"], default-features = fals data-url = "0.1.0" # Used by U2F, JWT and Postgres -openssl = "0.10.34" +openssl = "0.10.35" # URL encoding library percent-encoding = "2.1.0" diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 8cf62f45..a38a496b 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -580,24 +580,45 @@ struct PasswordHintData { #[post("/accounts/password-hint", data = "")] fn password_hint(data: JsonUpcase, conn: DbConn) -> EmptyResult { - let data: PasswordHintData = data.into_inner().data; + if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() { + err!("This server is not configured to provide password hints."); + } - let hint = match User::find_by_mail(&data.Email, &conn) { - Some(user) => user.password_hint, - None => return Ok(()), - }; + const NO_HINT: &str = "Sorry, you have no password hint..."; - if CONFIG.mail_enabled() { - mail::send_password_hint(&data.Email, hint)?; - } else if CONFIG.show_password_hint() { - if let Some(hint) = hint { - err!(format!("Your password hint is: {}", &hint)); - } else { - err!("Sorry, you have no password hint..."); + let data: PasswordHintData = data.into_inner().data; + let email = &data.Email; + + match User::find_by_mail(email, &conn) { + None => { + // To prevent user enumeration, act as if the user exists. + if CONFIG.mail_enabled() { + // There is still a timing side channel here in that the code + // paths that send mail take noticeably longer than ones that + // don't. Add a randomized sleep to mitigate this somewhat. + use rand::{thread_rng, Rng}; + let mut rng = thread_rng(); + let base = 1000; + let delta: i32 = 100; + let sleep_ms = (base + rng.gen_range(-delta..=delta)) as u64; + std::thread::sleep(std::time::Duration::from_millis(sleep_ms)); + Ok(()) + } else { + err!(NO_HINT); + } + } + Some(user) => { + let hint: Option = user.password_hint; + if CONFIG.mail_enabled() { + mail::send_password_hint(email, hint)?; + Ok(()) + } else if let Some(hint) = hint { + err!(format!("Your password hint is: {}", hint)); + } else { + err!(NO_HINT); + } } } - - Ok(()) } #[derive(Deserialize)] diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index db455231..f9133bec 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -871,7 +871,7 @@ fn save_attachment( Some(limit_kb) => { let left = (limit_kb * 1024) - Attachment::size_by_user(user_uuid, conn) + size_adjust; if left <= 0 { - err_discard!("Attachment size limit reached! Delete some files to open space", data) + err_discard!("Attachment storage limit reached! Delete some attachments to free up space", data) } Some(left as u64) } @@ -883,7 +883,7 @@ fn save_attachment( Some(limit_kb) => { let left = (limit_kb * 1024) - Attachment::size_by_org(org_uuid, conn) + size_adjust; if left <= 0 { - err_discard!("Attachment size limit reached! Delete some files to open space", data) + err_discard!("Attachment storage limit reached! Delete some attachments to free up space", data) } Some(left as u64) } @@ -937,7 +937,7 @@ fn save_attachment( return; } SaveResult::Partial(_, reason) => { - error = Some(format!("Attachment size limit exceeded with this file: {:?}", reason)); + error = Some(format!("Attachment storage limit exceeded with this file: {:?}", reason)); return; } SaveResult::Error(e) => { diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index f0488128..c1d2326c 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -687,6 +687,19 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase p.enabled, + None => false, + }; + + if org_twofactor_policy_enabled && user_twofactor_disabled { + err!("You cannot join this organization until you enable two-step login on your user account.") + } + user_org.status = UserOrgStatus::Accepted as i32; user_org.save(&conn)?; } @@ -1039,6 +1052,24 @@ fn put_policy( None => err!("Invalid policy type"), }; + if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { + let org_list = UserOrganization::find_by_org(&org_id, &conn); + + for user_org in org_list.into_iter() { + let user_twofactor_disabled = TwoFactor::find_by_user(&user_org.user_uuid, &conn).is_empty(); + + if user_twofactor_disabled && user_org.atype < UserOrgType::Admin { + if CONFIG.mail_enabled() { + let org = Organization::find_by_uuid(&user_org.org_uuid, &conn).unwrap(); + let user = User::find_by_uuid(&user_org.user_uuid, &conn).unwrap(); + + mail::send_2fa_removed_from_org(&user.email, &org.name)?; + } + user_org.delete(&conn)?; + } + } + } + let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) { Some(p) => p, None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 39133431..df8b2d3c 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -10,6 +10,7 @@ use crate::{ api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, auth::{Headers, Host}, db::{models::*, DbConn, DbPool}, + util::SafeString, CONFIG, }; @@ -173,7 +174,7 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn Some(limit_kb) => { let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn); if left <= 0 { - err!("Attachment size limit reached! Delete some files to open space") + err!("Attachment storage limit reached! Delete some attachments to free up space") } std::cmp::Ord::max(left as u64, SIZE_525_MB) } @@ -205,7 +206,7 @@ fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn } SaveResult::Partial(_, reason) => { std::fs::remove_file(&file_path).ok(); - err!(format!("Attachment size limit exceeded with this file: {:?}", reason)); + err!(format!("Attachment storage limit exceeded with this file: {:?}", reason)); } SaveResult::Error(e) => { std::fs::remove_file(&file_path).ok(); @@ -335,7 +336,7 @@ fn post_access_file( } #[get("/sends//?")] -fn download_send(send_id: String, file_id: String, t: String) -> Option { +fn download_send(send_id: SafeString, file_id: SafeString, t: String) -> Option { if let Ok(claims) = crate::auth::decode_send(&t) { if claims.sub == format!("{}/{}", send_id, file_id) { return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).ok(); diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 83661e37..d8448f45 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -7,10 +7,8 @@ use crate::{ api::{JsonResult, JsonUpcase, NumberOrString, PasswordData}, auth::Headers, crypto, - db::{ - models::{TwoFactor, User}, - DbConn, - }, + db::{models::*, DbConn}, + mail, CONFIG, }; pub mod authenticator; @@ -130,6 +128,23 @@ fn disable_twofactor(data: JsonUpcase, headers: Headers, c twofactor.delete(&conn)?; } + let twofactor_disabled = TwoFactor::find_by_user(&user.uuid, &conn).is_empty(); + + if twofactor_disabled { + let policy_type = OrgPolicyType::TwoFactorAuthentication; + let org_list = UserOrganization::find_by_user_and_policy(&user.uuid, policy_type, &conn); + + for user_org in org_list.into_iter() { + if user_org.atype < UserOrgType::Admin { + if CONFIG.mail_enabled() { + let org = Organization::find_by_uuid(&user_org.org_uuid, &conn).unwrap(); + mail::send_2fa_removed_from_org(&user.email, &org.name)?; + } + user_org.delete(&conn)?; + } + } + } + Ok(Json(json!({ "Enabled": false, "Type": type_, diff --git a/src/api/web.rs b/src/api/web.rs index 37950f0c..e543bc00 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -4,7 +4,7 @@ use rocket::{http::ContentType, response::content::Content, response::NamedFile, use rocket_contrib::json::Json; use serde_json::Value; -use crate::{error::Error, util::Cached, CONFIG}; +use crate::{CONFIG, error::Error, util::{Cached, SafeString}}; pub fn routes() -> Vec { // If addding more routes here, consider also adding them to @@ -56,7 +56,7 @@ fn web_files(p: PathBuf) -> Cached> { } #[get("/attachments//")] -fn attachments(uuid: String, file_id: String) -> Option { +fn attachments(uuid: SafeString, file_id: SafeString) -> Option { NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).ok() } diff --git a/src/config.rs b/src/config.rs index 6b4fce59..6e0c9f65 100644 --- a/src/config.rs +++ b/src/config.rs @@ -356,9 +356,9 @@ make_config! { /// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key hibp_api_key: Pass, true, option; - /// Per-user attachment limit (KB) |> Limit in kilobytes for a users attachments, once the limit is exceeded it won't be possible to upload more + /// Per-user attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per user. When this limit is reached, the user will not be allowed to upload further attachments. user_attachment_limit: i64, true, option; - /// Per-organization attachment limit (KB) |> Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more + /// Per-organization attachment storage limit (KB) |> Max kilobytes of attachment storage allowed per org. When this limit is reached, org members will not be allowed to upload further attachments for ciphers owned by that org. org_attachment_limit: i64, true, option; /// Trash auto-delete days |> Number of days to wait before auto-deleting a trashed item. @@ -388,9 +388,10 @@ make_config! { /// Password iterations |> Number of server-side passwords hashing iterations. /// The changes only apply when a user changes their password. Not recommended to lower the value password_iterations: i32, true, def, 100_000; - /// Show password hints |> Controls if the password hint should be shown directly in the web page. - /// Otherwise, if email is disabled, there is no way to see the password hint - show_password_hint: bool, true, def, true; + /// Show password hint |> Controls whether a password hint should be shown directly in the web page + /// if SMTP service is not configured. Not recommended for publicly-accessible instances as this + /// provides unauthenticated access to potentially sensitive data. + show_password_hint: bool, true, def, false; /// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session admin_token: Pass, true, option; @@ -857,6 +858,7 @@ where reg!("email/new_device_logged_in", ".html"); reg!("email/pw_hint_none", ".html"); reg!("email/pw_hint_some", ".html"); + reg!("email/send_2fa_removed_from_org", ".html"); reg!("email/send_org_invite", ".html"); reg!("email/twofactor_email", ".html"); reg!("email/verify_email", ".html"); diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 06d7a5fd..34eaedb1 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -22,7 +22,7 @@ db_object! { } } -#[derive(Copy, Clone, num_derive::FromPrimitive)] +#[derive(Copy, Clone, PartialEq, num_derive::FromPrimitive)] pub enum OrgPolicyType { TwoFactorAuthentication = 0, MasterPassword = 1, diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index f628d95f..e5141bb8 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -2,7 +2,7 @@ use num_traits::FromPrimitive; use serde_json::Value; use std::cmp::Ordering; -use super::{CollectionUser, OrgPolicy, User}; +use super::{CollectionUser, OrgPolicy, OrgPolicyType, User}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -544,6 +544,25 @@ impl UserOrganization { }} } + pub fn find_by_user_and_policy(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> Vec { + db_run! { conn: { + users_organizations::table + .inner_join( + org_policies::table.on( + org_policies::org_uuid.eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid)) + .and(org_policies::atype.eq(policy_type as i32)) + .and(org_policies::enabled.eq(true))) + ) + .filter( + users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + ) + .select(users_organizations::all_columns) + .load::(conn) + .unwrap_or_default().from_db() + }} + } + pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec { db_run! { conn: { users_organizations::table diff --git a/src/error.rs b/src/error.rs index 0378d4ad..b944be03 100644 --- a/src/error.rs +++ b/src/error.rs @@ -166,7 +166,7 @@ fn _serialize(e: &impl serde::Serialize, _msg: &str) -> String { fn _api_error(_: &impl std::any::Any, msg: &str) -> String { let json = json!({ - "Message": "", + "Message": msg, "error": "", "error_description": "", "ValidationErrors": {"": [ msg ]}, diff --git a/src/mail.rs b/src/mail.rs index 1cd875b8..094f5125 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -180,6 +180,18 @@ pub fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult { send_email(address, &subject, body_html, body_text) } +pub fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/send_2fa_removed_from_org", + json!({ + "url": CONFIG.domain(), + "org_name": org_name, + }), + )?; + + send_email(address, &subject, body_html, body_text) +} + pub fn send_invite( address: &str, uuid: &str, diff --git a/src/static/templates/email/email_footer.hbs b/src/static/templates/email/email_footer.hbs index b5aafc39..6d5f7be6 100644 --- a/src/static/templates/email/email_footer.hbs +++ b/src/static/templates/email/email_footer.hbs @@ -1,23 +1,22 @@ - - - - - + + - + + + + + + + + diff --git a/src/static/templates/email/send_2fa_removed_from_org.hbs b/src/static/templates/email/send_2fa_removed_from_org.hbs new file mode 100644 index 00000000..95d41b97 --- /dev/null +++ b/src/static/templates/email/send_2fa_removed_from_org.hbs @@ -0,0 +1,9 @@ +Removed from {{{org_name}}} + +You have been removed from organization *{{org_name}}* because your account does not have Two-step Login enabled. + + +You can enable Two-step Login in your account settings. + +=== +Github: https://github.com/dani-garcia/vaultwarden diff --git a/src/static/templates/email/send_2fa_removed_from_org.html.hbs b/src/static/templates/email/send_2fa_removed_from_org.html.hbs new file mode 100644 index 00000000..6588a320 --- /dev/null +++ b/src/static/templates/email/send_2fa_removed_from_org.html.hbs @@ -0,0 +1,16 @@ +Removed from {{{org_name}}} + +{{> email/email_header }} + + + + + + + +
+ You have been removed from organization {{org_name}} because your account does not have Two-step Login enabled. +
+ You can enable Two-step Login in your account settings. +
+{{> email/email_footer }} diff --git a/src/util.rs b/src/util.rs index 8512bc7b..f483b1dc 100644 --- a/src/util.rs +++ b/src/util.rs @@ -5,7 +5,8 @@ use std::io::Cursor; use rocket::{ fairing::{Fairing, Info, Kind}, - http::{ContentType, Header, HeaderMap, Method, Status}, + http::{ContentType, Header, HeaderMap, Method, RawStr, Status}, + request::FromParam, response::{self, Responder}, Data, Request, Response, Rocket, }; @@ -29,7 +30,10 @@ impl Fairing for AppHeaders { res.set_raw_header("X-Content-Type-Options", "nosniff"); res.set_raw_header("X-XSS-Protection", "1; mode=block"); let csp = format!( - "frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb moz-extension://* {};", + // Chrome Web Store: https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb + // Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh?hl=en-US + // Firefox Browser Add-ons: https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/ + "frame-ancestors 'self' chrome-extension://nngceckbapebfimnlniiiahkandclblb chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh moz-extension://* {};", CONFIG.allowed_iframe_ancestors() ); res.set_raw_header("Content-Security-Policy", csp); @@ -125,6 +129,36 @@ impl<'r, R: Responder<'r>> Responder<'r> for Cached { } } +pub struct SafeString(String); + +impl std::fmt::Display for SafeString { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl AsRef for SafeString { + #[inline] + fn as_ref(&self) -> &Path { + Path::new(&self.0) + } +} + +impl<'r> FromParam<'r> for SafeString { + type Error = (); + + #[inline(always)] + fn from_param(param: &'r RawStr) -> Result { + let s = param.percent_decode().map(|cow| cow.into_owned()).map_err(|_| ())?; + + if s.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) { + Ok(SafeString(s)) + } else { + Err(()) + } + } +} + // Log all the routes from the main paths list, and the attachments endpoint // Effectively ignores, any static file route, and the alive endpoint const LOGGED_ROUTES: [&str; 6] =