# Space Permission Cascading — Design Document **Date:** 2026-03-17 **Status:** Approved ## 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 1. Power levels defined in a Space cascade to all direct child rooms (Space always wins over per-room overrides). 2. Admins can define custom roles in a Space and assign them to users. 3. Child rooms can require one or more roles for access. 4. Enforcement is continuous — role revocation auto-kicks users from rooms they no longer qualify for. 5. Users are auto-joined to all qualifying child rooms when they join a Space or receive a new role. 6. Cascading applies to direct parent Space only; no nested cascade through sub-spaces. 7. Feature is toggled by a single server-wide config flag (`space_permission_cascading`), off by default. ## Configuration ```toml # 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. ### `m.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. ```json { "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. ### `m.space.role.member` (state key: user ID) Assigns roles to a user within the Space. ```json { "roles": ["nsfw", "vip"] } ``` ### `m.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. ```json { "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 `m.space.role.room` event in the parent Space. - If the room has `required_roles`, check the user's `m.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 `m.space.role.member` in 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 `m.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 `m.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 `m.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 | `m.space.roles` | | Space → user → roles | `m.space.role.member` | | Space → room → required roles| `m.space.role.room` | | Room → parent Space | `m.space.child` (reverse lookup) | The Space → child rooms mapping already exists. ### Cache invalidation triggers | Event changed | Action | |----------------------------|-----------------------------------------------------| | `m.space.roles` | Refresh role definitions, revalidate all members | | `m.space.role.member` | Refresh user's roles, trigger auto-join/kick | | `m.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 !admin space roles add [description] [power_level] !admin space roles remove !admin space roles assign !admin space roles revoke !admin space roles require !admin space roles unrequire !admin space roles user !admin space roles room ``` ## 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) and `mod` (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