diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 45cb084bdb..ce20055b91 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -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 diff --git a/synapse/rest/admin/experimental_features.py b/synapse/rest/admin/experimental_features.py index abdb937793..9c2927c1fd 100644 --- a/synapse/rest/admin/experimental_features.py +++ b/synapse/rest/admin/experimental_features.py @@ -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) diff --git a/synapse/rest/client/paginated_sync.py b/synapse/rest/client/paginated_sync.py index 38e515d7f4..5da0972067 100644 --- a/synapse/rest/client/paginated_sync.py +++ b/synapse/rest/client/paginated_sync.py @@ -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) diff --git a/tests/rest/client/sliding_sync/test_paginated_sync.py b/tests/rest/client/sliding_sync/test_paginated_sync.py index a0b4236f68..d611f5a6c2 100644 --- a/tests/rest/client/sliding_sync/test_paginated_sync.py +++ b/tests/rest/client/sliding_sync/test_paginated_sync.py @@ -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)