vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 85adcf1ae54b1f48b6f68162494c9d3c332c2163
parent 9abb4d2873b8295eb59d5e695f394ade43ecf346
Author: Daniel García <dani-garcia@users.noreply.github.com>
Date:   Tue, 19 Jan 2021 21:58:21 +0100

Merge pull request #1316 from BlackDex/admin-interface

Updated the admin interface
Diffstat:
Msrc/api/admin.rs | 100++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/api/core/mod.rs | 2+-
Msrc/config.rs | 75++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/main.rs | 2+-
Msrc/static/images/hibp.png | 0
Msrc/static/images/logo-gray.png | 0
Msrc/static/images/mail-github.png | 0
Msrc/static/images/shield-white.png | 0
Msrc/static/scripts/bootstrap-native.js | 55+++++++++++++++++++++++++++++++++----------------------
Msrc/static/scripts/bootstrap.css | 12+++++++++---
Msrc/static/scripts/datatables.css | 19++++++++++---------
Msrc/static/scripts/datatables.js | 20++++++++------------
Msrc/static/templates/admin/base.hbs | 5+++--
Msrc/static/templates/admin/diagnostics.hbs | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/static/templates/admin/login.hbs | 2+-
Msrc/static/templates/admin/organizations.hbs | 2+-
Msrc/static/templates/admin/settings.hbs | 2+-
Msrc/static/templates/admin/users.hbs | 57+++++++++++++++++++++++++++++++++++++++++++++------------
18 files changed, 384 insertions(+), 108 deletions(-)

diff --git a/src/api/admin.rs b/src/api/admin.rs @@ -1,8 +1,9 @@ use once_cell::sync::Lazy; use serde::de::DeserializeOwned; use serde_json::Value; -use std::process::Command; +use std::{env, process::Command, time::Duration}; +use reqwest::{blocking::Client, header::USER_AGENT}; use rocket::{ http::{Cookie, Cookies, SameSite}, request::{self, FlashMessage, Form, FromRequest, Outcome, Request}, @@ -18,7 +19,7 @@ use crate::{ db::{backup_database, models::*, DbConn, DbConnType}, error::{Error, MapResult}, mail, - util::{get_display_size, format_naive_datetime_local}, + util::{format_naive_datetime_local, get_display_size}, CONFIG, }; @@ -47,9 +48,20 @@ pub fn routes() -> Vec<Route> { users_overview, organizations_overview, diagnostics, + get_diagnostics_config ] } +static DB_TYPE: Lazy<&str> = Lazy::new(|| { + DbConnType::from_url(&CONFIG.database_url()) + .map(|t| match t { + DbConnType::sqlite => "SQLite", + DbConnType::mysql => "MySQL", + DbConnType::postgresql => "PostgreSQL", + }) + .unwrap_or("Unknown") +}); + static CAN_BACKUP: Lazy<bool> = Lazy::new(|| { DbConnType::from_url(&CONFIG.database_url()) .map(|t| t == DbConnType::sqlite) @@ -307,7 +319,8 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { None => json!("Never") }; usr - }).collect(); + }) + .collect(); let text = AdminTemplateData::users(users_json).render()?; Ok(Html(text)) @@ -362,14 +375,16 @@ fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { #[get("/organizations/overview")] fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> { let organizations = Organization::get_all(&conn); - let organizations_json: Vec<Value> = organizations.iter().map(|o| { + let organizations_json: Vec<Value> = organizations.iter() + .map(|o| { let mut org = o.to_json(); org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn)); org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &conn)); org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &conn)); org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32)); org - }).collect(); + }) + .collect(); let text = AdminTemplateData::organizations(organizations_json).render()?; Ok(Html(text)) @@ -391,77 +406,104 @@ struct GitCommit { } fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> { - use reqwest::{blocking::Client, header::USER_AGENT}; - use std::time::Duration; let github_api = Client::builder().build()?; - Ok( - github_api.get(url) + Ok(github_api + .get(url) .timeout(Duration::from_secs(10)) .header(USER_AGENT, "Bitwarden_RS") .send()? .error_for_status()? - .json::<T>()? - ) + .json::<T>()?) +} + +fn has_http_access() -> bool { + let http_access = Client::builder().build().unwrap(); + + match http_access + .head("https://github.com/dani-garcia/bitwarden_rs") + .timeout(Duration::from_secs(10)) + .header(USER_AGENT, "Bitwarden_RS") + .send() + { + Ok(r) => r.status().is_success(), + _ => false, + } } #[get("/diagnostics")] fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> { - use std::net::ToSocketAddrs; - use chrono::prelude::*; use crate::util::read_file_string; + use chrono::prelude::*; + use std::net::ToSocketAddrs; + // Get current running versions let vault_version_path = format!("{}/{}", CONFIG.web_vault_folder(), "version.json"); let vault_version_str = read_file_string(&vault_version_path)?; let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?; - let github_ips = ("github.com", 0).to_socket_addrs().map(|mut i| i.next()); - let (dns_resolved, dns_ok) = match github_ips { - Ok(Some(a)) => (a.ip().to_string(), true), - _ => ("Could not resolve domain name.".to_string(), false), + // Execute some environment checks + let running_within_docker = std::path::Path::new("/.dockerenv").exists(); + let has_http_access = has_http_access(); + let uses_proxy = env::var_os("HTTP_PROXY").is_some() + || env::var_os("http_proxy").is_some() + || env::var_os("HTTPS_PROXY").is_some() + || env::var_os("https_proxy").is_some(); + + // Check if we are able to resolve DNS entries + let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) { + Ok(Some(a)) => a.ip().to_string(), + _ => "Could not resolve domain name.".to_string(), }; - // If the DNS Check failed, do not even attempt to check for new versions since we were not able to resolve github.com - let (latest_release, latest_commit, latest_web_build) = if dns_ok { + // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway. + // TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already. + let (latest_release, latest_commit, latest_web_build) = if has_http_access { ( match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bitwarden_rs/releases/latest") { Ok(r) => r.tag_name, - _ => "-".to_string() + _ => "-".to_string(), }, match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/bitwarden_rs/commits/master") { Ok(mut c) => { c.sha.truncate(8); c.sha - }, - _ => "-".to_string() + } + _ => "-".to_string(), }, match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest") { Ok(r) => r.tag_name.trim_start_matches('v').to_string(), - _ => "-".to_string() + _ => "-".to_string(), }, ) } else { ("-".to_string(), "-".to_string(), "-".to_string()) }; - // Run the date check as the last item right before filling the json. - // This should ensure that the time difference between the browser and the server is as minimal as possible. - let dt = Utc::now(); - let server_time = dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(); - let diagnostics_json = json!({ "dns_resolved": dns_resolved, - "server_time": server_time, "web_vault_version": web_vault_version.version, "latest_release": latest_release, "latest_commit": latest_commit, "latest_web_build": latest_web_build, + "running_within_docker": running_within_docker, + "has_http_access": has_http_access, + "uses_proxy": uses_proxy, + "db_type": *DB_TYPE, + "admin_url": format!("{}/diagnostics", admin_url(Referer(None))), + "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference }); let text = AdminTemplateData::diagnostics(diagnostics_json).render()?; Ok(Html(text)) } +#[get("/diagnostics/config")] +fn get_diagnostics_config(_token: AdminToken) -> JsonResult { + let support_json = CONFIG.get_support_json(); + Ok(Json(support_json)) +} + #[post("/config", data = "<data>")] fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult { let data: ConfigBuilder = data.into_inner(); diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -172,7 +172,7 @@ fn hibp_breach(username: String) -> JsonResult { "Domain": "haveibeenpwned.com", "BreachDate": "2019-08-18T00:00:00Z", "AddedDate": "2019-08-18T00:00:00Z", - "Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username), + "Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noreferrer\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username), "LogoPath": "bwrs_static/hibp.png", "PwnCount": 0, "DataClasses": [ diff --git a/src/config.rs b/src/config.rs @@ -2,6 +2,7 @@ use std::process::exit; use std::sync::RwLock; use once_cell::sync::Lazy; +use regex::Regex; use reqwest::Url; use crate::{ @@ -22,6 +23,21 @@ pub static CONFIG: Lazy<Config> = Lazy::new(|| { }) }); +static PRIVACY_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[\w]").unwrap()); +const PRIVACY_CONFIG: &[&str] = &[ + "allowed_iframe_ancestors", + "database_url", + "domain_origin", + "domain_path", + "domain", + "helo_name", + "org_creation_users", + "signups_domains_whitelist", + "smtp_from", + "smtp_host", + "smtp_username", +]; + pub type Pass = String; macro_rules! make_config { @@ -52,6 +68,7 @@ macro_rules! make_config { } impl ConfigBuilder { + #[allow(clippy::field_reassign_with_default)] fn from_env() -> Self { match dotenv::from_path(".env") { Ok(_) => (), @@ -196,8 +213,37 @@ macro_rules! make_config { }, )+ ]}, )+ ]) } + + pub fn get_support_json(&self) -> serde_json::Value { + let cfg = { + let inner = &self.inner.read().unwrap(); + inner.config.clone() + }; + + json!({ $($( + stringify!($name): make_config!{ @supportstr $name, cfg.$name, $ty, $none_action }, + )+)+ }) + } + } + }; + + // Support string print + ( @supportstr $name:ident, $value:expr, Pass, option ) => { $value.as_ref().map(|_| String::from("***")) }; // Optional pass, we map to an Option<String> with "***" + ( @supportstr $name:ident, $value:expr, Pass, $none_action:ident ) => { String::from("***") }; // Required pass, we return "***" + ( @supportstr $name:ident, $value:expr, $ty:ty, option ) => { // Optional other value, we return as is or convert to string to apply the privacy config + if PRIVACY_CONFIG.contains(&stringify!($name)) { + json!($value.as_ref().map(|x| PRIVACY_REGEX.replace_all(&x.to_string(), "${1}*").to_string())) + } else { + json!($value) } }; + ( @supportstr $name:ident, $value:expr, $ty:ty, $none_action:ident ) => { // Required other value, we return as is or convert to string to apply the privacy config + if PRIVACY_CONFIG.contains(&stringify!($name)) { + json!(PRIVACY_REGEX.replace_all(&$value.to_string(), "${1}*").to_string()) + } else { + json!($value) + } + }; // Group or empty string ( @show ) => { "" }; @@ -458,7 +504,6 @@ make_config! { } fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { - // Validate connection URL is valid and DB feature is enabled DbConnType::from_url(&cfg.database_url)?; @@ -472,7 +517,9 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { let dom = cfg.domain.to_lowercase(); if !dom.starts_with("http://") && !dom.starts_with("https://") { - err!("DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'"); + err!( + "DOMAIN variable needs to contain the protocol (http, https). Use 'http[s]://bw.example.com' instead of 'bw.example.com'" + ); } let whitelist = &cfg.signups_domains_whitelist; @@ -481,10 +528,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } let org_creation_users = cfg.org_creation_users.trim().to_lowercase(); - if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none") { - if org_creation_users.split(',').any(|u| !u.contains('@')) { - err!("`ORG_CREATION_USERS` contains invalid email addresses"); - } + if !(org_creation_users.is_empty() || org_creation_users == "all" || org_creation_users == "none") + && org_creation_users.split(',').any(|u| !u.contains('@')) + { + err!("`ORG_CREATION_USERS` contains invalid email addresses"); } if let Some(ref token) = cfg.admin_token { @@ -529,7 +576,6 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { // Check if the icon blacklist regex is valid if let Some(ref r) = cfg.icon_blacklist_regex { - use regex::Regex; let validate_regex = Regex::new(&r); match validate_regex { Ok(_) => (), @@ -577,7 +623,12 @@ impl Config { validate_config(&config)?; Ok(Config { - inner: RwLock::new(Inner { templates: load_templates(&config.templates_folder), config, _env, _usr }), + inner: RwLock::new(Inner { + templates: load_templates(&config.templates_folder), + config, + _env, + _usr, + }), }) } @@ -650,7 +701,7 @@ impl Config { /// Tests whether the specified user is allowed to create an organization. pub fn is_org_creation_allowed(&self, email: &str) -> bool { let users = self.org_creation_users(); - if users == "" || users == "all" { + if users.is_empty() || users == "all" { true } else if users == "none" { false @@ -704,8 +755,10 @@ impl Config { let akey_s = data_encoding::BASE64.encode(&akey); // Save the new value - let mut builder = ConfigBuilder::default(); - builder._duo_akey = Some(akey_s.clone()); + let builder = ConfigBuilder { + _duo_akey: Some(akey_s.clone()), + ..Default::default() + }; self.update_config_partial(builder).ok(); akey_s diff --git a/src/main.rs b/src/main.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] #![cfg_attr(feature = "unstable", feature(ip))] -#![recursion_limit = "256"] +#![recursion_limit = "512"] extern crate openssl; #[macro_use] diff --git a/src/static/images/hibp.png b/src/static/images/hibp.png Binary files differ. diff --git a/src/static/images/logo-gray.png b/src/static/images/logo-gray.png Binary files differ. diff --git a/src/static/images/mail-github.png b/src/static/images/mail-github.png Binary files differ. diff --git a/src/static/images/shield-white.png b/src/static/images/shield-white.png Binary files differ. diff --git a/src/static/scripts/bootstrap-native.js b/src/static/scripts/bootstrap-native.js @@ -1,6 +1,6 @@ /*! - * Native JavaScript for Bootstrap v3.0.10 (https://thednp.github.io/bootstrap.native/) - * Copyright 2015-2020 © dnp_theme + * Native JavaScript for Bootstrap v3.0.15 (https://thednp.github.io/bootstrap.native/) + * Copyright 2015-2021 © dnp_theme * Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE) */ (function (global, factory) { @@ -15,10 +15,14 @@ var transitionDuration = 'webkitTransition' in document.head.style ? 'webkitTransitionDuration' : 'transitionDuration'; + var transitionProperty = 'webkitTransition' in document.head.style ? 'webkitTransitionProperty' : 'transitionProperty'; + function getElementTransitionDuration(element) { - var duration = supportTransition ? parseFloat(getComputedStyle(element)[transitionDuration]) : 0; - duration = typeof duration === 'number' && !isNaN(duration) ? duration * 1000 : 0; - return duration; + var computedStyle = getComputedStyle(element), + property = computedStyle[transitionProperty], + duration = supportTransition && property && property !== 'none' + ? parseFloat(computedStyle[transitionDuration]) : 0; + return !isNaN(duration) ? duration * 1000 : 0; } function emulateTransitionEnd(element,handler){ @@ -35,9 +39,15 @@ return selector instanceof Element ? selector : lookUp.querySelector(selector); } - function bootstrapCustomEvent(eventName, componentName, related) { + function bootstrapCustomEvent(eventName, componentName, eventProperties) { var OriginalCustomEvent = new CustomEvent( eventName + '.bs.' + componentName, {cancelable: true}); - OriginalCustomEvent.relatedTarget = related; + if (typeof eventProperties !== 'undefined') { + Object.keys(eventProperties).forEach(function (key) { + Object.defineProperty(OriginalCustomEvent, key, { + value: eventProperties[key] + }); + }); + } return OriginalCustomEvent; } @@ -352,7 +362,7 @@ }; self.slideTo = function (next) { if (vars.isSliding) { return; } - var activeItem = self.getActiveIndex(), orientation; + var activeItem = self.getActiveIndex(), orientation, eventProperties; if ( activeItem === next ) { return; } else if ( (activeItem < next ) || (activeItem === 0 && next === slides.length -1 ) ) { @@ -363,8 +373,9 @@ if ( next < 0 ) { next = slides.length - 1; } else if ( next >= slides.length ){ next = 0; } orientation = vars.direction === 'left' ? 'next' : 'prev'; - slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', slides[next]); - slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', slides[next]); + eventProperties = { relatedTarget: slides[next], direction: vars.direction, from: activeItem, to: next }; + slideCustomEvent = bootstrapCustomEvent('slide', 'carousel', eventProperties); + slidCustomEvent = bootstrapCustomEvent('slid', 'carousel', eventProperties); dispatchCustomEvent.call(element, slideCustomEvent); if (slideCustomEvent.defaultPrevented) { return; } vars.index = next; @@ -615,7 +626,7 @@ } } self.show = function () { - showCustomEvent = bootstrapCustomEvent('show', 'dropdown', relatedTarget); + showCustomEvent = bootstrapCustomEvent('show', 'dropdown', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(parent, showCustomEvent); if ( showCustomEvent.defaultPrevented ) { return; } menu.classList.add('show'); @@ -626,12 +637,12 @@ setTimeout(function () { setFocus( menu.getElementsByTagName('INPUT')[0] || element ); toggleDismiss(); - shownCustomEvent = bootstrapCustomEvent( 'shown', 'dropdown', relatedTarget); + shownCustomEvent = bootstrapCustomEvent('shown', 'dropdown', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(parent, shownCustomEvent); },1); }; self.hide = function () { - hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', relatedTarget); + hideCustomEvent = bootstrapCustomEvent('hide', 'dropdown', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(parent, hideCustomEvent); if ( hideCustomEvent.defaultPrevented ) { return; } menu.classList.remove('show'); @@ -643,7 +654,7 @@ setTimeout(function () { element.Dropdown && element.addEventListener('click',clickHandler,false); },1); - hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', relatedTarget); + hiddenCustomEvent = bootstrapCustomEvent('hidden', 'dropdown', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(parent, hiddenCustomEvent); }; self.toggle = function () { @@ -749,7 +760,7 @@ setFocus(modal); modal.isAnimating = false; toggleEvents(1); - shownCustomEvent = bootstrapCustomEvent('shown', 'modal', relatedTarget); + shownCustomEvent = bootstrapCustomEvent('shown', 'modal', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(modal, shownCustomEvent); } function triggerHide(force) { @@ -804,7 +815,7 @@ }; self.show = function () { if (modal.classList.contains('show') && !!modal.isAnimating ) {return} - showCustomEvent = bootstrapCustomEvent('show', 'modal', relatedTarget); + showCustomEvent = bootstrapCustomEvent('show', 'modal', { relatedTarget: relatedTarget }); dispatchCustomEvent.call(modal, showCustomEvent); if ( showCustomEvent.defaultPrevented ) { return; } modal.isAnimating = true; @@ -1193,7 +1204,7 @@ if (dropLink && !dropLink.classList.contains('active') ) { dropLink.classList.add('active'); } - dispatchCustomEvent.call(element, bootstrapCustomEvent( 'activate', 'scrollspy', vars.items[index])); + dispatchCustomEvent.call(element, bootstrapCustomEvent( 'activate', 'scrollspy', { relatedTarget: vars.items[index] })); } else if ( isActive && !inside ) { item.classList.remove('active'); if (dropLink && dropLink.classList.contains('active') && !item.parentNode.getElementsByClassName('active').length ) { @@ -1278,7 +1289,7 @@ } else { tabs.isAnimating = false; } - shownCustomEvent = bootstrapCustomEvent('shown', 'tab', activeTab); + shownCustomEvent = bootstrapCustomEvent('shown', 'tab', { relatedTarget: activeTab }); dispatchCustomEvent.call(next, shownCustomEvent); } function triggerHide() { @@ -1287,8 +1298,8 @@ nextContent.style.float = 'left'; containerHeight = activeContent.scrollHeight; } - showCustomEvent = bootstrapCustomEvent('show', 'tab', activeTab); - hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', next); + showCustomEvent = bootstrapCustomEvent('show', 'tab', { relatedTarget: activeTab }); + hiddenCustomEvent = bootstrapCustomEvent('hidden', 'tab', { relatedTarget: next }); dispatchCustomEvent.call(next, showCustomEvent); if ( showCustomEvent.defaultPrevented ) { return; } nextContent.classList.add('active'); @@ -1331,7 +1342,7 @@ nextContent = queryElement(next.getAttribute('href')); activeTab = getActiveTab(); activeContent = getActiveContent(); - hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', next); + hideCustomEvent = bootstrapCustomEvent( 'hide', 'tab', { relatedTarget: next }); dispatchCustomEvent.call(activeTab, hideCustomEvent); if (hideCustomEvent.defaultPrevented) { return; } tabs.isAnimating = true; @@ -1637,7 +1648,7 @@ } } - var version = "3.0.10"; + var version = "3.0.15"; var index = { Alert: Alert, diff --git a/src/static/scripts/bootstrap.css b/src/static/scripts/bootstrap.css @@ -1,10 +1,10 @@ /*! - * Bootstrap v4.5.2 (https://getbootstrap.com/) + * Bootstrap v4.5.3 (https://getbootstrap.com/) * Copyright 2011-2020 The Bootstrap Authors * Copyright 2011-2020 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ - :root { +:root { --blue: #007bff; --indigo: #6610f2; --purple: #6f42c1; @@ -216,6 +216,7 @@ caption { th { text-align: inherit; + text-align: -webkit-match-parent; } label { @@ -3750,6 +3751,8 @@ input[type="button"].btn-block { display: block; min-height: 1.5rem; padding-left: 1.5rem; + -webkit-print-color-adjust: exact; + color-adjust: exact; } .custom-control-inline { @@ -5289,6 +5292,7 @@ a.badge-dark:focus, a.badge-dark.focus { position: absolute; top: 0; right: 0; + z-index: 2; padding: 0.75rem 1.25rem; color: inherit; } @@ -10163,7 +10167,7 @@ a.text-dark:hover, a.text-dark:focus { .text-break { word-break: break-word !important; - overflow-wrap: break-word !important; + word-wrap: break-word !important; } .text-reset { @@ -10256,3 +10260,4 @@ a.text-dark:hover, a.text-dark:focus { border-color: #dee2e6; } } +/*# sourceMappingURL=bootstrap.css.map */ +\ No newline at end of file diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css @@ -4,12 +4,13 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs4/dt-1.10.22 + * https://datatables.net/download/#bs4/dt-1.10.23 * * Included libraries: - * DataTables 1.10.22 + * DataTables 1.10.23 */ +@charset "UTF-8"; table.dataTable { clear: both; margin-top: 6px !important; @@ -114,7 +115,7 @@ table.dataTable > thead .sorting_desc:before, table.dataTable > thead .sorting_asc_disabled:before, table.dataTable > thead .sorting_desc_disabled:before { right: 1em; - content: "\2191"; + content: "↑"; } table.dataTable > thead .sorting:after, table.dataTable > thead .sorting_asc:after, @@ -122,7 +123,7 @@ table.dataTable > thead .sorting_desc:after, table.dataTable > thead .sorting_asc_disabled:after, table.dataTable > thead .sorting_desc_disabled:after { right: 0.5em; - content: "\2193"; + content: "↓"; } table.dataTable > thead .sorting_asc:before, table.dataTable > thead .sorting_desc:after { @@ -165,9 +166,9 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { @media screen and (max-width: 767px) { div.dataTables_wrapper div.dataTables_length, - div.dataTables_wrapper div.dataTables_filter, - div.dataTables_wrapper div.dataTables_info, - div.dataTables_wrapper div.dataTables_paginate { +div.dataTables_wrapper div.dataTables_filter, +div.dataTables_wrapper div.dataTables_info, +div.dataTables_wrapper div.dataTables_paginate { text-align: center; } div.dataTables_wrapper div.dataTables_paginate ul.pagination { @@ -213,10 +214,10 @@ div.dataTables_scrollHead table.table-bordered { div.table-responsive > div.dataTables_wrapper > div.row { margin: 0; } -div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { +div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:first-child { padding-left: 0; } -div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { +div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last-child { padding-right: 0; } diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js @@ -4,20 +4,20 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs4/dt-1.10.22 + * https://datatables.net/download/#bs4/dt-1.10.23 * * Included libraries: - * DataTables 1.10.22 + * DataTables 1.10.23 */ -/*! DataTables 1.10.22 +/*! DataTables 1.10.23 * ©2008-2020 SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 1.10.22 + * @version 1.10.23 * @file jquery.dataTables.js * @author SpryMedia Ltd * @contact www.datatables.net @@ -2775,7 +2775,7 @@ for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ ) { // Protect against prototype pollution - if (a[i] === '__proto__') { + if (a[i] === '__proto__' || a[i] === 'constructor') { throw new Error('Cannot set prototype values'); } @@ -3157,7 +3157,7 @@ cells.push( nTd ); // Need to create the HTML if new, or if a rendering function is defined - if ( create || ((!nTrIn || oCol.mRender || oCol.mData !== i) && + if ( create || ((oCol.mRender || oCol.mData !== i) && (!$.isPlainObject(oCol.mData) || oCol.mData._ !== i+'.display') )) { nTd.innerHTML = _fnGetCellData( oSettings, iRow, i, 'display' ); @@ -3189,10 +3189,6 @@ _fnCallbackFire( oSettings, 'aoRowCreatedCallback', null, [nTr, rowData, iRow, cells] ); } - - // Remove once webkit bug 131819 and Chromium bug 365619 have been resolved - // and deployed - row.nTr.setAttribute( 'role', 'row' ); } @@ -9546,7 +9542,7 @@ * @type string * @default Version number */ - DataTable.version = "1.10.22"; + DataTable.version = "1.10.23"; /** * Private data store, containing all of the settings objects that are @@ -13970,7 +13966,7 @@ * * @type string */ - build:"bs4/dt-1.10.22", + build:"bs4/dt-1.10.23", /** diff --git a/src/static/templates/admin/base.hbs b/src/static/templates/admin/base.hbs @@ -4,6 +4,7 @@ <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="robots" content="noindex,nofollow" /> + <link rel="icon" type="image/png" href="{{urlpath}}/bwrs_static/shield-white.png"> <title>Bitwarden_rs Admin Panel</title> <link rel="stylesheet" href="{{urlpath}}/bwrs_static/bootstrap.css" /> <style> @@ -73,7 +74,7 @@ <body class="bg-light"> <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top"> - <div class="container"> + <div class="container-xl"> <a class="navbar-brand" href="{{urlpath}}/admin"><img class="pr-1" src="{{urlpath}}/bwrs_static/shield-white.png">Bitwarden_rs Admin</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> @@ -96,7 +97,7 @@ </li> {{/if}} <li class="nav-item"> - <a class="nav-link" href="{{urlpath}}/">Vault</a> + <a class="nav-link" href="{{urlpath}}/" target="_blank" rel="noreferrer">Vault</a> </li> </ul> diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs @@ -1,4 +1,4 @@ -<main class="container"> +<main class="container-xl"> <div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow"> <h6 class="border-bottom pb-2 mb-2">Diagnostics</h6> @@ -15,7 +15,7 @@ <span id="server-installed">{{version}}</span> </dd> <dt class="col-sm-5">Server Latest - <span class="badge badge-danger d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span> + <span class="badge badge-secondary d-none" id="server-failed" title="Unable to determine latest version.">Unknown</span> </dt> <dd class="col-sm-7"> <span id="server-latest">{{diagnostics.latest_release}}<span id="server-latest-commit" class="d-none">-{{diagnostics.latest_commit}}</span></span> @@ -28,7 +28,7 @@ <span id="web-installed">{{diagnostics.web_vault_version}}</span> </dd> <dt class="col-sm-5">Web Latest - <span class="badge badge-danger d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span> + <span class="badge badge-secondary d-none" id="web-failed" title="Unable to determine latest version.">Unknown</span> </dt> <dd class="col-sm-7"> <span id="web-latest">{{diagnostics.latest_web_build}}</span> @@ -41,6 +41,40 @@ <div class="row"> <div class="col-md"> <dl class="row"> + <dt class="col-sm-5">Running within Docker</dt> + <dd class="col-sm-7"> + {{#if diagnostics.running_within_docker}} + <span id="running-docker" class="d-block"><b>Yes</b></span> + {{/if}} + {{#unless diagnostics.running_within_docker}} + <span id="running-docker" class="d-block"><b>No</b></span> + {{/unless}} + </dd> + <dt class="col-sm-5">Uses a proxy</dt> + <dd class="col-sm-7"> + {{#if diagnostics.uses_proxy}} + <span id="running-docker" class="d-block"><b>Yes</b></span> + {{/if}} + {{#unless diagnostics.uses_proxy}} + <span id="running-docker" class="d-block"><b>No</b></span> + {{/unless}} + </dd> + <dt class="col-sm-5">Internet access + {{#if diagnostics.has_http_access}} + <span class="badge badge-success" id="internet-success" title="We have internet access!">Ok</span> + {{/if}} + {{#unless diagnostics.has_http_access}} + <span class="badge badge-danger" id="internet-warning" title="There seems to be no internet access. Please fix.">Error</span> + {{/unless}} + </dt> + <dd class="col-sm-7"> + {{#if diagnostics.has_http_access}} + <span id="running-docker" class="d-block"><b>Yes</b></span> + {{/if}} + {{#unless diagnostics.has_http_access}} + <span id="running-docker" class="d-block"><b>No</b></span> + {{/unless}} + </dd> <dt class="col-sm-5">DNS (github.com) <span class="badge badge-success d-none" id="dns-success" title="DNS Resolving works!">Ok</span> <span class="badge badge-danger d-none" id="dns-warning" title="DNS Resolving failed. Please fix.">Error</span> @@ -57,6 +91,44 @@ <span id="time-server" class="d-block"><b>Server:</b> <span id="time-server-string">{{diagnostics.server_time}}</span></span> <span id="time-browser" class="d-block"><b>Browser:</b> <span id="time-browser-string"></span></span> </dd> + + <dt class="col-sm-5">Domain configuration + <span class="badge badge-success d-none" id="domain-success" title="Domain variable seems to be correct.">Ok</span> + <span class="badge badge-danger d-none" id="domain-warning" title="Domain variable is not configured correctly.&#013;&#010;Some features may not work as expected!">Error</span> + </dt> + <dd class="col-sm-7"> + <span id="domain-server" class="d-block"><b>Server:</b> <span id="domain-server-string">{{diagnostics.admin_url}}</span></span> + <span id="domain-browser" class="d-block"><b>Browser:</b> <span id="domain-browser-string"></span></span> + </dd> + </dl> + </div> + </div> + + <h3>Support</h3> + <div class="row"> + <div class="col-md"> + <dl class="row"> + <dd class="col-sm-12"> + If you need support please check the following links first before you create a new issue: + <a href="https://bitwardenrs.discourse.group/" target="_blank" rel="noreferrer">Bitwarden_RS Forum</a> + | <a href="https://github.com/dani-garcia/bitwarden_rs/discussions" target="_blank" rel="noreferrer">Github Discussions</a> + </dd> + </dl> + <dl class="row"> + <dd class="col-sm-12"> + You can use the button below to pre-generate a string which you can copy/paste on either the Forum or when Creating a new issue at Github.<br> + We try to hide the most sensitive values from the generated support string by default, but please verify if there is nothing in there which you want to hide!<br> + </dd> + </dl> + <dl class="row"> + <dt class="col-sm-3"> + <button type="button" id="gen-support" class="btn btn-primary" onclick="generateSupportString(); return false;">Generate Support String</button> + <br><br> + <button type="button" id="copy-support" class="btn btn-info d-none" onclick="copyToClipboard(); return false;">Copy To Clipboard</button> + </dt> + <dd class="col-sm-9"> + <pre id="support-string" class="pre-scrollable d-none" style="width: 100%; height: 16em; size: 0.6em; border: 1px solid; padding: 4px;"></pre> + </dd> </dl> </div> </div> @@ -64,7 +136,12 @@ </main> <script> + dnsCheck = false; + timeCheck = false; + domainCheck = false; (() => { + // ================================ + // Date & Time Check const d = new Date(); const year = d.getUTCFullYear(); const month = String(d.getUTCMonth()+1).padStart(2, '0'); @@ -81,16 +158,21 @@ document.getElementById('time-warning').classList.remove('d-none'); } else { document.getElementById('time-success').classList.remove('d-none'); + timeCheck = true; } + // ================================ // Check if the output is a valid IP const isValidIp = value => (/^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/.test(value) ? true : false); if (isValidIp(document.getElementById('dns-resolved').innerText)) { document.getElementById('dns-success').classList.remove('d-none'); + dnsCheck = true; } else { document.getElementById('dns-warning').classList.remove('d-none'); } + // ================================ + // Version check for both bitwarden_rs and web-vault let serverInstalled = document.getElementById('server-installed').innerText; let serverLatest = document.getElementById('server-latest').innerText; let serverLatestCommit = document.getElementById('server-latest-commit').innerText.replace('-', ''); @@ -146,5 +228,56 @@ } } } + + // ================================ + // Check valid DOMAIN configuration + document.getElementById('domain-browser-string').innerText = location.href.toLowerCase(); + if (document.getElementById('domain-server-string').innerText.toLowerCase() == location.href.toLowerCase()) { + document.getElementById('domain-success').classList.remove('d-none'); + domainCheck = true; + } else { + document.getElementById('domain-warning').classList.remove('d-none'); + } })(); + + // ================================ + // Generate support string to be pasted on github or the forum + async function generateSupportString() { + supportString = "### Your environment (Generated via diagnostics page)\n"; + + supportString += "* Bitwarden_rs version: v{{ version }}\n"; + supportString += "* Web-vault version: v{{ diagnostics.web_vault_version }}\n"; + supportString += "* Running within Docker: {{ diagnostics.running_within_docker }}\n"; + supportString += "* Internet access: {{ diagnostics.has_http_access }}\n"; + supportString += "* Uses a proxy: {{ diagnostics.uses_proxy }}\n"; + supportString += "* DNS Check: " + dnsCheck + "\n"; + supportString += "* Time Check: " + timeCheck + "\n"; + supportString += "* Domain Configuration Check: " + domainCheck + "\n"; + supportString += "* Database type: {{ diagnostics.db_type }}\n"; + {{#case diagnostics.db_type "MySQL" "PostgreSQL"}} + supportString += "* Database version: [PLEASE PROVIDE DATABASE VERSION]\n"; + {{/case}} + + jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config'); + configJson = await jsonResponse.json(); + supportString += "\n### Config (Generated via diagnostics page)\n```json\n" + JSON.stringify(configJson, undefined, 2) + "\n```\n"; + + document.getElementById('support-string').innerText = supportString; + document.getElementById('support-string').classList.remove('d-none'); + document.getElementById('copy-support').classList.remove('d-none'); + } + + function copyToClipboard() { + const str = document.getElementById('support-string').innerText; + const el = document.createElement('textarea'); + el.value = str; + el.setAttribute('readonly', ''); + el.style.position = 'absolute'; + el.style.left = '-9999px'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + } + </script> diff --git a/src/static/templates/admin/login.hbs b/src/static/templates/admin/login.hbs @@ -1,4 +1,4 @@ -<main class="container"> +<main class="container-xl"> {{#if error}} <div class="align-items-center p-3 mb-3 text-white-50 bg-warning rounded shadow"> <div> diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs @@ -1,4 +1,4 @@ -<main class="container"> +<main class="container-xl"> <div id="organizations-block" class="my-3 p-3 bg-white rounded shadow"> <h6 class="border-bottom pb-2 mb-3">Organizations</h6> diff --git a/src/static/templates/admin/settings.hbs b/src/static/templates/admin/settings.hbs @@ -1,4 +1,4 @@ -<main class="container"> +<main class="container-xl"> <div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow"> <div> <h6 class="text-white mb-3">Configuration</h6> diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs @@ -1,4 +1,4 @@ -<main class="container"> +<main class="container-xl"> <div id="users-block" class="my-3 p-3 bg-white rounded shadow"> <h6 class="border-bottom pb-2 mb-3">Registered Users</h6> @@ -7,10 +7,12 @@ <thead> <tr> <th>User</th> - <th style="width:60px; min-width: 60px;">Items</th> + <th style="width:65px; min-width: 65px;">Created at</th> + <th style="width:70px; min-width: 65px;">Last Active</th> + <th style="width:35px; min-width: 35px;">Items</th> <th>Attachments</th> <th style="min-width: 120px;">Organizations</th> - <th style="width: 140px; min-width: 140px;">Actions</th> + <th style="width: 120px; min-width: 120px;">Actions</th> </tr> </thead> <tbody> @@ -21,8 +23,6 @@ <div class="float-left"> <strong>{{Name}}</strong> <span class="d-block">{{Email}}</span> - <span class="d-block">Created at: {{created_at}}</span> - <span class="d-block">Last active: {{last_active}}</span> <span class="d-block"> {{#unless user_enabled}} <span class="badge badge-danger mr-2" title="User is disabled">Disabled</span> @@ -40,6 +40,12 @@ </div> </td> <td> + <span class="d-block">{{created_at}}</span> + </td> + <td> + <span class="d-block">{{last_active}}</span> + </td> + <td> <span class="d-block">{{cipher_count}}</span> </td> <td> @@ -49,9 +55,11 @@ {{/if}} </td> <td> + <div class="overflow-auto" style="max-height: 120px;"> {{#each Organizations}} <span class="badge badge-primary" data-orgtype="{{Type}}">{{Name}}</span> {{/each}} + </div> </td> <td style="font-size: 90%; text-align: right; padding-right: 15px"> {{#if TwoFactorEnabled}} @@ -173,18 +181,43 @@ e.title = orgtype.name; }); + // Special sort function to sort dates in ISO format + jQuery.extend( jQuery.fn.dataTableExt.oSort, { + "date-iso-pre": function ( a ) { + let x; + let sortDate = a.replace(/(<([^>]+)>)/gi, "").trim(); + if ( sortDate !== '' ) { + let dtParts = sortDate.split(' '); + var timeParts = (undefined != dtParts[1]) ? dtParts[1].split(':') : [00,00,00]; + var dateParts = dtParts[0].split('-'); + x = (dateParts[0] + dateParts[1] + dateParts[2] + timeParts[0] + timeParts[1] + ((undefined != timeParts[2]) ? timeParts[2] : 0)) * 1; + if ( isNaN(x) ) { + x = 0; + } + } else { + x = Infinity; + } + return x; + }, + + "date-iso-asc": function ( a, b ) { + return a - b; + }, + + "date-iso-desc": function ( a, b ) { + return b - a; + } + }); + document.addEventListener("DOMContentLoaded", function(event) { $('#users-table').DataTable({ "responsive": true, "lengthMenu": [ [-1, 5, 10, 25, 50], ["All", 5, 10, 25, 50] ], "pageLength": -1, // Default show all - "columns": [ - null, // Userdata - null, // Items - null, // Attachments - null, // Organizations - { "searchable": false, "orderable": false }, // Actions - ], + "columnDefs": [ + { "targets": [1,2], "type": "date-iso" }, + { "targets": 6, "searchable": false, "orderable": false } + ] }); }); </script> \ No newline at end of file