From 87165364eb92c90f013134eed5878abd11c06d79 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 | 68 +++------- src/api/core/organizations.rs | 227 ++++++++++++++++++++++++++++------ src/db/models/cipher.rs | 1 + src/db/models/collection.rs | 28 +++++ src/db/models/group.rs | 27 ++++ src/db/models/organization.rs | 35 +++++- 6 files changed, 293 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 067718c98f..3717c5cd94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1098,9 +1098,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", @@ -2909,9 +2909,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", @@ -3708,15 +3708,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 +3722,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 +3746,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 +3760,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 +3790,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/src/api/core/organizations.rs b/src/api/core/organizations.rs index 38ff63b7cf..cbad42ef0a 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)) } @@ -657,11 +739,33 @@ async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut 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!({ @@ -1124,6 +1228,7 @@ async fn _confirm_invite( save_result } +// FIXME: Add Groups #[get("/organizations//users/")] async fn get_user(org_id: String, org_user_id: String, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { let user = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &mut conn).await { @@ -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 { @@ -2136,6 +2248,14 @@ impl SelectionReadOnly { } } + pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> SelectionReadOnly { + SelectionReadOnly { + Id: collection_user.user_uuid.clone(), + ReadOnly: collection_user.read_only, + HidePasswords: collection_user.hide_passwords, + } + } + pub fn to_json(&self) -> Value { json!(self) } @@ -2171,7 +2291,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 +2299,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 +2324,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 +2337,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, diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index b7d26bd3d7..33af5e7d98 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -740,6 +740,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..7ca961d158 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -68,6 +68,33 @@ impl Group { }) } + pub async fn to_json_details(&self, conn: &mut DbConn) -> Value { + use crate::util::format_date; + + let collections_groups = 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, + "CreationDate": format_date(&self.creation_date), + "RevisionDate": format_date(&self.revision_date), + "Collections": collections_groups + }) + } + pub fn set_external_id(&mut self, external_id: Option) { //Check if external id is empty. We don't want to have //empty strings in the database diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 34325b787b..e74e9032da 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -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,39 @@ 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_user(&self.user_uuid, conn) + .await + .iter() + .map(|cu| { + json!({ + "Id": cu.collection_uuid, + "ReadOnly": cu.read_only, + "HidePasswords": cu.hide_passwords, + }) + }) + .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) + }; + json!({ "Id": self.uuid, "UserId": self.user_uuid, "Name": user.name, "Email": user.email, + "Groups": groups, + "Collections": collections, "Status": status, "Type": self.atype,