From 705d840ea3271738db43e096a1c45d1c9a694d6f Mon Sep 17 00:00:00 2001 From: BlackDex Date: Wed, 3 Feb 2021 18:43:54 +0100 Subject: [PATCH] Extra features for admin interface. - Able to modify the user type per organization - Able to remove a whole organization - Added podman detection - Only show web-vault update when not running a containerized bitwarden_rs Solves #936 --- src/api/admin.rs | 59 +++++++++++++++-- src/config.rs | 12 +++- src/static/templates/admin/diagnostics.hbs | 27 ++++++-- src/static/templates/admin/organizations.hbs | 26 ++++++++ src/static/templates/admin/users.hbs | 70 +++++++++++++++++++- 5 files changed, 181 insertions(+), 13 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 857850ad..dacc1b7c 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -13,7 +13,7 @@ use rocket::{ use rocket_contrib::json::Json; use crate::{ - api::{ApiResult, EmptyResult, JsonResult}, + api::{ApiResult, EmptyResult, JsonResult, NumberOrString}, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, config::ConfigBuilder, db::{backup_database, models::*, DbConn, DbConnType}, @@ -40,6 +40,7 @@ pub fn routes() -> Vec { disable_user, enable_user, remove_2fa, + update_user_org_type, update_revision_users, post_config, delete_config, @@ -47,6 +48,7 @@ pub fn routes() -> Vec { test_smtp, users_overview, organizations_overview, + delete_organization, diagnostics, get_diagnostics_config ] @@ -367,6 +369,41 @@ fn remove_2fa(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { user.save(&conn) } +#[derive(Deserialize, Debug)] +struct UserOrgTypeData { + user_type: NumberOrString, + user_uuid: String, + org_uuid: String, +} + +#[post("/users/org_type", data = "")] +fn update_user_org_type(data: Json, _token: AdminToken, conn: DbConn) -> EmptyResult { + let data: UserOrgTypeData = data.into_inner(); + + let mut user_to_edit = match UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &conn) { + Some(user) => user, + None => err!("The specified user isn't member of the organization"), + }; + + let new_type = match UserOrgType::from_str(&data.user_type.into_string()) { + Some(new_type) => new_type as i32, + None => err!("Invalid type"), + }; + + if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner { + // Removing owner permmission, check that there are at least another owner + let num_owners = UserOrganization::find_by_org_and_type(&data.org_uuid, UserOrgType::Owner as i32, &conn).len(); + + if num_owners <= 1 { + err!("Can't change the type of the last owner") + } + } + + user_to_edit.atype = new_type as i32; + user_to_edit.save(&conn) +} + + #[post("/users/update_revision")] fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult { User::update_all_revisions(&conn) @@ -390,6 +427,12 @@ fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult/delete")] +fn delete_organization(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { + let org = Organization::find_by_uuid(&uuid, &conn).map_res("Organization doesn't exist")?; + org.delete(&conn) +} + #[derive(Deserialize)] struct WebVaultVersion { version: String, @@ -443,7 +486,7 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult> { let web_vault_version: WebVaultVersion = serde_json::from_str(&vault_version_str)?; // Execute some environment checks - let running_within_docker = std::path::Path::new("/.dockerenv").exists(); + let running_within_docker = std::path::Path::new("/.dockerenv").exists() || std::path::Path::new("/run/.containerenv").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() @@ -471,9 +514,15 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult> { } _ => "-".to_string(), }, - match get_github_api::("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(), + // Do not fetch the web-vault version when running within Docker. + // The web-vault version is embedded within the container it self, and should not be updated manually + if running_within_docker { + "-".to_string() + } else { + match get_github_api::("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(), + } }, ) } else { diff --git a/src/config.rs b/src/config.rs index 1422ddf2..9bc45872 100644 --- a/src/config.rs +++ b/src/config.rs @@ -872,14 +872,20 @@ fn js_escape_helper<'reg, 'rc>( .param(0) .ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?; + let no_quote = h + .param(1) + .is_some(); + let value = param .value() .as_str() .ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?; - let escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27"); - let quoted_value = format!(""{}"", escaped_value); + let mut escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27"); + if ! no_quote { + escaped_value = format!(""{}"", escaped_value); + } - out.write("ed_value)?; + out.write(&escaped_value)?; Ok(()) } diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index 0cfbc49c..fbb6a183 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -27,12 +27,14 @@
{{diagnostics.web_vault_version}}
+ {{#unless diagnostics.running_within_docker}}
Web Latest Unknown
{{diagnostics.latest_web_build}}
+ {{/unless}} @@ -93,8 +95,10 @@
Domain configuration - Ok - Error + Match + No Match + HTTPS + No HTTPS
Server: {{diagnostics.admin_url}} @@ -139,6 +143,7 @@ dnsCheck = false; timeCheck = false; domainCheck = false; + httpsCheck = false; (() => { // ================================ // Date & Time Check @@ -181,10 +186,12 @@ } const webInstalled = document.getElementById('web-installed').innerText; - const webLatest = document.getElementById('web-latest').innerText; - checkVersions('server', serverInstalled, serverLatest, serverLatestCommit); + + {{#unless diagnostics.running_within_docker}} + const webLatest = document.getElementById('web-latest').innerText; checkVersions('web', webInstalled, webLatest); + {{/unless}} function checkVersions(platform, installed, latest, commit=null) { if (installed === '-' || latest === '-') { @@ -238,6 +245,14 @@ } else { document.getElementById('domain-warning').classList.remove('d-none'); } + + // Check for HTTPS at domain-server-string + if (document.getElementById('domain-server-string').innerText.toLowerCase().startsWith('https://') ) { + document.getElementById('https-success').classList.remove('d-none'); + httpsCheck = true; + } else { + document.getElementById('https-warning').classList.remove('d-none'); + } })(); // ================================ @@ -253,10 +268,14 @@ 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 += "* Clients used: \n"; + supportString += "* Reverse proxy and version: \n"; + supportString += "* Other relevant information: \n"; jsonResponse = await fetch('{{urlpath}}/admin/diagnostics/config'); configJson = await jsonResponse.json(); diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs index bcb89af3..66956c8e 100644 --- a/src/static/templates/admin/organizations.hbs +++ b/src/static/templates/admin/organizations.hbs @@ -10,6 +10,7 @@ Users Items Attachments + Actions @@ -37,6 +38,9 @@ Size: {{attachment_size}} {{/if}} + + Delete Organization + {{/each}} @@ -50,6 +54,25 @@ \ No newline at end of file diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index dffa8123..3001b1e5 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -57,7 +57,7 @@
{{#each Organizations}} - {{Name}} + {{/each}}
@@ -100,6 +100,41 @@ + + @@ -220,4 +255,37 @@ ] }); }); + + var userOrgTypeDialog = document.getElementById('userOrgTypeDialog'); + // Fill the form and title + userOrgTypeDialog.addEventListener('show.bs.modal', function(event){ + let userOrgType = event.relatedTarget.getAttribute("data-orgtype"); + let userOrgTypeName = OrgTypes[userOrgType]["name"]; + let orgName = event.relatedTarget.getAttribute("data-orgname"); + let userEmail = event.relatedTarget.getAttribute("data-useremail"); + let orgUuid = event.relatedTarget.getAttribute("data-orguuid"); + let userUuid = event.relatedTarget.getAttribute("data-useruuid"); + + document.getElementById("userOrgTypeDialogTitle").innerHTML = "Update User Type:
Organization: " + orgName + "
User: " + userEmail; + document.getElementById("userOrgTypeUserUuid").value = userUuid; + document.getElementById("userOrgTypeOrgUuid").value = orgUuid; + document.getElementById("userOrgType"+userOrgTypeName).checked = true; + }, false); + + // Prevent accidental submission of the form with valid elements after the modal has been hidden. + userOrgTypeDialog.addEventListener('hide.bs.modal', function(event){ + document.getElementById("userOrgTypeDialogTitle").innerHTML = ''; + document.getElementById("userOrgTypeUserUuid").value = ''; + document.getElementById("userOrgTypeOrgUuid").value = ''; + }, false); + + function updateUserOrgType() { + let orgForm = document.getElementById("userOrgTypeForm"); + const data = JSON.stringify(Object.fromEntries(new FormData(orgForm).entries())); + + _post("{{urlpath}}/admin/users/org_type", + "Updated organization type of the user successfully", + "Error updating organization type of the user", data); + return false; + } \ No newline at end of file