feat(spaces): reject power level changes that conflict with space roles

Checks proposed m.room.power_levels events against Space-granted power
levels. Rejects if any user's proposed PL is below their Space role PL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ember33 2026-03-18 09:58:12 +01:00
parent 9eb2d2542a
commit f7cfc9d35d
2 changed files with 28 additions and 4 deletions

View file

@ -127,7 +127,7 @@ where
// Make unsigned fields correct. This is not properly documented in the spec,
// but state events need to have previous content in the unsigned field, so
// clients can easily interpret things like membership changes
if let Some(_state_key) = pdu.state_key() {
if let Some(state_key) = pdu.state_key() {
if let CanonicalJsonValue::Object(unsigned) = pdu_json
.entry("unsigned".to_owned())
.or_insert_with(|| CanonicalJsonValue::Object(BTreeMap::default()))
@ -226,7 +226,7 @@ where
let mut highlights = Vec::with_capacity(push_target.len().saturating_add(1));
if *pdu.kind() == TimelineEventType::RoomMember {
if let Some(_state_key) = pdu.state_key() {
if let Some(state_key) = pdu.state_key() {
let target_user_id = UserId::parse(state_key)?;
if self.services.users.is_active_local(target_user_id).await {
@ -327,7 +327,7 @@ where
}
},
| TimelineEventType::SpaceChild =>
if let Some(_state_key) = pdu.state_key() {
if let Some(state_key) = pdu.state_key() {
self.services
.spaces
.roomid_spacehierarchy_cache
@ -336,7 +336,7 @@ where
.remove(room_id);
},
| TimelineEventType::RoomMember => {
if let Some(_state_key) = pdu.state_key() {
if let Some(state_key) = pdu.state_key() {
// if the state_key fails
let target_user_id =
UserId::parse(state_key).expect("This state_key was previously validated");

View file

@ -97,6 +97,30 @@ pub async fn build_and_append_pdu(
)));
}
}
// Space permission cascading: reject power level changes that conflict
// with Space-granted levels
if self.services.roles.is_enabled()
&& *pdu.kind() == TimelineEventType::RoomPowerLevels
{
if let Some(parent_space) = self.services.roles.get_parent_space(&room_id).await {
use ruma::events::room::power_levels::RoomPowerLevelsEventContent;
if let Ok(proposed) = pdu.get_content::<RoomPowerLevelsEventContent>() {
for (user_id, proposed_pl) in &proposed.users {
if let Some(space_pl) =
self.services.roles.get_user_power_level(&parent_space, user_id).await
{
if i64::from(*proposed_pl) < space_pl {
return Err!(Request(Forbidden(
"Cannot set power level below Space-granted level"
)));
}
}
}
}
}
}
if *pdu.kind() == TimelineEventType::RoomCreate {
trace!("Creating shortroomid for {room_id}");
self.services