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 11 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
31 changes: 31 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4546,3 +4546,34 @@ background_updates:
min_batch_size: 10
default_batch_size: 50
```


---
## Extension features
Configuration for extension features for Synapse

---
### `extension_federation_whitelist_endpoint`

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
extension_federation_whitelist_endpoint: true
```
2 changes: 2 additions & 0 deletions synapse/config/_base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ from synapse.config import ( # noqa: F401
database,
emailconfig,
experimental,
extensions,
federation,
jwt,
key,
Expand Down Expand Up @@ -120,6 +121,7 @@ class RootConfig:
federation: federation.FederationConfig
retention: retention.RetentionConfig
background_updates: background_updates.BackgroundUpdateConfig
extensions: extensions.ExtensionsConfig

config_classes: List[Type["Config"]] = ...
config_files: List[str]
Expand Down
29 changes: 29 additions & 0 deletions synapse/config/extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2023 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 Any

from synapse.config._base import Config
from synapse.types import JsonDict


class ExtensionsConfig(Config):
"""Config section for enabling extension features"""

section = "extensions"

def read_config(self, config: JsonDict, **kwargs: Any) -> None:
self.federation_whitelist_endpoint: bool = config.get(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously @erikjohnston noted that the "extension" config section was created as this option didn't fit in anywhere else.

But would it not fit under "federation"?

class FederationConfig(Config):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be beneficial to follow the approach mentioned here: #17147 (comment)

ie. to move this to an in-tree-module and have the config follow the normal module config rules.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach mentioned in that PR was deemed undesirable for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But would it not fit under "federation"?

On this, @erikjohnston wrote on Matrix:

Yeah, maybe. I suppose the federation section is relatively small. I'm just thinking of what this will look like if we have e.g. 10 optional features like this, and whether we'd want them grouped together in one place in the docs or not
and whether its clearer in the code or not
I suppose we can cross that bridge when we come to it

I don't see this as an optional feature, more a config option like any other. And they won't end up all under federation of course, but spread across the config. I don't think they'll build up in any noticeable way as a group.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I'll move it under the federation section.

"extension_federation_whitelist_endpoint", False
)
2 changes: 2 additions & 0 deletions synapse/config/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .database import DatabaseConfig
from .emailconfig import EmailConfig
from .experimental import ExperimentalConfig
from .extensions import ExtensionsConfig
from .federation import FederationConfig
from .jwt import JWTConfig
from .key import KeyConfig
Expand Down Expand Up @@ -105,4 +106,5 @@ class HomeServerConfig(RootConfig):
RedisConfig,
ExperimentalConfig,
BackgroundUpdateConfig,
ExtensionsConfig,
]
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.extensions.federation_whitelist_endpoint:
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({"extension_federation_whitelist_endpoint": 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({"extension_federation_whitelist_endpoint": 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(
{
"extension_federation_whitelist_endpoint": 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(
{
"extension_federation_whitelist_endpoint": 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