diff --git a/src/api/client/membership/join.rs b/src/api/client/membership/join.rs index 1cb7b978..1d3fa090 100644 --- a/src/api/client/membership/join.rs +++ b/src/api/client/membership/join.rs @@ -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 { diff --git a/src/core/matrix/space_roles.rs b/src/core/matrix/space_roles.rs index b900a053..3d4f6e5b 100644 --- a/src/core/matrix/space_roles.rs +++ b/src/core/matrix/space_roles.rs @@ -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::(json).is_err()); + serde_json::from_str::(json).unwrap_err(); } #[test] fn wrong_type_for_roles_fails() { let json = r#"{"roles":"not_an_array"}"#; - assert!(serde_json::from_str::(json).is_err()); + serde_json::from_str::(json).unwrap_err(); } #[test] fn wrong_type_for_required_roles_fails() { let json = r#"{"required_roles":42}"#; - assert!(serde_json::from_str::(json).is_err()); + serde_json::from_str::(json).unwrap_err(); } } diff --git a/src/service/rooms/roles/cache_tests.rs b/src/service/rooms/roles/cache_tests.rs index 6e709351..31fece94 100644 --- a/src/service/rooms/roles/cache_tests.rs +++ b/src/service/rooms/roles/cache_tests.rs @@ -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>, user_roles: HashMap>>, @@ -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 { 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)); } diff --git a/src/service/rooms/roles/integration_tests.rs b/src/service/rooms/roles/integration_tests.rs index dbdbe547..344da40b 100644 --- a/src/service/rooms/roles/integration_tests.rs +++ b/src/service/rooms/roles/integration_tests.rs @@ -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 = 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 = 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 = 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>> = 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> = 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> = 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() diff --git a/src/service/rooms/roles/tests.rs b/src/service/rooms/roles/tests.rs index 1579cdb6..771f9196 100644 --- a/src/service/rooms/roles/tests.rs +++ b/src/service/rooms/roles/tests.rs @@ -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)]) -> BTreeMap { entries .iter() @@ -18,113 +16,65 @@ pub fn make_roles(entries: &[(&str, Option)]) -> BTreeMap HashSet { - roles.iter().map(|s| (*s).to_owned()).collect() -} - -pub fn make_requirements(roles: &[&str]) -> HashSet { - roles.iter().map(|s| (*s).to_owned()).collect() +pub fn make_set(items: &[&str]) -> HashSet { + 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 = 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 = 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 = HashSet::new(); - assert!(!roles_satisfy_requirements(&required, &user_assigned)); -} - -#[test] -fn qualifies_empty_requirements_empty_roles() { - let required: HashSet = HashSet::new(); - let user_assigned: HashSet = HashSet::new(); - assert!(roles_satisfy_requirements(&required, &user_assigned)); -} - -#[test] -fn room_to_space_lookup() { - let mut room_to_space: HashMap> = 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())); } diff --git a/src/service/rooms/timeline/build.rs b/src/service/rooms/timeline/build.rs index 25ebbd6d..9dfe1c69 100644 --- a/src/service/rooms/timeline/build.rs +++ b/src/service/rooms/timeline/build.rs @@ -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)>, - BTreeMap, - ); + type SpaceEnforcementData = + (Vec<(OwnedUserId, HashSet)>, BTreeMap); 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::() { - 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 = { 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; }