fix(spaces): improve feature flag isolation for disabled state

- Gate memory_usage() and clear_cache() with is_enabled()
- Gate populate_space() and get_parent_space() as defense-in-depth
- All admin commands now refuse when feature is disabled with
  a clear message pointing to the config option
- Prefix memory labels with space_ for disambiguation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ember33 2026-03-18 10:19:53 +01:00
parent 40646eb4ba
commit 6fa67ed489
2 changed files with 42 additions and 4 deletions

View file

@ -11,6 +11,19 @@ use conduwuit::matrix::pdu::PduBuilder;
use crate::{admin_command, admin_command_dispatch};
macro_rules! require_enabled {
($self:expr) => {
if !$self.services.rooms.roles.is_enabled() {
return $self
.write_str(
"Space permission cascading is disabled. \
Enable it with `space_permission_cascading = true` in your config.",
)
.await;
}
};
}
macro_rules! custom_state_pdu {
($event_type:expr, $state_key:expr, $content:expr) => {
PduBuilder {
@ -82,6 +95,7 @@ pub enum SpaceRolesCommand {
#[admin_command]
async fn list(&self, space: OwnedRoomOrAliasId) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,
@ -124,6 +138,7 @@ async fn add(
description: Option<String>,
power_level: Option<i64>,
) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,
@ -170,6 +185,7 @@ async fn add(
#[admin_command]
async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,
@ -216,6 +232,7 @@ async fn assign(
user_id: OwnedUserId,
role_name: String,
) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,
@ -266,6 +283,7 @@ async fn revoke(
user_id: OwnedUserId,
role_name: String,
) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,
@ -317,6 +335,7 @@ async fn require(
room_id: OwnedRoomId,
role_name: String,
) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,
@ -367,6 +386,7 @@ async fn unrequire(
room_id: OwnedRoomId,
role_name: String,
) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,
@ -413,6 +433,7 @@ async fn unrequire(
#[admin_command]
async fn user(&self, space: OwnedRoomOrAliasId, user_id: OwnedUserId) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,
@ -448,6 +469,7 @@ async fn user(&self, space: OwnedRoomOrAliasId, user_id: OwnedUserId) -> Result
#[admin_command]
async fn room(&self, space: OwnedRoomOrAliasId, room_id: OwnedRoomId) -> Result {
require_enabled!(self);
let space_id = self.services.rooms.alias.resolve(&space).await?;
if !matches!(
self.services.rooms.state_accessor.get_room_type(&space_id).await,

View file

@ -91,20 +91,28 @@ impl crate::Service for Service {
}
async fn memory_usage(&self, out: &mut (dyn Write + Send)) -> Result {
if !self.is_enabled() {
return Ok(());
}
let roles = self.roles.read().await.len();
let user_roles = self.user_roles.read().await.len();
let room_requirements = self.room_requirements.read().await.len();
let room_to_space = self.room_to_space.read().await.len();
writeln!(out, "roles: {roles}")?;
writeln!(out, "user_roles: {user_roles}")?;
writeln!(out, "room_requirements: {room_requirements}")?;
writeln!(out, "room_to_space: {room_to_space}")?;
writeln!(out, "space_roles_definitions: {roles}")?;
writeln!(out, "space_user_roles: {user_roles}")?;
writeln!(out, "space_room_requirements: {room_requirements}")?;
writeln!(out, "space_room_to_space_index: {room_to_space}")?;
Ok(())
}
async fn clear_cache(&self) {
if !self.is_enabled() {
return;
}
self.roles.write().await.clear();
self.user_roles.write().await.clear();
self.room_requirements.write().await.clear();
@ -218,6 +226,10 @@ pub async fn ensure_default_roles(&self, space_id: &RoomId) -> Result {
/// `m.space.child` state events and indexes them for fast lookup.
#[implement(Service)]
pub async fn populate_space(&self, space_id: &RoomId) {
if !self.is_enabled() {
return;
}
// 1. Read m.space.roles (state key: "")
let roles_event_type = StateEventType::from("m.space.roles".to_owned());
if let Ok(content) = self
@ -403,6 +415,10 @@ pub async fn user_qualifies_for_room(
/// Get the parent Space of a child room, if any.
#[implement(Service)]
pub async fn get_parent_space(&self, room_id: &RoomId) -> Option<OwnedRoomId> {
if !self.is_enabled() {
return None;
}
self.room_to_space.read().await.get(room_id).cloned()
}