From ea3fb0ec0c25b98edf431a4f82b97d7dcc7a8307 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Fri, 24 Feb 2023 19:55:07 +0100 Subject: [PATCH] WIP: Fix web-vault issues --- Cargo.lock | 145 +++++++++--------- Cargo.toml | 4 +- clippy.toml | 2 +- rust-toolchain | 2 +- src/api/core/accounts.rs | 3 + src/api/core/ciphers.rs | 78 ++++++++-- src/api/core/emergency_access.rs | 12 +- src/api/core/mod.rs | 1 + src/api/core/organizations.rs | 248 +++++++++++++++++++++++-------- src/db/models/cipher.rs | 49 ++++-- src/db/models/collection.rs | 28 ++++ src/db/models/group.rs | 27 +++- src/db/models/organization.rs | 39 ++++- 13 files changed, 473 insertions(+), 165 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 067718c98f..f89d118460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -898,6 +898,27 @@ dependencies = [ "syn", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -1098,9 +1119,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266041a359dfa931b370ef684cceb84b166beb14f7f0421f4a6a3d0c446d12e" +checksum = "33a20a288a94683f5f4da0adecdbe095c94a77c295e514cc6484e9394dd8376e" dependencies = [ "cc", "libc", @@ -1453,6 +1474,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "ipconfig" version = "0.3.1" @@ -1603,6 +1634,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.9" @@ -2401,15 +2438,6 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "reqwest" version = "0.11.14" @@ -2591,6 +2619,20 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + [[package]] name = "rustls" version = "0.20.8" @@ -2909,9 +2951,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2933,16 +2975,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" dependencies = [ "cfg-if", "fastrand", - "libc", "redox_syscall", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys 0.42.0", ] [[package]] @@ -2995,9 +3036,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa", "libc", @@ -3015,9 +3056,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" dependencies = [ "time-core", ] @@ -3708,15 +3749,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.39.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" dependencies = [ - "windows_aarch64_msvc 0.39.0", - "windows_i686_gnu 0.39.0", - "windows_i686_msvc 0.39.0", - "windows_x86_64_gnu 0.39.0", - "windows_x86_64_msvc 0.39.0", + "windows-targets", ] [[package]] @@ -3726,12 +3763,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.1", + "windows_x86_64_msvc", ] [[package]] @@ -3750,12 +3787,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.1", + "windows_x86_64_msvc", ] [[package]] @@ -3764,48 +3801,24 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" -[[package]] -name = "windows_aarch64_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" - [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" -[[package]] -name = "windows_i686_gnu" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" - [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" -[[package]] -name = "windows_i686_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" - [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" -[[package]] -name = "windows_x86_64_gnu" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" - [[package]] name = "windows_x86_64_gnu" version = "0.42.1" @@ -3818,12 +3831,6 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" -[[package]] -name = "windows_x86_64_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" - [[package]] name = "windows_x86_64_msvc" version = "0.42.1" diff --git a/Cargo.toml b/Cargo.toml index a231d709e0..4f791227f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "vaultwarden" version = "1.0.0" authors = ["Daniel GarcĂ­a "] edition = "2021" -rust-version = "1.61.0" +rust-version = "1.63.0" resolver = "2" repository = "https://github.com/dani-garcia/vaultwarden" @@ -92,7 +92,7 @@ uuid = { version = "1.3.0", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.23", features = ["clock", "serde"], default-features = false } chrono-tz = "0.8.1" -time = "0.3.19" +time = "0.3.20" # Job scheduler job_scheduler_ng = "2.0.4" diff --git a/clippy.toml b/clippy.toml index f5fcb56714..b3c3a24c90 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.61.0" +msrv = "1.63.0" diff --git a/rust-toolchain b/rust-toolchain index 65ee095984..737e2ba509 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.67.0 +1.67.1 diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 0354f46489..67633f0b8b 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -841,6 +841,8 @@ async fn _api_key( headers: Headers, mut conn: DbConn, ) -> JsonResult { + use crate::util::format_date; + let data: SecretVerificationRequest = data.into_inner().data; let mut user = headers.user; @@ -855,6 +857,7 @@ async fn _api_key( Ok(Json(json!({ "ApiKey": user.api_key, + "RevisionDate": format_date(&user.updated_at), "Object": "apiKey", }))) } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 0b0f10806c..14d4459789 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -56,7 +56,9 @@ pub fn routes() -> Vec { put_cipher_share, put_cipher_share_selected, post_cipher, + post_cipher_partial, put_cipher, + put_cipher_partial, delete_cipher_post, delete_cipher_post_admin, delete_cipher_put, @@ -109,7 +111,10 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json // Lets generate the ciphers_json using all the gathered info let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { - ciphers_json.push(c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), &mut conn).await); + ciphers_json.push( + c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn) + .await, + ); } let collections = Collection::find_by_user_uuid(headers.user.uuid.clone(), &mut conn).await; @@ -153,7 +158,10 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { - ciphers_json.push(c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), &mut conn).await); + ciphers_json.push( + c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn) + .await, + ); } Json(json!({ @@ -174,7 +182,7 @@ async fn get_cipher(uuid: String, headers: Headers, mut conn: DbConn) -> JsonRes err!("Cipher is not owned by user") } - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } #[get("/ciphers//admin")] @@ -235,6 +243,13 @@ pub struct CipherData { LastKnownRevisionDate: Option, } +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +pub struct PartialCipherData { + FolderId: Option, + Favorite: bool, +} + #[derive(Deserialize, Debug)] #[allow(non_snake_case)] pub struct Attachments2Data { @@ -314,7 +329,7 @@ async fn post_ciphers( update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherCreate) .await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } /// Enforces the personal ownership policy on user-owned ciphers, if applicable. @@ -646,7 +661,51 @@ async fn put_cipher( update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherUpdate) .await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) +} + +#[post("/ciphers//partial", data = "")] +async fn post_cipher_partial( + uuid: String, + data: JsonUpcase, + headers: Headers, + conn: DbConn, +) -> JsonResult { + put_cipher_partial(uuid, data, headers, conn).await +} + +// Only update the folder and favorite for the user, since this cipher is read-only +#[put("/ciphers//partial", data = "")] +async fn put_cipher_partial( + uuid: String, + data: JsonUpcase, + headers: Headers, + mut conn: DbConn, +) -> JsonResult { + let data: PartialCipherData = data.into_inner().data; + + let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await { + Some(cipher) => cipher, + None => err!("Cipher doesn't exist"), + }; + + if let Some(ref folder_id) = data.FolderId { + match Folder::find_by_uuid(folder_id, &mut conn).await { + Some(folder) => { + if folder.user_uuid != headers.user.uuid { + err!("Folder is not owned by user") + } + } + None => err!("Folder doesn't exist"), + } + } + + // Move cipher + cipher.move_to_folder(data.FolderId.clone(), &headers.user.uuid, &mut conn).await?; + // Update favorite + cipher.set_favorite(Some(data.Favorite), &headers.user.uuid, &mut conn).await?; + + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } #[derive(Deserialize)] @@ -873,7 +932,7 @@ async fn share_cipher_by_uuid( update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, ip, nt, ut).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await)) } /// v2 API for downloading an attachment. This just redirects the client to @@ -942,7 +1001,7 @@ async fn post_attachment_v2( "AttachmentId": attachment_id, "Url": url, "FileUploadType": FileUploadType::Direct as i32, - response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await, + response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await, }))) } @@ -1135,7 +1194,7 @@ async fn post_attachment( let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, ip, nt).await?; - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } #[post("/ciphers//attachment-admin", format = "multipart/form-data", data = "")] @@ -1616,7 +1675,7 @@ async fn _restore_cipher_by_uuid( .await; } - Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await)) + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await)) } async fn _restore_multiple_ciphers( @@ -1716,6 +1775,7 @@ pub struct CipherSyncData { pub user_group_full_access_for_organizations: HashSet, } +#[derive(Eq, PartialEq)] pub enum CipherSyncType { User, Organization, diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 1730de01ba..dd0617e275 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -590,8 +590,16 @@ async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbCo let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { - ciphers_json - .push(c.to_json(&headers.host, &emergency_access.grantor_uuid, Some(&cipher_sync_data), &mut conn).await); + ciphers_json.push( + c.to_json( + &headers.host, + &emergency_access.grantor_uuid, + Some(&cipher_sync_data), + CipherSyncType::User, + &mut conn, + ) + .await, + ); } Ok(Json(json!({ diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index d029cb60fb..6a483842a0 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -237,6 +237,7 @@ fn config() -> Json { "notifications": format!("{domain}/notifications"), "sso": "", }, + "object": "config", })) } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 38ff63b7cf..1353e61b86 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -118,12 +118,13 @@ struct OrganizationUpdateData { #[allow(non_snake_case)] struct NewCollectionData { Name: String, - Groups: Vec, + Groups: Vec, + Users: Vec, } #[derive(Deserialize)] #[allow(non_snake_case)] -struct NewCollectionGroupData { +struct NewCollectionObjectData { HidePasswords: bool, Id: String, ReadOnly: bool, @@ -311,29 +312,62 @@ async fn get_org_collections(org_id: String, _headers: ManagerHeadersLoose, mut } #[get("/organizations//collections/details")] -async fn get_org_collections_details(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { +async fn get_org_collections_details(org_id: String, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { let mut data = Vec::new(); + let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { + Some(u) => u, + None => err!("User is not part of organization"), + }; + + let coll_users = CollectionUser::find_by_organization(&org_id, &mut conn).await; + for col in Collection::find_by_organization(&org_id, &mut conn).await { - let groups: Vec = CollectionGroup::find_by_collection(&col.uuid, &mut conn) - .await + let groups: Vec = if CONFIG.org_groups_enabled() { + CollectionGroup::find_by_collection(&col.uuid, &mut conn) + .await + .iter() + .map(|collection_group| { + SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json() + }) + .collect() + } else { + // The Bitwarden clients seem to call this API regardless of whether groups are enabled, + // so just act as if there are no groups. + Vec::with_capacity(0) + }; + + let mut assigned = false; + let users: Vec = coll_users .iter() - .map(|collection_group| { - SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json() + .filter(|collection_user| collection_user.collection_uuid == col.uuid) + .map(|collection_user| { + // Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `CollectionUser::find_by_organization` call. + // We check here if the current user is assigned to this collection or not. + if collection_user.user_uuid == user_org.uuid { + assigned = true; + } + SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json() }) .collect(); + if user_org.access_all { + assigned = true; + } + let mut json_object = col.to_json(); + json_object["Assigned"] = json!(assigned); + json_object["Users"] = json!(users); json_object["Groups"] = json!(groups); - json_object["Object"] = json!("collectionGroupDetails"); + json_object["Object"] = json!("collectionAccessDetails"); data.push(json_object) } - Json(json!({ + Ok(Json(json!({ "Data": data, "Object": "list", "ContinuationToken": null, - })) + }))) } async fn _get_org_collections(org_id: &str, conn: &mut DbConn) -> Value { @@ -355,12 +389,6 @@ async fn post_organization_collections( None => err!("Can't find organization details"), }; - // Get the user_organization record so that we can check if the user has access to all collections. - let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { - Some(u) => u, - None => err!("User is not part of organization"), - }; - let collection = Collection::new(org.uuid, data.Name); collection.save(&mut conn).await?; @@ -381,11 +409,18 @@ async fn post_organization_collections( .await?; } - // If the user doesn't have access to all collections, only in case of a Manger, - // then we need to save the creating user uuid (Manager) to the users_collection table. - // Else the user will not have access to his own created collection. - if !user_org.access_all { - CollectionUser::save(&headers.user.uuid, &collection.uuid, false, false, &mut conn).await?; + for user in data.Users { + let org_user = match UserOrganization::find_by_uuid(&user.Id, &mut conn).await { + Some(u) => u, + None => err!("User is not part of organization"), + }; + + if org_user.access_all { + continue; + } + + CollectionUser::save(&org_user.user_uuid, &collection.uuid, user.ReadOnly, user.HidePasswords, &mut conn) + .await?; } Ok(Json(collection.to_json())) @@ -448,6 +483,21 @@ async fn post_organization_collection_update( CollectionGroup::new(col_id.clone(), group.Id, group.ReadOnly, group.HidePasswords).save(&mut conn).await?; } + CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?; + + for user in data.Users { + let org_user = match UserOrganization::find_by_uuid(&user.Id, &mut conn).await { + Some(u) => u, + None => err!("User is not part of organization"), + }; + + if org_user.access_all { + continue; + } + + CollectionUser::save(&org_user.user_uuid, &col_id, user.ReadOnly, user.HidePasswords, &mut conn).await?; + } + Ok(Json(collection.to_json())) } @@ -555,17 +605,49 @@ async fn get_org_collection_detail( err!("Collection is not owned by organization") } - let groups: Vec = CollectionGroup::find_by_collection(&collection.uuid, &mut conn) - .await - .iter() - .map(|collection_group| { - SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json() - }) - .collect(); + let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await { + Some(u) => u, + None => err!("User is not part of organization"), + }; + + let groups: Vec = if CONFIG.org_groups_enabled() { + CollectionGroup::find_by_collection(&collection.uuid, &mut conn) + .await + .iter() + .map(|collection_group| { + SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json() + }) + .collect() + } else { + // The Bitwarden clients seem to call this API regardless of whether groups are enabled, + // so just act as if there are no groups. + Vec::with_capacity(0) + }; + + let mut assigned = false; + let users: Vec = + CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid(&collection.uuid, &mut conn) + .await + .iter() + .map(|collection_user| { + // Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `find_by_collection_swap_user_uuid_with_org_user_uuid` call. + // We check here if the current user is assigned to this collection or not. + if collection_user.user_uuid == user_org.uuid { + assigned = true; + } + SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json() + }) + .collect(); + + if user_org.access_all { + assigned = true; + } let mut json_object = collection.to_json(); + json_object["Assigned"] = json!(assigned); + json_object["Users"] = json!(users); json_object["Groups"] = json!(groups); - json_object["Object"] = json!("collectionGroupDetails"); + json_object["Object"] = json!("collectionAccessDetails"); Ok(Json(json_object)) } @@ -652,16 +734,39 @@ async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut let mut ciphers_json = Vec::with_capacity(ciphers.len()); for c in ciphers { - ciphers_json.push(c.to_json(host, user_uuid, Some(&cipher_sync_data), conn).await); + ciphers_json + .push(c.to_json(host, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await); } json!(ciphers_json) } -#[get("/organizations//users")] -async fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json { +#[derive(FromForm)] +struct GetOrgUserData { + #[field(name = "includeCollections")] + include_collections: Option, + #[field(name = "includeGroups")] + include_groups: Option, +} + +// includeCollections +// includeGroups +#[get("/organizations//users?")] +async fn get_org_users( + data: GetOrgUserData, + org_id: String, + _headers: ManagerHeadersLoose, + mut conn: DbConn, +) -> Json { let mut users_json = Vec::new(); for u in UserOrganization::find_by_org(&org_id, &mut conn).await { - users_json.push(u.to_json_user_details(&mut conn).await); + users_json.push( + u.to_json_user_details( + data.include_collections.unwrap_or(false), + data.include_groups.unwrap_or(false), + &mut conn, + ) + .await, + ); } Json(json!({ @@ -2056,12 +2161,18 @@ async fn _restore_organization_user( #[get("/organizations//groups")] async fn get_groups(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult { - let groups = if CONFIG.org_groups_enabled() { - Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::() + let groups: Vec = if CONFIG.org_groups_enabled() { + // Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::() + let groups = Group::find_by_organization(&org_id, &mut conn).await; + let mut groups_json = Vec::with_capacity(groups.len()); + for g in groups { + groups_json.push(g.to_json_details(&mut conn).await) + } + groups_json } else { // The Bitwarden clients seem to call this API regardless of whether groups are enabled, // so just act as if there are no groups. - Value::Array(Vec::new()) + Vec::with_capacity(0) }; Ok(Json(json!({ @@ -2078,6 +2189,7 @@ struct GroupRequest { AccessAll: Option, ExternalId: Option, Collections: Vec, + Users: Vec, } impl GroupRequest { @@ -2120,19 +2232,19 @@ impl SelectionReadOnly { CollectionGroup::new(self.Id.clone(), groups_uuid, self.ReadOnly, self.HidePasswords) } - pub fn to_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly { + pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly { SelectionReadOnly { - Id: collection_group.collections_uuid.clone(), + Id: collection_group.groups_uuid.clone(), ReadOnly: collection_group.read_only, HidePasswords: collection_group.hide_passwords, } } - pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly { + pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> SelectionReadOnly { SelectionReadOnly { - Id: collection_group.groups_uuid.clone(), - ReadOnly: collection_group.read_only, - HidePasswords: collection_group.hide_passwords, + Id: collection_user.user_uuid.clone(), + ReadOnly: collection_user.read_only, + HidePasswords: collection_user.hide_passwords, } } @@ -2171,7 +2283,7 @@ async fn post_groups( log_event( EventType::GroupCreated as i32, &group.uuid, - org_id, + org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, @@ -2179,7 +2291,7 @@ async fn post_groups( ) .await; - add_update_group(group, group_request.Collections, &mut conn).await + add_update_group(group, group_request.Collections, group_request.Users, &org_id, &headers, &ip, &mut conn).await } #[put("/organizations//groups/", data = "")] @@ -2204,11 +2316,12 @@ async fn put_group( let updated_group = group_request.update_group(group)?; CollectionGroup::delete_all_by_group(&group_id, &mut conn).await?; + GroupUser::delete_all_by_group(&group_id, &mut conn).await?; log_event( EventType::GroupUpdated as i32, &updated_group.uuid, - org_id, + org_id.clone(), headers.user.uuid.clone(), headers.device.atype, &ip.ip, @@ -2216,18 +2329,42 @@ async fn put_group( ) .await; - add_update_group(updated_group, group_request.Collections, &mut conn).await + add_update_group(updated_group, group_request.Collections, group_request.Users, &org_id, &headers, &ip, &mut conn) + .await } -async fn add_update_group(mut group: Group, collections: Vec, conn: &mut DbConn) -> JsonResult { +async fn add_update_group( + mut group: Group, + collections: Vec, + users: Vec, + org_id: &str, + headers: &AdminHeaders, + ip: &ClientIp, + conn: &mut DbConn, +) -> JsonResult { group.save(conn).await?; for selection_read_only_request in collections { let mut collection_group = selection_read_only_request.to_collection_group(group.uuid.clone()); - collection_group.save(conn).await?; } + for assigned_user_id in users { + let mut user_entry = GroupUser::new(group.uuid.clone(), assigned_user_id.clone()); + user_entry.save(conn).await?; + + log_event( + EventType::OrganizationUserUpdatedGroups as i32, + &assigned_user_id, + String::from(org_id), + headers.user.uuid.clone(), + headers.device.atype, + &ip.ip, + conn, + ) + .await; + } + Ok(Json(json!({ "Id": group.uuid, "OrganizationId": group.organizations_uuid, @@ -2248,20 +2385,7 @@ async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHea _ => err!("Group could not be found!"), }; - let collections_groups = CollectionGroup::find_by_group(&group_id, &mut conn) - .await - .iter() - .map(|entry| SelectionReadOnly::to_group_details_read_only(entry).to_json()) - .collect::(); - - Ok(Json(json!({ - "Id": group.uuid, - "OrganizationId": group.organizations_uuid, - "Name": group.name, - "AccessAll": group.access_all, - "ExternalId": group.get_external_id(), - "Collections": collections_groups - }))) + Ok(Json(group.to_json_details(&mut conn).await)) } #[post("/organizations//groups//delete")] diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index b7d26bd3d7..79212f6abb 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -6,7 +6,7 @@ use super::{ Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization, }; -use crate::api::core::{CipherData, CipherSyncData}; +use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; use std::borrow::Cow; @@ -114,6 +114,7 @@ impl Cipher { host: &str, user_uuid: &str, cipher_sync_data: Option<&CipherSyncData>, + sync_type: CipherSyncType, conn: &mut DbConn, ) -> Value { use crate::util::format_date; @@ -134,12 +135,18 @@ impl Cipher { let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null); - let (read_only, hide_passwords) = match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await { - Some((ro, hp)) => (ro, hp), - None => { - error!("Cipher ownership assertion failure"); - (true, true) + // We don't need these values at all for Organizational syncs + // Skip any other database calls if this is the case and just return false. + let (read_only, hide_passwords) = if sync_type == CipherSyncType::User { + match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await { + Some((ro, hp)) => (ro, hp), + None => { + error!("Cipher ownership assertion failure"); + (true, true) + } } + } else { + (false, false) }; // Get the type_data or a default to an empty json object '{}'. @@ -192,8 +199,6 @@ impl Cipher { "CreationDate": format_date(&self.created_at), "RevisionDate": format_date(&self.updated_at), "DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))), - "FolderId": if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string() ) } else { self.get_folder_uuid(user_uuid, conn).await }, - "Favorite": if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_favorites.contains(&self.uuid) } else { self.is_favorite(user_uuid, conn).await }, "Reprompt": self.reprompt.unwrap_or(RepromptType::None as i32), "OrganizationId": self.organization_uuid, "Attachments": attachments_json, @@ -210,12 +215,6 @@ impl Cipher { "Data": data_json, - // These values are true by default, but can be false if the - // cipher belongs to a collection where the org owner has enabled - // the "Read Only" or "Hide Passwords" restrictions for the user. - "Edit": !read_only, - "ViewPassword": !hide_passwords, - "PasswordHistory": password_history_json, // All Cipher types are included by default as null, but only the matching one will be populated @@ -225,6 +224,27 @@ impl Cipher { "Identity": null, }); + // These values are only needed for user/default syncs + // Not during an organizational sync like `get_org_details` + // Skip adding these fields in that case + if sync_type == CipherSyncType::User { + json_object["FolderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { + cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string()) + } else { + self.get_folder_uuid(user_uuid, conn).await + }); + json_object["Favorite"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { + cipher_sync_data.cipher_favorites.contains(&self.uuid) + } else { + self.is_favorite(user_uuid, conn).await + }); + // These values are true by default, but can be false if the + // cipher belongs to a collection or group where the org owner has enabled + // the "Read Only" or "Hide Passwords" restrictions for the user. + json_object["Edit"] = json!(!read_only); + json_object["ViewPassword"] = json!(!hide_passwords); + } + let key = match self.atype { 1 => "Login", 2 => "SecureNote", @@ -740,6 +760,7 @@ impl Cipher { .or_filter(groups::access_all.eq(true)) //Access via group .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group .select(ciphers_collections::all_columns) + .distinct() .load::<(String, String)>(conn).unwrap_or_default() }} } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 1ae2949393..365efefb9b 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -396,6 +396,19 @@ impl CollectionUser { }} } + pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec { + db_run! { conn: { + users_collections::table + .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) + .filter(collections::org_uuid.eq(org_uuid)) + .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid))) + .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords)) + .load::(conn) + .expect("Error loading users_collections") + .from_db() + }} + } + pub async fn save( user_uuid: &str, collection_uuid: &str, @@ -479,6 +492,21 @@ impl CollectionUser { }} } + pub async fn find_by_collection_swap_user_uuid_with_org_user_uuid( + collection_uuid: &str, + conn: &mut DbConn, + ) -> Vec { + db_run! { conn: { + users_collections::table + .filter(users_collections::collection_uuid.eq(collection_uuid)) + .inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid))) + .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords)) + .load::(conn) + .expect("Error loading users_collections") + .from_db() + }} + } + pub async fn find_by_collection_and_user( collection_uuid: &str, user_uuid: &str, diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 6f267c10f4..8fbde2962b 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -64,7 +64,32 @@ impl Group { "AccessAll": self.access_all, "ExternalId": self.external_id, "CreationDate": format_date(&self.creation_date), - "RevisionDate": format_date(&self.revision_date) + "RevisionDate": format_date(&self.revision_date), + "Object": "group" + }) + } + + pub async fn to_json_details(&self, conn: &mut DbConn) -> Value { + let collections_groups: Vec = CollectionGroup::find_by_group(&self.uuid, conn) + .await + .iter() + .map(|entry| { + json!({ + "Id": entry.collections_uuid, + "ReadOnly": entry.read_only, + "HidePasswords": entry.hide_passwords + }) + }) + .collect(); + + json!({ + "Id": self.uuid, + "OrganizationId": self.organizations_uuid, + "Name": self.name, + "AccessAll": self.access_all, + "ExternalId": self.external_id, + "Collections": collections_groups, + "Object": "groupDetails" }) } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 34325b787b..6010c20725 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -326,7 +326,7 @@ impl UserOrganization { // TODO: Add support for Custom User Roles // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role // "Permissions": { - // "AccessEventLogs": false, // Not supported + // "AccessEventLogs": false, // "AccessImportExport": false, // "AccessReports": false, // "ManageAllCollections": false, @@ -337,9 +337,9 @@ impl UserOrganization { // "editAssignedCollections": false, // "deleteAssignedCollections": false, // "ManageCiphers": false, - // "ManageGroups": false, // Not supported + // "ManageGroups": false, // "ManagePolicies": false, - // "ManageResetPassword": false, // Not supported + // "ManageResetPassword": false, // "ManageSso": false, // Not supported // "ManageUsers": false, // "ManageScim": false, // Not supported (Not AGPLv3 Licensed) @@ -358,7 +358,12 @@ impl UserOrganization { }) } - pub async fn to_json_user_details(&self, conn: &mut DbConn) -> Value { + pub async fn to_json_user_details( + &self, + include_collections: bool, + include_groups: bool, + conn: &mut DbConn, + ) -> Value { let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap(); // Because BitWarden want the status to be -1 for revoked users we need to catch that here. @@ -371,11 +376,37 @@ impl UserOrganization { let twofactor_enabled = !TwoFactor::find_by_user(&user.uuid, conn).await.is_empty(); + let groups: Vec = if include_groups && CONFIG.org_groups_enabled() { + GroupUser::find_by_user(&self.uuid, conn).await.iter().map(|gu| gu.groups_uuid.clone()).collect() + } else { + // The Bitwarden clients seem to call this API regardless of whether groups are enabled, + // so just act as if there are no groups. + Vec::with_capacity(0) + }; + + let collections: Vec = if include_collections { + CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn) + .await + .iter() + .map(|cu| { + json!({ + "Id": cu.collection_uuid, + "ReadOnly": cu.read_only, + "HidePasswords": cu.hide_passwords, + }) + }) + .collect() + } else { + Vec::with_capacity(0) + }; + json!({ "Id": self.uuid, "UserId": self.user_uuid, "Name": user.name, "Email": user.email, + "Groups": groups, + "Collections": collections, "Status": status, "Type": self.atype,