diff --git a/synapse/config/_base.py b/synapse/config/_base.py
index 132e48447c..128617b8b3 100644
--- a/synapse/config/_base.py
+++ b/synapse/config/_base.py
@@ -295,7 +295,6 @@ class RootConfig(object):
report_stats=None,
open_private_ports=False,
listeners=None,
- database_conf=None,
tls_certificate_path=None,
tls_private_key_path=None,
acme_domain=None,
@@ -368,7 +367,6 @@ class RootConfig(object):
report_stats=report_stats,
open_private_ports=open_private_ports,
listeners=listeners,
- database_conf=database_conf,
tls_certificate_path=tls_certificate_path,
tls_private_key_path=tls_private_key_path,
acme_domain=acme_domain,
@@ -471,8 +469,8 @@ class RootConfig(object):
Returns: Config object, or None if --generate-config or --generate-keys was set
"""
- config_parser = argparse.ArgumentParser(add_help=False)
- config_parser.add_argument(
+ parser = argparse.ArgumentParser(description=description)
+ parser.add_argument(
"-c",
"--config-path",
action="append",
@@ -481,7 +479,7 @@ class RootConfig(object):
" may specify directories containing *.yaml files.",
)
- generate_group = config_parser.add_argument_group("Config generation")
+ generate_group = parser.add_argument_group("Config generation")
generate_group.add_argument(
"--generate-config",
action="store_true",
@@ -529,12 +527,13 @@ class RootConfig(object):
),
)
- config_args, remaining_args = config_parser.parse_known_args(argv)
+ cls.invoke_all_static("add_arguments", parser)
+ config_args = parser.parse_args(argv)
config_files = find_config_files(search_paths=config_args.config_path)
if not config_files:
- config_parser.error(
+ parser.error(
"Must supply a config file.\nA config file can be automatically"
' generated using "--generate-config -H SERVER_NAME'
' -c CONFIG-FILE"'
@@ -553,7 +552,7 @@ class RootConfig(object):
if config_args.generate_config:
if config_args.report_stats is None:
- config_parser.error(
+ parser.error(
"Please specify either --report-stats=yes or --report-stats=no\n\n"
+ MISSING_REPORT_STATS_SPIEL
)
@@ -612,15 +611,6 @@ class RootConfig(object):
)
generate_missing_configs = True
- parser = argparse.ArgumentParser(
- parents=[config_parser],
- description=description,
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
-
- obj.invoke_all_static("add_arguments", parser)
- args = parser.parse_args(remaining_args)
-
config_dict = read_config_files(config_files)
if generate_missing_configs:
obj.generate_missing_files(config_dict, config_dir_path)
@@ -629,7 +619,7 @@ class RootConfig(object):
obj.parse_config_dict(
config_dict, config_dir_path=config_dir_path, data_dir_path=data_dir_path
)
- obj.invoke_all("read_arguments", args)
+ obj.invoke_all("read_arguments", config_args)
return obj
@@ -668,6 +658,12 @@ def read_config_files(config_files):
for config_file in config_files:
with open(config_file) as file_stream:
yaml_config = yaml.safe_load(file_stream)
+
+ if not isinstance(yaml_config, dict):
+ err = "File %r is empty or doesn't parse into a key-value map. IGNORING."
+ print(err % (config_file,))
+ continue
+
specified_config.update(yaml_config)
if "server_name" not in specified_config:
diff --git a/synapse/config/_base.pyi b/synapse/config/_base.pyi
index 3053fc9d27..9e576060d4 100644
--- a/synapse/config/_base.pyi
+++ b/synapse/config/_base.pyi
@@ -13,6 +13,7 @@ from synapse.config import (
key,
logger,
metrics,
+ oidc_config,
password,
password_auth_providers,
push,
@@ -59,6 +60,7 @@ class RootConfig:
saml2: saml2_config.SAML2Config
cas: cas.CasConfig
sso: sso.SSOConfig
+ oidc: oidc_config.OIDCConfig
jwt: jwt_config.JWTConfig
password: password.PasswordConfig
email: emailconfig.EmailConfig
diff --git a/synapse/config/cache.py b/synapse/config/cache.py
new file mode 100644
index 0000000000..0672538796
--- /dev/null
+++ b/synapse/config/cache.py
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import re
+from typing import Callable, Dict
+
+from ._base import Config, ConfigError
+
+# The prefix for all cache factor-related environment variables
+_CACHE_PREFIX = "SYNAPSE_CACHE_FACTOR"
+
+# Map from canonicalised cache name to cache.
+_CACHES = {}
+
+_DEFAULT_FACTOR_SIZE = 0.5
+_DEFAULT_EVENT_CACHE_SIZE = "10K"
+
+
+class CacheProperties(object):
+ def __init__(self):
+ # The default factor size for all caches
+ self.default_factor_size = float(
+ os.environ.get(_CACHE_PREFIX, _DEFAULT_FACTOR_SIZE)
+ )
+ self.resize_all_caches_func = None
+
+
+properties = CacheProperties()
+
+
+def _canonicalise_cache_name(cache_name: str) -> str:
+ """Gets the canonical form of the cache name.
+
+ Since we specify cache names in config and environment variables we need to
+ ignore case and special characters. For example, some caches have asterisks
+ in their name to denote that they're not attached to a particular database
+ function, and these asterisks need to be stripped out
+ """
+
+ cache_name = re.sub(r"[^A-Za-z_1-9]", "", cache_name)
+
+ return cache_name.lower()
+
+
+def add_resizable_cache(cache_name: str, cache_resize_callback: Callable):
+ """Register a cache that's size can dynamically change
+
+ Args:
+ cache_name: A reference to the cache
+ cache_resize_callback: A callback function that will be ran whenever
+ the cache needs to be resized
+ """
+ # Some caches have '*' in them which we strip out.
+ cache_name = _canonicalise_cache_name(cache_name)
+
+ _CACHES[cache_name] = cache_resize_callback
+
+ # Ensure all loaded caches are sized appropriately
+ #
+ # This method should only run once the config has been read,
+ # as it uses values read from it
+ if properties.resize_all_caches_func:
+ properties.resize_all_caches_func()
+
+
+class CacheConfig(Config):
+ section = "caches"
+ _environ = os.environ
+
+ @staticmethod
+ def reset():
+ """Resets the caches to their defaults. Used for tests."""
+ properties.default_factor_size = float(
+ os.environ.get(_CACHE_PREFIX, _DEFAULT_FACTOR_SIZE)
+ )
+ properties.resize_all_caches_func = None
+ _CACHES.clear()
+
+ def generate_config_section(self, **kwargs):
+ return """\
+ ## Caching ##
+
+ # Caching can be configured through the following options.
+ #
+ # A cache 'factor' is a multiplier that can be applied to each of
+ # Synapse's caches in order to increase or decrease the maximum
+ # number of entries that can be stored.
+
+ # The number of events to cache in memory. Not affected by
+ # caches.global_factor.
+ #
+ #event_cache_size: 10K
+
+ caches:
+ # Controls the global cache factor, which is the default cache factor
+ # for all caches if a specific factor for that cache is not otherwise
+ # set.
+ #
+ # This can also be set by the "SYNAPSE_CACHE_FACTOR" environment
+ # variable. Setting by environment variable takes priority over
+ # setting through the config file.
+ #
+ # Defaults to 0.5, which will half the size of all caches.
+ #
+ #global_factor: 1.0
+
+ # A dictionary of cache name to cache factor for that individual
+ # cache. Overrides the global cache factor for a given cache.
+ #
+ # These can also be set through environment variables comprised
+ # of "SYNAPSE_CACHE_FACTOR_" + the name of the cache in capital
+ # letters and underscores. Setting by environment variable
+ # takes priority over setting through the config file.
+ # Ex. SYNAPSE_CACHE_FACTOR_GET_USERS_WHO_SHARE_ROOM_WITH_USER=2.0
+ #
+ # Some caches have '*' and other characters that are not
+ # alphanumeric or underscores. These caches can be named with or
+ # without the special characters stripped. For example, to specify
+ # the cache factor for `*stateGroupCache*` via an environment
+ # variable would be `SYNAPSE_CACHE_FACTOR_STATEGROUPCACHE=2.0`.
+ #
+ per_cache_factors:
+ #get_users_who_share_room_with_user: 2.0
+ """
+
+ def read_config(self, config, **kwargs):
+ self.event_cache_size = self.parse_size(
+ config.get("event_cache_size", _DEFAULT_EVENT_CACHE_SIZE)
+ )
+ self.cache_factors = {} # type: Dict[str, float]
+
+ cache_config = config.get("caches") or {}
+ self.global_factor = cache_config.get(
+ "global_factor", properties.default_factor_size
+ )
+ if not isinstance(self.global_factor, (int, float)):
+ raise ConfigError("caches.global_factor must be a number.")
+
+ # Set the global one so that it's reflected in new caches
+ properties.default_factor_size = self.global_factor
+
+ # Load cache factors from the config
+ individual_factors = cache_config.get("per_cache_factors") or {}
+ if not isinstance(individual_factors, dict):
+ raise ConfigError("caches.per_cache_factors must be a dictionary")
+
+ # Canonicalise the cache names *before* updating with the environment
+ # variables.
+ individual_factors = {
+ _canonicalise_cache_name(key): val
+ for key, val in individual_factors.items()
+ }
+
+ # Override factors from environment if necessary
+ individual_factors.update(
+ {
+ _canonicalise_cache_name(key[len(_CACHE_PREFIX) + 1 :]): float(val)
+ for key, val in self._environ.items()
+ if key.startswith(_CACHE_PREFIX + "_")
+ }
+ )
+
+ for cache, factor in individual_factors.items():
+ if not isinstance(factor, (int, float)):
+ raise ConfigError(
+ "caches.per_cache_factors.%s must be a number" % (cache,)
+ )
+ self.cache_factors[cache] = factor
+
+ # Resize all caches (if necessary) with the new factors we've loaded
+ self.resize_all_caches()
+
+ # Store this function so that it can be called from other classes without
+ # needing an instance of Config
+ properties.resize_all_caches_func = self.resize_all_caches
+
+ def resize_all_caches(self):
+ """Ensure all cache sizes are up to date
+
+ For each cache, run the mapped callback function with either
+ a specific cache factor or the default, global one.
+ """
+ for cache_name, callback in _CACHES.items():
+ new_factor = self.cache_factors.get(cache_name, self.global_factor)
+ callback(new_factor)
diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py
index f0171bb5b2..82f04d7966 100644
--- a/synapse/config/captcha.py
+++ b/synapse/config/captcha.py
@@ -24,7 +24,6 @@ class CaptchaConfig(Config):
self.enable_registration_captcha = config.get(
"enable_registration_captcha", False
)
- self.captcha_bypass_secret = config.get("captcha_bypass_secret")
self.recaptcha_siteverify_api = config.get(
"recaptcha_siteverify_api",
"https://www.recaptcha.net/recaptcha/api/siteverify",
@@ -33,27 +32,26 @@ class CaptchaConfig(Config):
def generate_config_section(self, **kwargs):
return """\
## Captcha ##
- # See docs/CAPTCHA_SETUP for full details of configuring this.
+ # See docs/CAPTCHA_SETUP.md for full details of configuring this.
- # This homeserver's ReCAPTCHA public key.
+ # This homeserver's ReCAPTCHA public key. Must be specified if
+ # enable_registration_captcha is enabled.
#
#recaptcha_public_key: "YOUR_PUBLIC_KEY"
- # This homeserver's ReCAPTCHA private key.
+ # This homeserver's ReCAPTCHA private key. Must be specified if
+ # enable_registration_captcha is enabled.
#
#recaptcha_private_key: "YOUR_PRIVATE_KEY"
- # Enables ReCaptcha checks when registering, preventing signup
+ # Uncomment to enable ReCaptcha checks when registering, preventing signup
# unless a captcha is answered. Requires a valid ReCaptcha
- # public/private key.
+ # public/private key. Defaults to 'false'.
#
- #enable_registration_captcha: false
-
- # A secret key used to bypass the captcha test entirely.
- #
- #captcha_bypass_secret: "YOUR_SECRET_HERE"
+ #enable_registration_captcha: true
# The API endpoint to use for verifying m.login.recaptcha responses.
+ # Defaults to "https://www.recaptcha.net/recaptcha/api/siteverify".
#
- #recaptcha_siteverify_api: "https://www.recaptcha.net/recaptcha/api/siteverify"
+ #recaptcha_siteverify_api: "https://my.recaptcha.site"
"""
diff --git a/synapse/config/database.py b/synapse/config/database.py
index 219b32f670..1064c2697b 100644
--- a/synapse/config/database.py
+++ b/synapse/config/database.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
+# Copyright 2020 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,14 +15,61 @@
# limitations under the License.
import logging
import os
-from textwrap import indent
-
-import yaml
from synapse.config._base import Config, ConfigError
logger = logging.getLogger(__name__)
+NON_SQLITE_DATABASE_PATH_WARNING = """\
+Ignoring 'database_path' setting: not using a sqlite3 database.
+--------------------------------------------------------------------------------
+"""
+
+DEFAULT_CONFIG = """\
+## Database ##
+
+# The 'database' setting defines the database that synapse uses to store all of
+# its data.
+#
+# 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or
+# 'psycopg2' (for PostgreSQL).
+#
+# 'args' gives options which are passed through to the database engine,
+# except for options starting 'cp_', which are used to configure the Twisted
+# connection pool. For a reference to valid arguments, see:
+# * for sqlite: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
+# * for postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
+# * for the connection pool: https://twistedmatrix.com/documents/current/api/twisted.enterprise.adbapi.ConnectionPool.html#__init__
+#
+#
+# Example SQLite configuration:
+#
+#database:
+# name: sqlite3
+# args:
+# database: /path/to/homeserver.db
+#
+#
+# Example Postgres configuration:
+#
+#database:
+# name: psycopg2
+# args:
+# user: synapse
+# password: secretpassword
+# database: synapse
+# host: localhost
+# cp_min: 5
+# cp_max: 10
+#
+# For more information on using Synapse with Postgres, see `docs/postgres.md`.
+#
+database:
+ name: sqlite3
+ args:
+ database: %(database_path)s
+"""
+
class DatabaseConnectionConfig:
"""Contains the connection config for a particular database.
@@ -36,10 +84,12 @@ class DatabaseConnectionConfig:
"""
def __init__(self, name: str, db_config: dict):
- if db_config["name"] not in ("sqlite3", "psycopg2"):
- raise ConfigError("Unsupported database type %r" % (db_config["name"],))
+ db_engine = db_config.get("name", "sqlite3")
+
+ if db_engine not in ("sqlite3", "psycopg2"):
+ raise ConfigError("Unsupported database type %r" % (db_engine,))
- if db_config["name"] == "sqlite3":
+ if db_engine == "sqlite3":
db_config.setdefault("args", {}).update(
{"cp_min": 1, "cp_max": 1, "check_same_thread": False}
)
@@ -56,9 +106,12 @@ class DatabaseConnectionConfig:
class DatabaseConfig(Config):
section = "database"
- def read_config(self, config, **kwargs):
- self.event_cache_size = self.parse_size(config.get("event_cache_size", "10K"))
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.databases = []
+ def read_config(self, config, **kwargs):
# We *experimentally* support specifying multiple databases via the
# `databases` key. This is a map from a label to database config in the
# same format as the `database` config option, plus an extra
@@ -76,12 +129,13 @@ class DatabaseConfig(Config):
multi_database_config = config.get("databases")
database_config = config.get("database")
+ database_path = config.get("database_path")
if multi_database_config and database_config:
- raise ConfigError("Can't specify both 'database' and 'datbases' in config")
+ raise ConfigError("Can't specify both 'database' and 'databases' in config")
if multi_database_config:
- if config.get("database_path"):
+ if database_path:
raise ConfigError("Can't specify 'database_path' with 'databases'")
self.databases = [
@@ -89,65 +143,55 @@ class DatabaseConfig(Config):
for name, db_conf in multi_database_config.items()
]
- else:
- if database_config is None:
- database_config = {"name": "sqlite3", "args": {}}
-
+ if database_config:
self.databases = [DatabaseConnectionConfig("master", database_config)]
- self.set_databasepath(config.get("database_path"))
-
- def generate_config_section(self, data_dir_path, database_conf, **kwargs):
- if not database_conf:
- database_path = os.path.join(data_dir_path, "homeserver.db")
- database_conf = (
- """# The database engine name
- name: "sqlite3"
- # Arguments to pass to the engine
- args:
- # Path to the database
- database: "%(database_path)s"
- """
- % locals()
- )
- else:
- database_conf = indent(yaml.dump(database_conf), " " * 10).lstrip()
+ if database_path:
+ if self.databases and self.databases[0].name != "sqlite3":
+ logger.warning(NON_SQLITE_DATABASE_PATH_WARNING)
+ return
- return (
- """\
- ## Database ##
+ database_config = {"name": "sqlite3", "args": {}}
+ self.databases = [DatabaseConnectionConfig("master", database_config)]
+ self.set_databasepath(database_path)
- database:
- %(database_conf)s
- # Number of events to cache in memory.
- #
- #event_cache_size: 10K
- """
- % locals()
- )
+ def generate_config_section(self, data_dir_path, **kwargs):
+ return DEFAULT_CONFIG % {
+ "database_path": os.path.join(data_dir_path, "homeserver.db")
+ }
def read_arguments(self, args):
- self.set_databasepath(args.database_path)
+ """
+ Cases for the cli input:
+ - If no databases are configured and no database_path is set, raise.
+ - No databases and only database_path available ==> sqlite3 db.
+ - If there are multiple databases and a database_path raise an error.
+ - If the database set in the config file is sqlite then
+ overwrite with the command line argument.
+ """
- def set_databasepath(self, database_path):
- if database_path is None:
+ if args.database_path is None:
+ if not self.databases:
+ raise ConfigError("No database config provided")
return
- if database_path != ":memory:":
- database_path = self.abspath(database_path)
+ if len(self.databases) == 0:
+ database_config = {"name": "sqlite3", "args": {}}
+ self.databases = [DatabaseConnectionConfig("master", database_config)]
+ self.set_databasepath(args.database_path)
+ return
- # We only support setting a database path if we have a single sqlite3
- # database.
- if len(self.databases) != 1:
- raise ConfigError("Cannot specify 'database_path' with multiple databases")
+ if self.get_single_database().name == "sqlite3":
+ self.set_databasepath(args.database_path)
+ else:
+ logger.warning(NON_SQLITE_DATABASE_PATH_WARNING)
- database = self.get_single_database()
- if database.config["name"] != "sqlite3":
- # We don't raise here as we haven't done so before for this case.
- logger.warn("Ignoring 'database_path' for non-sqlite3 database")
- return
+ def set_databasepath(self, database_path):
+
+ if database_path != ":memory:":
+ database_path = self.abspath(database_path)
- database.config["args"]["database"] = database_path
+ self.databases[0].config["args"]["database"] = database_path
@staticmethod
def add_arguments(parser):
@@ -162,7 +206,7 @@ class DatabaseConfig(Config):
def get_single_database(self) -> DatabaseConnectionConfig:
"""Returns the database if there is only one, useful for e.g. tests
"""
- if len(self.databases) != 1:
+ if not self.databases:
raise Exception("More than one database exists")
return self.databases[0]
diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py
index f31fc85ec8..ca61214454 100644
--- a/synapse/config/emailconfig.py
+++ b/synapse/config/emailconfig.py
@@ -108,9 +108,14 @@ class EmailConfig(Config):
if self.trusted_third_party_id_servers:
# XXX: It's a little confusing that account_threepid_delegate_email is modified
# both in RegistrationConfig and here. We should factor this bit out
- self.account_threepid_delegate_email = self.trusted_third_party_id_servers[
- 0
- ] # type: Optional[str]
+
+ first_trusted_identity_server = self.trusted_third_party_id_servers[0]
+
+ # trusted_third_party_id_servers does not contain a scheme whereas
+ # account_threepid_delegate_email is expected to. Presume https
+ self.account_threepid_delegate_email = (
+ "https://" + first_trusted_identity_server
+ ) # type: Optional[str]
self.using_identity_server_from_trusted_list = True
else:
raise ConfigError(
@@ -306,8 +311,8 @@ class EmailConfig(Config):
# Username/password for authentication to the SMTP server. By default, no
# authentication is attempted.
#
- # smtp_user: "exampleusername"
- # smtp_pass: "examplepassword"
+ #smtp_user: "exampleusername"
+ #smtp_pass: "examplepassword"
# Uncomment the following to require TLS transport security for SMTP.
# By default, Synapse will connect over plain text, and will then switch to
diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py
index b4bca08b20..2c7b3a699f 100644
--- a/synapse/config/homeserver.py
+++ b/synapse/config/homeserver.py
@@ -17,6 +17,7 @@
from ._base import RootConfig
from .api import ApiConfig
from .appservice import AppServiceConfig
+from .cache import CacheConfig
from .captcha import CaptchaConfig
from .cas import CasConfig
from .consent_config import ConsentConfig
@@ -27,10 +28,12 @@ from .jwt_config import JWTConfig
from .key import KeyConfig
from .logger import LoggingConfig
from .metrics import MetricsConfig
+from .oidc_config import OIDCConfig
from .password import PasswordConfig
from .password_auth_providers import PasswordAuthProviderConfig
from .push import PushConfig
from .ratelimiting import RatelimitConfig
+from .redis import RedisConfig
from .registration import RegistrationConfig
from .repository import ContentRepositoryConfig
from .room_directory import RoomDirectoryConfig
@@ -53,6 +56,7 @@ class HomeServerConfig(RootConfig):
config_classes = [
ServerConfig,
TlsConfig,
+ CacheConfig,
DatabaseConfig,
LoggingConfig,
RatelimitConfig,
@@ -65,6 +69,7 @@ class HomeServerConfig(RootConfig):
AppServiceConfig,
KeyConfig,
SAML2Config,
+ OIDCConfig,
CasConfig,
SSOConfig,
JWTConfig,
@@ -82,4 +87,5 @@ class HomeServerConfig(RootConfig):
RoomDirectoryConfig,
ThirdPartyRulesConfig,
TracerConfig,
+ RedisConfig,
]
diff --git a/synapse/config/key.py b/synapse/config/key.py
index 066e7838c3..b529ea5da0 100644
--- a/synapse/config/key.py
+++ b/synapse/config/key.py
@@ -175,8 +175,8 @@ class KeyConfig(Config):
)
form_secret = 'form_secret: "%s"' % random_string_with_symbols(50)
else:
- macaroon_secret_key = "# macaroon_secret_key: <PRIVATE STRING>"
- form_secret = "# form_secret: <PRIVATE STRING>"
+ macaroon_secret_key = "#macaroon_secret_key: <PRIVATE STRING>"
+ form_secret = "#form_secret: <PRIVATE STRING>"
return (
"""\
diff --git a/synapse/config/logger.py b/synapse/config/logger.py
index a25c70e928..49f6c32beb 100644
--- a/synapse/config/logger.py
+++ b/synapse/config/logger.py
@@ -257,5 +257,6 @@ def setup_logging(
logging.warning("***** STARTING SERVER *****")
logging.warning("Server %s version %s", sys.argv[0], get_version_string(synapse))
logging.info("Server hostname: %s", config.server_name)
+ logging.info("Instance name: %s", hs.get_instance_name())
return logger
diff --git a/synapse/config/metrics.py b/synapse/config/metrics.py
index 22538153e1..6aad0d37c0 100644
--- a/synapse/config/metrics.py
+++ b/synapse/config/metrics.py
@@ -86,17 +86,18 @@ class MetricsConfig(Config):
# enabled by default, either for performance reasons or limited use.
#
metrics_flags:
- # Publish synapse_federation_known_servers, a g auge of the number of
+ # Publish synapse_federation_known_servers, a gauge of the number of
# servers this homeserver knows about, including itself. May cause
# performance problems on large homeservers.
#
#known_servers: true
# Whether or not to report anonymized homeserver usage statistics.
+ #
"""
if report_stats is None:
- res += "# report_stats: true|false\n"
+ res += "#report_stats: true|false\n"
else:
res += "report_stats: %s\n" % ("true" if report_stats else "false")
diff --git a/synapse/config/oidc_config.py b/synapse/config/oidc_config.py
new file mode 100644
index 0000000000..586038078f
--- /dev/null
+++ b/synapse/config/oidc_config.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 Quentin Gliech
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from synapse.python_dependencies import DependencyException, check_requirements
+from synapse.util.module_loader import load_module
+
+from ._base import Config, ConfigError
+
+DEFAULT_USER_MAPPING_PROVIDER = "synapse.handlers.oidc_handler.JinjaOidcMappingProvider"
+
+
+class OIDCConfig(Config):
+ section = "oidc"
+
+ def read_config(self, config, **kwargs):
+ self.oidc_enabled = False
+
+ oidc_config = config.get("oidc_config")
+
+ if not oidc_config or not oidc_config.get("enabled", False):
+ return
+
+ try:
+ check_requirements("oidc")
+ except DependencyException as e:
+ raise ConfigError(e.message)
+
+ public_baseurl = self.public_baseurl
+ if public_baseurl is None:
+ raise ConfigError("oidc_config requires a public_baseurl to be set")
+ self.oidc_callback_url = public_baseurl + "_synapse/oidc/callback"
+
+ self.oidc_enabled = True
+ self.oidc_discover = oidc_config.get("discover", True)
+ self.oidc_issuer = oidc_config["issuer"]
+ self.oidc_client_id = oidc_config["client_id"]
+ self.oidc_client_secret = oidc_config["client_secret"]
+ self.oidc_client_auth_method = oidc_config.get(
+ "client_auth_method", "client_secret_basic"
+ )
+ self.oidc_scopes = oidc_config.get("scopes", ["openid"])
+ self.oidc_authorization_endpoint = oidc_config.get("authorization_endpoint")
+ self.oidc_token_endpoint = oidc_config.get("token_endpoint")
+ self.oidc_userinfo_endpoint = oidc_config.get("userinfo_endpoint")
+ self.oidc_jwks_uri = oidc_config.get("jwks_uri")
+ self.oidc_subject_claim = oidc_config.get("subject_claim", "sub")
+ self.oidc_skip_verification = oidc_config.get("skip_verification", False)
+
+ ump_config = oidc_config.get("user_mapping_provider", {})
+ ump_config.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER)
+ ump_config.setdefault("config", {})
+
+ (
+ self.oidc_user_mapping_provider_class,
+ self.oidc_user_mapping_provider_config,
+ ) = load_module(ump_config)
+
+ # Ensure loaded user mapping module has defined all necessary methods
+ required_methods = [
+ "get_remote_user_id",
+ "map_user_attributes",
+ ]
+ missing_methods = [
+ method
+ for method in required_methods
+ if not hasattr(self.oidc_user_mapping_provider_class, method)
+ ]
+ if missing_methods:
+ raise ConfigError(
+ "Class specified by oidc_config."
+ "user_mapping_provider.module is missing required "
+ "methods: %s" % (", ".join(missing_methods),)
+ )
+
+ def generate_config_section(self, config_dir_path, server_name, **kwargs):
+ return """\
+ # Enable OpenID Connect for registration and login. Uses authlib.
+ #
+ oidc_config:
+ # enable OpenID Connect. Defaults to false.
+ #
+ #enabled: true
+
+ # use the OIDC discovery mechanism to discover endpoints. Defaults to true.
+ #
+ #discover: true
+
+ # the OIDC issuer. Used to validate tokens and discover the providers endpoints. Required.
+ #
+ #issuer: "https://accounts.example.com/"
+
+ # oauth2 client id to use. Required.
+ #
+ #client_id: "provided-by-your-issuer"
+
+ # oauth2 client secret to use. Required.
+ #
+ #client_secret: "provided-by-your-issuer"
+
+ # auth method to use when exchanging the token.
+ # Valid values are "client_secret_basic" (default), "client_secret_post" and "none".
+ #
+ #client_auth_method: "client_secret_basic"
+
+ # list of scopes to ask. This should include the "openid" scope. Defaults to ["openid"].
+ #
+ #scopes: ["openid"]
+
+ # the oauth2 authorization endpoint. Required if provider discovery is disabled.
+ #
+ #authorization_endpoint: "https://accounts.example.com/oauth2/auth"
+
+ # the oauth2 token endpoint. Required if provider discovery is disabled.
+ #
+ #token_endpoint: "https://accounts.example.com/oauth2/token"
+
+ # the OIDC userinfo endpoint. Required if discovery is disabled and the "openid" scope is not asked.
+ #
+ #userinfo_endpoint: "https://accounts.example.com/userinfo"
+
+ # URI where to fetch the JWKS. Required if discovery is disabled and the "openid" scope is used.
+ #
+ #jwks_uri: "https://accounts.example.com/.well-known/jwks.json"
+
+ # skip metadata verification. Defaults to false.
+ # Use this if you are connecting to a provider that is not OpenID Connect compliant.
+ # Avoid this in production.
+ #
+ #skip_verification: false
+
+
+ # An external module can be provided here as a custom solution to mapping
+ # attributes returned from a OIDC provider onto a matrix user.
+ #
+ user_mapping_provider:
+ # The custom module's class. Uncomment to use a custom module.
+ # Default is {mapping_provider!r}.
+ #
+ #module: mapping_provider.OidcMappingProvider
+
+ # Custom configuration values for the module. Below options are intended
+ # for the built-in provider, they should be changed if using a custom
+ # module. This section will be passed as a Python dictionary to the
+ # module's `parse_config` method.
+ #
+ # Below is the config of the default mapping provider, based on Jinja2
+ # templates. Those templates are used to render user attributes, where the
+ # userinfo object is available through the `user` variable.
+ #
+ config:
+ # name of the claim containing a unique identifier for the user.
+ # Defaults to `sub`, which OpenID Connect compliant providers should provide.
+ #
+ #subject_claim: "sub"
+
+ # Jinja2 template for the localpart of the MXID
+ #
+ localpart_template: "{{{{ user.preferred_username }}}}"
+
+ # Jinja2 template for the display name to set on first login. Optional.
+ #
+ #display_name_template: "{{{{ user.given_name }}}} {{{{ user.last_name }}}}"
+ """.format(
+ mapping_provider=DEFAULT_USER_MAPPING_PROVIDER
+ )
diff --git a/synapse/config/password.py b/synapse/config/password.py
index 2c13810ab8..6b2dae78b0 100644
--- a/synapse/config/password.py
+++ b/synapse/config/password.py
@@ -34,8 +34,8 @@ class PasswordConfig(Config):
self.password_pepper = password_config.get("pepper", "")
# Password policy
- self.password_policy = password_config.get("policy", {})
- self.password_policy_enabled = self.password_policy.pop("enabled", False)
+ self.password_policy = password_config.get("policy") or {}
+ self.password_policy_enabled = self.password_policy.get("enabled", False)
def generate_config_section(self, config_dir_path, server_name, **kwargs):
return """\
@@ -55,33 +55,38 @@ class PasswordConfig(Config):
#
#pepper: "EVEN_MORE_SECRET"
- # Define and enforce a password policy. Each parameter is optional, boolean
- # parameters default to 'false' and integer parameters default to 0.
- # This is an early implementation of MSC2000.
+ # Define and enforce a password policy. Each parameter is optional.
+ # This is an implementation of MSC2000.
#
- #policy:
+ policy:
# Whether to enforce the password policy.
+ # Defaults to 'false'.
#
#enabled: true
# Minimum accepted length for a password.
+ # Defaults to 0.
#
#minimum_length: 15
# Whether a password must contain at least one digit.
+ # Defaults to 'false'.
#
#require_digit: true
# Whether a password must contain at least one symbol.
# A symbol is any character that's not a number or a letter.
+ # Defaults to 'false'.
#
#require_symbol: true
# Whether a password must contain at least one lowercase letter.
+ # Defaults to 'false'.
#
#require_lowercase: true
# Whether a password must contain at least one lowercase letter.
+ # Defaults to 'false'.
#
#require_uppercase: true
"""
diff --git a/synapse/config/password_auth_providers.py b/synapse/config/password_auth_providers.py
index 9746bbc681..4fda8ae987 100644
--- a/synapse/config/password_auth_providers.py
+++ b/synapse/config/password_auth_providers.py
@@ -35,7 +35,7 @@ class PasswordAuthProviderConfig(Config):
if ldap_config.get("enabled", False):
providers.append({"module": LDAP_PROVIDER, "config": ldap_config})
- providers.extend(config.get("password_providers", []))
+ providers.extend(config.get("password_providers") or [])
for provider in providers:
mod_name = provider["module"]
@@ -52,7 +52,19 @@ class PasswordAuthProviderConfig(Config):
def generate_config_section(self, **kwargs):
return """\
- #password_providers:
+ # Password providers allow homeserver administrators to integrate
+ # their Synapse installation with existing authentication methods
+ # ex. LDAP, external tokens, etc.
+ #
+ # For more information and known implementations, please see
+ # https://github.com/matrix-org/synapse/blob/master/docs/password_auth_providers.md
+ #
+ # Note: instances wishing to use SAML or CAS authentication should
+ # instead use the `saml2_config` or `cas_config` options,
+ # respectively.
+ #
+ password_providers:
+ # # Example config for an LDAP auth provider
# - module: "ldap_auth_provider.LdapAuthProvider"
# config:
# enabled: true
diff --git a/synapse/config/redis.py b/synapse/config/redis.py
new file mode 100644
index 0000000000..d5d3ca1c9e
--- /dev/null
+++ b/synapse/config/redis.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 The Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from synapse.config._base import Config
+from synapse.python_dependencies import check_requirements
+
+
+class RedisConfig(Config):
+ section = "redis"
+
+ def read_config(self, config, **kwargs):
+ redis_config = config.get("redis", {})
+ self.redis_enabled = redis_config.get("enabled", False)
+
+ if not self.redis_enabled:
+ return
+
+ check_requirements("redis")
+
+ self.redis_host = redis_config.get("host", "localhost")
+ self.redis_port = redis_config.get("port", 6379)
+ self.redis_password = redis_config.get("password")
diff --git a/synapse/config/registration.py b/synapse/config/registration.py
index 687433e88a..ac71c09775 100644
--- a/synapse/config/registration.py
+++ b/synapse/config/registration.py
@@ -154,8 +154,9 @@ class RegistrationConfig(Config):
raise ConfigError("Invalid auto_join_rooms entry %s" % (room_alias,))
self.autocreate_auto_join_rooms = config.get("autocreate_auto_join_rooms", True)
- self.disable_set_displayname = config.get("disable_set_displayname", False)
- self.disable_set_avatar_url = config.get("disable_set_avatar_url", False)
+ self.enable_set_displayname = config.get("enable_set_displayname", True)
+ self.enable_set_avatar_url = config.get("enable_set_avatar_url", True)
+ self.enable_3pid_changes = config.get("enable_3pid_changes", True)
self.replicate_user_profiles_to = config.get("replicate_user_profiles_to", [])
if not isinstance(self.replicate_user_profiles_to, list):
@@ -181,9 +182,7 @@ class RegistrationConfig(Config):
random_string_with_symbols(50),
)
else:
- registration_shared_secret = (
- "# registration_shared_secret: <PRIVATE STRING>"
- )
+ registration_shared_secret = "#registration_shared_secret: <PRIVATE STRING>"
return (
"""\
@@ -419,6 +418,29 @@ class RegistrationConfig(Config):
#email: https://example.com # Delegate email sending to example.com
#msisdn: http://localhost:8090 # Delegate SMS sending to this local process
+ # Whether users are allowed to change their displayname after it has
+ # been initially set. Useful when provisioning users based on the
+ # contents of a third-party directory.
+ #
+ # Does not apply to server administrators. Defaults to 'true'
+ #
+ #enable_set_displayname: false
+
+ # Whether users are allowed to change their avatar after it has been
+ # initially set. Useful when provisioning users based on the contents
+ # of a third-party directory.
+ #
+ # Does not apply to server administrators. Defaults to 'true'
+ #
+ #enable_set_avatar_url: false
+
+ # Whether users can change the 3PIDs associated with their accounts
+ # (email address and msisdn).
+ #
+ # Defaults to 'true'
+ #
+ #enable_3pid_changes: false
+
# Users who register on this homeserver will automatically be joined
# to these rooms
#
diff --git a/synapse/config/repository.py b/synapse/config/repository.py
index 5ebc2ea1f1..944ea80e17 100644
--- a/synapse/config/repository.py
+++ b/synapse/config/repository.py
@@ -198,6 +198,10 @@ class ContentRepositoryConfig(Config):
self.url_preview_url_blacklist = config.get("url_preview_url_blacklist", ())
+ self.url_preview_accept_language = config.get(
+ "url_preview_accept_language"
+ ) or ["en"]
+
def generate_config_section(self, data_dir_path, **kwargs):
media_store = os.path.join(data_dir_path, "media_store")
uploads_path = os.path.join(data_dir_path, "uploads")
@@ -226,12 +230,11 @@ class ContentRepositoryConfig(Config):
#
#media_storage_providers:
# - module: file_system
- # # Whether to write new local files.
+ # # Whether to store newly uploaded local files
# store_local: false
- # # Whether to write new remote media
+ # # Whether to store newly downloaded remote files
# store_remote: false
- # # Whether to block upload requests waiting for write to this
- # # provider to complete
+ # # Whether to wait for successful storage for local uploads
# store_synchronous: false
# config:
# directory: /mnt/some/other/directory
@@ -359,6 +362,31 @@ class ContentRepositoryConfig(Config):
# The largest allowed URL preview spidering size in bytes
#
#max_spider_size: 10M
+
+ # A list of values for the Accept-Language HTTP header used when
+ # downloading webpages during URL preview generation. This allows
+ # Synapse to specify the preferred languages that URL previews should
+ # be in when communicating with remote servers.
+ #
+ # Each value is a IETF language tag; a 2-3 letter identifier for a
+ # language, optionally followed by subtags separated by '-', specifying
+ # a country or region variant.
+ #
+ # Multiple values can be provided, and a weight can be added to each by
+ # using quality value syntax (;q=). '*' translates to any language.
+ #
+ # Defaults to "en".
+ #
+ # Example:
+ #
+ # url_preview_accept_language:
+ # - en-UK
+ # - en-US;q=0.9
+ # - fr;q=0.8
+ # - *;q=0.7
+ #
+ url_preview_accept_language:
+ # - en
"""
% locals()
)
diff --git a/synapse/config/saml2_config.py b/synapse/config/saml2_config.py
index 8fe64d90f8..726a27d7b2 100644
--- a/synapse/config/saml2_config.py
+++ b/synapse/config/saml2_config.py
@@ -248,32 +248,32 @@ class SAML2Config(Config):
# remote:
# - url: https://our_idp/metadata.xml
#
- # # By default, the user has to go to our login page first. If you'd like
- # # to allow IdP-initiated login, set 'allow_unsolicited: true' in a
- # # 'service.sp' section:
- # #
- # #service:
- # # sp:
- # # allow_unsolicited: true
+ # # By default, the user has to go to our login page first. If you'd like
+ # # to allow IdP-initiated login, set 'allow_unsolicited: true' in a
+ # # 'service.sp' section:
+ # #
+ # #service:
+ # # sp:
+ # # allow_unsolicited: true
#
- # # The examples below are just used to generate our metadata xml, and you
- # # may well not need them, depending on your setup. Alternatively you
- # # may need a whole lot more detail - see the pysaml2 docs!
+ # # The examples below are just used to generate our metadata xml, and you
+ # # may well not need them, depending on your setup. Alternatively you
+ # # may need a whole lot more detail - see the pysaml2 docs!
#
- # description: ["My awesome SP", "en"]
- # name: ["Test SP", "en"]
+ # description: ["My awesome SP", "en"]
+ # name: ["Test SP", "en"]
#
- # organization:
- # name: Example com
- # display_name:
- # - ["Example co", "en"]
- # url: "http://example.com"
+ # organization:
+ # name: Example com
+ # display_name:
+ # - ["Example co", "en"]
+ # url: "http://example.com"
#
- # contact_person:
- # - given_name: Bob
- # sur_name: "the Sysadmin"
- # email_address": ["admin@example.com"]
- # contact_type": technical
+ # contact_person:
+ # - given_name: Bob
+ # sur_name: "the Sysadmin"
+ # email_address": ["admin@example.com"]
+ # contact_type": technical
# Instead of putting the config inline as above, you can specify a
# separate pysaml2 configuration file:
diff --git a/synapse/config/server.py b/synapse/config/server.py
index f5942c45c2..b96b68f685 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -440,7 +440,7 @@ class ServerConfig(Config):
)
self.limit_remote_rooms = LimitRemoteRoomsConfig(
- **config.get("limit_remote_rooms", {})
+ **(config.get("limit_remote_rooms") or {})
)
bind_port = config.get("bind_port")
@@ -511,10 +511,24 @@ class ServerConfig(Config):
"cleanup_extremities_with_dummy_events", True
)
+ # The number of forward extremities in a room needed to send a dummy event.
+ self.dummy_events_threshold = config.get("dummy_events_threshold", 10)
+
self.enable_ephemeral_messages = config.get("enable_ephemeral_messages", False)
+ # Inhibits the /requestToken endpoints from returning an error that might leak
+ # information about whether an e-mail address is in use or not on this
+ # homeserver, and instead return a 200 with a fake sid if this kind of error is
+ # met, without sending anything.
+ # This is a compromise between sending an email, which could be a spam vector,
+ # and letting the client know which email address is bound to an account and
+ # which one isn't.
+ self.request_token_inhibit_3pid_errors = config.get(
+ "request_token_inhibit_3pid_errors", False,
+ )
+
def has_tls_listener(self) -> bool:
- return any(l["tls"] for l in self.listeners)
+ return any(listener["tls"] for listener in self.listeners)
def generate_config_section(
self, server_name, data_dir_path, open_private_ports, listeners, **kwargs
@@ -610,10 +624,15 @@ class ServerConfig(Config):
#
pid_file: %(pid_file)s
- # The path to the web client which will be served at /_matrix/client/
- # if 'webclient' is configured under the 'listeners' configuration.
+ # The absolute URL to the web client which /_matrix/client will redirect
+ # to if 'webclient' is configured under the 'listeners' configuration.
+ #
+ # This option can be also set to the filesystem path to the web client
+ # which will be served at /_matrix/client/ if 'webclient' is configured
+ # under the 'listeners' configuration, however this is a security risk:
+ # https://github.com/matrix-org/synapse#security-note
#
- #web_client_location: "/path/to/web/root"
+ #web_client_location: https://riot.example.com/
# The public-facing base URL that clients use to access this HS
# (not including _matrix/...). This is the same URL a user would
@@ -813,6 +832,18 @@ class ServerConfig(Config):
# bind_addresses: ['::1', '127.0.0.1']
# type: manhole
+ # Forward extremities can build up in a room due to networking delays between
+ # homeservers. Once this happens in a large room, calculation of the state of
+ # that room can become quite expensive. To mitigate this, once the number of
+ # forward extremities reaches a given threshold, Synapse will send an
+ # org.matrix.dummy_event event, which will reduce the forward extremities
+ # in the room.
+ #
+ # This setting defines the threshold (i.e. number of forward extremities in the
+ # room) at which dummy events are sent. The default value is 10.
+ #
+ #dummy_events_threshold: 5
+
## Homeserver blocking ##
@@ -870,22 +901,27 @@ class ServerConfig(Config):
# Used by phonehome stats to group together related servers.
#server_context: context
- # Resource-constrained homeserver Settings
+ # Resource-constrained homeserver settings
#
- # If limit_remote_rooms.enabled is True, the room complexity will be
- # checked before a user joins a new remote room. If it is above
- # limit_remote_rooms.complexity, it will disallow joining or
- # instantly leave.
+ # When this is enabled, the room "complexity" will be checked before a user
+ # joins a new remote room. If it is above the complexity limit, the server will
+ # disallow joining, or will instantly leave.
#
- # limit_remote_rooms.complexity_error can be set to customise the text
- # displayed to the user when a room above the complexity threshold has
- # its join cancelled.
+ # Room complexity is an arbitrary measure based on factors such as the number of
+ # users in the room.
#
- # Uncomment the below lines to enable:
- #limit_remote_rooms:
- # enabled: true
- # complexity: 1.0
- # complexity_error: "This room is too complex."
+ limit_remote_rooms:
+ # Uncomment to enable room complexity checking.
+ #
+ #enabled: true
+
+ # the limit above which rooms cannot be joined. The default is 1.0.
+ #
+ #complexity: 0.5
+
+ # override the error which is returned when the room is too complex.
+ #
+ #complexity_error: "This room is too complex."
# Whether to require a user to be in the room to add an alias to it.
# Defaults to 'true'.
@@ -1041,6 +1077,16 @@ class ServerConfig(Config):
# - shortest_max_lifetime: 3d
# longest_max_lifetime: 1y
# interval: 1d
+
+ # Inhibits the /requestToken endpoints from returning an error that might leak
+ # information about whether an e-mail address is in use or not on this
+ # homeserver.
+ # Note that for some endpoints the error situation is the e-mail already being
+ # used, and for others the error is entering the e-mail being unused.
+ # If this option is enabled, instead of returning an error, these endpoints will
+ # act as if no error happened and return a fake session ID ('sid') to clients.
+ #
+ #request_token_inhibit_3pid_errors: true
"""
% locals()
)
diff --git a/synapse/config/server_notices_config.py b/synapse/config/server_notices_config.py
index 6ea2ea8869..6c427b6f92 100644
--- a/synapse/config/server_notices_config.py
+++ b/synapse/config/server_notices_config.py
@@ -51,7 +51,7 @@ class ServerNoticesConfig(Config):
None if server notices are not enabled.
server_notices_mxid_avatar_url (str|None):
- The display name to use for the server notices user.
+ The MXC URL for the avatar of the server notices user.
None if server notices are not enabled.
server_notices_room_name (str|None):
diff --git a/synapse/config/spam_checker.py b/synapse/config/spam_checker.py
index 36e0ddab5c..3d067d29db 100644
--- a/synapse/config/spam_checker.py
+++ b/synapse/config/spam_checker.py
@@ -13,6 +13,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from typing import Any, Dict, List, Tuple
+
+from synapse.config import ConfigError
from synapse.util.module_loader import load_module
from ._base import Config
@@ -22,16 +25,35 @@ class SpamCheckerConfig(Config):
section = "spamchecker"
def read_config(self, config, **kwargs):
- self.spam_checker = None
+ self.spam_checkers = [] # type: List[Tuple[Any, Dict]]
+
+ spam_checkers = config.get("spam_checker") or []
+ if isinstance(spam_checkers, dict):
+ # The spam_checker config option used to only support one
+ # spam checker, and thus was simply a dictionary with module
+ # and config keys. Support this old behaviour by checking
+ # to see if the option resolves to a dictionary
+ self.spam_checkers.append(load_module(spam_checkers))
+ elif isinstance(spam_checkers, list):
+ for spam_checker in spam_checkers:
+ if not isinstance(spam_checker, dict):
+ raise ConfigError("spam_checker syntax is incorrect")
- provider = config.get("spam_checker", None)
- if provider is not None:
- self.spam_checker = load_module(provider)
+ self.spam_checkers.append(load_module(spam_checker))
+ else:
+ raise ConfigError("spam_checker syntax is incorrect")
def generate_config_section(self, **kwargs):
return """\
- #spam_checker:
- # module: "my_custom_project.SuperSpamChecker"
- # config:
- # example_option: 'things'
+ # Spam checkers are third-party modules that can block specific actions
+ # of local users, such as creating rooms and registering undesirable
+ # usernames, as well as remote users by redacting incoming events.
+ #
+ spam_checker:
+ #- module: "my_custom_project.SuperSpamChecker"
+ # config:
+ # example_option: 'things'
+ #- module: "some_other_project.BadEventStopper"
+ # config:
+ # example_stop_events_from: ['@bad:example.com']
"""
diff --git a/synapse/config/sso.py b/synapse/config/sso.py
index 95762689bc..aff642f015 100644
--- a/synapse/config/sso.py
+++ b/synapse/config/sso.py
@@ -12,6 +12,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+import os
from typing import Any, Dict
import pkg_resources
@@ -35,10 +36,29 @@ class SSOConfig(Config):
if not template_dir:
template_dir = pkg_resources.resource_filename("synapse", "res/templates",)
- self.sso_redirect_confirm_template_dir = template_dir
+ self.sso_template_dir = template_dir
+ self.sso_account_deactivated_template = self.read_file(
+ os.path.join(self.sso_template_dir, "sso_account_deactivated.html"),
+ "sso_account_deactivated_template",
+ )
+ self.sso_auth_success_template = self.read_file(
+ os.path.join(self.sso_template_dir, "sso_auth_success.html"),
+ "sso_auth_success_template",
+ )
self.sso_client_whitelist = sso_config.get("client_whitelist") or []
+ # Attempt to also whitelist the server's login fallback, since that fallback sets
+ # the redirect URL to itself (so it can process the login token then return
+ # gracefully to the client). This would make it pointless to ask the user for
+ # confirmation, since the URL the confirmation page would be showing wouldn't be
+ # the client's.
+ # public_baseurl is an optional setting, so we only add the fallback's URL to the
+ # list if it's provided (because we can't figure out what that URL is otherwise).
+ if self.public_baseurl:
+ login_fallback_url = self.public_baseurl + "_matrix/static/client/login"
+ self.sso_client_whitelist.append(login_fallback_url)
+
def generate_config_section(self, **kwargs):
return """\
# Additional settings to use with single-sign on systems such as SAML2 and CAS.
@@ -54,6 +74,10 @@ class SSOConfig(Config):
# phishing attacks from evil.site. To avoid this, include a slash after the
# hostname: "https://my.client/".
#
+ # If public_baseurl is set, then the login fallback page (used by clients
+ # that don't natively support the required login flows) is whitelisted in
+ # addition to any URLs in this list.
+ #
# By default, this list is empty.
#
#client_whitelist:
@@ -85,6 +109,37 @@ class SSOConfig(Config):
#
# * server_name: the homeserver's name.
#
+ # * HTML page which notifies the user that they are authenticating to confirm
+ # an operation on their account during the user interactive authentication
+ # process: 'sso_auth_confirm.html'.
+ #
+ # When rendering, this template is given the following variables:
+ # * redirect_url: the URL the user is about to be redirected to. Needs
+ # manual escaping (see
+ # https://jinja.palletsprojects.com/en/2.11.x/templates/#html-escaping).
+ #
+ # * description: the operation which the user is being asked to confirm
+ #
+ # * HTML page shown after a successful user interactive authentication session:
+ # 'sso_auth_success.html'.
+ #
+ # Note that this page must include the JavaScript which notifies of a successful authentication
+ # (see https://matrix.org/docs/spec/client_server/r0.6.0#fallback).
+ #
+ # This template has no additional variables.
+ #
+ # * HTML page shown during single sign-on if a deactivated user (according to Synapse's database)
+ # attempts to login: 'sso_account_deactivated.html'.
+ #
+ # This template has no additional variables.
+ #
+ # * HTML page to display to users if something goes wrong during the
+ # OpenID Connect authentication process: 'sso_error.html'.
+ #
+ # When rendering, this template is given two variables:
+ # * error: the technical name of the error
+ # * error_description: a human-readable message for the error
+ #
# You can see the default templates at:
# https://github.com/matrix-org/synapse/tree/master/synapse/res/templates
#
diff --git a/synapse/config/workers.py b/synapse/config/workers.py
index fef72ed974..ed06b91a54 100644
--- a/synapse/config/workers.py
+++ b/synapse/config/workers.py
@@ -13,7 +13,29 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from ._base import Config
+import attr
+
+from ._base import Config, ConfigError
+
+
+@attr.s
+class InstanceLocationConfig:
+ """The host and port to talk to an instance via HTTP replication.
+ """
+
+ host = attr.ib(type=str)
+ port = attr.ib(type=int)
+
+
+@attr.s
+class WriterLocations:
+ """Specifies the instances that write various streams.
+
+ Attributes:
+ events: The instance that writes to the event and backfill streams.
+ """
+
+ events = attr.ib(default="master", type=str)
class WorkerConfig(Config):
@@ -71,6 +93,27 @@ class WorkerConfig(Config):
elif not bind_addresses:
bind_addresses.append("")
+ # A map from instance name to host/port of their HTTP replication endpoint.
+ instance_map = config.get("instance_map") or {}
+ self.instance_map = {
+ name: InstanceLocationConfig(**c) for name, c in instance_map.items()
+ }
+
+ # Map from type of streams to source, c.f. WriterLocations.
+ writers = config.get("stream_writers") or {}
+ self.writers = WriterLocations(**writers)
+
+ # Check that the configured writer for events also appears in
+ # `instance_map`.
+ if (
+ self.writers.events != "master"
+ and self.writers.events not in self.instance_map
+ ):
+ raise ConfigError(
+ "Instance %r is configured to write events but does not appear in `instance_map` config."
+ % (self.writers.events,)
+ )
+
def read_arguments(self, args):
# We support a bunch of command line arguments that override options in
# the config. A lot of these options have a worker_* prefix when running
|