Skip to content

relay: enforce ban/timeout write-block on command kinds - #3442

Open
Maxwellimus wants to merge 1 commit into
mainfrom
maxwell/relay-command-kind-ban-gate
Open

relay: enforce ban/timeout write-block on command kinds#3442
Maxwellimus wants to merge 1 commit into
mainfrom
maxwell/relay-command-kind-ban-gate

Conversation

@Maxwellimus

Copy link
Copy Markdown

Summary

ingest_event routes command kinds — DM open/add/hide, workflow
define/trigger, and approval grant/deny — straight to
command_executor::handle_command and returns before the durable
ban/timeout write-block gate that ordinary writes pass through.
handle_command performs no restriction check of its own, so a member
who has been banned or timed out could still:

  • open, add to, or hide DM channels,
  • define and trigger workflows,
  • grant or deny approvals,

by sending the corresponding signed event over POST /events (or the
WebSocket). The moderation-command and relay-admin kinds are also routed
around the shared gate, but each re-checks the ban inside its own
handler; command kinds were the one exempt path with no compensating
enforcement.

Fix

Enforce the durable ban/timeout write-block on command kinds before
routing to the executor:

  • A banned member's command kind is refused 403 blocked:.
  • A timed-out member's command kind is refused 403 restricted:.
    Command kinds are ordinary writes, not administrative capability, so
    an active timeout blocks them — the relay-admin timeout exemption does
    not extend here.
  • An unrestricted member is unaffected.

The ban/timeout decision is extracted into restriction_block /
enforce_moderation_restriction, now the single source of truth shared
by both the command-kind path and the ordinary-write path, so the two
can't drift. The restriction-state lookup fails closed: a DB error
surfaces as 500 error: rather than admitting the write.

Tests

  • Unit tests for restriction_block: banned → blocked:, active
    timeout → restricted:, expired timeout → no block, unrestricted → no
    block.
  • New #[ignore]d e2e regression test
    (regression_command_kind_ban_gate.rs): against a live relay, a
    banned member and a timed-out member each have their DM_OPEN command
    refused (403 with the exact wire contract) and create no DM channel,
    while an unrestricted member's DM_OPEN still succeeds and mutates.
    Pre-fix, the banned member's DM_OPEN is accepted (200) — the test
    fails on that assertion.

🤖 Generated with Claude Code

ingest_event routed command kinds — DM open/add/hide, workflow
define/trigger, approval grant/deny — straight to
command_executor::handle_command and returned before the durable
ban/timeout write-block gate that ordinary writes pass through.
handle_command performs no restriction check of its own, so a banned or
timed-out member could still open DMs, define or trigger workflows, and
grant or deny approvals over signed POST /events. The moderation-command
and relay-admin kinds are also routed around the shared gate, but each
re-checks the ban inside its own handler; command kinds had no such
check.

Enforce the gate before routing command kinds. Command kinds are
ordinary writes, not administrative capability, so they get the full
gate including timeouts — the relay-admin timeout exemption does not
extend to them. Extract the ban/timeout decision into
restriction_block/enforce_moderation_restriction so the command-kind
path and the ordinary-write path share one implementation and cannot
drift; the restriction-state lookup fails closed on a DB error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Max Lampert <maxwell@squareup.com>
@Maxwellimus
Maxwellimus marked this pull request as ready for review July 29, 2026 03:24
@Maxwellimus
Maxwellimus requested a review from a team as a code owner July 29, 2026 03:24
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