Skip to content

feat(relay): add moderator relay-member role with full capability grid - #4771

Open
wpfleger96 wants to merge 2 commits into
mainfrom
wpfleger/moderator-role
Open

feat(relay): add moderator relay-member role with full capability grid#4771
wpfleger96 wants to merge 2 commits into
mainfrom
wpfleger/moderator-role

Conversation

@wpfleger96

Copy link
Copy Markdown
Member

Summary

Adds a tenant-scoped moderator role to relay_members that holds ViewQueue, ResolveReport, DeleteMessage, Kick, Timeout, and Untimeout capabilities community-wide. Ban/Unban remain admin+ only. Guard rails prevent a moderator from Kicking or Timing-out the community owner or a fellow admin.

This is Track 2 from the relay-admin-auth plan (plan v3.2, Thufir FINAL APPROVE pass 3/3). Track 1 (desktop admin surface) is in a separate PR by Hayt.

Backend / Relay

Migration: migrations/0028_relay_moderator_role.sql — additive, drops and recreates the relay_members.role CHECK constraint to include 'moderator'. 0001 and 0027 checksums unchanged. Migration count assertion advances to 28.

Policy seam (moderation_authz.rs): ModerationAuthority::CommunityModerator added; decide_authority gains the full moderator grid with exhaustive unit tests (13 total).

Kind 9005 (moderator delete): Validator keeps author + channel-owner/admin paths byte-for-byte; gains an additive relay-role path through authorize_moderation_action(DeleteMessage) with target-role guard. Kind 5 stays author/agent-owner only.

Kind 9001 (moderator kick): Additive authorize_moderation_action(Kick) path in the validator; handle_remove_user routes to new remove_member_as_community_moderator() when actor has no channel role. Db::remove_member() is byte-for-byte unchanged. The shared post-mutation path (cache invalidation, subscription eviction, workflow disable, system message, discovery, notification) fires regardless of path.

Kind 9044 (resolve): Builds one ResolvedReportAction {required_capability, target_author, channel_id} from the stored report + get_event_by_id_including_deleted() before authorization. Normalization matrix enforced: delete/kick require event target + channel; ban/timeout require pubkey or resolvable effective author, blob rejected; dismiss/escalate decision-only. Capability map: delete→DeleteMessage, kick→Kick, ban→Ban, timeout→Timeout, dismiss/escalate→ResolveReport. A moderator's 9044 with action=ban is rejected at the seam.

Relay admin (9030/9031/9032): role=moderator grantable by admin/owner via 9030; owner-only 9032 accepts it; 9031 admin removal uses atomic role = ANY(ARRAY['member','moderator']) predicate — no TOCTOU race, NotFound/RoleMismatch/IsOwner semantics preserved.

Role-surface sweep: relay main.rs metrics role set, buzz-admin CLI validation, deploy/compose/run.sh docs all updated.

Audit: persisted-role criterion dropped per plan. actor_pubkey remains the persisted attribution. Authorization attempts logged at attempt time; success-with-authority logged only after mutation commits.

Desktop

moderationCapabilities(role): Single source of truth helper in features/moderation/lib/capabilities.ts consumed by both MessageModerationMenuItems and ModerationQueueCard. Moderators get delete/kick/timeout/untimeout/viewQueue/resolve — never ban/unban.

MessageModerationMenuItems: Capability-aware per action; Ban/Unban entries hidden for moderators.

ModerationQueueCard: Uses moderationCapabilities; passes canBan into resolvableActions; uses moderatorDeleteMessage (kind 9005) for queue delete action. Queue timeout excluded (duration-picker follow-up, unchanged from before).

TimelineMessageRow: Moderator delete affordance via 9005 path when caps.canDelete && !isOwnMessage.

CommunityMembersSettingsCard: Moderator role shown with ShieldHalf icon; role management extended to moderator tier.

Role propagation: relayMembers.ts, types.ts, e2eBridge.ts — all role unions include 'moderator'; downgrade-to-member normalization updated to preserve moderator.

events.rs: VALID_RELAY_ROLES includes 'moderator'.

New Tauri command: commands/moderator.rsmoderator_delete_message builds kind 9005 inline; extracted to new file to keep messages.rs within the file-size ratchet ceiling.

Tests Added

  • 13 unit tests for decide_authority (moderator grid, guard rails, channel-role interaction)
  • 9 unit tests for moderationCapabilities() exhaustive cases
  • 5 unit tests for resolvableActions(canBan=false)
  • 3 DB integration tests for remove_member_as_community_moderator (ordinary kick succeeds, last-owner guard fires, non-member returns MemberNotFound) — #[ignore = "requires Postgres"]
  • 1 DB integration test for admin remove member/moderator matrix — #[ignore = "requires Postgres"]

Verification Priorities (Thufir)

  1. 9001 sole call site: remove_member_as_community_moderator() has exactly one call site at side_effects.rs:1478.
  2. Tenant fence: actor and target roles both fetched under tenant.community() in authorize_moderation_action.
  3. 0028 constraint name: DROP CONSTRAINT IF EXISTS relay_members_role_check — uses the generated name; IF EXISTS for idempotency.
  4. Tombstoned resolve: build_resolved_action uses get_event_by_id_including_deleted() so delete→9005→9044 succeeds after tombstoning.

npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 2 commits August 4, 2026 15:13
Add a tenant-scoped 'moderator' role to relay_members that holds
ViewQueue, ResolveReport, DeleteMessage, Kick, Timeout, Untimeout
capabilities community-wide. Ban/Unban remain admin+ only. Guard
rails prevent a moderator from Kicking or Timing-out the community
owner or a fellow admin.

Relay/DB changes:
- migrations/0028_relay_moderator_role.sql: additive migration that
  drops and recreates the relay_members.role CHECK to include
  'moderator'; schema/schema.sql snapshot updated; migration count
  assertion advanced to 28
- moderation_authz.rs: ModerationAuthority::CommunityModerator; full
  decide_authority arm for 'moderator' with exhaustive unit tests
- side_effects.rs: kind 9005 validator gains additive relay-role path
  through authorize_moderation_action(DeleteMessage); kind 9001 gains
  additive Kick path + routes to remove_member_as_community_moderator
  (new preauthorized DB mutation) when actor has no channel role
- moderation_commands.rs: 9044 resolve builds one resolved report-action
  value (required_capability, target_author, channel_id) from stored
  report + get_event_by_id_including_deleted; normalization matrix
  enforced before authorization; capability map delete→DeleteMessage,
  kick→Kick, ban→Ban, timeout→Timeout, dismiss/escalate→ResolveReport
- relay_admin.rs: 9030/9032 accept 'moderator'; 9031 admin removal uses
  atomic role = ANY('member','moderator') predicate (no TOCTOU race)
- buzz-db channel.rs: remove_member_as_community_moderator acquires same
  per-channel membership lock as remove_member, re-checks last-owner
  protection in-transaction, performs soft removal without channel role
- relay main.rs metrics role set extended; buzz-admin CLI role validation
  extended; deploy/compose/run.sh docs updated

Desktop changes:
- capabilities.ts: moderationCapabilities(role) helper returning
  typed capability struct; single source of truth consumed by both
  MessageModerationMenuItems and ModerationQueueCard
- ModerationQueueCard: uses moderationCapabilities; passes canBan into
  resolvableActions; uses moderatorDeleteMessage (kind 9005) for delete
- MessageModerationMenuItems: capability-aware per action; Ban/Unban
  hidden for moderators
- TimelineMessageRow: moderator delete affordance via 9005 path when
  canDelete && !isOwnMessage
- CommunityMembersSettingsCard: moderator role shown with ShieldHalf
  icon; role management extended to moderator tier
- relayMembers.ts/types.ts/e2eBridge.ts: 'moderator' added to all role
  unions; downgrade-to-member normalization updated
- events.rs: VALID_RELAY_ROLES includes 'moderator'
- commands/moderator.rs: new Tauri command moderator_delete_message
  builds kind 9005 inline (avoids file-size ratchet growth on messages.rs)
- shared/api/moderator.ts: moderatorDeleteMessage TS wrapper

Tests added:
- 13 unit tests for decide_authority (moderator grid exhaustive)
- 9 unit tests for moderationCapabilities exhaustive cases
- 5 unit tests for resolvableActions(canBan=false)
- 3 DB integration tests for remove_member_as_community_moderator
  (ordinary kick, last-owner guard, non-member not-found) [ignored]
- 1 DB integration test for admin remove member/moderator matrix [ignored]

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
* origin/main:
  fix(desktop): close reconnect gaps that previously required CMD+R (#4737)

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 4, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant