diff --git a/changelog.d/16848.feature b/changelog.d/16848.feature
new file mode 100644
index 0000000000..1a72bad013
--- /dev/null
+++ b/changelog.d/16848.feature
@@ -0,0 +1 @@
+Add a feature that allows clients to query the configured federation whitelist. Disabled by default.
diff --git a/docs/usage/configuration/config_documentation.md b/docs/usage/configuration/config_documentation.md
index 2046bf4564..2257318bcc 100644
--- a/docs/usage/configuration/config_documentation.md
+++ b/docs/usage/configuration/config_documentation.md
@@ -1232,6 +1232,31 @@ federation_domain_whitelist:
- syd.example.com
```
---
+### `federation_whitelist_endpoint_enabled`
+
+Enables an endpoint for fetching the federation whitelist config.
+
+The request method and path is `GET /_synapse/client/config/federation_whitelist`, and the
+response format is:
+
+```json
+{
+ "whitelist_enabled": true, // Whether the federation whitelist is being enforced
+ "whitelist": [ // Which server names are allowed by the whitelist
+ "example.com"
+ ]
+}
+```
+
+If `whitelist_enabled` is `false` then the server is permitted to federate with all others.
+
+The endpoint requires authentication.
+
+Example configuration:
+```yaml
+federation_whitelist_endpoint_enabled: true
+```
+---
### `federation_metrics_domains`
Report prometheus metrics on the age of PDUs being sent to and received from
diff --git a/synapse/config/federation.py b/synapse/config/federation.py
index 9032effac3..cf29fa2562 100644
--- a/synapse/config/federation.py
+++ b/synapse/config/federation.py
@@ -42,6 +42,10 @@ class FederationConfig(Config):
for domain in federation_domain_whitelist:
self.federation_domain_whitelist[domain] = True
+ self.federation_whitelist_endpoint_enabled = config.get(
+ "federation_whitelist_endpoint_enabled", False
+ )
+
federation_metrics_domains = config.get("federation_metrics_domains") or []
validate_config(
_METRICS_FOR_DOMAINS_SCHEMA,
diff --git a/synapse/rest/synapse/client/__init__.py b/synapse/rest/synapse/client/__init__.py
index ba6576d4db..7b5bfc0421 100644
--- a/synapse/rest/synapse/client/__init__.py
+++ b/synapse/rest/synapse/client/__init__.py
@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Mapping
from twisted.web.resource import Resource
+from synapse.rest.synapse.client.federation_whitelist import FederationWhitelistResource
from synapse.rest.synapse.client.new_user_consent import NewUserConsentResource
from synapse.rest.synapse.client.pick_idp import PickIdpResource
from synapse.rest.synapse.client.pick_username import pick_username_resource
@@ -77,6 +78,9 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc
# To be removed in Synapse v1.32.0.
resources["/_matrix/saml2"] = res
+ if hs.config.federation.federation_whitelist_endpoint_enabled:
+ resources[FederationWhitelistResource.PATH] = FederationWhitelistResource(hs)
+
if hs.config.experimental.msc4108_enabled:
resources["/_synapse/client/rendezvous"] = MSC4108RendezvousSessionResource(hs)
diff --git a/synapse/rest/synapse/client/federation_whitelist.py b/synapse/rest/synapse/client/federation_whitelist.py
new file mode 100644
index 0000000000..2b8f0320e0
--- /dev/null
+++ b/synapse/rest/synapse/client/federation_whitelist.py
@@ -0,0 +1,66 @@
+#
+# This file is licensed under the Affero General Public License (AGPL) version 3.
+#
+# Copyright (C) 2024 New Vector, Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# See the GNU Affero General Public License for more details:
+# <https://www.gnu.org/licenses/agpl-3.0.html>.
+#
+
+import logging
+from typing import TYPE_CHECKING, Tuple
+
+from synapse.http.server import DirectServeJsonResource
+from synapse.http.site import SynapseRequest
+from synapse.types import JsonDict
+
+if TYPE_CHECKING:
+ from synapse.server import HomeServer
+
+logger = logging.getLogger(__name__)
+
+
+class FederationWhitelistResource(DirectServeJsonResource):
+ """Custom endpoint (disabled by default) to fetch the federation whitelist
+ config.
+
+ Only enabled if `federation_whitelist_endpoint_enabled` feature is enabled.
+
+ Response format:
+
+ {
+ "whitelist_enabled": true, // Whether the federation whitelist is being enforced
+ "whitelist": [ // Which server names are allowed by the whitelist
+ "example.com"
+ ]
+ }
+ """
+
+ PATH = "/_synapse/client/v1/config/federation_whitelist"
+
+ def __init__(self, hs: "HomeServer"):
+ super().__init__()
+
+ self._federation_whitelist = hs.config.federation.federation_domain_whitelist
+
+ self._auth = hs.get_auth()
+
+ async def _async_render_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+ await self._auth.get_user_by_req(request)
+
+ whitelist = []
+ if self._federation_whitelist:
+ # federation_whitelist is actually a dict, not a list
+ whitelist = list(self._federation_whitelist)
+
+ return_dict: JsonDict = {
+ "whitelist_enabled": self._federation_whitelist is not None,
+ "whitelist": whitelist,
+ }
+
+ return 200, return_dict
diff --git a/tests/rest/synapse/__init__.py b/tests/rest/synapse/__init__.py
new file mode 100644
index 0000000000..e5138f67e1
--- /dev/null
+++ b/tests/rest/synapse/__init__.py
@@ -0,0 +1,12 @@
+#
+# This file is licensed under the Affero General Public License (AGPL) version 3.
+#
+# Copyright (C) 2024 New Vector, Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# See the GNU Affero General Public License for more details:
+# <https://www.gnu.org/licenses/agpl-3.0.html>.
diff --git a/tests/rest/synapse/client/__init__.py b/tests/rest/synapse/client/__init__.py
new file mode 100644
index 0000000000..e5138f67e1
--- /dev/null
+++ b/tests/rest/synapse/client/__init__.py
@@ -0,0 +1,12 @@
+#
+# This file is licensed under the Affero General Public License (AGPL) version 3.
+#
+# Copyright (C) 2024 New Vector, Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# See the GNU Affero General Public License for more details:
+# <https://www.gnu.org/licenses/agpl-3.0.html>.
diff --git a/tests/rest/synapse/client/test_federation_whitelist.py b/tests/rest/synapse/client/test_federation_whitelist.py
new file mode 100644
index 0000000000..f0067a8f2b
--- /dev/null
+++ b/tests/rest/synapse/client/test_federation_whitelist.py
@@ -0,0 +1,119 @@
+#
+# This file is licensed under the Affero General Public License (AGPL) version 3.
+#
+# Copyright (C) 2024 New Vector, Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# See the GNU Affero General Public License for more details:
+# <https://www.gnu.org/licenses/agpl-3.0.html>.
+
+from typing import Dict
+
+from twisted.web.resource import Resource
+
+from synapse.rest import admin
+from synapse.rest.client import login
+from synapse.rest.synapse.client import build_synapse_client_resource_tree
+
+from tests import unittest
+
+
+class FederationWhitelistTests(unittest.HomeserverTestCase):
+ servlets = [
+ admin.register_servlets_for_client_rest_resource,
+ login.register_servlets,
+ ]
+
+ def create_resource_dict(self) -> Dict[str, Resource]:
+ base = super().create_resource_dict()
+ base.update(build_synapse_client_resource_tree(self.hs))
+ return base
+
+ def test_default(self) -> None:
+ "If the config option is not enabled, the endpoint should 404"
+ channel = self.make_request(
+ "GET", "/_synapse/client/v1/config/federation_whitelist", shorthand=False
+ )
+
+ self.assertEqual(channel.code, 404)
+
+ @unittest.override_config({"federation_whitelist_endpoint_enabled": True})
+ def test_no_auth(self) -> None:
+ "Endpoint requires auth when enabled"
+
+ channel = self.make_request(
+ "GET", "/_synapse/client/v1/config/federation_whitelist", shorthand=False
+ )
+
+ self.assertEqual(channel.code, 401)
+
+ @unittest.override_config({"federation_whitelist_endpoint_enabled": True})
+ def test_no_whitelist(self) -> None:
+ "Test when there is no whitelist configured"
+
+ self.register_user("user", "password")
+ tok = self.login("user", "password")
+
+ channel = self.make_request(
+ "GET",
+ "/_synapse/client/v1/config/federation_whitelist",
+ shorthand=False,
+ access_token=tok,
+ )
+
+ self.assertEqual(channel.code, 200)
+ self.assertEqual(
+ channel.json_body, {"whitelist_enabled": False, "whitelist": []}
+ )
+
+ @unittest.override_config(
+ {
+ "federation_whitelist_endpoint_enabled": True,
+ "federation_domain_whitelist": ["example.com"],
+ }
+ )
+ def test_whitelist(self) -> None:
+ "Test when there is a whitelist configured"
+
+ self.register_user("user", "password")
+ tok = self.login("user", "password")
+
+ channel = self.make_request(
+ "GET",
+ "/_synapse/client/v1/config/federation_whitelist",
+ shorthand=False,
+ access_token=tok,
+ )
+
+ self.assertEqual(channel.code, 200)
+ self.assertEqual(
+ channel.json_body, {"whitelist_enabled": True, "whitelist": ["example.com"]}
+ )
+
+ @unittest.override_config(
+ {
+ "federation_whitelist_endpoint_enabled": True,
+ "federation_domain_whitelist": ["example.com", "example.com"],
+ }
+ )
+ def test_whitelist_no_duplicates(self) -> None:
+ "Test when there is a whitelist configured with duplicates, no duplicates are returned"
+
+ self.register_user("user", "password")
+ tok = self.login("user", "password")
+
+ channel = self.make_request(
+ "GET",
+ "/_synapse/client/v1/config/federation_whitelist",
+ shorthand=False,
+ access_token=tok,
+ )
+
+ self.assertEqual(channel.code, 200)
+ self.assertEqual(
+ channel.json_body, {"whitelist_enabled": True, "whitelist": ["example.com"]}
+ )
|