From fda77afc2a802f6e7607c19ad12b5f93520a688e Mon Sep 17 00:00:00 2001 From: Stefan Melmuk <509385+stefan0xC@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:28:19 +0200 Subject: [PATCH] add group support for Cipher::get_collections() (#4592) * add group support for Cipher::get_collections() join group infos assigned to a collection to check whether user has been given access to all collections via any group or they have access to a specific collection via any group membership * fix Collection::is_writable_by_user() prevent side effects if groups are disabled * differentiate the /collection endpoints * return cipherDetails on post_collections_update() * add collections_v2 endpoint --- src/api/core/ciphers.rs | 100 +++++++++++++++++++++++-- src/db/models/cipher.rs | 143 +++++++++++++++++++++++++++++------- src/db/models/collection.rs | 98 +++++++++++++----------- 3 files changed, 267 insertions(+), 74 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 956f4891f5..c2c78b33b4 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -79,6 +79,8 @@ pub fn routes() -> Vec { delete_all, move_cipher_selected, move_cipher_selected_put, + put_collections2_update, + post_collections2_update, put_collections_update, post_collections_update, post_collections_admin, @@ -702,6 +704,33 @@ struct CollectionsAdminData { collection_ids: Vec, } +#[put("/ciphers//collections_v2", data = "")] +async fn put_collections2_update( + uuid: &str, + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + post_collections2_update(uuid, data, headers, conn, nt).await +} + +#[post("/ciphers//collections_v2", data = "")] +async fn post_collections2_update( + uuid: &str, + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + let cipher_details = post_collections_update(uuid, data, headers, conn, nt).await?; + Ok(Json(json!({ // AttachmentUploadDataResponseModel + "object": "optionalCipherDetails", + "unavailable": false, + "cipher": *cipher_details + }))) +} + #[put("/ciphers//collections", data = "")] async fn put_collections_update( uuid: &str, @@ -709,8 +738,8 @@ async fn put_collections_update( headers: Headers, conn: DbConn, nt: Notify<'_>, -) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn, nt).await +) -> JsonResult { + post_collections_update(uuid, data, headers, conn, nt).await } #[post("/ciphers//collections", data = "")] @@ -718,10 +747,65 @@ async fn post_collections_update( uuid: &str, data: Json, headers: Headers, - conn: DbConn, + mut conn: DbConn, nt: Notify<'_>, -) -> EmptyResult { - post_collections_admin(uuid, data, headers, conn, nt).await +) -> JsonResult { + let data: CollectionsAdminData = data.into_inner(); + + let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { + Some(cipher) => cipher, + None => err!("Cipher doesn't exist"), + }; + + if !cipher.is_write_accessible_to_user(&headers.user.uuid, &mut conn).await { + err!("Cipher is not write accessible") + } + + let posted_collections = HashSet::::from_iter(data.collection_ids); + let current_collections = + HashSet::::from_iter(cipher.get_collections(headers.user.uuid.clone(), &mut conn).await); + + for collection in posted_collections.symmetric_difference(¤t_collections) { + match Collection::find_by_uuid(collection, &mut conn).await { + None => err!("Invalid collection ID provided"), + Some(collection) => { + if collection.is_writable_by_user(&headers.user.uuid, &mut conn).await { + if posted_collections.contains(&collection.uuid) { + // Add to collection + CollectionCipher::save(&cipher.uuid, &collection.uuid, &mut conn).await?; + } else { + // Remove from collection + CollectionCipher::delete(&cipher.uuid, &collection.uuid, &mut conn).await?; + } + } else { + err!("No rights to modify the collection") + } + } + } + } + + nt.send_cipher_update( + UpdateType::SyncCipherUpdate, + &cipher, + &cipher.update_users_revision(&mut conn).await, + &headers.device.uuid, + Some(Vec::from_iter(posted_collections)), + &mut conn, + ) + .await; + + log_event( + EventType::CipherUpdatedCollections as i32, + &cipher.uuid, + &cipher.organization_uuid.clone().unwrap(), + &headers.user.uuid, + headers.device.atype, + &headers.ip.ip, + &mut conn, + ) + .await; + + Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await)) } #[put("/ciphers//collections-admin", data = "")] @@ -754,9 +838,9 @@ async fn post_collections_admin( err!("Cipher is not write accessible") } - let posted_collections: HashSet = data.collection_ids.iter().cloned().collect(); - let current_collections: HashSet = - cipher.get_collections(headers.user.uuid.clone(), &mut conn).await.iter().cloned().collect(); + let posted_collections = HashSet::::from_iter(data.collection_ids); + let current_collections = + HashSet::::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid(collection, &mut conn).await { diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 545463d39f..446749d492 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -212,7 +212,7 @@ impl Cipher { Cow::from(Vec::with_capacity(0)) } } else { - Cow::from(self.get_collections(user_uuid.to_string(), conn).await) + Cow::from(self.get_admin_collections(user_uuid.to_string(), conn).await) }; // There are three types of cipher response models in upstream @@ -779,30 +779,123 @@ impl Cipher { } pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { - db_run! {conn: { - ciphers_collections::table - .inner_join(collections::table.on( - collections::uuid.eq(ciphers_collections::collection_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::org_uuid.eq(collections::org_uuid).and( - users_organizations::user_uuid.eq(user_id.clone()) - ) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and( - users_collections::user_uuid.eq(user_id.clone()) - ) - )) - .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) - .filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection - users_organizations::access_all.eq(true).or( // User has access all - users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner - ) - )) - .select(ciphers_collections::collection_uuid) - .load::(conn).unwrap_or_default() - }} + if CONFIG.org_groups_enabled() { + db_run! {conn: { + ciphers_collections::table + .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .inner_join(collections::table.on( + collections::uuid.eq(ciphers_collections::collection_uuid) + )) + .left_join(users_organizations::table.on( + users_organizations::org_uuid.eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_id.clone())) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_id.clone())) + )) + .left_join(groups_users::table.on( + groups_users::users_organizations_uuid.eq(users_organizations::uuid) + )) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .left_join(collections_groups::table.on( + collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) + .and(collections_groups::groups_uuid.eq(groups::uuid)) + )) + .filter(users_organizations::access_all.eq(true) // User has access all + .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .and(users_collections::read_only.eq(false))) + .or(groups::access_all.eq(true)) // Access via groups + .or(collections_groups::collections_uuid.is_not_null() // Access via groups + .and(collections_groups::read_only.eq(false))) + ) + .select(ciphers_collections::collection_uuid) + .load::(conn).unwrap_or_default() + }} + } else { + db_run! {conn: { + ciphers_collections::table + .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .inner_join(collections::table.on( + collections::uuid.eq(ciphers_collections::collection_uuid) + )) + .inner_join(users_organizations::table.on( + users_organizations::org_uuid.eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_id.clone())) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_id.clone())) + )) + .filter(users_organizations::access_all.eq(true) // User has access all + .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .and(users_collections::read_only.eq(false))) + ) + .select(ciphers_collections::collection_uuid) + .load::(conn).unwrap_or_default() + }} + } + } + + pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { + if CONFIG.org_groups_enabled() { + db_run! {conn: { + ciphers_collections::table + .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .inner_join(collections::table.on( + collections::uuid.eq(ciphers_collections::collection_uuid) + )) + .left_join(users_organizations::table.on( + users_organizations::org_uuid.eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_id.clone())) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_id.clone())) + )) + .left_join(groups_users::table.on( + groups_users::users_organizations_uuid.eq(users_organizations::uuid) + )) + .left_join(groups::table.on(groups::uuid.eq(groups_users::groups_uuid))) + .left_join(collections_groups::table.on( + collections_groups::collections_uuid.eq(ciphers_collections::collection_uuid) + .and(collections_groups::groups_uuid.eq(groups::uuid)) + )) + .filter(users_organizations::access_all.eq(true) // User has access all + .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .and(users_collections::read_only.eq(false))) + .or(groups::access_all.eq(true)) // Access via groups + .or(collections_groups::collections_uuid.is_not_null() // Access via groups + .and(collections_groups::read_only.eq(false))) + .or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner + ) + .select(ciphers_collections::collection_uuid) + .load::(conn).unwrap_or_default() + }} + } else { + db_run! {conn: { + ciphers_collections::table + .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .inner_join(collections::table.on( + collections::uuid.eq(ciphers_collections::collection_uuid) + )) + .inner_join(users_organizations::table.on( + users_organizations::org_uuid.eq(collections::org_uuid) + .and(users_organizations::user_uuid.eq(user_id.clone())) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(ciphers_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_id.clone())) + )) + .filter(users_organizations::access_all.eq(true) // User has access all + .or(users_collections::user_uuid.eq(user_id) // User has access to collection + .and(users_collections::read_only.eq(false))) + .or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner + ) + .select(ciphers_collections::collection_uuid) + .load::(conn).unwrap_or_default() + }} + } } /// Return a Vec with (cipher_uuid, collection_uuid) diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 0d43975781..3ba6c516d0 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -371,48 +371,64 @@ impl Collection { pub async fn is_writable_by_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool { let user_uuid = user_uuid.to_string(); - db_run! { conn: { - collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid.clone()) - ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) - ) - )) - .left_join(groups_users::table.on( - groups_users::users_organizations_uuid.eq(users_organizations::uuid) - )) - .left_join(groups::table.on( - groups::uuid.eq(groups_users::groups_uuid) - )) - .left_join(collections_groups::table.on( - collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( - collections_groups::collections_uuid.eq(collections::uuid) - ) - )) - .filter(collections::uuid.eq(&self.uuid)) - .filter( - users_collections::collection_uuid.eq(&self.uuid).and(users_collections::read_only.eq(false)).or(// Directly accessed collection - users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner - )).or( - groups::access_all.eq(true) // access_all in groups - ).or( // access via groups - groups_users::users_organizations_uuid.eq(users_organizations::uuid).and( - collections_groups::collections_uuid.is_not_null().and( - collections_groups::read_only.eq(false)) + if CONFIG.org_groups_enabled() { + db_run! { conn: { + collections::table + .filter(collections::uuid.eq(&self.uuid)) + .inner_join(users_organizations::table.on( + collections::org_uuid.eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone())) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid)) + )) + .left_join(groups_users::table.on( + groups_users::users_organizations_uuid.eq(users_organizations::uuid) + )) + .left_join(groups::table.on( + groups::uuid.eq(groups_users::groups_uuid) + )) + .left_join(collections_groups::table.on( + collections_groups::groups_uuid.eq(groups_users::groups_uuid) + .and(collections_groups::collections_uuid.eq(collections::uuid)) + )) + .filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + .or(users_organizations::access_all.eq(true)) // access_all via membership + .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection + .and(users_collections::read_only.eq(false))) + .or(groups::access_all.eq(true)) // access_all via group + .or(collections_groups::collections_uuid.is_not_null() // write access given via group + .and(collections_groups::read_only.eq(false))) ) - ) - ) - .count() - .first::(conn) - .ok() - .unwrap_or(0) != 0 - }} + .count() + .first::(conn) + .ok() + .unwrap_or(0) != 0 + }} + } else { + db_run! { conn: { + collections::table + .filter(collections::uuid.eq(&self.uuid)) + .inner_join(users_organizations::table.on( + collections::org_uuid.eq(users_organizations::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid.clone())) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(collections::uuid) + .and(users_collections::user_uuid.eq(user_uuid)) + )) + .filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + .or(users_organizations::access_all.eq(true)) // access_all via membership + .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection + .and(users_collections::read_only.eq(false))) + ) + .count() + .first::(conn) + .ok() + .unwrap_or(0) != 0 + }} + } } pub async fn hide_passwords_for_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {