From 87ad6f156c860150efe37a1f7b8b2a10ff05905a Mon Sep 17 00:00:00 2001 From: ember33 Date: Thu, 19 Mar 2026 22:42:57 +0100 Subject: [PATCH] feat(spaces): add custom state event types and config for space permission cascading Add four custom Matrix state event content types for space role management: space roles definitions, per-user role assignments, per-room role requirements, and per-space cascading override. Add server config options: space_permission_cascading (default false) as the server-wide toggle, and space_roles_cache_flush_threshold (default 1000) for cache management. --- conduwuit-example.toml | 12 +++++ src/core/config/mod.rs | 18 ++++++++ src/core/matrix/mod.rs | 1 + src/core/matrix/space_roles.rs | 81 ++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 src/core/matrix/space_roles.rs diff --git a/conduwuit-example.toml b/conduwuit-example.toml index 82b78fde..f0272532 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -470,6 +470,18 @@ # #suspend_on_register = false +# Server-wide default for space permission cascading (power levels and +# role-based access). Individual Spaces can override this via the +# `com.continuwuity.space.cascading` state event or the admin command +# `!admin space roles enable/disable `. +# +#space_permission_cascading = false + +# Maximum number of spaces to cache role data for. When exceeded the +# cache is cleared and repopulated on demand. +# +#space_roles_cache_flush_threshold = 1000 + # Enabling this setting opens registration to anyone without restrictions. # This makes your server vulnerable to abuse # diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index a642f5b7..1239b805 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -603,6 +603,22 @@ pub struct Config { #[serde(default)] pub suspend_on_register: bool, + /// Server-wide default for space permission cascading (power levels and + /// role-based access). Individual Spaces can override this via the + /// `com.continuwuity.space.cascading` state event or the admin command + /// `!admin space roles enable/disable `. + /// + /// default: false + #[serde(default)] + pub space_permission_cascading: bool, + + /// Maximum number of spaces to cache role data for. When exceeded the + /// cache is cleared and repopulated on demand. + /// + /// default: 1000 + #[serde(default = "default_space_roles_cache_flush_threshold")] + pub space_roles_cache_flush_threshold: u32, + /// Enabling this setting opens registration to anyone without restrictions. /// This makes your server vulnerable to abuse #[serde(default)] @@ -2826,3 +2842,5 @@ fn default_ldap_search_filter() -> String { "(objectClass=*)".to_owned() } fn default_ldap_uid_attribute() -> String { String::from("uid") } fn default_ldap_name_attribute() -> String { String::from("givenName") } + +fn default_space_roles_cache_flush_threshold() -> u32 { 1000 } diff --git a/src/core/matrix/mod.rs b/src/core/matrix/mod.rs index b38d4c9a..08a88971 100644 --- a/src/core/matrix/mod.rs +++ b/src/core/matrix/mod.rs @@ -2,6 +2,7 @@ pub mod event; pub mod pdu; +pub mod space_roles; pub mod state_key; pub mod state_res; diff --git a/src/core/matrix/space_roles.rs b/src/core/matrix/space_roles.rs new file mode 100644 index 00000000..dc83c604 --- /dev/null +++ b/src/core/matrix/space_roles.rs @@ -0,0 +1,81 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +pub const SPACE_ROLES_EVENT_TYPE: &str = "com.continuwuity.space.roles"; +pub const SPACE_ROLE_MEMBER_EVENT_TYPE: &str = "com.continuwuity.space.role.member"; +pub const SPACE_ROLE_ROOM_EVENT_TYPE: &str = "com.continuwuity.space.role.room"; +pub const SPACE_CASCADING_EVENT_TYPE: &str = "com.continuwuity.space.cascading"; + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct SpaceRolesEventContent { + pub roles: BTreeMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct RoleDefinition { + pub description: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub power_level: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct SpaceRoleMemberEventContent { + pub roles: Vec, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +pub struct SpaceRoleRoomEventContent { + pub required_roles: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +pub struct SpaceCascadingEventContent { + pub enabled: bool, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn space_roles_roundtrip() { + let mut roles = BTreeMap::new(); + roles.insert("admin".to_owned(), RoleDefinition { + description: "Space administrator".to_owned(), + power_level: Some(100), + }); + roles.insert("nsfw".to_owned(), RoleDefinition { + description: "NSFW access".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(); + assert_eq!(deserialized.roles["admin"].power_level, Some(100)); + assert!(deserialized.roles["nsfw"].power_level.is_none()); + } + + #[test] + fn power_level_omitted_in_serialization_when_none() { + let role = RoleDefinition { + description: "Test".to_owned(), + power_level: None, + }; + let json = serde_json::to_string(&role).unwrap(); + assert!(!json.contains("power_level")); + } + + #[test] + fn negative_power_level() { + let json = r#"{"description":"Restricted","power_level":-10}"#; + let role: RoleDefinition = serde_json::from_str(json).unwrap(); + assert_eq!(role.power_level, Some(-10)); + } + + #[test] + fn missing_description_fails() { + let json = r#"{"power_level":100}"#; + serde_json::from_str::(json).unwrap_err(); + } +}