diff --git a/migrations/mysql/2025-01-09-172300_add_manage/down.sql b/migrations/mysql/2025-01-09-172300_add_manage/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/mysql/2025-01-09-172300_add_manage/up.sql b/migrations/mysql/2025-01-09-172300_add_manage/up.sql new file mode 100644 index 0000000000..e234cc6e5f --- /dev/null +++ b/migrations/mysql/2025-01-09-172300_add_manage/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users_collections +ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE; + +ALTER TABLE collections_groups +ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/postgresql/2025-01-09-172300_add_manage/down.sql b/migrations/postgresql/2025-01-09-172300_add_manage/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/postgresql/2025-01-09-172300_add_manage/up.sql b/migrations/postgresql/2025-01-09-172300_add_manage/up.sql new file mode 100644 index 0000000000..e234cc6e5f --- /dev/null +++ b/migrations/postgresql/2025-01-09-172300_add_manage/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users_collections +ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE; + +ALTER TABLE collections_groups +ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/sqlite/2025-01-09-172300_add_manage/down.sql b/migrations/sqlite/2025-01-09-172300_add_manage/down.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/migrations/sqlite/2025-01-09-172300_add_manage/up.sql b/migrations/sqlite/2025-01-09-172300_add_manage/up.sql new file mode 100644 index 0000000000..4b4b07a5e8 --- /dev/null +++ b/migrations/sqlite/2025-01-09-172300_add_manage/up.sql @@ -0,0 +1,5 @@ +ALTER TABLE users_collections +ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE + +ALTER TABLE collections_groups +ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 45e5e0a332..287764d553 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -139,6 +139,7 @@ struct NewCollectionGroupData { hide_passwords: bool, id: GroupId, read_only: bool, + manage: bool, } #[derive(Deserialize)] @@ -147,6 +148,7 @@ struct NewCollectionMemberData { hide_passwords: bool, id: MembershipId, read_only: bool, + manage: bool, } #[derive(Deserialize)] @@ -369,10 +371,10 @@ async fn get_org_collections_details( // get the users assigned directly to the given collection let users: Vec = col_users .iter() - .filter(|collection_user| collection_user.collection_uuid == col.uuid) - .map(|collection_user| { - collection_user.to_json_details_for_user( - *membership_type.get(&collection_user.membership_uuid).unwrap_or(&(MembershipType::User as i32)), + .filter(|collection_member| collection_member.collection_uuid == col.uuid) + .map(|collection_member| { + collection_member.to_json_details_for_member( + *membership_type.get(&collection_member.membership_uuid).unwrap_or(&(MembershipType::User as i32)), ) }) .collect(); @@ -436,7 +438,7 @@ async fn post_organization_collections( .await; for group in data.groups { - CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords) + CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage) .save(&mut conn) .await?; } @@ -450,12 +452,19 @@ async fn post_organization_collections( continue; } - CollectionUser::save(&member.user_uuid, &collection.uuid, user.read_only, user.hide_passwords, &mut conn) - .await?; + CollectionUser::save( + &member.user_uuid, + &collection.uuid, + user.read_only, + user.hide_passwords, + user.manage, + &mut conn, + ) + .await?; } if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all { - CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, &mut conn).await?; + CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, false, &mut conn).await?; } Ok(Json(collection.to_json())) @@ -512,7 +521,9 @@ async fn post_organization_collection_update( CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?; for group in data.groups { - CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords).save(&mut conn).await?; + CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage) + .save(&mut conn) + .await?; } CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?; @@ -526,7 +537,8 @@ async fn post_organization_collection_update( continue; } - CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, &mut conn).await?; + CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &mut conn) + .await?; } Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await)) @@ -684,10 +696,10 @@ async fn get_org_collection_detail( CollectionUser::find_by_collection_swap_user_uuid_with_member_uuid(&collection.uuid, &mut conn) .await .iter() - .map(|collection_user| { - collection_user.to_json_details_for_user( + .map(|collection_member| { + collection_member.to_json_details_for_member( *membership_type - .get(&collection_user.membership_uuid) + .get(&collection_member.membership_uuid) .unwrap_or(&(MembershipType::User as i32)), ) }) @@ -757,7 +769,7 @@ async fn put_collection_users( continue; } - CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, &mut conn).await?; + CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, d.manage, &mut conn).await?; } Ok(()) @@ -865,6 +877,7 @@ struct CollectionData { id: CollectionId, read_only: bool, hide_passwords: bool, + manage: bool, } #[derive(Deserialize)] @@ -873,6 +886,7 @@ struct MembershipData { id: MembershipId, read_only: bool, hide_passwords: bool, + manage: bool, } #[derive(Deserialize)] @@ -1011,6 +1025,7 @@ async fn send_invite( &collection.uuid, col.read_only, col.hide_passwords, + col.manage, &mut conn, ) .await?; @@ -1508,6 +1523,7 @@ async fn edit_member( &collection.uuid, col.read_only, col.hide_passwords, + col.manage, &mut conn, ) .await?; @@ -2485,11 +2501,12 @@ struct SelectedCollection { id: CollectionId, read_only: bool, hide_passwords: bool, + manage: bool, } impl SelectedCollection { pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup { - CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords) + CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage) } } diff --git a/src/api/icons.rs b/src/api/icons.rs index 921d48b917..fc4e0ccfea 100644 --- a/src/api/icons.rs +++ b/src/api/icons.rs @@ -291,9 +291,7 @@ fn get_favicons_node(dom: Tokenizer, FaviconEmitter>, icons: &m TAG_HEAD if token.closing => { break; } - _ => { - continue; - } + _ => {} } } diff --git a/src/api/notifications.rs b/src/api/notifications.rs index a8083a9f76..de97be6fc1 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -157,7 +157,6 @@ fn websockets_hub<'r>( if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) { yield Message::binary(INITIAL_RESPONSE); - continue; } } @@ -225,7 +224,6 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) { yield Message::binary(INITIAL_RESPONSE); - continue; } } diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 3b8d238448..c751491ea5 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -158,16 +158,16 @@ impl Cipher { // 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 { + 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), + Some((ro, hp, mn)) => (ro, hp, mn), None => { error!("Cipher ownership assertion failure"); - (true, true) + (true, true, false) } } } else { - (false, false) + (false, false, false) }; let fields_json: Vec<_> = self @@ -567,14 +567,14 @@ impl Cipher { /// Returns the user's access restrictions to this cipher. A return value /// of None means that this cipher does not belong to the user, and is /// not in any collection the user has access to. Otherwise, the user has - /// access to this cipher, and Some(read_only, hide_passwords) represents + /// access to this cipher, and Some(read_only, hide_passwords, manage) represents /// the access restrictions. pub async fn get_access_restrictions( &self, user_uuid: &UserId, cipher_sync_data: Option<&CipherSyncData>, conn: &mut DbConn, - ) -> Option<(bool, bool)> { + ) -> Option<(bool, bool, bool)> { // Check whether this cipher is directly owned by the user, or is in // a collection that the user has full access to. If so, there are no // access restrictions. @@ -582,21 +582,21 @@ impl Cipher { || self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await || self.is_in_full_access_group(user_uuid, cipher_sync_data, conn).await { - return Some((false, false)); + return Some((false, false, true)); } let rows = if let Some(cipher_sync_data) = cipher_sync_data { - let mut rows: Vec<(bool, bool)> = Vec::new(); + let mut rows: Vec<(bool, bool, bool)> = Vec::new(); if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) { for collection in collections { //User permissions - if let Some(uc) = cipher_sync_data.user_collections.get(collection) { - rows.push((uc.read_only, uc.hide_passwords)); + if let Some(cu) = cipher_sync_data.user_collections.get(collection) { + rows.push((cu.read_only, cu.hide_passwords, cu.manage)); } //Group permissions if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) { - rows.push((cg.read_only, cg.hide_passwords)); + rows.push((cg.read_only, cg.hide_passwords, cg.manage)); } } } @@ -623,15 +623,21 @@ impl Cipher { // booleans and this behavior isn't portable anyway. let mut read_only = true; let mut hide_passwords = true; - for (ro, hp) in rows.iter() { + let mut manage = false; + for (ro, hp, mn) in rows.iter() { read_only &= ro; hide_passwords &= hp; + manage &= mn; } - Some((read_only, hide_passwords)) + Some((read_only, hide_passwords, manage)) } - async fn get_user_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> { + async fn get_user_collections_access_flags( + &self, + user_uuid: &UserId, + conn: &mut DbConn, + ) -> Vec<(bool, bool, bool)> { db_run! {conn: { // Check whether this cipher is in any collections accessible to the // user. If so, retrieve the access flags for each collection. @@ -642,13 +648,17 @@ impl Cipher { .inner_join(users_collections::table.on( ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) .and(users_collections::user_uuid.eq(user_uuid)))) - .select((users_collections::read_only, users_collections::hide_passwords)) - .load::<(bool, bool)>(conn) + .select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) + .load::<(bool, bool, bool)>(conn) .expect("Error getting user access restrictions") }} } - async fn get_group_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> { + async fn get_group_collections_access_flags( + &self, + user_uuid: &UserId, + conn: &mut DbConn, + ) -> Vec<(bool, bool, bool)> { if !CONFIG.org_groups_enabled() { return Vec::new(); } @@ -668,15 +678,15 @@ impl Cipher { users_organizations::uuid.eq(groups_users::users_organizations_uuid) )) .filter(users_organizations::user_uuid.eq(user_uuid)) - .select((collections_groups::read_only, collections_groups::hide_passwords)) - .load::<(bool, bool)>(conn) + .select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage)) + .load::<(bool, bool, bool)>(conn) .expect("Error getting group access restrictions") }} } pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool { match self.get_access_restrictions(user_uuid, None, conn).await { - Some((read_only, _hide_passwords)) => !read_only, + Some((read_only, _hide_passwords, manage)) => !read_only || manage, None => false, } } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 2302b49391..2286ee0448 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -27,6 +27,7 @@ db_object! { pub collection_uuid: CollectionId, pub read_only: bool, pub hide_passwords: bool, + pub manage: bool, } #[derive(Identifiable, Queryable, Insertable)] @@ -83,18 +84,26 @@ impl Collection { cipher_sync_data: Option<&crate::api::core::CipherSyncData>, conn: &mut DbConn, ) -> Value { - let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data { + let (read_only, hide_passwords, manage) = if let Some(cipher_sync_data) = cipher_sync_data { match cipher_sync_data.members.get(&self.org_uuid) { - // Only for Manager types Bitwarden returns true for the can_manage option - // Owners and Admins always have true + // Only for Manager types Bitwarden returns true for the manage option + // Owners and Admins always have true. Users are not able to have full access Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager), Some(m) => { // Only let a manager manage collections when the have full read/write access let is_manager = m.atype == MembershipType::Manager; - if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) { - (uc.read_only, uc.hide_passwords, is_manager && !uc.read_only && !uc.hide_passwords) + if let Some(cu) = cipher_sync_data.user_collections.get(&self.uuid) { + ( + cu.read_only, + cu.hide_passwords, + cu.manage || (is_manager && !cu.read_only && !cu.hide_passwords), + ) } else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) { - (cg.read_only, cg.hide_passwords, is_manager && !cg.read_only && !cg.hide_passwords) + ( + cg.read_only, + cg.hide_passwords, + cg.manage || (is_manager && !cg.read_only && !cg.hide_passwords), + ) } else { (false, false, false) } @@ -104,17 +113,14 @@ impl Collection { } else { match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await { Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager), + Some(_) if self.is_manageable_by_user(user_uuid, conn).await => (false, false, true), Some(m) => { let is_manager = m.atype == MembershipType::Manager; let read_only = !self.is_writable_by_user(user_uuid, conn).await; let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await; (read_only, hide_passwords, is_manager && !read_only && !hide_passwords) } - _ => ( - !self.is_writable_by_user(user_uuid, conn).await, - self.hide_passwords_for_user(user_uuid, conn).await, - false, - ), + _ => (true, true, false), } }; @@ -122,7 +128,7 @@ impl Collection { json_object["object"] = json!("collectionDetails"); json_object["readOnly"] = json!(read_only); json_object["hidePasswords"] = json!(hide_passwords); - json_object["manage"] = json!(can_manage); + json_object["manage"] = json!(manage); json_object } @@ -507,6 +513,52 @@ impl Collection { .unwrap_or(0) != 0 }} } + + pub async fn is_manageable_by_user(&self, user_uuid: &UserId, 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::manage.eq(true)).or(// Directly accessed collection + users_organizations::access_all.eq(true).or( // access_all in Organization + users_organizations::atype.le(MembershipType::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::manage.eq(true)) + ) + ) + ) + .count() + .first::(conn) + .ok() + .unwrap_or(0) != 0 + }} + } } /// Database methods @@ -537,7 +589,7 @@ impl CollectionUser { .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)) + .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) .load::(conn) .expect("Error loading users_collections") .from_db() @@ -550,6 +602,7 @@ impl CollectionUser { collection_uuid: &CollectionId, read_only: bool, hide_passwords: bool, + manage: bool, conn: &mut DbConn, ) -> EmptyResult { User::update_uuid_revision(user_uuid, conn).await; @@ -562,6 +615,7 @@ impl CollectionUser { users_collections::collection_uuid.eq(collection_uuid), users_collections::read_only.eq(read_only), users_collections::hide_passwords.eq(hide_passwords), + users_collections::manage.eq(manage), )) .execute(conn) { @@ -576,6 +630,7 @@ impl CollectionUser { users_collections::collection_uuid.eq(collection_uuid), users_collections::read_only.eq(read_only), users_collections::hide_passwords.eq(hide_passwords), + users_collections::manage.eq(manage), )) .execute(conn) .map_res("Error adding user to collection") @@ -590,12 +645,14 @@ impl CollectionUser { users_collections::collection_uuid.eq(collection_uuid), users_collections::read_only.eq(read_only), users_collections::hide_passwords.eq(hide_passwords), + users_collections::manage.eq(manage), )) .on_conflict((users_collections::user_uuid, users_collections::collection_uuid)) .do_update() .set(( users_collections::read_only.eq(read_only), users_collections::hide_passwords.eq(hide_passwords), + users_collections::manage.eq(manage), )) .execute(conn) .map_res("Error adding user to collection") @@ -636,7 +693,7 @@ impl CollectionUser { 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)) + .select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords, users_collections::manage)) .load::(conn) .expect("Error loading users_collections") .from_db() @@ -787,15 +844,17 @@ pub struct CollectionMembership { pub collection_uuid: CollectionId, pub read_only: bool, pub hide_passwords: bool, + pub manage: bool, } impl CollectionMembership { - pub fn to_json_details_for_user(&self, membership_type: i32) -> Value { + pub fn to_json_details_for_member(&self, membership_type: i32) -> Value { json!({ "id": self.membership_uuid, "readOnly": self.read_only, "hidePasswords": self.hide_passwords, "manage": membership_type >= MembershipType::Admin + || self.manage || (membership_type == MembershipType::Manager && !self.read_only && !self.hide_passwords), @@ -810,6 +869,7 @@ impl From for CollectionMembership { collection_uuid: c.collection_uuid, read_only: c.read_only, hide_passwords: c.hide_passwords, + manage: c.manage, } } } diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 5a72418d04..e85b8c05cf 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -29,6 +29,7 @@ db_object! { pub groups_uuid: GroupId, pub read_only: bool, pub hide_passwords: bool, + pub manage: bool, } #[derive(Identifiable, Queryable, Insertable)] @@ -92,7 +93,7 @@ impl Group { "id": entry.collections_uuid, "readOnly": entry.read_only, "hidePasswords": entry.hide_passwords, - "manage": !entry.read_only && !entry.hide_passwords, + "manage": entry.manage, }) }) .collect(); @@ -118,12 +119,19 @@ impl Group { } impl CollectionGroup { - pub fn new(collections_uuid: CollectionId, groups_uuid: GroupId, read_only: bool, hide_passwords: bool) -> Self { + pub fn new( + collections_uuid: CollectionId, + groups_uuid: GroupId, + read_only: bool, + hide_passwords: bool, + manage: bool, + ) -> Self { Self { collections_uuid, groups_uuid, read_only, hide_passwords, + manage, } } @@ -131,11 +139,12 @@ impl CollectionGroup { // If both read_only and hide_passwords are false, then manage should be true // You can't have an entry with read_only and manage, or hide_passwords and manage // Or an entry with everything to false + // For backwards compaibility and migration proposes we keep checking read_only and hide_password json!({ "id": self.groups_uuid, "readOnly": self.read_only, "hidePasswords": self.hide_passwords, - "manage": !self.read_only && !self.hide_passwords, + "manage": self.manage || (!self.read_only && !self.hide_passwords), }) } } @@ -319,6 +328,7 @@ impl CollectionGroup { collections_groups::groups_uuid.eq(&self.groups_uuid), collections_groups::read_only.eq(&self.read_only), collections_groups::hide_passwords.eq(&self.hide_passwords), + collections_groups::manage.eq(&self.manage), )) .execute(conn) { @@ -333,6 +343,7 @@ impl CollectionGroup { collections_groups::groups_uuid.eq(&self.groups_uuid), collections_groups::read_only.eq(&self.read_only), collections_groups::hide_passwords.eq(&self.hide_passwords), + collections_groups::manage.eq(&self.manage), )) .execute(conn) .map_res("Error adding group to collection") @@ -347,12 +358,14 @@ impl CollectionGroup { collections_groups::groups_uuid.eq(&self.groups_uuid), collections_groups::read_only.eq(self.read_only), collections_groups::hide_passwords.eq(self.hide_passwords), + collections_groups::manage.eq(self.manage), )) .on_conflict((collections_groups::collections_uuid, collections_groups::groups_uuid)) .do_update() .set(( collections_groups::read_only.eq(self.read_only), collections_groups::hide_passwords.eq(self.hide_passwords), + collections_groups::manage.eq(self.manage), )) .execute(conn) .map_res("Error adding group to collection") diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index af273804b4..6e6ee77ad7 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -522,13 +522,13 @@ impl Membership { .await .into_iter() .filter_map(|c| { - let (read_only, hide_passwords, can_manage) = if self.has_full_access() { + let (read_only, hide_passwords, manage) = if self.has_full_access() { (false, false, self.atype >= MembershipType::Manager) } else if let Some(cu) = cu.get(&c.uuid) { ( cu.read_only, cu.hide_passwords, - self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords, + cu.manage || (self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords), ) // If previous checks failed it might be that this user has access via a group, but we should not return those elements here // Those are returned via a special group endpoint @@ -542,7 +542,7 @@ impl Membership { "id": c.uuid, "readOnly": read_only, "hidePasswords": hide_passwords, - "manage": can_manage, + "manage": manage, })) }) .collect() @@ -611,6 +611,7 @@ impl Membership { "id": self.uuid, "readOnly": col_user.read_only, "hidePasswords": col_user.hide_passwords, + "manage": col_user.manage, }) } @@ -622,11 +623,12 @@ impl Membership { CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn).await; collections .iter() - .map(|c| { + .map(|cu| { json!({ - "id": c.collection_uuid, - "readOnly": c.read_only, - "hidePasswords": c.hide_passwords, + "id": cu.collection_uuid, + "readOnly": cu.read_only, + "hidePasswords": cu.hide_passwords, + "manage": cu.manage, }) }) .collect() diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index fa84ed05bd..573e4503b3 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -226,6 +226,7 @@ table! { collection_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, + manage -> Bool, } } @@ -295,6 +296,7 @@ table! { groups_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, + manage -> Bool, } } diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index d1ea4b02b5..a3707adf3a 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -226,6 +226,7 @@ table! { collection_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, + manage -> Bool, } } @@ -295,6 +296,7 @@ table! { groups_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, + manage -> Bool, } } diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index d1ea4b02b5..a3707adf3a 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -226,6 +226,7 @@ table! { collection_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, + manage -> Bool, } } @@ -295,6 +296,7 @@ table! { groups_uuid -> Text, read_only -> Bool, hide_passwords -> Bool, + manage -> Bool, } }