Skip to content

Blocking a user does not prevent them sending into an existing conversation (C8 gates on participant, not connection status) #352

Description

@TortoiseWolfe

Summary

Blocking a user does not stop them sending. user_connections.status = 'blocked' is enforced nowhere on the send path — on either backend. Once a 1:1 conversation exists, sending is gated on participant, not on connection status, so a blocked user keeps messaging the person who blocked them.

Found while shipping #265's C3 increment (PR #351). Pre-existing and orthogonal to that work, so it was deliberately left out of scope rather than widening the increment.

Verified

Confirmed at all three layers on main (8a705ac):

1. RLSsupabase/migrations/20251006_complete_monolithic_setup.sql, messages INSERT (1:1 :1945, group :2715):

FOR INSERT WITH CHECK (
  sender_id = auth.uid() AND
  EXISTS (
    SELECT 1 FROM conversations
    WHERE conversations.id = conversation_id AND (
      conversations.participant_1_id = auth.uid() OR
      conversations.participant_2_id = auth.uid()
    )
  )
);

No reference to user_connections. Grepping the whole migration, user_connections is consulted only by admin-stats functions and its own policies — never by a messages policy. 'blocked' appears only in the status CHECK constraint and an admin dashboard count.

2. TypeScriptsrc/services/messaging/message-service.ts has no connection-status check on sendMessage (only doc-comment prose mentioning "connections").

3. .NETConversationsController.Send gates on MessagingQueries.CanAccessConversation (participant / active member). HasAcceptedConnection exists but is called only by Create (the C3 path added in #351).

So both backends agree — which means this is a genuine contract gap, not provider drift.

Repro

  1. A and B connect (accepted), open a conversation, exchange messages.
  2. B blocks A → user_connections.status = 'blocked'.
  3. A sends into the existing conversation → succeeds.

Note step 1 matters: with no pre-existing conversation, C3 (getOrCreateConversation) correctly refuses to create one, since it requires status = 'accepted'. The hole is only for conversations that already exist — i.e. exactly the people most likely to be blocked.

Why it wasn't caught

C8's contract text is "On send, sender must be the caller AND an active participant/member." That is implemented faithfully. The rule as written simply never contemplated block; the conformance suite asserts the rule, and the rule is the gap.

Design questions to settle before implementing

Blocking has more surface than the send path, and the answers should be decided together rather than patched piecemeal:

  • Send — reject outright, or accept-and-drop (so the blocker isn't signalled)?
  • Read — can a blocked user still read history? RLS conversations SELECT (:1843) and messages SELECT (:1933) are participant-scoped and connection-independent, so today: yes.
  • Directionalityunique_connection is (requester_id, addressee_id) and is not symmetric. Does a block by either party stop both directions? (Almost certainly yes, but the check must test both orderings, the way C3's HasAcceptedConnection does.)
  • Realtime — a blocked sender's writes currently still fire postgres_changes to the blocker.
  • Groups — does a 1:1 block affect a shared group conversation?

Definition of done

Per docs/messaging/AUTHORIZATION-CONTRACT.md's maintenance rule, this becomes a real clause only with canonical rule text + a conformance case:

  • Decide the semantics above and write the rule text into the contract doc
  • Enforce it in RLS and explicitly in the .NET server
  • Conformance case driving both providers (seed accepted → converse → block → assert the send is refused and no row lands)
  • The .NET runner's assertRefusal hook must pin a 403, not a 5xx — otherwise RLS masks a missing C# check and the case passes with the rule deleted. See the mutation-test writeup in AUTHORIZATION-CONTRACT.md → "C3 and the two things a conformance case must prove".

Context

Part of the #280 enterprise arc / #265 seam-expansion. Sits naturally alongside the remaining backlog items (group management, key rotation, GDPR), since block semantics touch group membership too.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions