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 5323283f98b898cb8e4c535fcdd0272d24caeec0
parent 16eb0a56f9767b33256ab6c9d66b5dd49af22e6e
Author: Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>
Date:   Sun, 28 Mar 2021 01:31:38 +0100

Merge pull request #1548 from BlackDex/admin-interface

Updated diagnostics page
Diffstat:
Msrc/api/admin.rs | 35+++++++++++++++++++++++++++++++++--
Msrc/db/mod.rs | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/static/templates/admin/diagnostics.hbs | 68++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
3 files changed, 141 insertions(+), 26 deletions(-)

diff --git a/src/api/admin.rs b/src/api/admin.rs @@ -16,7 +16,7 @@ use crate::{ api::{ApiResult, EmptyResult, JsonResult, NumberOrString}, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, config::ConfigBuilder, - db::{backup_database, models::*, DbConn, DbConnType}, + db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, error::{Error, MapResult}, mail, util::{format_naive_datetime_local, get_display_size, is_running_in_docker}, @@ -96,6 +96,27 @@ impl<'a, 'r> FromRequest<'a, 'r> for Referer { } } +#[derive(Debug)] +struct IpHeader(Option<String>); + +impl<'a, 'r> FromRequest<'a, 'r> for IpHeader { + type Error = (); + + fn from_request(req: &'a Request<'r>) -> Outcome<Self, Self::Error> { + if req.headers().get_one(&CONFIG.ip_header()).is_some() { + Outcome::Success(IpHeader(Some(CONFIG.ip_header()))) + } else if req.headers().get_one("X-Client-IP").is_some() { + Outcome::Success(IpHeader(Some(String::from("X-Client-IP")))) + } else if req.headers().get_one("X-Real-IP").is_some() { + Outcome::Success(IpHeader(Some(String::from("X-Real-IP")))) + } else if req.headers().get_one("X-Forwarded-For").is_some() { + Outcome::Success(IpHeader(Some(String::from("X-Forwarded-For")))) + } else { + Outcome::Success(IpHeader(None)) + } + } +} + /// Used for `Location` response headers, which must specify an absolute URI /// (see https://tools.ietf.org/html/rfc2616#section-14.30). fn admin_url(referer: Referer) -> String { @@ -475,7 +496,7 @@ fn has_http_access() -> bool { } #[get("/diagnostics")] -fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> { +fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> { use crate::util::read_file_string; use chrono::prelude::*; use std::net::ToSocketAddrs; @@ -529,6 +550,11 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> { ("-".to_string(), "-".to_string(), "-".to_string()) }; + let ip_header_name = match &ip_header.0 { + Some(h) => h, + _ => "" + }; + let diagnostics_json = json!({ "dns_resolved": dns_resolved, "web_vault_version": web_vault_version.version, @@ -537,8 +563,13 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult<Html<String>> { "latest_web_build": latest_web_build, "running_within_docker": running_within_docker, "has_http_access": has_http_access, + "ip_header_exists": &ip_header.0.is_some(), + "ip_header_match": ip_header_name == &CONFIG.ip_header(), + "ip_header_name": ip_header_name, + "ip_header_config": &CONFIG.ip_header(), "uses_proxy": uses_proxy, "db_type": *DB_TYPE, + "db_version": get_sql_server_version(&conn), "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 }); diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -125,16 +125,34 @@ macro_rules! db_run { $($( #[cfg($db)] crate::db::DbConn::$db(ref $conn) => { - paste::paste! { + paste::paste! { #[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *}; #[allow(unused)] use [<__ $db _model>]::*; - #[allow(unused)] use crate::db::FromDb; + #[allow(unused)] use crate::db::FromDb; } $body }, )+)+ } }; + + // Same for all dbs + ( @raw $conn:ident: $body:block ) => { + db_run! { @raw $conn: sqlite, mysql, postgresql $body } + }; + + // Different code for each db + ( @raw $conn:ident: $( $($db:ident),+ $body:block )+ ) => { + #[allow(unused)] use diesel::prelude::*; + match $conn { + $($( + #[cfg($db)] + crate::db::DbConn::$db(ref $conn) => { + $body + }, + )+)+ + } + }; } @@ -144,7 +162,7 @@ pub trait FromDb { fn from_db(self) -> Self::Output; } -// For each struct eg. Cipher, we create a CipherDb inside a module named __$db_model (where $db is sqlite, mysql or postgresql), +// For each struct eg. Cipher, we create a CipherDb inside a module named __$db_model (where $db is sqlite, mysql or postgresql), // to implement the Diesel traits. We also provide methods to convert between them and the basic structs. Later, that module will be auto imported when using db_run! #[macro_export] macro_rules! db_object { @@ -154,10 +172,10 @@ macro_rules! db_object { $( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty ),+ $(,)? } - )+ ) => { + )+ ) => { // Create the normal struct, without attributes $( pub struct $name { $( /*$( #[$field_attr] )**/ $vis $field : $typ, )+ } )+ - + #[cfg(sqlite)] pub mod __sqlite_model { $( db_object! { @db sqlite | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ } #[cfg(mysql)] @@ -178,7 +196,7 @@ macro_rules! db_object { )+ } impl [<$name Db>] { - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention)] #[inline(always)] pub fn to_db(x: &super::$name) -> Self { Self { $( $field: x.$field.clone(), )+ } } } @@ -222,6 +240,36 @@ pub fn backup_database() -> Result<(), Error> { Ok(()) } + +use diesel::sql_types::Text; +#[derive(QueryableByName,Debug)] +struct SqlVersion { + #[sql_type = "Text"] + version: String, +} + +/// Get the SQL Server version +pub fn get_sql_server_version(conn: &DbConn) -> String { + db_run! {@raw conn: + postgresql, mysql { + match diesel::sql_query("SELECT version() AS version;").get_result::<SqlVersion>(conn).ok() { + Some(v) => { + v.version + }, + _ => "Unknown".to_string() + } + } + sqlite { + match diesel::sql_query("SELECT sqlite_version() AS version;").get_result::<SqlVersion>(conn).ok() { + Some(v) => { + v.version + }, + _ => "Unknown".to_string() + } + } + } +} + /// Attempts to retrieve a single connection from the managed database pool. If /// no pool is currently managed, fails with an `InternalServerError` status. If /// no connections are available, fails with a `ServiceUnavailable` status. @@ -263,7 +311,7 @@ mod sqlite_migrations { let connection = diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url())?; // Disable Foreign Key Checks during migration - + // Scoped to a connection. diesel::sql_query("PRAGMA foreign_keys = OFF") .execute(&connection) @@ -314,7 +362,7 @@ mod postgresql_migrations { let connection = diesel::pg::PgConnection::establish(&crate::CONFIG.database_url())?; // Disable Foreign Key Checks during migration - + // FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html, // "SET CONSTRAINTS sets the behavior of constraint checking within the // current transaction", so this setting probably won't take effect for diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs @@ -2,7 +2,7 @@ <div id="diagnostics-block" class="my-3 p-3 bg-white rounded shadow"> <h6 class="border-bottom pb-2 mb-2">Diagnostics</h6> - <h3>Version</h3> + <h3>Versions</h3> <div class="row"> <div class="col-md"> <dl class="row"> @@ -35,6 +35,10 @@ <span id="web-latest">{{diagnostics.latest_web_build}}</span> </dd> {{/unless}} + <dt class="col-sm-5">Database</dt> + <dd class="col-sm-7"> + <span><b>{{diagnostics.db_type}}:</b> {{diagnostics.db_version}}</span> + </dd> </dl> </div> </div> @@ -46,35 +50,65 @@ <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> + <span class="d-block"><b>Yes</b></span> {{/if}} {{#unless diagnostics.running_within_docker}} - <span id="running-docker" class="d-block"><b>No</b></span> + <span class="d-block"><b>No</b></span> {{/unless}} </dd> - <dt class="col-sm-5">Uses a proxy</dt> + <dt class="col-sm-5">Uses a reverse proxy</dt> <dd class="col-sm-7"> - {{#if diagnostics.uses_proxy}} - <span id="running-docker" class="d-block"><b>Yes</b></span> + {{#if diagnostics.ip_header_exists}} + <span class="d-block" title="IP Header found."><b>Yes</b></span> {{/if}} - {{#unless diagnostics.uses_proxy}} - <span id="running-docker" class="d-block"><b>No</b></span> + {{#unless diagnostics.ip_header_exists}} + <span class="d-block" title="No IP Header found."><b>No</b></span> + {{/unless}} + </dd> + {{!-- Only show this if the IP Header Exists --}} + {{#if diagnostics.ip_header_exists}} + <dt class="col-sm-5">IP header + {{#if diagnostics.ip_header_match}} + <span class="badge badge-success" title="IP_HEADER config seems to be valid.">Match</span> + {{/if}} + {{#unless diagnostics.ip_header_match}} + <span class="badge badge-danger" title="IP_HEADER config seems to be invalid. IP's in the log could be invalid. Please fix.">No Match</span> + {{/unless}} + </dt> + <dd class="col-sm-7"> + {{#if diagnostics.ip_header_match}} + <span class="d-block"><b>Config/Server:</b> {{ diagnostics.ip_header_name }}</span> + {{/if}} + {{#unless diagnostics.ip_header_match}} + <span class="d-block"><b>Config:</b> {{ diagnostics.ip_header_config }}</span> + <span class="d-block"><b>Server:</b> {{ diagnostics.ip_header_name }}</span> {{/unless}} </dd> + {{/if}} + {{!-- End if IP Header Exists --}} <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> + <span class="badge badge-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> + <span class="badge badge-danger" 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> + <span class="d-block"><b>Yes</b></span> {{/if}} {{#unless diagnostics.has_http_access}} - <span id="running-docker" class="d-block"><b>No</b></span> + <span class="d-block"><b>No</b></span> + {{/unless}} + </dd> + <dt class="col-sm-5">Internet access via a proxy</dt> + <dd class="col-sm-7"> + {{#if diagnostics.uses_proxy}} + <span class="d-block" title="Internet access goes via a proxy (HTTPS_PROXY or HTTP_PROXY is configured)."><b>Yes</b></span> + {{/if}} + {{#unless diagnostics.uses_proxy}} + <span class="d-block" title="We have direct internet access, no outgoing proxy configured."><b>No</b></span> {{/unless}} </dd> <dt class="col-sm-5">DNS (github.com) @@ -263,16 +297,18 @@ 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 += "* Uses a reverse proxy: {{ diagnostics.ip_header_exists }}\n"; + {{#if diagnostics.ip_header_exists}} + supportString += "* IP Header check: {{ diagnostics.ip_header_match }} ({{ diagnostics.ip_header_name }})\n"; + {{/if}} supportString += "* Internet access: {{ diagnostics.has_http_access }}\n"; - supportString += "* Uses a proxy: {{ diagnostics.uses_proxy }}\n"; + supportString += "* Internet access via a proxy: {{ diagnostics.uses_proxy }}\n"; supportString += "* DNS Check: " + dnsCheck + "\n"; supportString += "* Time Check: " + timeCheck + "\n"; supportString += "* Domain Configuration Check: " + domainCheck + "\n"; supportString += "* HTTPS Check: " + httpsCheck + "\n"; supportString += "* Database type: {{ diagnostics.db_type }}\n"; - {{#case diagnostics.db_type "MySQL" "PostgreSQL"}} - supportString += "* Database version: [PLEASE PROVIDE DATABASE VERSION]\n"; - {{/case}} + supportString += "* Database version: {{ diagnostics.db_version }}\n"; supportString += "* Clients used: \n"; supportString += "* Reverse proxy and version: \n"; supportString += "* Other relevant information: \n";