From 835d434d929fbe1f62be10a3fad855fab1fb669c Mon Sep 17 00:00:00 2001 From: ember33 Date: Tue, 17 Mar 2026 14:59:48 +0100 Subject: [PATCH] docs: add design doc for space permission cascading Covers power level cascading from Spaces to child rooms, role-based room access control, continuous enforcement, and admin room commands. Feature will be behind a server-wide config flag. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...03-17-space-permission-cascading-design.md | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/plans/2026-03-17-space-permission-cascading-design.md diff --git a/docs/plans/2026-03-17-space-permission-cascading-design.md b/docs/plans/2026-03-17-space-permission-cascading-design.md new file mode 100644 index 00000000..60d3e967 --- /dev/null +++ b/docs/plans/2026-03-17-space-permission-cascading-design.md @@ -0,0 +1,226 @@ +# 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