diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index 94a25c7ee8..3beaeb8869 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -20,14 +20,15 @@
#
import enum
-from typing import TYPE_CHECKING, Any, Optional
+from functools import cache
+from typing import TYPE_CHECKING, Any, Iterable, Optional
import attr
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.config._base import Config, RootConfig, read_file
from synapse.types import JsonDict
# Determine whether authlib is installed.
@@ -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: Iterable[str]) -> 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,15 +208,48 @@ 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.
"""
+ _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) -> None:
"""Checks for any configuration conflicts with other parts of Synapse.
|