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
This commit is contained in:
Stefan Melmuk 2024-07-04 20:28:19 +02:00 committed by GitHub
parent d9835f530c
commit fda77afc2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 267 additions and 74 deletions

View File

@ -79,6 +79,8 @@ pub fn routes() -> Vec<Route> {
delete_all, delete_all,
move_cipher_selected, move_cipher_selected,
move_cipher_selected_put, move_cipher_selected_put,
put_collections2_update,
post_collections2_update,
put_collections_update, put_collections_update,
post_collections_update, post_collections_update,
post_collections_admin, post_collections_admin,
@ -702,6 +704,33 @@ struct CollectionsAdminData {
collection_ids: Vec<String>, collection_ids: Vec<String>,
} }
#[put("/ciphers/<uuid>/collections_v2", data = "<data>")]
async fn put_collections2_update(
uuid: &str,
data: Json<CollectionsAdminData>,
headers: Headers,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
post_collections2_update(uuid, data, headers, conn, nt).await
}
#[post("/ciphers/<uuid>/collections_v2", data = "<data>")]
async fn post_collections2_update(
uuid: &str,
data: Json<CollectionsAdminData>,
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/<uuid>/collections", data = "<data>")] #[put("/ciphers/<uuid>/collections", data = "<data>")]
async fn put_collections_update( async fn put_collections_update(
uuid: &str, uuid: &str,
@ -709,8 +738,8 @@ async fn put_collections_update(
headers: Headers, headers: Headers,
conn: DbConn, conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> EmptyResult { ) -> JsonResult {
post_collections_admin(uuid, data, headers, conn, nt).await post_collections_update(uuid, data, headers, conn, nt).await
} }
#[post("/ciphers/<uuid>/collections", data = "<data>")] #[post("/ciphers/<uuid>/collections", data = "<data>")]
@ -718,10 +747,65 @@ async fn post_collections_update(
uuid: &str, uuid: &str,
data: Json<CollectionsAdminData>, data: Json<CollectionsAdminData>,
headers: Headers, headers: Headers,
conn: DbConn, mut conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> EmptyResult { ) -> JsonResult {
post_collections_admin(uuid, data, headers, conn, nt).await 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::<String>::from_iter(data.collection_ids);
let current_collections =
HashSet::<String>::from_iter(cipher.get_collections(headers.user.uuid.clone(), &mut conn).await);
for collection in posted_collections.symmetric_difference(&current_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/<uuid>/collections-admin", data = "<data>")] #[put("/ciphers/<uuid>/collections-admin", data = "<data>")]
@ -754,9 +838,9 @@ async fn post_collections_admin(
err!("Cipher is not write accessible") err!("Cipher is not write accessible")
} }
let posted_collections: HashSet<String> = data.collection_ids.iter().cloned().collect(); let posted_collections = HashSet::<String>::from_iter(data.collection_ids);
let current_collections: HashSet<String> = let current_collections =
cipher.get_collections(headers.user.uuid.clone(), &mut conn).await.iter().cloned().collect(); HashSet::<String>::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &mut conn).await);
for collection in posted_collections.symmetric_difference(&current_collections) { for collection in posted_collections.symmetric_difference(&current_collections) {
match Collection::find_by_uuid(collection, &mut conn).await { match Collection::find_by_uuid(collection, &mut conn).await {

View File

@ -212,7 +212,7 @@ impl Cipher {
Cow::from(Vec::with_capacity(0)) Cow::from(Vec::with_capacity(0))
} }
} else { } 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 // There are three types of cipher response models in upstream
@ -779,31 +779,124 @@ impl Cipher {
} }
pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> { pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> {
if CONFIG.org_groups_enabled() {
db_run! {conn: { db_run! {conn: {
ciphers_collections::table 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::<String>(conn).unwrap_or_default()
}}
} else {
db_run! {conn: {
ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
.inner_join(collections::table.on( .inner_join(collections::table.on(
collections::uuid.eq(ciphers_collections::collection_uuid) collections::uuid.eq(ciphers_collections::collection_uuid)
)) ))
.inner_join(users_organizations::table.on( .inner_join(users_organizations::table.on(
users_organizations::org_uuid.eq(collections::org_uuid).and( users_organizations::org_uuid.eq(collections::org_uuid)
users_organizations::user_uuid.eq(user_id.clone()) .and(users_organizations::user_uuid.eq(user_id.clone()))
)
)) ))
.left_join(users_collections::table.on( .left_join(users_collections::table.on(
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and( users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
users_collections::user_uuid.eq(user_id.clone()) .and(users_collections::user_uuid.eq(user_id.clone()))
)
)) ))
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) .filter(users_organizations::access_all.eq(true) // User has access all
.filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection .or(users_collections::user_uuid.eq(user_id) // User has access to collection
users_organizations::access_all.eq(true).or( // User has access all .and(users_collections::read_only.eq(false)))
users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner
) )
))
.select(ciphers_collections::collection_uuid) .select(ciphers_collections::collection_uuid)
.load::<String>(conn).unwrap_or_default() .load::<String>(conn).unwrap_or_default()
}} }}
} }
}
pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> {
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::<String>(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::<String>(conn).unwrap_or_default()
}}
}
}
/// Return a Vec with (cipher_uuid, collection_uuid) /// Return a Vec with (cipher_uuid, collection_uuid)
/// This is used during a full sync so we only need one query for all collections accessible. /// This is used during a full sync so we only need one query for all collections accessible.

View File

@ -371,17 +371,17 @@ impl Collection {
pub async fn is_writable_by_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool { pub async fn is_writable_by_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
let user_uuid = user_uuid.to_string(); let user_uuid = user_uuid.to_string();
if CONFIG.org_groups_enabled() {
db_run! { conn: { db_run! { conn: {
collections::table collections::table
.left_join(users_collections::table.on( .filter(collections::uuid.eq(&self.uuid))
users_collections::collection_uuid.eq(collections::uuid).and( .inner_join(users_organizations::table.on(
users_collections::user_uuid.eq(user_uuid.clone()) collections::org_uuid.eq(users_organizations::org_uuid)
) .and(users_organizations::user_uuid.eq(user_uuid.clone()))
)) ))
.left_join(users_organizations::table.on( .left_join(users_collections::table.on(
collections::org_uuid.eq(users_organizations::org_uuid).and( users_collections::collection_uuid.eq(collections::uuid)
users_organizations::user_uuid.eq(user_uuid) .and(users_collections::user_uuid.eq(user_uuid))
)
)) ))
.left_join(groups_users::table.on( .left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid) groups_users::users_organizations_uuid.eq(users_organizations::uuid)
@ -390,29 +390,45 @@ impl Collection {
groups::uuid.eq(groups_users::groups_uuid) groups::uuid.eq(groups_users::groups_uuid)
)) ))
.left_join(collections_groups::table.on( .left_join(collections_groups::table.on(
collections_groups::groups_uuid.eq(groups_users::groups_uuid).and( collections_groups::groups_uuid.eq(groups_users::groups_uuid)
collections_groups::collections_uuid.eq(collections::uuid) .and(collections_groups::collections_uuid.eq(collections::uuid))
)
)) ))
.filter(collections::uuid.eq(&self.uuid)) .filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
.filter( .or(users_organizations::access_all.eq(true)) // access_all via membership
users_collections::collection_uuid.eq(&self.uuid).and(users_collections::read_only.eq(false)).or(// Directly accessed collection .or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection
users_organizations::access_all.eq(true).or( // access_all in Organization .and(users_collections::read_only.eq(false)))
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner .or(groups::access_all.eq(true)) // access_all via group
)).or( .or(collections_groups::collections_uuid.is_not_null() // write access given via group
groups::access_all.eq(true) // access_all in groups .and(collections_groups::read_only.eq(false)))
).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))
)
)
) )
.count() .count()
.first::<i64>(conn) .first::<i64>(conn)
.ok() .ok()
.unwrap_or(0) != 0 .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::<i64>(conn)
.ok()
.unwrap_or(0) != 0
}}
}
} }
pub async fn hide_passwords_for_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool { pub async fn hide_passwords_for_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {