Gate the paginated sync endpoint per user, disabled by default

Add MSC4525 to the per-user ExperimentalFeature registry: the servlet is
always registered but 404s unless the feature is enabled globally
(msc4525_enabled) or for the requesting user via the admin
experimental-features API.
This commit is contained in:
Matthew Hodgson
2026-08-07 13:24:05 +03:00
parent 8ec2cde02d
commit 360b033178
4 changed files with 64 additions and 4 deletions
+2 -1
View File
@@ -122,7 +122,8 @@ class ExperimentalConfig(Config):
# MSC4525: Paginated Sync, a simplified dialect of Simplified Sliding
# Sync (MSC4186) without lists/ranges/subscriptions, where the server
# pages the client through changed rooms (most recently active first)
# with bounded response sizes.
# with bounded response sizes. Disabled by default; can also be enabled
# for individual users via the admin experimental-features API.
self.msc4525_enabled: bool = experimental.get("msc4525_enabled", False)
# MSC3773: Thread notifications
@@ -44,6 +44,7 @@ class ExperimentalFeature(str, Enum):
MSC3881 = "msc3881"
MSC3575 = "msc3575"
MSC4222 = "msc4222"
MSC4525 = "msc4525"
def is_globally_enabled(self, config: "HomeServerConfig") -> bool:
if self is ExperimentalFeature.MSC3881:
@@ -52,6 +53,8 @@ class ExperimentalFeature(str, Enum):
return config.experimental.msc3575_enabled
if self is ExperimentalFeature.MSC4222:
return config.experimental.msc4222_enabled
if self is ExperimentalFeature.MSC4525:
return config.experimental.msc4525_enabled
assert_never(self)
+9 -3
View File
@@ -31,6 +31,7 @@ from synapse.http.servlet import (
)
from synapse.http.site import SynapseRequest
from synapse.logging.opentracing import log_kv, set_tag
from synapse.rest.admin.experimental_features import ExperimentalFeature
from synapse.rest.client._base import client_patterns
from synapse.rest.client.sync import SlidingSyncRestServlet
from synapse.types import JsonDict, Requester, SlidingSyncStreamToken
@@ -67,7 +68,11 @@ class PaginatedSyncRestServlet(SlidingSyncRestServlet):
self.paginated_sync_handler = hs.get_paginated_sync_handler()
async def on_POST(self, request: SynapseRequest) -> tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request, allow_guest=True)
# Disabled by default; enabled either globally (`msc4525_enabled`) or
# per user via the admin experimental-features API. 404s otherwise.
requester = await self.auth.get_user_by_req_experimental_feature(
request, allow_guest=True, feature=ExperimentalFeature.MSC4525
)
user = requester.user
timeout = parse_integer(request, "timeout", default=0)
@@ -156,5 +161,6 @@ class PaginatedSyncRestServlet(SlidingSyncRestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
if hs.config.experimental.msc4525_enabled:
PaginatedSyncRestServlet(hs).register(http_server)
# Always registered; access is gated per user (or globally) via
# `ExperimentalFeature.MSC4525` in `on_POST`.
PaginatedSyncRestServlet(hs).register(http_server)
@@ -408,3 +408,53 @@ class PaginatedSyncTestCase(unittest.HomeserverTestCase):
response = self._sync(body, pos=pos)
account_data_response = response["extensions"]["account_data"]["rooms"]
self.assertIn(room_id, account_data_response, response)
class PaginatedSyncPerUserEnablementTestCase(unittest.HomeserverTestCase):
"""The endpoint is disabled by default and enablable per user via the
admin experimental-features API (`ExperimentalFeature.MSC4525`)."""
servlets = [
synapse.rest.admin.register_servlets,
login.register_servlets,
room.register_servlets,
paginated_sync.register_servlets,
]
sync_endpoint = "/_matrix/client/unstable/org.matrix.msc4525/sync?timeout=0"
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.user = self.register_user("alice", "password")
self.tok = self.login("alice", "password")
self.admin_tok = self.login(
self.register_user("admin", "password", admin=True), "password"
)
def test_disabled_by_default_and_enablable_per_user(self) -> None:
body = {"page_size": 10, "limit": 5}
channel = self.make_request(
"POST", self.sync_endpoint, body, access_token=self.tok
)
self.assertEqual(channel.code, 404, channel.json_body)
# Enable the feature for alice only.
channel = self.make_request(
"PUT",
f"/_synapse/admin/v1/experimental_features/{self.user}",
{"features": {"msc4525": True}},
access_token=self.admin_tok,
)
self.assertEqual(channel.code, 200, channel.json_body)
channel = self.make_request(
"POST", self.sync_endpoint, body, access_token=self.tok
)
self.assertEqual(channel.code, 200, channel.json_body)
# Other users remain gated.
bob_tok = self.login(self.register_user("bob", "password"), "password")
channel = self.make_request(
"POST", self.sync_endpoint, body, access_token=bob_tok
)
self.assertEqual(channel.code, 404, channel.json_body)