continuwuity/docs/plans/2026-03-17-space-permission-cascading-design.md
ember33 cee5aa476c chore(spaces): fix doc comments, design doc accuracy, consistent error style
- Fix doc comment referencing room_to_space instead of space_to_rooms
- Add space_to_rooms forward index to design doc index table
- Use Err! consistently for validation errors in admin commands
- Rename test to follow deserialize_ prefix convention

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 12:35:35 +01:00

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

  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

# 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.room event in the parent Space.
  • If the room has required_roles, check the user's com.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.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 a 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 a 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 a 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 Spaces m.space.child (reverse lookup)
Space → child rooms m.space.child (forward index)

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) 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