Andrey Antukh 6060a7140f 🐛 Revoke websocket subscriptions when access is revoked
GHSA-m53j-2766-6jqw. A subscription is authorized once, when it is
opened, and that decision is trusted for the rest of the connection's
life. A member whose access is revoked keeps receiving the live file
diff for as long as their tab stays open.

Closing the connection from the mutation does not work: the registry
is local to one backend instance, while the RPC that revokes access
runs on whichever instance received the request. So the revocation
travels over the message bus instead, which every instance shares, and
each instance re-verifies the subscriptions it owns.

`revalidate-profile-subscriptions` looks the profile up in the new
index, then re-checks every file and team subscription with the same
`has-read-permissions?` predicates that `:subscribe-file` and
`:subscribe-team` authorize with. The event is only the trigger; the
fresh check is the decision, so the watcher keeps no second copy of the
permission rules. That is what makes a downgrade to viewer keep its
subscription, and keeps the read-only access of a non-member
organization owner.

`close-team-subscription` mirrors `close-file-subscription` for team
traffic. Both now forget the subscription instead of leaving a stale
one behind, which makes them idempotent and stops `:pointer-update`
from publishing to a topic the connection no longer relays.

The internal topic is separate from the `uuid/zero` system topic, which
is piped straight to the client. No client can reach it:
`:subscribe-file` and `:subscribe-team` only accept ids that already
passed a permission check.

Closes #12155

AI-assisted-by: space-bunny-free
2026-10-08 13:08:50 +02:00
..
2026-09-29 09:07:34 +02:00