fix(spaces): use highest-wins PL logic across multiple parent spaces
Some checks failed
Documentation / Build and Deploy Documentation (pull_request) Has been skipped
Checks / Prek / Pre-commit & Formatting (pull_request) Failing after 5s
Checks / Prek / Clippy and Cargo Tests (pull_request) Failing after 5s
Update flake hashes / update-flake-hashes (pull_request) Failing after 6s

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.
This commit is contained in:
ember33 2026-03-19 22:26:30 +01:00
parent 4df2fe2923
commit 982c01e562

View file

@ -556,23 +556,33 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re
.collect() .collect()
.await; .await;
let all_parents = self.get_parent_spaces(room_id).await;
let mut changed = false; let mut changed = false;
for user_id in &members { for user_id in &members {
if user_id == server_user { if user_id == server_user {
continue; 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<i64> = 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 let current_pl = power_levels_content
.users .users
.get(user_id) .get(user_id)
.copied() .copied()
.unwrap_or(power_levels_content.users_default); .unwrap_or(power_levels_content.users_default);
if current_pl != space_pl_int { if current_pl != effective_pl_int {
power_levels_content power_levels_content
.users .users
.insert(user_id.clone(), space_pl_int); .insert(user_id.clone(), effective_pl_int);
changed = true; changed = true;
} }
} }
@ -754,67 +764,64 @@ pub async fn validate_pl_change(
return Ok(()); return Ok(());
} }
type SpaceEnforcementData = let mut effective_pls: HashMap<OwnedUserId, i64> = HashMap::new();
(Vec<(OwnedUserId, HashSet<String>)>, BTreeMap<String, RoleDefinition>); {
let space_data: Vec<SpaceEnforcementData> = {
let roles_guard = self.roles.read().await; let roles_guard = self.roles.read().await;
let user_roles_guard = self.user_roles.read().await; let user_roles_guard = self.user_roles.read().await;
parent_spaces for ps in &parent_spaces {
.iter() let Some(space_users) = user_roles_guard.get(ps) else {
.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 {
continue; continue;
} };
let space_pl = assigned_roles let Some(role_defs) = roles_guard.get(ps) else {
.iter() continue;
.filter_map(|r| role_defs.get(r)?.power_level) };
.max(); for (user_id, assigned_roles) in space_users {
if let Some(space_pl) = space_pl { let pl = assigned_roles
match proposed.users.get(user_id) { .iter()
| None if i64::from(proposed.users_default) != space_pl => { .filter_map(|r| role_defs.get(r)?.power_level)
debug_warn!( .max();
user_id = %user_id, if let Some(pl) = pl {
room_id = %room_id, effective_pls
space_pl, .entry(user_id.clone())
"Rejecting PL change: space-managed user omitted" .and_modify(|current| *current = (*current).max(pl))
); .or_insert(pl);
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"
)));
},
| _ => {},
} }
} }
} }
} }
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(()) Ok(())
} }