diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index bae9cc8047..881aafc3f0 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -20,6 +20,7 @@
#
import enum
+from functools import cache
from typing import TYPE_CHECKING, Any, Optional
import attr
@@ -27,8 +28,8 @@ import attr.validators
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
from synapse.config import ConfigError
-from synapse.config._base import Config, RootConfig
-from synapse.types import JsonDict
+from synapse.config._base import Config, RootConfig, read_file
+from synapse.types import JsonDict, StrSequence
# Determine whether authlib is installed.
try:
@@ -43,6 +44,12 @@ if TYPE_CHECKING:
from authlib.jose.rfc7517 import JsonWebKey
+@cache
+def read_secret_from_file_once(file_path: Any, config_path: StrSequence) -> str:
+ """Returns the memoized secret read from file."""
+ return read_file(file_path, config_path).strip()
+
+
class ClientAuthMethod(enum.Enum):
"""List of supported client auth methods."""
@@ -63,6 +70,40 @@ def _parse_jwks(jwks: Optional[JsonDict]) -> Optional["JsonWebKey"]:
return JsonWebKey.import_key(jwks)
+def _check_client_secret(
+ instance: "MSC3861", _attribute: attr.Attribute, _value: Optional[str]
+) -> None:
+ if instance._client_secret and instance._client_secret_path:
+ raise ConfigError(
+ (
+ "You have configured both "
+ "`experimental_features.msc3861.client_secret` and "
+ "`experimental_features.msc3861.client_secret_path`. "
+ "These are mutually incompatible."
+ ),
+ ("experimental", "msc3861", "client_secret"),
+ )
+ # Check client secret can be retrieved
+ instance.client_secret()
+
+
+def _check_admin_token(
+ instance: "MSC3861", _attribute: attr.Attribute, _value: Optional[str]
+) -> None:
+ if instance._admin_token and instance._admin_token_path:
+ raise ConfigError(
+ (
+ "You have configured both "
+ "`experimental_features.msc3861.admin_token` and "
+ "`experimental_features.msc3861.admin_token_path`. "
+ "These are mutually incompatible."
+ ),
+ ("experimental", "msc3861", "admin_token"),
+ )
+ # Check client secret can be retrieved
+ instance.admin_token()
+
+
@attr.s(slots=True, frozen=True)
class MSC3861:
"""Configuration for MSC3861: Matrix architecture change to delegate authentication via OIDC"""
@@ -97,15 +138,30 @@ class MSC3861:
)
"""The auth method used when calling the introspection endpoint."""
- client_secret: Optional[str] = attr.ib(
+ _client_secret: Optional[str] = attr.ib(
default=None,
- validator=attr.validators.optional(attr.validators.instance_of(str)),
+ validator=[
+ attr.validators.optional(attr.validators.instance_of(str)),
+ _check_client_secret,
+ ],
)
"""
The client secret to use when calling the introspection endpoint,
when using any of the client_secret_* client auth methods.
"""
+ _client_secret_path: Optional[str] = attr.ib(
+ default=None,
+ validator=[
+ attr.validators.optional(attr.validators.instance_of(str)),
+ _check_client_secret,
+ ],
+ )
+ """
+ Alternative to `client_secret`: allows the secret to be specified in an
+ external file.
+ """
+
jwk: Optional["JsonWebKey"] = attr.ib(default=None, converter=_parse_jwks)
"""
The JWKS to use when calling the introspection endpoint,
@@ -133,7 +189,7 @@ class MSC3861:
ClientAuthMethod.CLIENT_SECRET_BASIC,
ClientAuthMethod.CLIENT_SECRET_JWT,
)
- and self.client_secret is None
+ and self.client_secret() is None
):
raise ConfigError(
f"A client secret must be provided when using the {value} client auth method",
@@ -152,16 +208,51 @@ class MSC3861:
)
"""The URL of the My Account page on the OIDC Provider as per MSC2965."""
- admin_token: Optional[str] = attr.ib(
+ _admin_token: Optional[str] = attr.ib(
default=None,
- validator=attr.validators.optional(attr.validators.instance_of(str)),
+ validator=[
+ attr.validators.optional(attr.validators.instance_of(str)),
+ _check_admin_token,
+ ],
)
"""
A token that should be considered as an admin token.
This is used by the OIDC provider, to make admin calls to Synapse.
"""
- def check_config_conflicts(self, root: RootConfig) -> None:
+ _admin_token_path: Optional[str] = attr.ib(
+ default=None,
+ validator=[
+ attr.validators.optional(attr.validators.instance_of(str)),
+ _check_admin_token,
+ ],
+ )
+ """
+ Alternative to `admin_token`: allows the secret to be specified in an
+ external file.
+ """
+
+ def client_secret(self) -> Optional[str]:
+ """Returns the secret given via `client_secret` or `client_secret_path`."""
+ if self._client_secret_path:
+ return read_secret_from_file_once(
+ self._client_secret_path,
+ ("experimental_features", "msc3861", "client_secret_path"),
+ )
+ return self._client_secret
+
+ def admin_token(self) -> Optional[str]:
+ """Returns the admin token given via `admin_token` or `admin_token_path`."""
+ if self._admin_token_path:
+ return read_secret_from_file_once(
+ self._admin_token_path,
+ ("experimental_features", "msc3861", "admin_token_path"),
+ )
+ return self._admin_token
+
+ def check_config_conflicts(
+ self, root: RootConfig, allow_secrets_in_config: bool
+ ) -> None:
"""Checks for any configuration conflicts with other parts of Synapse.
Raises:
@@ -171,6 +262,24 @@ class MSC3861:
if not self.enabled:
return
+ if self._client_secret and not allow_secrets_in_config:
+ raise ConfigError(
+ "Config options that expect an in-line secret as value are disabled",
+ ("experimental", "msc3861", "client_secret"),
+ )
+
+ if self.jwk and not allow_secrets_in_config:
+ raise ConfigError(
+ "Config options that expect an in-line secret as value are disabled",
+ ("experimental", "msc3861", "jwk"),
+ )
+
+ if self._admin_token and not allow_secrets_in_config:
+ raise ConfigError(
+ "Config options that expect an in-line secret as value are disabled",
+ ("experimental", "msc3861", "admin_token"),
+ )
+
if (
root.auth.password_enabled_for_reauth
or root.auth.password_enabled_for_login
@@ -195,8 +304,6 @@ class MSC3861:
if (
root.oidc.oidc_enabled
- or root.saml2.saml2_enabled
- or root.cas.cas_enabled
or root.jwt.jwt_enabled
):
raise ConfigError("SSO cannot be enabled when OAuth delegation is enabled")
@@ -236,12 +343,6 @@ class MSC3861:
("session_lifetime",),
)
- if root.registration.enable_3pid_changes:
- raise ConfigError(
- "enable_3pid_changes cannot be enabled when OAuth delegation is enabled",
- ("enable_3pid_changes",),
- )
-
@attr.s(auto_attribs=True, frozen=True, slots=True)
class MSC3866Config:
@@ -261,7 +362,9 @@ class ExperimentalConfig(Config):
section = "experimental"
- def read_config(self, config: JsonDict, **kwargs: Any) -> None:
+ def read_config(
+ self, config: JsonDict, allow_secrets_in_config: bool, **kwargs: Any
+ ) -> None:
experimental = config.get("experimental_features") or {}
# MSC3026 (busy presence state)
@@ -288,9 +391,6 @@ class ExperimentalConfig(Config):
),
)
- # MSC3244 (room version capabilities)
- self.msc3244_enabled: bool = experimental.get("msc3244_enabled", True)
-
# MSC3266 (room summary api)
self.msc3266_enabled: bool = experimental.get("msc3266_enabled", False)
@@ -338,8 +438,10 @@ class ExperimentalConfig(Config):
# MSC3391: Removing account data.
self.msc3391_enabled = experimental.get("msc3391_enabled", False)
- # MSC3575 (Sliding Sync API endpoints)
- self.msc3575_enabled: bool = experimental.get("msc3575_enabled", False)
+ # MSC3575 (Sliding Sync) alternate endpoints, c.f. MSC4186.
+ #
+ # This is enabled by default as a replacement for the sliding sync proxy.
+ self.msc3575_enabled: bool = experimental.get("msc3575_enabled", True)
# MSC3773: Thread notifications
self.msc3773_enabled: bool = experimental.get("msc3773_enabled", False)
@@ -363,11 +465,6 @@ class ExperimentalConfig(Config):
# MSC3874: Filtering /messages with rel_types / not_rel_types.
self.msc3874_enabled: bool = experimental.get("msc3874_enabled", False)
- # MSC3886: Simple client rendezvous capability
- self.msc3886_endpoint: Optional[str] = experimental.get(
- "msc3886_endpoint", None
- )
-
# MSC3890: Remotely silence local notifications
# Note: This option requires "experimental_features.msc3391_enabled" to be
# set to "true", in order to communicate account data deletions to clients.
@@ -408,7 +505,9 @@ class ExperimentalConfig(Config):
) from exc
# Check that none of the other config options conflict with MSC3861 when enabled
- self.msc3861.check_config_conflicts(self.root)
+ self.msc3861.check_config_conflicts(
+ self.root, allow_secrets_in_config=allow_secrets_in_config
+ )
self.msc4028_push_encrypted_events = experimental.get(
"msc4028_push_encrypted_events", False
@@ -439,12 +538,23 @@ class ExperimentalConfig(Config):
("experimental", "msc4108_delegation_endpoint"),
)
- self.msc3823_account_suspension = experimental.get(
- "msc3823_account_suspension", False
- )
+ # MSC4133: Custom profile fields
+ self.msc4133_enabled: bool = experimental.get("msc4133_enabled", False)
+
+ # MSC4210: Remove legacy mentions
+ self.msc4210_enabled: bool = experimental.get("msc4210_enabled", False)
+
+ # MSC4222: Adding `state_after` to sync v2
+ self.msc4222_enabled: bool = experimental.get("msc4222_enabled", False)
- # MSC4151: Report room API (Client-Server API)
- self.msc4151_enabled: bool = experimental.get("msc4151_enabled", False)
+ # MSC4076: Add `disable_badge_count`` to pusher configuration
+ self.msc4076_enabled: bool = experimental.get("msc4076_enabled", False)
+
+ # MSC4263: Preventing MXID enumeration via key queries
+ self.msc4263_limit_key_queries_to_users_who_share_rooms = experimental.get(
+ "msc4263_limit_key_queries_to_users_who_share_rooms",
+ False,
+ )
- # MSC4156: Migrate server_name to via
- self.msc4156_enabled: bool = experimental.get("msc4156_enabled", False)
+ # MSC4155: Invite filtering
+ self.msc4155_enabled: bool = experimental.get("msc4155_enabled", False)
|