Critical fixes: - handle_space_child_change now reads the actual m.space.child state event and checks if via is empty; removes child from index on removal instead of unconditionally adding - Server user is exempted from PL rejection guard so sync_power_levels can function without being blocked by its own protection - PL rejection now also checks that space-managed users aren't omitted from proposed power level events Important fixes: - room_to_space changed from 1:1 to 1:many (HashMap<RoomId, HashSet<RoomId>>) so a room can belong to multiple parent spaces; get_parent_space renamed to get_parent_spaces; join gating checks all parents (qualify in any) - All custom event types renamed from m.space.* to com.continuwuity.space.* to avoid squatting on the Matrix namespace - Cache cleanup on child removal from space - Added tokio Semaphore (capacity 4) to limit concurrent enforcement tasks - Server user membership checked before enforcement in auto_join, kick, and sync_power_levels to avoid noisy errors Suggestions: - Replaced expect() calls with proper error propagation via map_err/? - Fixed indentation in timeline/mod.rs line 116 - handle_space_child_change now directly joins users to the specific new child room instead of scanning all children via auto_join_qualifying_rooms Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.5 KiB
Space Permission Cascading — Design Document
Date: 2026-03-17 Status: Implemented
Overview
Server-side feature that allows user rights in a Space to cascade down to its direct child rooms. Includes power level cascading and role-based room access control. Enabled via a server-wide configuration flag, disabled by default.
Requirements
- Power levels defined in a Space cascade to all direct child rooms (Space always wins over per-room overrides).
- Admins can define custom roles in a Space and assign them to users.
- Child rooms can require one or more roles for access.
- Enforcement is continuous — role revocation auto-kicks users from rooms they no longer qualify for.
- Users are auto-joined to all qualifying child rooms when they join a Space or receive a new role.
- Cascading applies to direct parent Space only; no nested cascade through sub-spaces.
- Feature is toggled by a single server-wide config flag
(
space_permission_cascading), off by default.
Configuration
# conduwuit-example.toml
# Enable space permission cascading (power levels and role-based access).
# When enabled, power levels cascade from Spaces to child rooms and rooms
# can require roles for access. Applies to all Spaces on this server.
# Default: false
space_permission_cascading = false
Custom State Events
All events live in the Space room.
com.continuwuity.space.roles (state key: "")
Defines the available roles for the Space. Two default roles (admin and mod)
are created automatically when a Space is first encountered with the feature
enabled.
{
"roles": {
"admin": {
"description": "Space administrator",
"power_level": 100
},
"mod": {
"description": "Space moderator",
"power_level": 50
},
"nsfw": {
"description": "Access to NSFW content"
},
"vip": {
"description": "VIP member"
}
}
}
description(string, required): Human-readable description.power_level(integer, optional): If present, users with this role receive this power level in all child rooms. When a user holds multiple roles with power levels, the highest value wins.
com.continuwuity.space.role.member (state key: user ID)
Assigns roles to a user within the Space.
{
"roles": ["nsfw", "vip"]
}
com.continuwuity.space.role.room (state key: room ID)
Declares which roles a child room requires. A user must hold all listed roles to access the room.
{
"required_roles": ["nsfw"]
}
Enforcement Rules
All enforcement is skipped when space_permission_cascading = false.
1. Join gating
When a user attempts to join a room that is a direct child of a Space:
- Look up the room's
com.continuwuity.space.role.roomevent in the parent Space. - If the room has
required_roles, check the user'scom.continuwuity.space.role.member. - Reject the join if the user is missing any required role.
2. Power level override
For every user in a child room of a Space:
- Look up their roles via
com.continuwuity.space.role.memberin the parent Space. - For each role that has a
power_level, take the highest value. - Override the user's power level in the child room's
m.room.power_levels. - Reject attempts to manually set per-room power levels that conflict with Space-granted levels.
3. Role revocation
When an com.continuwuity.space.role.member event is updated and a role is removed:
- Identify all child rooms that require the removed role.
- Auto-kick the user from rooms they no longer qualify for.
- Recalculate and update the user's power level in all child rooms.
4. Room requirement change
When an com.continuwuity.space.role.room event is updated with new requirements:
- Check all current members of the room.
- Auto-kick members who do not hold all newly required roles.
5. Auto-join on role grant
When an com.continuwuity.space.role.member event is updated and a role is added:
- Find all child rooms where the user now meets all required roles.
- Auto-join the user to qualifying rooms they are not already in.
This also applies when a user first joins the Space — they are auto-joined to all child rooms they qualify for. Rooms with no role requirements auto-join all Space members.
6. New child room
When a new m.space.child event is added to a Space:
- Auto-join all qualifying Space members to the new child room.
Caching & Indexing
The source of truth is always the state events. The server maintains an
in-memory index for fast enforcement lookups, following the same patterns as the
existing roomid_spacehierarchy_cache.
Index structures
| Index | Source event |
|---|---|
| Space → roles defined | com.continuwuity.space.roles |
| Space → user → roles | com.continuwuity.space.role.member |
| Space → room → required roles | com.continuwuity.space.role.room |
| Room → parent Space | m.space.child (reverse lookup) |
The Space → child rooms mapping already exists.
Cache invalidation triggers
| Event changed | Action |
|---|---|
com.continuwuity.space.roles |
Refresh role definitions, revalidate all members |
com.continuwuity.space.role.member |
Refresh user's roles, trigger auto-join/kick |
com.continuwuity.space.role.room |
Refresh room requirements, trigger auto-join/kick |
m.space.child added |
Index new child, auto-join qualifying members |
m.space.child removed |
Remove from index (no auto-kick) |
| Server startup | Full rebuild from state events |
Admin Room Commands
Roles are managed via the existing admin room interface, which sends the appropriate state events under the hood and triggers enforcement.
!admin space roles list <space>
!admin space roles add <space> <role_name> [description] [power_level]
!admin space roles remove <space> <role_name>
!admin space roles assign <space> <user_id> <role_name>
!admin space roles revoke <space> <user_id> <role_name>
!admin space roles require <space> <room_id> <role_name>
!admin space roles unrequire <space> <room_id> <role_name>
!admin space roles user <space> <user_id>
!admin space roles room <space> <room_id>
Architecture
Approach: Hybrid — state events for definition, database cache for enforcement.
- State events are the source of truth and federate normally.
- The server maintains an in-memory cache/index for fast enforcement.
- Cache is invalidated on relevant state event changes and fully rebuilt on startup.
- All enforcement hooks (join gating, PL override, auto-join, auto-kick) check the feature flag first and no-op when disabled.
- Existing clients can manage roles via Developer Tools (custom state events). The admin room commands provide a user-friendly interface.
Scope
In scope
- Server-wide feature flag
- Custom state events for role definition, assignment, and room requirements
- Power level cascading (Space always wins)
- Continuous enforcement (auto-join, auto-kick)
- Admin room commands
- In-memory caching with invalidation
- Default
admin(PL 100) andmod(PL 50) roles
Out of scope
- Client-side UI for role management
- Nested cascade through sub-spaces
- Per-space opt-in/opt-out (it is server-wide)
- Federation-specific logic beyond normal state event replication