From 982c01e56272dd435f45acb3b2185195381c9d38 Mon Sep 17 00:00:00 2001 From: ember33 Date: Thu, 19 Mar 2026 22:26:30 +0100 Subject: [PATCH] fix(spaces): use highest-wins PL logic across multiple parent spaces When a room is a child of multiple spaces, sync_power_levels now computes the maximum power level across ALL parent spaces for each user, not just the triggering space. validate_pl_change similarly computes the effective PL as the max across all parents before rejecting conflicting proposals. --- src/service/rooms/roles/mod.rs | 121 +++++++++++++++++---------------- 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/src/service/rooms/roles/mod.rs b/src/service/rooms/roles/mod.rs index 4a28bcb4..e93c5ef9 100644 --- a/src/service/rooms/roles/mod.rs +++ b/src/service/rooms/roles/mod.rs @@ -556,23 +556,33 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re .collect() .await; + let all_parents = self.get_parent_spaces(room_id).await; + let mut changed = false; for user_id in &members { if user_id == server_user { continue; } - if let Some(space_pl) = self.get_user_power_level(space_id, user_id).await { - let space_pl_int = Int::new_saturating(space_pl); + + let mut max_pl: Option = None; + for parent in &all_parents { + if let Some(pl) = self.get_user_power_level(parent, user_id).await { + max_pl = Some(max_pl.map_or(pl, |current| current.max(pl))); + } + } + + if let Some(effective_pl) = max_pl { + let effective_pl_int = Int::new_saturating(effective_pl); let current_pl = power_levels_content .users .get(user_id) .copied() .unwrap_or(power_levels_content.users_default); - if current_pl != space_pl_int { + if current_pl != effective_pl_int { power_levels_content .users - .insert(user_id.clone(), space_pl_int); + .insert(user_id.clone(), effective_pl_int); changed = true; } } @@ -754,67 +764,64 @@ pub async fn validate_pl_change( return Ok(()); } - type SpaceEnforcementData = - (Vec<(OwnedUserId, HashSet)>, BTreeMap); - let space_data: Vec = { + let mut effective_pls: HashMap = HashMap::new(); + { let roles_guard = self.roles.read().await; let user_roles_guard = self.user_roles.read().await; - parent_spaces - .iter() - .filter_map(|ps| { - let space_users = user_roles_guard.get(ps)?; - let role_defs = roles_guard.get(ps)?; - Some(( - space_users - .iter() - .map(|(u, r)| (u.clone(), r.clone())) - .collect(), - role_defs.clone(), - )) - }) - .collect() - }; - - for (space_users, role_defs) in &space_data { - for (user_id, assigned_roles) in space_users { - if !self.services.state_cache.is_joined(user_id, room_id).await { + for ps in &parent_spaces { + let Some(space_users) = user_roles_guard.get(ps) else { continue; - } - let space_pl = assigned_roles - .iter() - .filter_map(|r| role_defs.get(r)?.power_level) - .max(); - if let Some(space_pl) = space_pl { - match proposed.users.get(user_id) { - | None if i64::from(proposed.users_default) != space_pl => { - debug_warn!( - user_id = %user_id, - room_id = %room_id, - space_pl, - "Rejecting PL change: space-managed user omitted" - ); - return Err!(Request(Forbidden( - "Cannot omit a user whose power level is managed by Space roles" - ))); - }, - | Some(pl) if i64::from(*pl) != space_pl => { - debug_warn!( - user_id = %user_id, - room_id = %room_id, - proposed_pl = i64::from(*pl), - space_pl, - "Rejecting PL change conflicting with space role" - ); - return Err!(Request(Forbidden( - "Cannot change power level that is set by Space roles" - ))); - }, - | _ => {}, + }; + let Some(role_defs) = roles_guard.get(ps) else { + continue; + }; + for (user_id, assigned_roles) in space_users { + let pl = assigned_roles + .iter() + .filter_map(|r| role_defs.get(r)?.power_level) + .max(); + if let Some(pl) = pl { + effective_pls + .entry(user_id.clone()) + .and_modify(|current| *current = (*current).max(pl)) + .or_insert(pl); } } } } + for (user_id, effective_pl) in &effective_pls { + if !self.services.state_cache.is_joined(user_id, room_id).await { + continue; + } + match proposed.users.get(user_id) { + | None if i64::from(proposed.users_default) != *effective_pl => { + debug_warn!( + user_id = %user_id, + room_id = %room_id, + effective_pl, + "Rejecting PL change: space-managed user omitted" + ); + return Err!(Request(Forbidden( + "Cannot omit a user whose power level is managed by Space roles" + ))); + }, + | Some(pl) if i64::from(*pl) != *effective_pl => { + debug_warn!( + user_id = %user_id, + room_id = %room_id, + proposed_pl = i64::from(*pl), + effective_pl, + "Rejecting PL change conflicting with space role" + ); + return Err!(Request(Forbidden( + "Cannot change power level that is set by Space roles" + ))); + }, + | _ => {}, + } + } + Ok(()) }