diff --git a/Cargo.lock b/Cargo.lock index ae469d23..9edd20bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -552,6 +552,12 @@ dependencies = [ "stacker", ] +[[package]] +name = "codemap" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1231,6 +1237,19 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "grass_compiler" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9e3df7f0222ce5184154973d247c591d9aadc28ce7a73c6cd31100c9facff6" +dependencies = [ + "codemap", + "indexmap", + "lasso", + "once_cell", + "phf", +] + [[package]] name = "h2" version = "0.3.26" @@ -1886,6 +1905,15 @@ dependencies = [ "log", ] +[[package]] +name = "lasso" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -2484,6 +2512,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros", "phf_shared", ] @@ -2507,6 +2536,19 @@ dependencies = [ "rand", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_shared" version = "0.11.2" @@ -4056,6 +4098,7 @@ dependencies = [ "fern", "futures", "governor", + "grass_compiler", "handlebars", "hickory-resolver", "html5gum", diff --git a/Cargo.toml b/Cargo.toml index 78a1a4b5..150b3b9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,6 +163,9 @@ argon2 = "0.5.3" # Reading a password from the cli for generating the Argon2id ADMIN_TOKEN rpassword = "7.3.1" +# Loading a dynamic CSS Stylesheet +grass_compiler = { version = "0.13.4", default-features = false } + # Strip debuginfo from the release builds # The symbols are the provide better panic traces # Also enable fat LTO and use 1 codegen unit for optimizations diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index e715d8bd..e6654add 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -1136,15 +1136,15 @@ async fn post_auth_request( #[get("/auth-requests/")] async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - if headers.user.uuid != uuid { - err!("AuthRequest doesn't exist", "User uuid's do not match") - } - let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await { Some(auth_request) => auth_request, None => err!("AuthRequest doesn't exist", "Record not found"), }; + if headers.user.uuid != auth_request.user_uuid { + err!("AuthRequest doesn't exist", "User uuid's do not match") + } + let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); Ok(Json(json!({ @@ -1190,15 +1190,18 @@ async fn put_auth_request( err!("AuthRequest doesn't exist", "User uuid's do not match") } - auth_request.approved = Some(data.request_approved); - auth_request.enc_key = Some(data.key); - auth_request.master_password_hash = data.master_password_hash; - auth_request.response_device_id = Some(data.device_identifier.clone()); - auth_request.save(&mut conn).await?; + if data.request_approved { + auth_request.approved = Some(data.request_approved); + auth_request.enc_key = Some(data.key); + auth_request.master_password_hash = data.master_password_hash; + auth_request.response_device_id = Some(data.device_identifier.clone()); + auth_request.save(&mut conn).await?; - if auth_request.approved.unwrap_or(false) { ant.send_auth_response(&auth_request.user_uuid, &auth_request.uuid).await; nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await; + } else { + // If denied, there's no reason to keep the request + auth_request.delete(&mut conn).await?; } let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date)); diff --git a/src/api/identity.rs b/src/api/identity.rs index 672f128c..003e4d97 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -165,20 +165,22 @@ async fn _password_login( // Set the user_uuid here to be passed back used for event logging. *user_uuid = Some(user.uuid.clone()); - // Check password - let password = data.password.as_ref().unwrap(); - if let Some(auth_request_uuid) = data.auth_request.clone() { - if let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await { - if !auth_request.check_access_code(password) { - err!( - "Username or access code is incorrect. Try again", - format!("IP: {}. Username: {}.", ip.ip, username), - ErrorEvent { - event: EventType::UserFailedLogIn, - } - ) + // Check if the user is disabled + if !user.enabled { + err!( + "This user has been disabled", + format!("IP: {}. Username: {}.", ip.ip, username), + ErrorEvent { + event: EventType::UserFailedLogIn } - } else { + ) + } + + let password = data.password.as_ref().unwrap(); + + // If we get an auth request, we don't check the user's password, but the access code of the auth request + if let Some(ref auth_request_uuid) = data.auth_request { + let Some(auth_request) = AuthRequest::find_by_uuid(auth_request_uuid.as_str(), conn).await else { err!( "Auth request not found. Try again.", format!("IP: {}. Username: {}.", ip.ip, username), @@ -186,6 +188,23 @@ async fn _password_login( event: EventType::UserFailedLogIn, } ) + }; + + // Delete the request after we used it + auth_request.delete(conn).await?; + + if auth_request.user_uuid != user.uuid + || !auth_request.approved.unwrap_or(false) + || ip.ip.to_string() != auth_request.request_ip + || !auth_request.check_access_code(password) + { + err!( + "Username or access code is incorrect. Try again", + format!("IP: {}. Username: {}.", ip.ip, username), + ErrorEvent { + event: EventType::UserFailedLogIn, + } + ) } } else if !user.check_valid_password(password) { err!( @@ -197,8 +216,8 @@ async fn _password_login( ) } - // Change the KDF Iterations - if user.password_iterations != CONFIG.password_iterations() { + // Change the KDF Iterations (only when not logging in with an auth request) + if data.auth_request.is_none() && user.password_iterations != CONFIG.password_iterations() { user.password_iterations = CONFIG.password_iterations(); user.set_password(password, None, false, None); @@ -207,17 +226,6 @@ async fn _password_login( } } - // Check if the user is disabled - if !user.enabled { - err!( - "This user has been disabled", - format!("IP: {}. Username: {}.", ip.ip, username), - ErrorEvent { - event: EventType::UserFailedLogIn - } - ) - } - let now = Utc::now().naive_utc(); if user.verified_at.is_none() && CONFIG.mail_enabled() && CONFIG.signups_verify() { diff --git a/src/api/web.rs b/src/api/web.rs index 6983719b..a96d7e2a 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -1,13 +1,20 @@ +use once_cell::sync::Lazy; use std::path::{Path, PathBuf}; -use rocket::{fs::NamedFile, http::ContentType, response::content::RawHtml as Html, serde::json::Json, Catcher, Route}; +use rocket::{ + fs::NamedFile, + http::ContentType, + response::{content::RawCss as Css, content::RawHtml as Html, Redirect}, + serde::json::Json, + Catcher, Route, +}; use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, error::Error, - util::{Cached, SafeString}, + util::{get_web_vault_version, Cached, SafeString}, CONFIG, }; @@ -16,7 +23,7 @@ pub fn routes() -> Vec { // crate::utils::LOGGED_ROUTES to make sure they appear in the log let mut routes = routes![attachments, alive, alive_head, static_files]; if CONFIG.web_vault_enabled() { - routes.append(&mut routes![web_index, web_index_head, app_id, web_files]); + routes.append(&mut routes![web_index, web_index_direct, web_index_head, app_id, web_files, vaultwarden_css]); } #[cfg(debug_assertions)] @@ -45,11 +52,101 @@ fn not_found() -> ApiResult> { Ok(Html(text)) } +#[get("/css/vaultwarden.css")] +fn vaultwarden_css() -> Cached> { + // Configure the web-vault version as an integer so it can be used as a comparison smaller or greater then. + // The default is based upon the version since this feature is added. + static WEB_VAULT_VERSION: Lazy = Lazy::new(|| { + let re = regex::Regex::new(r"(\d{4})\.(\d{1,2})\.(\d{1,2})").unwrap(); + let vault_version = get_web_vault_version(); + + let (major, minor, patch) = match re.captures(&vault_version) { + Some(c) if c.len() == 4 => ( + c.get(1).unwrap().as_str().parse().unwrap(), + c.get(2).unwrap().as_str().parse().unwrap(), + c.get(3).unwrap().as_str().parse().unwrap(), + ), + _ => (2024, 6, 2), + }; + format!("{major}{minor:02}{patch:02}").parse::().unwrap() + }); + + // Configure the Vaultwarden version as an integer so it can be used as a comparison smaller or greater then. + // The default is based upon the version since this feature is added. + static VW_VERSION: Lazy = Lazy::new(|| { + let re = regex::Regex::new(r"(\d{1})\.(\d{1,2})\.(\d{1,2})").unwrap(); + let vw_version = crate::VERSION.unwrap_or("1.32.1"); + + let (major, minor, patch) = match re.captures(vw_version) { + Some(c) if c.len() == 4 => ( + c.get(1).unwrap().as_str().parse().unwrap(), + c.get(2).unwrap().as_str().parse().unwrap(), + c.get(3).unwrap().as_str().parse().unwrap(), + ), + _ => (1, 32, 1), + }; + format!("{major}{minor:02}{patch:02}").parse::().unwrap() + }); + + let css_options = json!({ + "web_vault_version": *WEB_VAULT_VERSION, + "vw_version": *VW_VERSION, + "signup_disabled": !CONFIG.signups_allowed() && CONFIG.signups_domains_whitelist().is_empty(), + "mail_enabled": CONFIG.mail_enabled(), + "yubico_enabled": CONFIG._enable_yubico() && (CONFIG.yubico_client_id().is_some() == CONFIG.yubico_secret_key().is_some()), + "emergency_access_allowed": CONFIG.emergency_access_allowed(), + "sends_allowed": CONFIG.sends_allowed(), + "load_user_scss": true, + }); + + let scss = match CONFIG.render_template("scss/vaultwarden.scss", &css_options) { + Ok(t) => t, + Err(e) => { + // Something went wrong loading the template. Use the fallback + warn!("Loading scss/vaultwarden.scss.hbs or scss/user.vaultwarden.scss.hbs failed. {e}"); + CONFIG + .render_fallback_template("scss/vaultwarden.scss", &css_options) + .expect("Fallback scss/vaultwarden.scss.hbs to render") + } + }; + + let css = match grass_compiler::from_string( + scss, + &grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed), + ) { + Ok(css) => css, + Err(e) => { + // Something went wrong compiling the scss. Use the fallback + warn!("Compiling the Vaultwarden SCSS styles failed. {e}"); + let mut css_options = css_options; + css_options["load_user_scss"] = json!(false); + let scss = CONFIG + .render_fallback_template("scss/vaultwarden.scss", &css_options) + .expect("Fallback scss/vaultwarden.scss.hbs to render"); + grass_compiler::from_string( + scss, + &grass_compiler::Options::default().style(grass_compiler::OutputStyle::Compressed), + ) + .expect("SCSS to compile") + } + }; + + // Cache for one day should be enough and not too much + Cached::ttl(Css(css), 86_400, false) +} + #[get("/")] async fn web_index() -> Cached> { Cached::short(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join("index.html")).await.ok(), false) } +// Make sure that `/index.html` redirect to actual domain path. +// If not, this might cause issues with the web-vault +#[get("/index.html")] +fn web_index_direct() -> Redirect { + Redirect::to(format!("{}/", CONFIG.domain_path())) +} + #[head("/")] fn web_index_head() -> EmptyResult { // Add an explicit HEAD route to prevent uptime monitoring services from diff --git a/src/config.rs b/src/config.rs index aa6b1145..61a47b76 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1269,11 +1269,16 @@ impl Config { let hb = load_templates(CONFIG.templates_folder()); hb.render(name, data).map_err(Into::into) } else { - let hb = &CONFIG.inner.read().unwrap().templates; + let hb = &self.inner.read().unwrap().templates; hb.render(name, data).map_err(Into::into) } } + pub fn render_fallback_template(&self, name: &str, data: &T) -> Result { + let hb = &self.inner.read().unwrap().templates; + hb.render(&format!("fallback_{name}"), data).map_err(Into::into) + } + pub fn set_rocket_shutdown_handle(&self, handle: rocket::Shutdown) { self.inner.write().unwrap().rocket_shutdown_handle = Some(handle); } @@ -1312,6 +1317,11 @@ where reg!($name); reg!(concat!($name, $ext)); }}; + (@withfallback $name:expr) => {{ + let template = include_str!(concat!("static/templates/", $name, ".hbs")); + hb.register_template_string($name, template).unwrap(); + hb.register_template_string(concat!("fallback_", $name), template).unwrap(); + }}; } // First register default templates here @@ -1355,6 +1365,9 @@ where reg!("404"); + reg!(@withfallback "scss/vaultwarden.scss"); + reg!("scss/user.vaultwarden.scss"); + // And then load user templates to overwrite the defaults // Use .hbs extension for the files // Templates get registered with their relative name diff --git a/src/static/templates/scss/user.vaultwarden.scss.hbs b/src/static/templates/scss/user.vaultwarden.scss.hbs new file mode 100644 index 00000000..c0b8ed2a --- /dev/null +++ b/src/static/templates/scss/user.vaultwarden.scss.hbs @@ -0,0 +1 @@ +/* See the wiki for examples and details: https://github.com/dani-garcia/vaultwarden/wiki/Customize-Vaultwarden-CSS */ diff --git a/src/static/templates/scss/vaultwarden.scss.hbs b/src/static/templates/scss/vaultwarden.scss.hbs new file mode 100644 index 00000000..3fc3e70e --- /dev/null +++ b/src/static/templates/scss/vaultwarden.scss.hbs @@ -0,0 +1,105 @@ +/**** START Static Vaultwarden changes ****/ +/* This combines all selectors extending it into one */ +%vw-hide { + display: none !important; +} + +/* This allows searching for the combined style in the browsers dev-tools (look into the head tag) */ +.vw-hide, +head { + @extend %vw-hide; +} + +/* Hide the Subscription Page tab */ +bit-nav-item[route="settings/subscription"] { + @extend %vw-hide; +} + +/* Hide any link pointing to Free Bitwarden Families */ +a[href$="/settings/sponsored-families"] { + @extend %vw-hide; +} + +/* Hide the `Enterprise Single Sign-On` button on the login page */ +a[routerlink="/sso"] { + @extend %vw-hide; +} + +/* Hide Two-Factor menu in Organization settings */ +bit-nav-item[route="settings/two-factor"], +a[href$="/settings/two-factor"] { + @extend %vw-hide; +} + +/* Hide Business Owned checkbox */ +app-org-info > form:nth-child(1) > div:nth-child(3) { + @extend %vw-hide; +} + +/* Hide the `This account is owned by a business` checkbox and label */ +#ownedBusiness, +label[for^="ownedBusiness"] { + @extend %vw-hide; +} + +/* Hide the radio button and label for the `Custom` org user type */ +#userTypeCustom, +label[for^="userTypeCustom"] { + @extend %vw-hide; +} + +/* Hide Business Name */ +app-org-account form div bit-form-field.tw-block:nth-child(3) { + @extend %vw-hide; +} + +/* Hide organization plans */ +app-organization-plans > form > bit-section:nth-child(2) { + @extend %vw-hide; +} + +/* Hide Device Verification form at the Two Step Login screen */ +app-security > app-two-factor-setup > form { + @extend %vw-hide; +} +/**** END Static Vaultwarden Changes ****/ +/**** START Dynamic Vaultwarden Changes ****/ +{{#if signup_disabled}} +/* Hide the register link on the login screen */ +app-frontend-layout > app-login > form > div > div > div > p { + @extend %vw-hide; +} +{{/if}} + +/* Hide `Email` 2FA if mail is not enabled */ +{{#unless mail_enabled}} +app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(5) { + @extend %vw-hide; +} +{{/unless}} + +/* Hide `YubiKey OTP security key` 2FA if it is not enabled */ +{{#unless yubico_enabled}} +app-two-factor-setup ul.list-group.list-group-2fa li.list-group-item:nth-child(2) { + @extend %vw-hide; +} +{{/unless}} + +/* Hide Emergency Access if not allowed */ +{{#unless emergency_access_allowed}} +bit-nav-item[route="settings/emergency-access"] { + @extend %vw-hide; +} +{{/unless}} + +/* Hide Sends if not allowed */ +{{#unless sends_allowed}} +bit-nav-item[route="sends"] { + @extend %vw-hide; +} +{{/unless}} +/**** End Dynamic Vaultwarden Changes ****/ +/**** Include a special user stylesheet for custom changes ****/ +{{#if load_user_scss}} +{{> scss/user.vaultwarden.scss }} +{{/if}}