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
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:
parent
4df2fe2923
commit
982c01e562
1 changed files with 64 additions and 57 deletions
|
|
@ -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<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
|
||||
.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<String>)>, BTreeMap<String, RoleDefinition>);
|
||||
let space_data: Vec<SpaceEnforcementData> = {
|
||||
let mut effective_pls: HashMap<OwnedUserId, i64> = 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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue