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 5da96d36e6fc8243d394dabca103624c03ea79e1
parent aebda93afedf36182e767d947631c44ae14a968b
Author: Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>
Date:   Sun, 25 Sep 2022 19:03:55 +0200

Merge branch 'BlackDex-fix-org-export'

Diffstat:
Msrc/api/core/organizations.rs | 47+++++++++++++++++++++++++++++++++++++----------
Msrc/util.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 90 insertions(+), 10 deletions(-)

diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -10,7 +10,9 @@ use crate::{ }, auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, db::{models::*, DbConn}, - mail, CONFIG, + mail, + util::convert_json_key_lcase_first, + CONFIG, }; use futures::{stream, stream::StreamExt}; @@ -68,7 +70,8 @@ pub fn routes() -> Vec<Route> { activate_organization_user, bulk_activate_organization_user, restore_organization_user, - bulk_restore_organization_user + bulk_restore_organization_user, + get_org_export ] } @@ -246,15 +249,19 @@ async fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> { #[get("/organizations/<org_id>/collections")] async fn get_org_collections(org_id: String, _headers: ManagerHeadersLoose, conn: DbConn) -> Json<Value> { - Json(json!({ + Json(_get_org_collections(&org_id, &conn).await) +} + +async fn _get_org_collections(org_id: &str, conn: &DbConn) -> Value { + json!({ "Data": - Collection::find_by_organization(&org_id, &conn).await + Collection::find_by_organization(org_id, conn).await .iter() .map(Collection::to_json) .collect::<Value>(), "Object": "list", "ContinuationToken": null, - })) + }) } #[post("/organizations/<org_id>/collections", data = "<data>")] @@ -491,22 +498,26 @@ struct OrgIdData { #[get("/ciphers/organization-details?<data..>")] async fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> Json<Value> { - let ciphers = Cipher::find_by_org(&data.organization_id, &conn).await; - let cipher_sync_data = CipherSyncData::new(&headers.user.uuid, &ciphers, CipherSyncType::Organization, &conn).await; + Json(_get_org_details(&data.organization_id, &headers.host, &headers.user.uuid, &conn).await) +} + +async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &DbConn) -> Value { + let ciphers = Cipher::find_by_org(org_id, conn).await; + let cipher_sync_data = CipherSyncData::new(user_uuid, &ciphers, CipherSyncType::Organization, conn).await; let ciphers_json = stream::iter(ciphers) .then(|c| async { let c = c; // Move out this single variable - c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), &conn).await + c.to_json(host, user_uuid, Some(&cipher_sync_data), conn).await }) .collect::<Vec<Value>>() .await; - Json(json!({ + json!({ "Data": ciphers_json, "Object": "list", "ContinuationToken": null, - })) + }) } #[get("/organizations/<org_id>/users")] @@ -1690,3 +1701,19 @@ async fn _restore_organization_user( } Ok(()) } + +// This is a new function active since the v2022.9.x clients. +// It combines the previous two calls done before. +// We call those two functions here and combine them our selfs. +// +// NOTE: It seems clients can't handle uppercase-first keys!! +// We need to convert all keys so they have the first character to be a lowercase. +// Else the export will be just an empty JSON file. +#[get("/organizations/<org_id>/export")] +async fn get_org_export(org_id: String, headers: AdminHeaders, conn: DbConn) -> Json<Value> { + // Also both main keys here need to be lowercase, else the export will fail. + Json(json!({ + "collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &conn).await), + "ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &conn).await), + })) +} diff --git a/src/util.rs b/src/util.rs @@ -357,6 +357,7 @@ pub fn get_uuid() -> String { use std::str::FromStr; +#[inline] pub fn upcase_first(s: &str) -> String { let mut c = s.chars(); match c.next() { @@ -365,6 +366,15 @@ pub fn upcase_first(s: &str) -> String { } } +#[inline] +pub fn lcase_first(s: &str) -> String { + let mut c = s.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_lowercase().collect::<String>() + c.as_str(), + } +} + pub fn try_parse_string<S, T>(string: Option<S>) -> Option<T> where S: AsRef<str>, @@ -650,3 +660,46 @@ pub fn get_reqwest_client_builder() -> ClientBuilder { headers.insert(header::USER_AGENT, header::HeaderValue::from_static("Vaultwarden")); Client::builder().default_headers(headers).timeout(Duration::from_secs(10)) } + +pub fn convert_json_key_lcase_first(src_json: Value) -> Value { + match src_json { + Value::Array(elm) => { + let mut new_array: Vec<Value> = Vec::with_capacity(elm.len()); + + for obj in elm { + new_array.push(convert_json_key_lcase_first(obj)); + } + Value::Array(new_array) + } + + Value::Object(obj) => { + let mut json_map = JsonMap::new(); + for (key, value) in obj.iter() { + match (key, value) { + (key, Value::Object(elm)) => { + let inner_value = convert_json_key_lcase_first(Value::Object(elm.clone())); + json_map.insert(lcase_first(key), inner_value); + } + + (key, Value::Array(elm)) => { + let mut inner_array: Vec<Value> = Vec::with_capacity(elm.len()); + + for inner_obj in elm { + inner_array.push(convert_json_key_lcase_first(inner_obj.clone())); + } + + json_map.insert(lcase_first(key), Value::Array(inner_array)); + } + + (key, value) => { + json_map.insert(lcase_first(key), value.clone()); + } + } + } + + Value::Object(json_map) + } + + value => value, + } +}