Skip to content

Commit 47fb4b4

Browse files
Introduce RootConfig.validate_config() which can be subclassed in HomeServerConfig to do cross-config class validation (#19027)
This means we can move the open registration config validation from `setup()` to `HomeServerConfig.validate_config()` (much more sane). Spawning from looking at this area of code in #19015
1 parent 715cc5e commit 47fb4b4

File tree

7 files changed

+147
-34
lines changed

7 files changed

+147
-34
lines changed

changelog.d/19027.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Introduce `RootConfig.validate_config()` which can be subclassed in `HomeServerConfig` to do cross-config class validation.

synapse/app/homeserver.py

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ def create_homeserver(
367367

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

@@ -377,22 +377,6 @@ def create_homeserver(
377377
if config.server.gc_seconds:
378378
synapse.metrics.MIN_TIME_BETWEEN_GCS = config.server.gc_seconds
379379

380-
if (
381-
config.registration.enable_registration
382-
and not config.registration.enable_registration_without_verification
383-
):
384-
if (
385-
not config.captcha.enable_registration_captcha
386-
and not config.registration.registrations_require_3pid
387-
and not config.registration.registration_requires_token
388-
):
389-
raise ConfigError(
390-
"You have enabled open registration without any verification. This is a known vector for "
391-
"spam and abuse. If you would like to allow public registration, please consider adding email, "
392-
"captcha, or token-based verification. Otherwise this check can be removed by setting the "
393-
"`enable_registration_without_verification` config option to `true`."
394-
)
395-
396380
hs = SynapseHomeServer(
397381
hostname=config.server.server_name,
398382
config=config,

synapse/config/_base.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -545,18 +545,22 @@ def generate_config(
545545

546546
@classmethod
547547
def load_config(
548-
cls: Type[TRootConfig], description: str, argv: List[str]
548+
cls: Type[TRootConfig], description: str, argv_options: List[str]
549549
) -> TRootConfig:
550550
"""Parse the commandline and config files
551551
552552
Doesn't support config-file-generation: used by the worker apps.
553553
554+
Args:
555+
description: TODO
556+
argv_options: The options passed to Synapse. Usually `sys.argv[1:]`.
557+
554558
Returns:
555559
Config object.
556560
"""
557561
config_parser = argparse.ArgumentParser(description=description)
558562
cls.add_arguments_to_parser(config_parser)
559-
obj, _ = cls.load_config_with_parser(config_parser, argv)
563+
obj, _ = cls.load_config_with_parser(config_parser, argv_options)
560564

561565
return obj
562566

@@ -609,6 +613,10 @@ def load_config_with_parser(
609613
610614
Used for workers where we want to add extra flags/subcommands.
611615
616+
Note: This is the common denominator for loading config and is also used by
617+
`load_config` and `load_or_generate_config`. Which is why we call
618+
`validate_config()` here.
619+
612620
Args:
613621
parser
614622
argv_options: The options passed to Synapse. Usually `sys.argv[1:]`.
@@ -642,6 +650,10 @@ def load_config_with_parser(
642650

643651
obj.invoke_all("read_arguments", config_args)
644652

653+
# Now that we finally have the full config sections parsed, allow subclasses to
654+
# do some extra validation across the entire config.
655+
obj.validate_config()
656+
645657
return obj, config_args
646658

647659
@classmethod
@@ -842,15 +854,7 @@ def load_or_generate_config(
842854
):
843855
return None
844856

845-
obj.parse_config_dict(
846-
config_dict,
847-
config_dir_path=config_dir_path,
848-
data_dir_path=data_dir_path,
849-
allow_secrets_in_config=config_args.secrets_in_config,
850-
)
851-
obj.invoke_all("read_arguments", config_args)
852-
853-
return obj
857+
return cls.load_config(description, argv_options)
854858

855859
def parse_config_dict(
856860
self,
@@ -911,6 +915,20 @@ def reload_config_section(self, section_name: str) -> Config:
911915
existing_config.root = None
912916
return existing_config
913917

918+
def validate_config(self) -> None:
919+
"""
920+
Additional config validation across all config sections.
921+
922+
Override this in subclasses to add extra validation. This is called once all
923+
config option values have been populated.
924+
925+
XXX: This should only validate, not modify the configuration, as the final
926+
config state is required for proper validation across all config sections.
927+
928+
Raises:
929+
ConfigError: if the config is invalid.
930+
"""
931+
914932

915933
def read_config_files(config_files: Iterable[str]) -> Dict[str, Any]:
916934
"""Read the config files and shallowly merge them into a dict.

synapse/config/_base.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,19 +158,19 @@ class RootConfig:
158158
) -> str: ...
159159
@classmethod
160160
def load_or_generate_config(
161-
cls: Type[TRootConfig], description: str, argv: List[str]
161+
cls: Type[TRootConfig], description: str, argv_options: List[str]
162162
) -> Optional[TRootConfig]: ...
163163
@classmethod
164164
def load_config(
165-
cls: Type[TRootConfig], description: str, argv: List[str]
165+
cls: Type[TRootConfig], description: str, argv_options: List[str]
166166
) -> TRootConfig: ...
167167
@classmethod
168168
def add_arguments_to_parser(
169169
cls, config_parser: argparse.ArgumentParser
170170
) -> None: ...
171171
@classmethod
172172
def load_config_with_parser(
173-
cls: Type[TRootConfig], parser: argparse.ArgumentParser, argv: List[str]
173+
cls: Type[TRootConfig], parser: argparse.ArgumentParser, argv_options: List[str]
174174
) -> Tuple[TRootConfig, argparse.Namespace]: ...
175175
def generate_missing_files(
176176
self, config_dict: dict, config_dir_path: str

synapse/config/homeserver.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
# [This file includes modifications made by New Vector Limited]
1919
#
2020
#
21-
from ._base import RootConfig
21+
22+
from ._base import ConfigError, RootConfig
2223
from .account_validity import AccountValidityConfig
2324
from .api import ApiConfig
2425
from .appservice import AppServiceConfig
@@ -67,6 +68,10 @@
6768

6869

6970
class HomeServerConfig(RootConfig):
71+
"""
72+
Top-level config object for Synapse homeserver (main process and workers).
73+
"""
74+
7075
config_classes = [
7176
ModulesConfig,
7277
ServerConfig,
@@ -115,3 +120,22 @@ class HomeServerConfig(RootConfig):
115120
# This must be last, as it checks for conflicts with other config options.
116121
MasConfig,
117122
]
123+
124+
def validate_config(
125+
self,
126+
) -> None:
127+
if (
128+
self.registration.enable_registration
129+
and not self.registration.enable_registration_without_verification
130+
):
131+
if (
132+
not self.captcha.enable_registration_captcha
133+
and not self.registration.registrations_require_3pid
134+
and not self.registration.registration_requires_token
135+
):
136+
raise ConfigError(
137+
"You have enabled open registration without any verification. This is a known vector for "
138+
"spam and abuse. If you would like to allow public registration, please consider adding email, "
139+
"captcha, or token-based verification. Otherwise this check can be removed by setting the "
140+
"`enable_registration_without_verification` config option to `true`."
141+
)

tests/config/test_load.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,14 @@ def test_load_succeeds_if_macaroon_secret_key_missing(self) -> None:
9999
def test_disable_registration(self) -> None:
100100
self.generate_config()
101101
self.add_lines_to_config(
102-
["enable_registration: true", "disable_registration: true"]
102+
[
103+
"enable_registration: true",
104+
"disable_registration: true",
105+
# We're not worried about open registration in this test. This test is
106+
# focused on making sure that enable/disable_registration properly
107+
# override each other.
108+
"enable_registration_without_verification: true",
109+
]
103110
)
104111
# Check that disable_registration clobbers enable_registration.
105112
config = HomeServerConfig.load_config("", ["-c", self.config_file])

tests/config/test_registration_config.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#
2020
#
2121

22+
import argparse
23+
2224
import synapse.app.homeserver
2325
from synapse.config import ConfigError
2426
from synapse.config.homeserver import HomeServerConfig
@@ -99,6 +101,10 @@ def test_session_lifetime_must_not_be_exceeded_by_smaller_lifetimes(self) -> Non
99101
)
100102

101103
def test_refuse_to_start_if_open_registration_and_no_verification(self) -> None:
104+
"""
105+
Test that our utilities to start the main Synapse homeserver process refuses
106+
to start if we detect open registration.
107+
"""
102108
self.generate_config()
103109
self.add_lines_to_config(
104110
[
@@ -111,7 +117,7 @@ def test_refuse_to_start_if_open_registration_and_no_verification(self) -> None:
111117
)
112118

113119
# Test that allowing open registration without verification raises an error
114-
with self.assertRaises(ConfigError):
120+
with self.assertRaises(SystemExit):
115121
# Do a normal homeserver creation and setup
116122
homeserver_config = synapse.app.homeserver.load_or_generate_config(
117123
["-c", self.config_file]
@@ -122,3 +128,76 @@ def test_refuse_to_start_if_open_registration_and_no_verification(self) -> None:
122128
# earlier, but in the future, the code could be refactored to raise the
123129
# error in a different place.
124130
synapse.app.homeserver.setup(hs)
131+
132+
def test_load_config_error_if_open_registration_and_no_verification(self) -> None:
133+
"""
134+
Test that `HomeServerConfig.load_config(...)` raises an exception when we detect open
135+
registration.
136+
"""
137+
self.generate_config()
138+
self.add_lines_to_config(
139+
[
140+
" ",
141+
"enable_registration: true",
142+
"registrations_require_3pid: []",
143+
"enable_registration_captcha: false",
144+
"registration_requires_token: false",
145+
]
146+
)
147+
148+
# Test that allowing open registration without verification raises an error
149+
with self.assertRaises(ConfigError):
150+
_homeserver_config = HomeServerConfig.load_config(
151+
description="test", argv_options=["-c", self.config_file]
152+
)
153+
154+
def test_load_or_generate_config_error_if_open_registration_and_no_verification(
155+
self,
156+
) -> None:
157+
"""
158+
Test that `HomeServerConfig.load_or_generate_config(...)` raises an exception when we detect open
159+
registration.
160+
"""
161+
self.generate_config()
162+
self.add_lines_to_config(
163+
[
164+
" ",
165+
"enable_registration: true",
166+
"registrations_require_3pid: []",
167+
"enable_registration_captcha: false",
168+
"registration_requires_token: false",
169+
]
170+
)
171+
172+
# Test that allowing open registration without verification raises an error
173+
with self.assertRaises(ConfigError):
174+
_homeserver_config = HomeServerConfig.load_or_generate_config(
175+
description="test", argv_options=["-c", self.config_file]
176+
)
177+
178+
def test_load_config_with_parser_error_if_open_registration_and_no_verification(
179+
self,
180+
) -> None:
181+
"""
182+
Test that `HomeServerConfig.load_config_with_parser(...)` raises an exception when we detect open
183+
registration.
184+
"""
185+
self.generate_config()
186+
self.add_lines_to_config(
187+
[
188+
" ",
189+
"enable_registration: true",
190+
"registrations_require_3pid: []",
191+
"enable_registration_captcha: false",
192+
"registration_requires_token: false",
193+
]
194+
)
195+
196+
# Test that allowing open registration without verification raises an error
197+
with self.assertRaises(ConfigError):
198+
config_parser = argparse.ArgumentParser(description="test")
199+
HomeServerConfig.add_arguments_to_parser(config_parser)
200+
201+
_homeserver_config = HomeServerConfig.load_config_with_parser(
202+
parser=config_parser, argv_options=["-c", self.config_file]
203+
)

0 commit comments

Comments
 (0)