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) <noreply@anthropic.com>
This commit is contained in:
parent
3121229707
commit
835d434d92
1 changed files with 226 additions and 0 deletions
226
docs/plans/2026-03-17-space-permission-cascading-design.md
Normal file
226
docs/plans/2026-03-17-space-permission-cascading-design.md
Normal file
|
|
@ -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 <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) 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
|
||||||
Loading…
Add table
Reference in a new issue