refactor(spaces): fix clippy, remove redundant code, consolidate tests
Some checks failed
Documentation / Build and Deploy Documentation (pull_request) Has been skipped
Checks / Prek / Pre-commit & Formatting (pull_request) Failing after 6s
Checks / Prek / Clippy and Cargo Tests (pull_request) Failing after 5s
Update flake hashes / update-flake-hashes (pull_request) Failing after 5s

- Fix assert!(x.is_err()) clippy errors -> unwrap_err()
- Remove redundant first PL conflict loop in build.rs (second loop
  covers all cases)
- Remove unused OwnedRoomId from SpaceEnforcementData tuple
- Merge make_user_roles/make_requirements into single make_set helper
- Remove trivial tests (HashMap::get, serde defaults, BTreeMap ordering)
- Remove duplicate tests between tests.rs and integration_tests.rs
- MockCache now delegates to existing free functions
- Remove unnecessary scope braces in join.rs
This commit is contained in:
ember33 2026-03-19 16:44:54 +01:00
parent 5b56a8b6ed
commit 879383bd9d
6 changed files with 76 additions and 350 deletions

View file

@ -347,27 +347,25 @@ pub async fn join_room_by_id_helper(
}
}
{
let parent_spaces = services.rooms.roles.get_parent_spaces(room_id).await;
if !parent_spaces.is_empty() {
let mut qualifies_in_any = false;
for parent_space in &parent_spaces {
if services
.rooms
.roles
.user_qualifies_for_room(parent_space, room_id, sender_user)
.await
{
qualifies_in_any = true;
break;
}
}
if !qualifies_in_any {
return Err!(Request(Forbidden(
"You do not have the required Space roles to join this room"
)));
let parent_spaces = services.rooms.roles.get_parent_spaces(room_id).await;
if !parent_spaces.is_empty() {
let mut qualifies_in_any = false;
for parent_space in &parent_spaces {
if services
.rooms
.roles
.user_qualifies_for_room(parent_space, room_id, sender_user)
.await
{
qualifies_in_any = true;
break;
}
}
if !qualifies_in_any {
return Err!(Request(Forbidden(
"You do not have the required Space roles to join this room"
)));
}
}
if server_in_room {

View file

@ -39,7 +39,7 @@ mod tests {
use super::*;
#[test]
fn serialize_space_roles() {
fn space_roles_roundtrip() {
let mut roles = BTreeMap::new();
roles.insert("admin".to_owned(), RoleDefinition {
description: "Space administrator".to_owned(),
@ -57,46 +57,6 @@ mod tests {
assert!(deserialized.roles["nsfw"].power_level.is_none());
}
#[test]
fn serialize_role_member() {
let content = SpaceRoleMemberEventContent {
roles: vec!["nsfw".to_owned(), "vip".to_owned()],
};
let json = serde_json::to_string(&content).unwrap();
let deserialized: SpaceRoleMemberEventContent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.roles, vec!["nsfw", "vip"]);
}
#[test]
fn serialize_role_room() {
let content = SpaceRoleRoomEventContent { required_roles: vec!["nsfw".to_owned()] };
let json = serde_json::to_string(&content).unwrap();
let deserialized: SpaceRoleRoomEventContent = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.required_roles, vec!["nsfw"]);
}
#[test]
fn empty_roles_deserialize() {
let json = r#"{"roles":{}}"#;
let content: SpaceRolesEventContent = serde_json::from_str(json).unwrap();
assert!(content.roles.is_empty());
}
#[test]
fn deserialize_role_with_power_level() {
let json = r#"{"description":"Admin","power_level":100}"#;
let role: RoleDefinition = serde_json::from_str(json).unwrap();
assert_eq!(role.description, "Admin");
assert_eq!(role.power_level, Some(100));
}
#[test]
fn deserialize_role_without_power_level() {
let json = r#"{"description":"NSFW access"}"#;
let role: RoleDefinition = serde_json::from_str(json).unwrap();
assert!(role.power_level.is_none());
}
#[test]
fn power_level_omitted_in_serialization_when_none() {
let role = RoleDefinition {
@ -107,40 +67,6 @@ mod tests {
assert!(!json.contains("power_level"));
}
#[test]
fn empty_member_roles() {
let content = SpaceRoleMemberEventContent { roles: vec![] };
let json = serde_json::to_string(&content).unwrap();
let deserialized: SpaceRoleMemberEventContent = serde_json::from_str(&json).unwrap();
assert!(deserialized.roles.is_empty());
}
#[test]
fn empty_room_requirements() {
let content = SpaceRoleRoomEventContent { required_roles: vec![] };
let json = serde_json::to_string(&content).unwrap();
let deserialized: SpaceRoleRoomEventContent = serde_json::from_str(&json).unwrap();
assert!(deserialized.required_roles.is_empty());
}
#[test]
fn roles_ordering_preserved() {
let mut roles = BTreeMap::new();
roles.insert("zebra".to_owned(), RoleDefinition {
description: "Z".to_owned(),
power_level: None,
});
roles.insert("alpha".to_owned(), RoleDefinition {
description: "A".to_owned(),
power_level: None,
});
let content = SpaceRolesEventContent { roles };
let json = serde_json::to_string(&content).unwrap();
let deserialized: SpaceRolesEventContent = serde_json::from_str(&json).unwrap();
let keys: Vec<_> = deserialized.roles.keys().collect();
assert_eq!(keys, vec!["alpha", "zebra"]);
}
#[test]
fn negative_power_level() {
let json = r#"{"description":"Restricted","power_level":-10}"#;
@ -148,28 +74,21 @@ mod tests {
assert_eq!(role.power_level, Some(-10));
}
#[test]
fn deserialize_ignores_unknown_fields() {
let json = r#"{"roles":["nsfw"],"extra_field":"ignored"}"#;
let content: SpaceRoleMemberEventContent = serde_json::from_str(json).unwrap();
assert_eq!(content.roles, vec!["nsfw"]);
}
#[test]
fn missing_description_fails() {
let json = r#"{"power_level":100}"#;
assert!(serde_json::from_str::<RoleDefinition>(json).is_err());
serde_json::from_str::<RoleDefinition>(json).unwrap_err();
}
#[test]
fn wrong_type_for_roles_fails() {
let json = r#"{"roles":"not_an_array"}"#;
assert!(serde_json::from_str::<SpaceRoleMemberEventContent>(json).is_err());
serde_json::from_str::<SpaceRoleMemberEventContent>(json).unwrap_err();
}
#[test]
fn wrong_type_for_required_roles_fails() {
let json = r#"{"required_roles":42}"#;
assert!(serde_json::from_str::<SpaceRoleRoomEventContent>(json).is_err());
serde_json::from_str::<SpaceRoleRoomEventContent>(json).unwrap_err();
}
}

View file

@ -1,17 +1,13 @@
//! Cache consistency tests using a MockCache that mirrors the Service's
//! cache structures. These tests validate the algorithm/logic but do NOT
//! exercise the actual Service methods (which require async service
//! dependencies). See `tests.rs` for tests that call the extracted pure
//! logic functions directly.
use std::collections::{BTreeMap, HashMap, HashSet};
use conduwuit_core::matrix::space_roles::RoleDefinition;
use ruma::{OwnedRoomId, OwnedUserId, room_id, user_id};
use super::tests::{make_requirements, make_roles, make_user_roles};
use super::{
compute_user_power_level, roles_satisfy_requirements,
tests::{make_roles, make_set},
};
/// Simulates the full cache state for a space.
struct MockCache {
roles: HashMap<OwnedRoomId, BTreeMap<String, RoleDefinition>>,
user_roles: HashMap<OwnedRoomId, HashMap<OwnedUserId, HashSet<String>>>,
@ -75,35 +71,22 @@ impl MockCache {
room: &OwnedRoomId,
user: &OwnedUserId,
) -> bool {
let reqs = self.room_requirements.get(space).and_then(|r| r.get(room));
match reqs {
| None => true,
| Some(required) if required.is_empty() => true,
| Some(required) => {
let assigned = self.user_roles.get(space).and_then(|u| u.get(user));
match assigned {
| None => false,
| Some(roles) => required.iter().all(|r| roles.contains(r)),
}
},
let Some(required) = self.room_requirements.get(space).and_then(|r| r.get(room)) else {
return true;
};
if required.is_empty() {
return true;
}
let Some(assigned) = self.user_roles.get(space).and_then(|u| u.get(user)) else {
return false;
};
roles_satisfy_requirements(required, assigned)
}
fn get_power_level(&self, space: &OwnedRoomId, user: &OwnedUserId) -> Option<i64> {
let role_defs = self.roles.get(space)?;
let assigned = self.user_roles.get(space)?.get(user)?;
assigned
.iter()
.filter_map(|r| role_defs.get(r)?.power_level)
.max()
}
fn clear(&mut self) {
self.roles.clear();
self.user_roles.clear();
self.room_requirements.clear();
self.room_to_space.clear();
compute_user_power_level(role_defs, assigned)
}
}
@ -117,10 +100,10 @@ fn cache_populate_and_lookup() {
cache.add_space(space.clone(), make_roles(&[("admin", Some(100)), ("nsfw", None)]));
cache.add_child(&space, child.clone());
cache.assign_role(&space, alice.clone(), "nsfw".to_owned());
cache.set_room_requirements(&space, child.clone(), make_requirements(&["nsfw"]));
cache.set_room_requirements(&space, child.clone(), make_set(&["nsfw"]));
assert!(cache.user_qualifies(&space, &child, &alice));
assert_eq!(cache.get_power_level(&space, &alice), None); // nsfw has no PL
assert_eq!(cache.get_power_level(&space, &alice), None);
}
#[test]
@ -132,11 +115,10 @@ fn cache_invalidation_on_role_revoke() {
cache.add_space(space.clone(), make_roles(&[("nsfw", None)]));
cache.assign_role(&space, alice.clone(), "nsfw".to_owned());
cache.set_room_requirements(&space, child.clone(), make_requirements(&["nsfw"]));
cache.set_room_requirements(&space, child.clone(), make_set(&["nsfw"]));
assert!(cache.user_qualifies(&space, &child, &alice));
// Revoke
cache.revoke_role(&space, &alice, "nsfw");
assert!(!cache.user_qualifies(&space, &child, &alice));
}
@ -150,50 +132,14 @@ fn cache_invalidation_on_requirement_change() {
cache.add_space(space.clone(), make_roles(&[("nsfw", None), ("vip", None)]));
cache.assign_role(&space, alice.clone(), "vip".to_owned());
cache.set_room_requirements(&space, child.clone(), make_requirements(&["vip"]));
cache.set_room_requirements(&space, child.clone(), make_set(&["vip"]));
assert!(cache.user_qualifies(&space, &child, &alice));
// Add nsfw requirement
cache.set_room_requirements(&space, child.clone(), make_requirements(&["vip", "nsfw"]));
cache.set_room_requirements(&space, child.clone(), make_set(&["vip", "nsfw"]));
assert!(!cache.user_qualifies(&space, &child, &alice));
}
#[test]
fn cache_clear_empties_all() {
let mut cache = MockCache::new();
let space = room_id!("!space:example.com").to_owned();
cache.add_space(space.clone(), make_roles(&[("admin", Some(100))]));
cache.assign_role(&space, user_id!("@alice:example.com").to_owned(), "admin".to_owned());
cache.clear();
assert!(cache.roles.is_empty());
assert!(cache.user_roles.is_empty());
assert!(cache.room_requirements.is_empty());
assert!(cache.room_to_space.is_empty());
}
#[test]
fn cache_reverse_lookup_consistency() {
let mut cache = MockCache::new();
let space = room_id!("!space:example.com").to_owned();
let child1 = room_id!("!child1:example.com").to_owned();
let child2 = room_id!("!child2:example.com").to_owned();
cache.add_child(&space, child1.clone());
cache.add_child(&space, child2.clone());
assert!(cache.room_to_space.get(&child1).unwrap().contains(&space));
assert!(cache.room_to_space.get(&child2).unwrap().contains(&space));
assert!(
cache
.room_to_space
.get(room_id!("!unknown:example.com"))
.is_none()
);
}
#[test]
fn cache_power_level_updates_on_role_change() {
let mut cache = MockCache::new();
@ -202,18 +148,14 @@ fn cache_power_level_updates_on_role_change() {
cache.add_space(space.clone(), make_roles(&[("admin", Some(100)), ("mod", Some(50))]));
// No roles -> no PL
assert_eq!(cache.get_power_level(&space, &alice), None);
// Assign mod -> PL 50
cache.assign_role(&space, alice.clone(), "mod".to_owned());
assert_eq!(cache.get_power_level(&space, &alice), Some(50));
// Also assign admin -> PL 100 (highest wins)
cache.assign_role(&space, alice.clone(), "admin".to_owned());
assert_eq!(cache.get_power_level(&space, &alice), Some(100));
// Revoke admin -> back to PL 50
cache.revoke_role(&space, &alice, "admin");
assert_eq!(cache.get_power_level(&space, &alice), Some(50));
}

View file

@ -1,48 +1,29 @@
use std::collections::{HashMap, HashSet};
use ruma::{room_id, user_id};
use super::{
compute_user_power_level, roles_satisfy_requirements,
tests::{make_requirements, make_roles, make_user_roles},
};
#[test]
fn scenario_user_gains_and_loses_access() {
let room_reqs = make_requirements(&["nsfw"]);
let no_roles: HashSet<String> = HashSet::new();
assert!(!roles_satisfy_requirements(&room_reqs, &no_roles));
let with_nsfw = make_user_roles(&["nsfw"]);
assert!(roles_satisfy_requirements(&room_reqs, &with_nsfw));
let no_roles: HashSet<String> = HashSet::new();
assert!(!roles_satisfy_requirements(&room_reqs, &no_roles));
}
use super::{roles_satisfy_requirements, tests::make_set};
#[test]
fn scenario_room_adds_requirement_existing_members_checked() {
let alice_roles = make_user_roles(&["vip"]);
let bob_roles = make_user_roles(&["vip", "nsfw"]);
let alice_roles = make_set(&["vip"]);
let bob_roles = make_set(&["vip", "nsfw"]);
let empty_reqs: HashSet<String> = HashSet::new();
assert!(roles_satisfy_requirements(&empty_reqs, &alice_roles));
assert!(roles_satisfy_requirements(&empty_reqs, &bob_roles));
let new_reqs = make_requirements(&["nsfw"]);
let new_reqs = make_set(&["nsfw"]);
assert!(!roles_satisfy_requirements(&new_reqs, &alice_roles));
assert!(roles_satisfy_requirements(&new_reqs, &bob_roles));
}
#[test]
fn scenario_multiple_rooms_different_requirements() {
let alice_roles = make_user_roles(&["nsfw", "vip"]);
let bob_roles = make_user_roles(&["nsfw"]);
let alice_roles = make_set(&["nsfw", "vip"]);
let bob_roles = make_set(&["nsfw"]);
let nsfw_reqs = make_requirements(&["nsfw"]);
let vip_reqs = make_requirements(&["vip"]);
let both_reqs = make_requirements(&["nsfw", "vip"]);
let nsfw_reqs = make_set(&["nsfw"]);
let vip_reqs = make_set(&["vip"]);
let both_reqs = make_set(&["nsfw", "vip"]);
assert!(roles_satisfy_requirements(&nsfw_reqs, &alice_roles));
assert!(roles_satisfy_requirements(&vip_reqs, &alice_roles));
@ -53,44 +34,15 @@ fn scenario_multiple_rooms_different_requirements() {
assert!(!roles_satisfy_requirements(&both_reqs, &bob_roles));
}
#[test]
fn scenario_power_level_cascading_highest_wins() {
let roles = make_roles(&[("admin", Some(100)), ("mod", Some(50)), ("helper", Some(25))]);
let admin_mod = make_user_roles(&["admin", "mod"]);
assert_eq!(compute_user_power_level(&roles, &admin_mod), Some(100));
let helper = make_user_roles(&["helper"]);
assert_eq!(compute_user_power_level(&roles, &helper), Some(25));
}
#[test]
fn scenario_roles_do_not_cascade_across_spaces() {
let space_a = room_id!("!space_a:example.com");
let space_b = room_id!("!space_b:example.com");
let alice = user_id!("@alice:example.com");
let mut space_user_roles: HashMap<_, HashMap<_, HashSet<String>>> = HashMap::new();
space_user_roles
.entry(space_a.to_owned())
.or_default()
.insert(alice.to_owned(), make_user_roles(&["nsfw"]));
let alice_roles_in_b = space_user_roles
.get(space_b)
.and_then(|users| users.get(alice));
assert!(alice_roles_in_b.is_none());
}
#[test]
fn scenario_identify_auto_join_candidates() {
let alice_roles = make_user_roles(&["nsfw", "vip"]);
let alice_roles = make_set(&["nsfw", "vip"]);
let mut room_reqs: HashMap<String, HashSet<String>> = HashMap::new();
room_reqs.insert("general".to_owned(), HashSet::new());
room_reqs.insert("nsfw-chat".to_owned(), make_requirements(&["nsfw"]));
room_reqs.insert("vip-lounge".to_owned(), make_requirements(&["vip"]));
room_reqs.insert("staff-only".to_owned(), make_requirements(&["staff"]));
room_reqs.insert("nsfw-chat".to_owned(), make_set(&["nsfw"]));
room_reqs.insert("vip-lounge".to_owned(), make_set(&["vip"]));
room_reqs.insert("staff-only".to_owned(), make_set(&["staff"]));
let qualifying: Vec<_> = room_reqs
.iter()
@ -106,13 +58,13 @@ fn scenario_identify_auto_join_candidates() {
#[test]
fn scenario_identify_kick_candidates_after_role_revocation() {
let alice_roles_after = make_user_roles(&["vip"]);
let alice_roles_after = make_set(&["vip"]);
let mut rooms: HashMap<String, HashSet<String>> = HashMap::new();
rooms.insert("general".to_owned(), HashSet::new());
rooms.insert("nsfw-chat".to_owned(), make_requirements(&["nsfw"]));
rooms.insert("vip-lounge".to_owned(), make_requirements(&["vip"]));
rooms.insert("nsfw-vip".to_owned(), make_requirements(&["nsfw", "vip"]));
rooms.insert("nsfw-chat".to_owned(), make_set(&["nsfw"]));
rooms.insert("vip-lounge".to_owned(), make_set(&["vip"]));
rooms.insert("nsfw-vip".to_owned(), make_set(&["nsfw", "vip"]));
let kick_from: Vec<_> = rooms
.iter()

View file

@ -1,11 +1,9 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use std::collections::{BTreeMap, HashSet};
use conduwuit_core::matrix::space_roles::RoleDefinition;
use ruma::{OwnedRoomId, room_id};
use super::{compute_user_power_level, roles_satisfy_requirements};
/// Helper to build a role definitions map.
pub fn make_roles(entries: &[(&str, Option<i64>)]) -> BTreeMap<String, RoleDefinition> {
entries
.iter()
@ -18,113 +16,65 @@ pub fn make_roles(entries: &[(&str, Option<i64>)]) -> BTreeMap<String, RoleDefin
.collect()
}
pub fn make_user_roles(roles: &[&str]) -> HashSet<String> {
roles.iter().map(|s| (*s).to_owned()).collect()
}
pub fn make_requirements(roles: &[&str]) -> HashSet<String> {
roles.iter().map(|s| (*s).to_owned()).collect()
pub fn make_set(items: &[&str]) -> HashSet<String> {
items.iter().map(|s| (*s).to_owned()).collect()
}
#[test]
fn power_level_single_role() {
let roles = make_roles(&[("admin", Some(100)), ("mod", Some(50))]);
let user_assigned = make_user_roles(&["admin"]);
assert_eq!(compute_user_power_level(&roles, &user_assigned), Some(100));
assert_eq!(compute_user_power_level(&roles, &make_set(&["admin"])), Some(100));
}
#[test]
fn power_level_multiple_roles_takes_highest() {
let roles = make_roles(&[("admin", Some(100)), ("mod", Some(50)), ("helper", Some(25))]);
let user_assigned = make_user_roles(&["mod", "helper"]);
assert_eq!(compute_user_power_level(&roles, &user_assigned), Some(50));
assert_eq!(compute_user_power_level(&roles, &make_set(&["mod", "helper"])), Some(50));
}
#[test]
fn power_level_no_power_roles() {
let roles = make_roles(&[("nsfw", None), ("vip", None)]);
let user_assigned = make_user_roles(&["nsfw", "vip"]);
assert_eq!(compute_user_power_level(&roles, &user_assigned), None);
assert_eq!(compute_user_power_level(&roles, &make_set(&["nsfw", "vip"])), None);
}
#[test]
fn power_level_mixed_roles() {
let roles = make_roles(&[("mod", Some(50)), ("nsfw", None)]);
let user_assigned = make_user_roles(&["mod", "nsfw"]);
assert_eq!(compute_user_power_level(&roles, &user_assigned), Some(50));
assert_eq!(compute_user_power_level(&roles, &make_set(&["mod", "nsfw"])), Some(50));
}
#[test]
fn power_level_no_roles_assigned() {
let roles = make_roles(&[("admin", Some(100))]);
let user_assigned: HashSet<String> = HashSet::new();
assert_eq!(compute_user_power_level(&roles, &user_assigned), None);
assert_eq!(compute_user_power_level(&roles, &HashSet::new()), None);
}
#[test]
fn power_level_unknown_role_ignored() {
let roles = make_roles(&[("admin", Some(100))]);
let user_assigned = make_user_roles(&["nonexistent"]);
assert_eq!(compute_user_power_level(&roles, &user_assigned), None);
assert_eq!(compute_user_power_level(&roles, &make_set(&["nonexistent"])), None);
}
#[test]
fn qualifies_with_all_required_roles() {
let required = make_requirements(&["nsfw", "vip"]);
let user_assigned = make_user_roles(&["nsfw", "vip", "extra"]);
assert!(roles_satisfy_requirements(&required, &user_assigned));
assert!(roles_satisfy_requirements(
&make_set(&["nsfw", "vip"]),
&make_set(&["nsfw", "vip", "extra"]),
));
}
#[test]
fn does_not_qualify_missing_one_role() {
let required = make_requirements(&["nsfw", "vip"]);
let user_assigned = make_user_roles(&["nsfw"]);
assert!(!roles_satisfy_requirements(&required, &user_assigned));
assert!(!roles_satisfy_requirements(&make_set(&["nsfw", "vip"]), &make_set(&["nsfw"]),));
}
#[test]
fn qualifies_with_no_requirements() {
let required: HashSet<String> = HashSet::new();
let user_assigned = make_user_roles(&["nsfw"]);
assert!(roles_satisfy_requirements(&required, &user_assigned));
assert!(roles_satisfy_requirements(&HashSet::new(), &make_set(&["nsfw"])));
}
#[test]
fn does_not_qualify_with_no_roles() {
let required = make_requirements(&["nsfw"]);
let user_assigned: HashSet<String> = HashSet::new();
assert!(!roles_satisfy_requirements(&required, &user_assigned));
}
#[test]
fn qualifies_empty_requirements_empty_roles() {
let required: HashSet<String> = HashSet::new();
let user_assigned: HashSet<String> = HashSet::new();
assert!(roles_satisfy_requirements(&required, &user_assigned));
}
#[test]
fn room_to_space_lookup() {
let mut room_to_space: HashMap<OwnedRoomId, HashSet<OwnedRoomId>> = HashMap::new();
let space = room_id!("!space:example.com").to_owned();
let child = room_id!("!child:example.com").to_owned();
room_to_space
.entry(child.clone())
.or_default()
.insert(space.clone());
assert!(room_to_space.get(&child).unwrap().contains(&space));
assert!(
room_to_space
.get(room_id!("!unknown:example.com"))
.is_none()
);
}
#[test]
fn default_roles_contain_admin_and_mod() {
let roles = make_roles(&[("admin", Some(100)), ("mod", Some(50))]);
assert!(roles.contains_key("admin"));
assert!(roles.contains_key("mod"));
assert_eq!(roles["admin"].power_level, Some(100));
assert_eq!(roles["mod"].power_level, Some(50));
assert!(!roles_satisfy_requirements(&make_set(&["nsfw"]), &HashSet::new()));
}

View file

@ -100,13 +100,8 @@ pub async fn build_and_append_pdu(
)));
}
}
// Space permission cascading: reject power level changes that conflict
// with Space-granted levels (exempt the server user so sync_power_levels works)
type SpaceEnforcementData = (
ruma::OwnedRoomId,
Vec<(OwnedUserId, HashSet<String>)>,
BTreeMap<String, RoleDefinition>,
);
type SpaceEnforcementData =
(Vec<(OwnedUserId, HashSet<String>)>, BTreeMap<String, RoleDefinition>);
if *pdu.kind() == TimelineEventType::RoomPowerLevels
&& pdu.sender()
@ -116,33 +111,6 @@ pub async fn build_and_append_pdu(
let parent_spaces = self.services.roles.get_parent_spaces(&room_id).await;
if let Ok(proposed) = pdu.get_content::<RoomPowerLevelsEventContent>() {
for parent_space in &parent_spaces {
// Check proposed users don't conflict with space-granted PLs
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 {
debug_warn!(
user_id = %user_id,
room_id = %room_id,
proposed_pl = i64::from(*proposed_pl),
space_pl,
"Rejecting PL change conflicting with space role"
);
return Err!(Request(Forbidden(
"Cannot change power level that is set by Space roles"
)));
}
}
}
}
// Also check that space-managed users aren't omitted
// Clone data out of guards to avoid holding locks across await
let space_data: Vec<SpaceEnforcementData> = {
let user_roles_guard = self.services.roles.user_roles.read().await;
let roles_guard = self.services.roles.roles.read().await;
@ -152,7 +120,6 @@ pub async fn build_and_append_pdu(
let space_users = user_roles_guard.get(ps)?;
let role_defs = roles_guard.get(ps)?;
Some((
ps.clone(),
space_users
.iter()
.map(|(u, r)| (u.clone(), r.clone()))
@ -162,11 +129,9 @@ pub async fn build_and_append_pdu(
})
.collect()
};
// Guards dropped here
for (_parent_space, space_users, role_defs) in &space_data {
for (space_users, role_defs) in &space_data {
for (user_id, assigned_roles) in space_users {
// Only enforce for users who are actually members of this room
if !self.services.state_cache.is_joined(user_id, &room_id).await {
continue;
}