Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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/19027.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Move open registration config validation from `setup(...)` to `load_or_generate_config(...)`.
21 changes: 2 additions & 19 deletions synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ def setup(
freeze: bool = True,
) -> SynapseHomeServer:
"""
Create and setup a Synapse homeserver instance given a configuration.
Create and setup the main Synapse homeserver instance given a configuration.

Args:
config: The configuration for the homeserver.
Expand All @@ -369,33 +369,16 @@ def setup(

if config.worker.worker_app:
raise ConfigError(
"You have specified `worker_app` in the config but are attempting to start a non-worker "
"You have specified `worker_app` in the config but are attempting to setup a non-worker "
"instance. Please use `python -m synapse.app.generic_worker` instead (or remove the option if this is the main process)."
)
sys.exit(1)

events.USE_FROZEN_DICTS = config.server.use_frozen_dicts
synapse.util.caches.TRACK_MEMORY_USAGE = config.caches.track_memory_usage

if config.server.gc_seconds:
synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds

if (
config.registration.enable_registration
and not config.registration.enable_registration_without_verification
):
if (
not config.captcha.enable_registration_captcha
and not config.registration.registrations_require_3pid
and not config.registration.registration_requires_token
):
raise ConfigError(
"You have enabled open registration without any verification. This is a known vector for "
"spam and abuse. If you would like to allow public registration, please consider adding email, "
"captcha, or token-based verification. Otherwise this check can be removed by setting the "
"`enable_registration_without_verification` config option to `true`."
)

hs = SynapseHomeServer(
config.server.server_name,
config=config,
Expand Down
36 changes: 25 additions & 11 deletions synapse/config/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,18 +545,22 @@ def generate_config(

@classmethod
def load_config(
cls: Type[TRootConfig], description: str, argv: List[str]
cls: Type[TRootConfig], description: str, argv_options: List[str]
) -> TRootConfig:
"""Parse the commandline and config files

Doesn't support config-file-generation: used by the worker apps.

Args:
description: TODO
argv_options: The options passed to Synapse. Usually `sys.argv[1:]`.

Returns:
Config object.
"""
config_parser = argparse.ArgumentParser(description=description)
cls.add_arguments_to_parser(config_parser)
obj, _ = cls.load_config_with_parser(config_parser, argv)
obj, _ = cls.load_config_with_parser(config_parser, argv_options)

return obj

Expand Down Expand Up @@ -609,6 +613,10 @@ def load_config_with_parser(

Used for workers where we want to add extra flags/subcommands.

Note: This is the common denominator for loading config and is also used by
`load_config` and `load_or_generate_config`. Which is why we call
`validate_config()` here.

Args:
parser
argv_options: The options passed to Synapse. Usually `sys.argv[1:]`.
Expand Down Expand Up @@ -642,6 +650,10 @@ def load_config_with_parser(

obj.invoke_all("read_arguments", config_args)

# Now that we finally have the full config sections parsed, allow subclasses to
# do some extra validation across the entire config.
obj.validate_config()

return obj, config_args

@classmethod
Expand Down Expand Up @@ -842,15 +854,7 @@ def load_or_generate_config(
):
return None

obj.parse_config_dict(
config_dict,
config_dir_path=config_dir_path,
data_dir_path=data_dir_path,
allow_secrets_in_config=config_args.secrets_in_config,
)
obj.invoke_all("read_arguments", config_args)

return obj
return cls.load_config(description, argv_options)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change makes it so we go through the normal load_config(...) code path instead of inventing yet another way to load config.

It also means we can settle on calling validate_config(...) in one place as everything flows to load_config_with_parser(...) now.


def parse_config_dict(
self,
Expand Down Expand Up @@ -911,6 +915,16 @@ def reload_config_section(self, section_name: str) -> Config:
existing_config.root = None
return existing_config

def validate_config(self) -> None:
"""
Additional config validation across all config sections.

Override this in subclasses to add extra validation.

Raises:
ConfigError: if the config is invalid.
"""


def read_config_files(config_files: Iterable[str]) -> Dict[str, Any]:
"""Read the config files and shallowly merge them into a dict.
Expand Down
6 changes: 3 additions & 3 deletions synapse/config/_base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -156,19 +156,19 @@ class RootConfig:
) -> str: ...
@classmethod
def load_or_generate_config(
cls: Type[TRootConfig], description: str, argv: List[str]
cls: Type[TRootConfig], description: str, argv_options: List[str]
) -> Optional[TRootConfig]: ...
@classmethod
def load_config(
cls: Type[TRootConfig], description: str, argv: List[str]
cls: Type[TRootConfig], description: str, argv_options: List[str]
) -> TRootConfig: ...
@classmethod
def add_arguments_to_parser(
cls, config_parser: argparse.ArgumentParser
) -> None: ...
@classmethod
def load_config_with_parser(
cls: Type[TRootConfig], parser: argparse.ArgumentParser, argv: List[str]
cls: Type[TRootConfig], parser: argparse.ArgumentParser, argv_options: List[str]
) -> Tuple[TRootConfig, argparse.Namespace]: ...
def generate_missing_files(
self, config_dict: dict, config_dir_path: str
Expand Down
26 changes: 25 additions & 1 deletion synapse/config/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
# [This file includes modifications made by New Vector Limited]
#
#
from ._base import RootConfig

from ._base import ConfigError, RootConfig
from .account_validity import AccountValidityConfig
from .api import ApiConfig
from .appservice import AppServiceConfig
Expand Down Expand Up @@ -66,6 +67,10 @@


class HomeServerConfig(RootConfig):
"""
Top-level config object for Synapse homeserver (main process and workers).
"""

config_classes = [
ModulesConfig,
ServerConfig,
Expand Down Expand Up @@ -113,3 +118,22 @@ class HomeServerConfig(RootConfig):
# This must be last, as it checks for conflicts with other config options.
MasConfig,
]

def validate_config(
self,
) -> None:
if (
self.registration.enable_registration
and not self.registration.enable_registration_without_verification
):
if (
not self.captcha.enable_registration_captcha
and not self.registration.registrations_require_3pid
and not self.registration.registration_requires_token
):
raise ConfigError(
"You have enabled open registration without any verification. This is a known vector for "
"spam and abuse. If you would like to allow public registration, please consider adding email, "
"captcha, or token-based verification. Otherwise this check can be removed by setting the "
"`enable_registration_without_verification` config option to `true`."
)
Copy link
Contributor Author

@MadLittleMods MadLittleMods Oct 8, 2025

Choose a reason for hiding this comment

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

The only real change is that this has moved from a place that only runs for the main Synapse instance to everything including workers.

But I assume that's fine. We want the same warning for workers as we wouldn't want open registration there either.

81 changes: 80 additions & 1 deletion tests/config/test_registration_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#
#

import argparse

import synapse.app.homeserver
from synapse.config import ConfigError
from synapse.config.homeserver import HomeServerConfig
Expand Down Expand Up @@ -99,6 +101,10 @@ def test_session_lifetime_must_not_be_exceeded_by_smaller_lifetimes(self) -> Non
)

def test_refuse_to_start_if_open_registration_and_no_verification(self) -> None:
"""
Test that our utilities to start the main Synapse homeserver process refuses
to start if we detect open registration.
"""
self.generate_config()
self.add_lines_to_config(
[
Expand All @@ -111,8 +117,81 @@ def test_refuse_to_start_if_open_registration_and_no_verification(self) -> None:
)

# Test that allowing open registration without verification raises an error
with self.assertRaises(ConfigError):
with self.assertRaises(SystemExit):
homeserver_config = synapse.app.homeserver.load_or_generate_config(
["-c", self.config_file]
)
synapse.app.homeserver.setup(homeserver_config)

def test_load_config_error_if_open_registration_and_no_verification(self) -> None:
"""
Test that `HomeServerConfig.load_config(...)` raises an exception when we detect open
registration.
"""
self.generate_config()
self.add_lines_to_config(
[
" ",
"enable_registration: true",
"registrations_require_3pid: []",
"enable_registration_captcha: false",
"registration_requires_token: false",
]
)

# Test that allowing open registration without verification raises an error
with self.assertRaises(ConfigError):
_homeserver_config = HomeServerConfig.load_config(
description="test", argv_options=["-c", self.config_file]
)

def test_load_or_generate_config_error_if_open_registration_and_no_verification(
self,
) -> None:
"""
Test that `HomeServerConfig.load_or_generate_config(...)` raises an exception when we detect open
registration.
"""
self.generate_config()
self.add_lines_to_config(
[
" ",
"enable_registration: true",
"registrations_require_3pid: []",
"enable_registration_captcha: false",
"registration_requires_token: false",
]
)

# Test that allowing open registration without verification raises an error
with self.assertRaises(ConfigError):
_homeserver_config = HomeServerConfig.load_or_generate_config(
description="test", argv_options=["-c", self.config_file]
)

def test_load_config_with_parser_error_if_open_registration_and_no_verification(
self,
) -> None:
"""
Test that `HomeServerConfig.load_config_with_parser(...)` raises an exception when we detect open
registration.
"""
self.generate_config()
self.add_lines_to_config(
[
" ",
"enable_registration: true",
"registrations_require_3pid: []",
"enable_registration_captcha: false",
"registration_requires_token: false",
]
)

# Test that allowing open registration without verification raises an error
with self.assertRaises(ConfigError):
config_parser = argparse.ArgumentParser(description="test")
HomeServerConfig.add_arguments_to_parser(config_parser)

_homeserver_config = HomeServerConfig.load_config_with_parser(
parser=config_parser, argv_options=["-c", self.config_file]
)
Loading