diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 3ccead45..989ca47d 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2463,13 +2463,19 @@ async fn get_groups_data( err!("Organization not found", "Organization id's do not match"); } - // For now, both the group list and the details view require full access to the organization - // (directly or via a group). Bitwarden also lets a manager of a specific collection read the - // plain list (to assign groups); handling that case is left for a follow-up. + // The details view (group→collection/user mappings) needs full org access; the plain list only + // needs manage access to a collection, so a manager of a collection (directly or via a group) + // can load it to assign groups. let has_full_access = headers.membership.has_full_access() || (CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&org_id, &headers.membership.uuid, &conn).await); - if !has_full_access { + let allowed = if details { + has_full_access + } else { + has_full_access + || Collection::has_manageable_collection_by_user(&org_id, &headers.membership.user_uuid, &conn).await + }; + if !allowed { err_code!("Resource not found.", "User does not have access", rocket::http::Status::NotFound.code); } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index f29843f7..2dd2f442 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -623,6 +623,49 @@ impl Collection { pub async fn is_manageable_by_user(&self, user_uuid: &UserId, conn: &DbConn) -> bool { Self::is_coll_manageable_by_user(&self.uuid, user_uuid, conn).await } + + // Whether the user has manage access to at least one collection in the org, directly or via a + // group. Org-scoped counterpart of is_coll_manageable_by_user. + pub async fn has_manageable_collection_by_user( + org_uuid: &OrganizationId, + user_uuid: &UserId, + conn: &DbConn, + ) -> bool { + let org_uuid = org_uuid.to_string(); + let user_uuid = user_uuid.to_string(); + conn.run(move |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( + collections_groups::table.on(collections_groups::groups_uuid + .eq(groups_users::groups_uuid) + .and(collections_groups::collections_uuid.eq(collections::uuid))), + ) + .filter(collections::org_uuid.eq(&org_uuid)) + .filter( + // Manage permission on a collection assigned directly or via a group. + users_collections::manage + .eq(true) + .or(collections_groups::manage.eq(true)), + ) + .count() + .first::(conn) + .ok() + .unwrap_or(0) + != 0 + }) + .await + } } /// Database methods