Skip to content

Commit

Permalink
An federation whitelist query endpoint extension (#16848)
Browse files Browse the repository at this point in the history
This is to allow clients to query the configured federation whitelist.
Disabled by default.

---------

Co-authored-by: Devon Hudson <devonhudson@librem.one>
Co-authored-by: devonh <devon.dmytro@gmail.com>
Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
  • Loading branch information
4 people committed May 13, 2024
1 parent 59ac541 commit 038b9ec
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog.d/16848.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a feature that allows clients to query the configured federation whitelist. Disabled by default.
25 changes: 25 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions synapse/config/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
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,
Expand Down
4 changes: 4 additions & 0 deletions synapse/rest/synapse/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

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
Expand Down Expand Up @@ -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)

Expand Down
66 changes: 66 additions & 0 deletions synapse/rest/synapse/client/federation_whitelist.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions tests/rest/synapse/__init__.py
Original file line number Diff line number Diff line change
@@ -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>.
12 changes: 12 additions & 0 deletions tests/rest/synapse/client/__init__.py
Original file line number Diff line number Diff line change
@@ -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>.
119 changes: 119 additions & 0 deletions tests/rest/synapse/client/test_federation_whitelist.py
Original file line number Diff line number Diff line change
@@ -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"]}
)

0 comments on commit 038b9ec

Please sign in to comment.