Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An federation whitelist query endpoint extension #16848

Merged
merged 15 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/16848.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an extension feature that allows clients to query the configured federation whitelist. Disabled by default.
devonh marked this conversation as resolved.
Show resolved Hide resolved
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:
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved

```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
67 changes: 67 additions & 0 deletions synapse/rest/synapse/client/federation_whitelist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#
# 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` extension feature is
enabled.
devonh marked this conversation as resolved.
Show resolved Hide resolved

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/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/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/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/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/config/federation_whitelist",
shorthand=False,
access_token=tok,
)

self.assertEqual(channel.code, 200)
self.assertEqual(
channel.json_body, {"whitelist_enabled": True, "whitelist": ["example.com"]}
)
devonh marked this conversation as resolved.
Show resolved Hide resolved

@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/config/federation_whitelist",
shorthand=False,
access_token=tok,
)

self.assertEqual(channel.code, 200)
self.assertEqual(
channel.json_body, {"whitelist_enabled": True, "whitelist": ["example.com"]}
)
Loading