feat(relay): add moderator relay-member role with full capability grid - #4771
Open
wpfleger96 wants to merge 2 commits into
Open
feat(relay): add moderator relay-member role with full capability grid#4771wpfleger96 wants to merge 2 commits into
wpfleger96 wants to merge 2 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a tenant-scoped
moderatorrole torelay_membersthat 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 therelay_members.roleCHECK constraint to include'moderator'.0001and0027checksums unchanged. Migration count assertion advances to 28.Policy seam (
moderation_authz.rs):ModerationAuthority::CommunityModeratoradded;decide_authoritygains 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_userroutes to newremove_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 withaction=banis rejected at the seam.Relay admin (9030/9031/9032):
role=moderatorgrantable by admin/owner via 9030; owner-only 9032 accepts it; 9031 admin removal uses atomicrole = ANY(ARRAY['member','moderator'])predicate — no TOCTOU race,NotFound/RoleMismatch/IsOwnersemantics preserved.Role-surface sweep: relay
main.rsmetrics role set,buzz-adminCLI validation,deploy/compose/run.shdocs all updated.Audit: persisted-role criterion dropped per plan.
actor_pubkeyremains 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 infeatures/moderation/lib/capabilities.tsconsumed by bothMessageModerationMenuItemsandModerationQueueCard. Moderators get delete/kick/timeout/untimeout/viewQueue/resolve — never ban/unban.MessageModerationMenuItems: Capability-aware per action; Ban/Unban entries hidden for moderators.ModerationQueueCard: UsesmoderationCapabilities; passescanBanintoresolvableActions; usesmoderatorDeleteMessage(kind 9005) for queue delete action. Queue timeout excluded (duration-picker follow-up, unchanged from before).TimelineMessageRow: Moderator delete affordance via 9005 path whencaps.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 preservemoderator.events.rs:VALID_RELAY_ROLESincludes'moderator'.New Tauri command:
commands/moderator.rs—moderator_delete_messagebuilds kind 9005 inline; extracted to new file to keepmessages.rswithin the file-size ratchet ceiling.Tests Added
decide_authority(moderator grid, guard rails, channel-role interaction)moderationCapabilities()exhaustive casesresolvableActions(canBan=false)remove_member_as_community_moderator(ordinary kick succeeds, last-owner guard fires, non-member returns MemberNotFound) —#[ignore = "requires Postgres"]#[ignore = "requires Postgres"]Verification Priorities (Thufir)
remove_member_as_community_moderator()has exactly one call site atside_effects.rs:1478.tenant.community()inauthorize_moderation_action.DROP CONSTRAINT IF EXISTS relay_members_role_check— uses the generated name;IF EXISTSfor idempotency.build_resolved_actionusesget_event_by_id_including_deleted()so delete→9005→9044 succeeds after tombstoning.