diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index c3940b3c..f4c22203 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -430,6 +430,12 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea let has_full_access_to_org = member.has_full_access() || (CONFIG.org_groups_enabled() && GroupUser::has_full_access_by_member(&org_id, &member.uuid, &conn).await); + // Custom users with a manage permission need the full collection list (metadata only) + // so the web client can render member/group collection assignments without crashing on + // collections it can't otherwise see. This exposes names/ids only, never cipher contents. + let can_read_collection_list = + member.manage_users || member.manage_groups || member.manage_policies; + // Get all admins, owners and managers who can manage/access all // Those are currently not listed in the col_users but need to be listed too. let manage_all_members: Vec = Membership::find_confirmed_and_manage_all_by_org(&org_id, &conn) @@ -453,8 +459,20 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea || (CONFIG.org_groups_enabled() && GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &conn).await); - // If the user is a manager, and is not assigned to this collection, skip this and continue with the next collection + // If the user is a manager and is not assigned to this collection, normally skip it. + // Exception: custom users with a manage permission get a metadata-only entry (no user + // or group access details) so the web client can resolve assignment references without + // crashing. This never exposes cipher contents. if !assigned { + if can_read_collection_list { + let mut json_object = col.to_json_details(&headers.user.uuid, None, &conn).await; + json_object["assigned"] = json!(false); + json_object["users"] = json!(Vec::::new()); + json_object["groups"] = json!(Vec::::new()); + json_object["object"] = json!("collectionAccessDetails"); + json_object["unmanaged"] = json!(false); + data.push(json_object); + } continue; } @@ -514,7 +532,10 @@ async fn post_organization_collections( let data: FullCollectionData = data.into_inner(); data.validate(&org_id, &conn).await?; - if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all { + // Managers and custom users may only create collections if they have full access. + // (A custom user with manage_users/manage_groups/manage_policies but no collection + // access must not be able to create collections.) + if !headers.membership.has_full_access() && headers.membership.atype < MembershipType::Admin { err!("You don't have permission to create collections") } @@ -585,6 +606,13 @@ async fn post_bulk_access_collections( err!("Can't find organization details") } + // Security: only callers who can actually manage collections (Admins/Owners, or users with + // full access) may change collection access in bulk. A custom user with only manage_users / + // manage_groups / manage_policies must not be able to modify collection assignments here. + if !headers.membership.has_full_access() { + err!("You don't have permission to modify collection access") + } + for col_id in data.collection_ids { let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &conn).await else { err!("Collection not found") @@ -1150,8 +1178,17 @@ async fn send_invite( ) .await; + // Security: only callers who can manage collections (Admins/Owners, or users with full + // access) may assign collection access when inviting. A custom user with only manage_users + // can invite members, but cannot grant them collection access. + let caller_can_manage_collections = headers.membership_type >= MembershipType::Admin + || match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { + Some(m) => m.has_full_access(), + None => false, + }; + // If no accessAll, add the collections received - if !access_all { + if !access_all && caller_can_manage_collections { for col in data.collections.iter().flatten() { match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn).await { None => err!("Collection not found in Organization"), @@ -1170,9 +1207,20 @@ async fn send_invite( } } - for group_id in &data.groups { - let mut group_entry = GroupUser::new(group_id.clone(), new_member.uuid.clone()); - group_entry.save(&conn).await?; + // Security: assigning groups can indirectly grant collection access via the groups' + // collections. Only callers who may manage groups (Admins/Owners or users with + // manage_groups) are allowed to assign groups when inviting. + let caller_can_manage_groups = headers.membership_type >= MembershipType::Admin + || match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { + Some(m) => m.manage_groups, + None => false, + }; + + if caller_can_manage_groups { + for group_id in &data.groups { + let mut group_entry = GroupUser::new(group_id.clone(), new_member.uuid.clone()); + group_entry.save(&conn).await?; + } } } @@ -1598,6 +1646,7 @@ async fn edit_member( { err!("Only Admins or Owners can grant custom management permissions") } + member_to_edit.access_all = access_all; member_to_edit.manage_users = manage_users; member_to_edit.manage_groups = manage_groups; @@ -1608,36 +1657,59 @@ async fn edit_member( // We need to perform the check after changing the type since `admin` is exempt. OrgPolicy::check_user_allowed(&member_to_edit, "modify", &conn).await?; - // Delete all the odd collections - for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &member_to_edit.user_uuid, &conn).await { - c.delete(&conn).await?; - } - - // If no accessAll, add the collections received - if !access_all { - for col in data.collections.iter().flatten() { - match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn).await { - None => err!("Collection not found in Organization"), - Some(collection) => { - CollectionUser::save( - &member_to_edit.user_uuid, - &collection.uuid, - col.read_only, - col.hide_passwords, - col.manage, - &conn, - ) - .await?; + // Security: only callers who can actually manage collections (Admins/Owners, or users + // with full access) may change a member's collection assignments. A custom user with only + // manage_users must not be able to add/remove collection access, so we leave the existing + // assignments untouched for them. + let caller_can_manage_collections = headers.membership_type >= MembershipType::Admin + || match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { + Some(m) => m.has_full_access(), + None => false, + }; + + if caller_can_manage_collections { + // Delete all the odd collections + for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &member_to_edit.user_uuid, &conn).await { + c.delete(&conn).await?; + } + + // If no accessAll, add the collections received + if !access_all { + for col in data.collections.iter().flatten() { + match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn).await { + None => err!("Collection not found in Organization"), + Some(collection) => { + CollectionUser::save( + &member_to_edit.user_uuid, + &collection.uuid, + col.read_only, + col.hide_passwords, + col.manage, + &conn, + ) + .await?; + } } } } } - GroupUser::delete_all_by_member(&member_to_edit.uuid, &conn).await?; + // Security: changing a member's group membership can indirectly grant collection access + // (via the groups' collections). Only callers who may manage groups (Admins/Owners or users + // with manage_groups) are allowed to change it. For others we leave group membership untouched. + let caller_can_manage_groups = headers.membership_type >= MembershipType::Admin + || match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { + Some(m) => m.manage_groups, + None => false, + }; - for group_id in data.groups.iter().flatten() { - let mut group_entry = GroupUser::new(group_id.clone(), member_to_edit.uuid.clone()); - group_entry.save(&conn).await?; + if caller_can_manage_groups { + GroupUser::delete_all_by_member(&member_to_edit.uuid, &conn).await?; + + for group_id in data.groups.iter().flatten() { + let mut group_entry = GroupUser::new(group_id.clone(), member_to_edit.uuid.clone()); + group_entry.save(&conn).await?; + } } log_event( @@ -2591,7 +2663,17 @@ async fn post_groups( ) .await; - add_update_group(group, group_request.collections, group_request.users, org_id, &headers, &conn).await + // Security: only callers who can manage collections may assign collections to a new group. + // A custom user with only manage_groups can create the group, but without collection access. + let caller_can_manage_collections = headers.membership_type >= MembershipType::Admin + || match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { + Some(m) => m.has_full_access(), + None => false, + }; + let collections_to_apply = + if caller_can_manage_collections { group_request.collections } else { Vec::new() }; + + add_update_group(group, collections_to_apply, group_request.users, org_id, &headers, &conn).await } #[put("/organizations//groups/", data = "")] @@ -2618,7 +2700,19 @@ async fn put_group( let updated_group = group_request.update_group(group); - CollectionGroup::delete_all_by_group(&group_id, &org_id, &conn).await?; + // Security: only callers who can actually manage collections (Admins/Owners, or users with + // full access) may change a group's collection assignments. A custom user with only + // manage_groups must not be able to add/remove collection access. For them we keep the + // group's existing collection assignments untouched (neither cleared nor overwritten). + let caller_can_manage_collections = headers.membership_type >= MembershipType::Admin + || match Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { + Some(m) => m.has_full_access(), + None => false, + }; + + if caller_can_manage_collections { + CollectionGroup::delete_all_by_group(&group_id, &org_id, &conn).await?; + } GroupUser::delete_all_by_group(&group_id, &org_id, &conn).await?; log_event( @@ -2632,7 +2726,10 @@ async fn put_group( ) .await; - add_update_group(updated_group, group_request.collections, group_request.users, org_id, &headers, &conn).await + // Only pass collection changes through if the caller is allowed to manage collections. + let collections_to_apply = + if caller_can_manage_collections { group_request.collections } else { Vec::new() }; + add_update_group(updated_group, collections_to_apply, group_request.users, org_id, &headers, &conn).await } async fn add_update_group( @@ -2680,10 +2777,10 @@ async fn add_update_group( async fn get_group_details( org_id: OrganizationId, group_id: GroupId, - headers: ManageGroupsHeaders, + headers: ManagerHeadersLoose, conn: DbConn, ) -> JsonResult { - if org_id != headers.org_id { + if org_id != headers.membership.org_uuid { err!("Organization not found", "Organization id's do not match"); } if !CONFIG.org_groups_enabled() { @@ -2766,8 +2863,8 @@ async fn bulk_delete_groups( } #[get("/organizations//groups/", rank = 2)] -async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: ManageGroupsHeaders, conn: DbConn) -> JsonResult { - if org_id != headers.org_id { +async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { + if org_id != headers.membership.org_uuid { err!("Organization not found", "Organization id's do not match"); } if !CONFIG.org_groups_enabled() { @@ -2785,10 +2882,10 @@ async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: ManageGro async fn get_group_members( org_id: OrganizationId, group_id: GroupId, - headers: ManageGroupsHeaders, + headers: ManagerHeadersLoose, conn: DbConn, ) -> JsonResult { - if org_id != headers.org_id { + if org_id != headers.membership.org_uuid { err!("Organization not found", "Organization id's do not match"); } if !CONFIG.org_groups_enabled() {