Add collaborative editing channels for wiki pages
What does this MR do and why?
Adds the ActionCable channel that relays Yjs updates between clients editing the same wiki page, plus the wiki_collaborative_editing flag definition that gates it.
The server is not a CRDT peer. It authorises the subscriber, appends opaque base64 updates to the Redis log and rebroadcasts them; all merging happens in the browser. An earlier attempt used Rust bindings for a Ruby-side CRDT and proved unsustainable to maintain, so CRDT logic stays out of Ruby.
Nothing here is reachable by a user. There is no client-side provider yet and no wiring into the wiki form, so the channel exists but nothing subscribes to it. Nothing to screenshot.
Subclass contract
BaseChannel resolves in this order: find_container, feature_enabled?(container), authorized?(container), find_document(container), document_key(document). Another editable surface should mean a new subclass, not changes to the base class.
Two decisions worth calling out, both driven by the ordering:
- The flag is scoped to
container.root_ancestor, not the user. A session is shared, so a per-user actor would let one participant edit through the CRDT while another edited the same page through the plain form, and the two would silently overwrite each other. - Authorisation runs on the container, before the page lookup. A wiki page is a Git blob, so resolving one costs a Gitaly call, and an unauthorised subscriber must not be able to trigger that by guessing paths.
Security properties
AppSec review was scoped to this slice.
- Subscription is rejected unless the flag is on for the container root ancestor, the container has a wiki, the user has
:create_wikion it, and the page exists. - Granular token scopes are checked explicitly via
granular_authorization_denied?, matchingNoteable::NotesChannel.AccessTokenValidationServiceskips the legacy scope check for granular tokens, andAbility.allowed?reads role membership rather than token scopes, so without this a granular PAT lackingcreate_wikicould open a session. This was not in the original unsplit MR. - Display identity is stamped from
current_useron every awareness relay, never read from the payload. The payload is opaque binary the server cannot inspect, so a name oravatarUrlcarried inside it would be whatever the sender chose — the latter being an IP and timing beacon every peer's browser would load. - Payloads are capped at 1 MB (
MAX_PAYLOAD_BYTES); updates are rate limited to 600/min viaGitlab::ApplicationRateLimiter, keyed in Redis on[current_user, document_key]so extra subscriptions to the same document cannot multiply the allowance. - Non-string payloads and unknown message types are ignored.
Wire protocol
request_snapshot carries a compaction token which the client must echo back on its snapshot message; document_full tells a client the log is at its ceiling. Both were added to DocumentStore in !253421 (merged) and are consumed here. ActionCableProvider needs updating for both when it lands in the next slice.
The durable copy of a page is still the Git commit written on save. The Redis log is ephemeral, so a late joiner can catch up and a session survives every client briefly disconnecting.
Part of a split
Slice 3 of 5, splitting !250607 (closed) as requested at !250607 (comment 3708895581). The original list skipped a 3, so this is the channels/AppSec item numbered 4 there.
- Slice 1, merged: !251691 (merged)
- Slice 2, merged: !252572 (merged)
- Store hardening, merged: !253421 (merged)
- Slice 4:
ActionCableProvider, the client-side provider - Slice 5: wiring —
wiki_form.vue,CollaboratorsIndicator, and making the feature reachable
Testing
44 examples across the two channel specs. RuboCop clean, bundle exec rake gitlab:permissions:validate passes.
References
- Original unsplit MR: !250607 (closed)
- Review comment requesting the split: !250607 (comment 3708895581)
- Store hardening this depends on: !253421 (merged)
- Feature work item: #14367
- Flag rollout work item: #616943
How to set up and validate locally
Nothing to validate through the UI — no client subscribes to the channel yet.
bundle exec rspec spec/channels/collaborative_editing/The specs cover the subscription guards (flag actor, permissions, granular tokens, missing page or container), identity stamping against a spoofed payload, the payload and rate limits, and the compaction and document-full paths.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.