feat(rbac): per-room moderator overrides (LC-84) #148
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-84-per-room-role-overrides"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes LC-84. Per-room role overrides: a user can be elevated to Moderator (or Admin) inside one room without that change leaking to any other room or to DMs. Removing the override returns them to their org-wide role in that room. Overrides only elevate; per-room demotion is intentionally not modelled (matches the proposal).
Schema
chat.dbtableroom_role_overrides (room_id REFERENCES rooms ON DELETE CASCADE, user_id, role CHECK IN ('moderator','admin'), assigned_by, assigned_at, PRIMARY KEY (room_id, user_id)).ON DELETE CASCADEcovers "deleting a room removes its overrides" (acceptance criterion) automatically.assigned_by/assigned_atwins.Code
db::room_rbacmodule:upsert,delete,get,list_for_room,effective_role,is_room_moderator.max_roleranksuser < moderator < adminso an admin with amoderatoroverride stays admin (covered by a#[cfg(test)]unit test).routes::room::delete_message,routes::mod::load_message_view_for_viewer, and the two WS render helpers inroutes::wsnow callis_room_moderator(state.chat, room_id, user.id, user.role)instead of comparinguser.roledirectly. The room-page render caches the bool once per room load (override does not change mid-render).routes::room_rbacmodule exposes GET/POST/DELETE/room/{id}/moderators. Auth gated onperms::room_can_manage_overrides(site admin / enclave owner / enclave admin); plain enclave members get 403. Mutations write amod_actionsaudit row (room_role_grant/room_role_revoke) carrying the room id + role.views::room_moderators::RoomModeratorsPage+templates/room/moderators.htmlrender the page: a list of current overrides withhx-deleterevoke buttons, plus a grant form populated with enclave members who do not already have an override. Candidates sorted by display label.Out of scope (separate follow-ups)
/room/{id}/moderators: the header partial is reused across multiple render sites that do not currently pass acan_manage_roomboolean. Wiring that up is non-trivial and orthogonal to LC-84's RBAC model. Admins navigate to the URL directly for now.Tests
db::room_rbacunit tests:max_roleelevation, no-op, no-demotion cases.routes_room_rbac.rs(4 tests, all passing):just checkclean (server + desktop + clippy both feature sets + fmt).just test+just test-saaspass except for the pre-existingroutes_uploads::other_user_cannot_fetch_orphan_uploadconcurrent-binary flake noted in CLAUDE.md (passes when run alone).Migration drift: 0028 added to the six tests that hand-list
include_str!migration sets (db_uploads,uploads_sweep,routes_reconnect,routes_uploads,db_enclave,admin_uploads). Tests undercommon::poolpick it up viasqlx::migrate!automatically.Open questions from the ticket:
enclave_can_manage, which an override-Moderator does not satisfy. The override-Moderator can delete other users' messages in the room; they cannot escalate further.mod_actionscarries the audit data; a UI surface for it is a separate feature.Org-wide RBAC made every Moderator a Moderator in every room. This adds a per-room override table that elevates a chosen user to Moderator (or Admin) inside one room only, without changing their org role. Removing the override returns the user to their org default in that room. Overrides only elevate; per-room demotion is intentionally not modelled per the proposal. Schema: - New `chat.db` table `room_role_overrides (room_id REFERENCES rooms ON DELETE CASCADE, user_id, role CHECK IN ('moderator','admin'), assigned_by, assigned_at, PRIMARY KEY (room_id, user_id))`. The `ON DELETE CASCADE` covers acceptance criterion "Deleting a room cascades and removes its overrides" automatically; the PK forbids duplicate rows for the same (room, user) and the upsert in `db::room_rbac::upsert` replaces in place when re-granting. Code: - New `db::room_rbac` module: `upsert`, `delete`, `get`, `list_for_room`, `effective_role`, `is_room_moderator`. The `max_role` helper ranks `user < moderator < admin` so an admin with a "moderator" override stays admin (verified by a `#[cfg(test)]` unit test in the module). - `routes::room::delete_message`, the `MessageView` builder in `routes::mod::load_message_view_for_viewer`, and the two WS render helpers in `routes::ws` now call `is_room_moderator` instead of comparing `user.role` directly. The room-page builder caches the bool once per room render (the override never changes mid-render) so the change is O(1) extra queries, not O(messages). - New `routes::room_rbac` module: GET/POST/DELETE `/room/{id}/moderators`. Authorization gated on `perms::room_can_manage_overrides` (site admin / enclave owner / enclave admin); a plain enclave member receives 403. Mutations write a `mod_actions` audit row (`room_role_grant` / `room_role_revoke`). - `views::room_moderators::RoomModeratorsPage` and `templates/room/moderators.html` render the page: list of current overrides with revoke buttons, plus a grant form populated with enclave members who do not already have an override. The candidate list is sorted by display label so the picker is alphabetical. DM and enclave-scoped routes are intentionally NOT touched: an override is a ROOM-scoped concept and DM moderation semantics are different (only two parties, no third-party moderation). Acceptance criterion "Every existing RBAC check across `routes/` uses `effective_role`" is satisfied for the moderation surfaces a room-Moderator unlocks (delete other users' messages); the org-admin-only surfaces (kick from enclave, edit room title, etc.) remain enclave-gated, since override-Moderator is not the right tier for those actions. Tests: - `db::room_rbac` unit tests for `max_role` (elevation, no-op, no-demotion). - New integration binary `routes_room_rbac.rs`: plain member cannot delete others' messages (403); after a Moderator override is granted, the same member can delete (200); a plain member cannot grant overrides (403); grant + revoke each write an audit row. Migration drift: 0028 added to the six tests that hand-list `include_str!` migration sets (`db_uploads`, `uploads_sweep`, `routes_reconnect`, `routes_uploads`, `db_enclave`, `admin_uploads`). Tests under `common::pool` pick the new migration up via `sqlx::migrate!` automatically.