mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-06-07 19:12:40 -04:00
Add a module type for account validity (#9884)
This adds an API for third-party plugin modules to implement account validity, so they can provide this feature instead of Synapse. The module implementing the current behaviour for this feature can be found at https://github.com/matrix-org/synapse-email-account-validity. To allow for a smooth transition between the current feature and the new module, hooks have been added to the existing account validity endpoints to allow their behaviours to be overridden by a module.
This commit is contained in:
parent
d427f64724
commit
36dc15412d
13 changed files with 438 additions and 228 deletions
1
changelog.d/9884.feature
Normal file
1
changelog.d/9884.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add a module type for the account validity feature.
|
|
@ -63,7 +63,7 @@ Modules can register web resources onto Synapse's web server using the following
|
||||||
API method:
|
API method:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def ModuleApi.register_web_resource(path: str, resource: IResource)
|
def ModuleApi.register_web_resource(path: str, resource: IResource) -> None
|
||||||
```
|
```
|
||||||
|
|
||||||
The path is the full absolute path to register the resource at. For example, if you
|
The path is the full absolute path to register the resource at. For example, if you
|
||||||
|
@ -91,12 +91,17 @@ are split in categories. A single module may implement callbacks from multiple c
|
||||||
and is under no obligation to implement all callbacks from the categories it registers
|
and is under no obligation to implement all callbacks from the categories it registers
|
||||||
callbacks for.
|
callbacks for.
|
||||||
|
|
||||||
|
Modules can register callbacks using one of the module API's `register_[...]_callbacks`
|
||||||
|
methods. The callback functions are passed to these methods as keyword arguments, with
|
||||||
|
the callback name as the argument name and the function as its value. This is demonstrated
|
||||||
|
in the example below. A `register_[...]_callbacks` method exists for each module type
|
||||||
|
documented in this section.
|
||||||
|
|
||||||
#### Spam checker callbacks
|
#### Spam checker callbacks
|
||||||
|
|
||||||
To register one of the callbacks described in this section, a module needs to use the
|
Spam checker callbacks allow module developers to implement spam mitigation actions for
|
||||||
module API's `register_spam_checker_callbacks` method. The callback functions are passed
|
Synapse instances. Spam checker callbacks can be registered using the module API's
|
||||||
to `register_spam_checker_callbacks` as keyword arguments, with the callback name as the
|
`register_spam_checker_callbacks` method.
|
||||||
argument name and the function as its value. This is demonstrated in the example below.
|
|
||||||
|
|
||||||
The available spam checker callbacks are:
|
The available spam checker callbacks are:
|
||||||
|
|
||||||
|
@ -115,7 +120,7 @@ async def user_may_invite(inviter: str, invitee: str, room_id: str) -> bool
|
||||||
|
|
||||||
Called when processing an invitation. The module must return a `bool` indicating whether
|
Called when processing an invitation. The module must return a `bool` indicating whether
|
||||||
the inviter can invite the invitee to the given room. Both inviter and invitee are
|
the inviter can invite the invitee to the given room. Both inviter and invitee are
|
||||||
represented by their Matrix user ID (i.e. `@alice:example.com`).
|
represented by their Matrix user ID (e.g. `@alice:example.com`).
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def user_may_create_room(user: str) -> bool
|
async def user_may_create_room(user: str) -> bool
|
||||||
|
@ -188,6 +193,36 @@ async def check_media_file_for_spam(
|
||||||
Called when storing a local or remote file. The module must return a boolean indicating
|
Called when storing a local or remote file. The module must return a boolean indicating
|
||||||
whether the given file can be stored in the homeserver's media store.
|
whether the given file can be stored in the homeserver's media store.
|
||||||
|
|
||||||
|
#### Account validity callbacks
|
||||||
|
|
||||||
|
Account validity callbacks allow module developers to add extra steps to verify the
|
||||||
|
validity on an account, i.e. see if a user can be granted access to their account on the
|
||||||
|
Synapse instance. Account validity callbacks can be registered using the module API's
|
||||||
|
`register_account_validity_callbacks` method.
|
||||||
|
|
||||||
|
The available account validity callbacks are:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def is_user_expired(user: str) -> Optional[bool]
|
||||||
|
```
|
||||||
|
|
||||||
|
Called when processing any authenticated request (except for logout requests). The module
|
||||||
|
can return a `bool` to indicate whether the user has expired and should be locked out of
|
||||||
|
their account, or `None` if the module wasn't able to figure it out. The user is
|
||||||
|
represented by their Matrix user ID (e.g. `@alice:example.com`).
|
||||||
|
|
||||||
|
If the module returns `True`, the current request will be denied with the error code
|
||||||
|
`ORG_MATRIX_EXPIRED_ACCOUNT` and the HTTP status code 403. Note that this doesn't
|
||||||
|
invalidate the user's access token.
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def on_user_registration(user: str) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
Called after successfully registering a user, in case the module needs to perform extra
|
||||||
|
operations to keep track of them. (e.g. add them to a database table). The user is
|
||||||
|
represented by their Matrix user ID.
|
||||||
|
|
||||||
### Porting an existing module that uses the old interface
|
### Porting an existing module that uses the old interface
|
||||||
|
|
||||||
In order to port a module that uses Synapse's old module interface, its author needs to:
|
In order to port a module that uses Synapse's old module interface, its author needs to:
|
||||||
|
|
|
@ -1310,91 +1310,6 @@ account_threepid_delegates:
|
||||||
#auto_join_rooms_for_guests: false
|
#auto_join_rooms_for_guests: false
|
||||||
|
|
||||||
|
|
||||||
## Account Validity ##
|
|
||||||
|
|
||||||
# Optional account validity configuration. This allows for accounts to be denied
|
|
||||||
# any request after a given period.
|
|
||||||
#
|
|
||||||
# Once this feature is enabled, Synapse will look for registered users without an
|
|
||||||
# expiration date at startup and will add one to every account it found using the
|
|
||||||
# current settings at that time.
|
|
||||||
# This means that, if a validity period is set, and Synapse is restarted (it will
|
|
||||||
# then derive an expiration date from the current validity period), and some time
|
|
||||||
# after that the validity period changes and Synapse is restarted, the users'
|
|
||||||
# expiration dates won't be updated unless their account is manually renewed. This
|
|
||||||
# date will be randomly selected within a range [now + period - d ; now + period],
|
|
||||||
# where d is equal to 10% of the validity period.
|
|
||||||
#
|
|
||||||
account_validity:
|
|
||||||
# The account validity feature is disabled by default. Uncomment the
|
|
||||||
# following line to enable it.
|
|
||||||
#
|
|
||||||
#enabled: true
|
|
||||||
|
|
||||||
# The period after which an account is valid after its registration. When
|
|
||||||
# renewing the account, its validity period will be extended by this amount
|
|
||||||
# of time. This parameter is required when using the account validity
|
|
||||||
# feature.
|
|
||||||
#
|
|
||||||
#period: 6w
|
|
||||||
|
|
||||||
# The amount of time before an account's expiry date at which Synapse will
|
|
||||||
# send an email to the account's email address with a renewal link. By
|
|
||||||
# default, no such emails are sent.
|
|
||||||
#
|
|
||||||
# If you enable this setting, you will also need to fill out the 'email' and
|
|
||||||
# 'public_baseurl' configuration sections.
|
|
||||||
#
|
|
||||||
#renew_at: 1w
|
|
||||||
|
|
||||||
# The subject of the email sent out with the renewal link. '%(app)s' can be
|
|
||||||
# used as a placeholder for the 'app_name' parameter from the 'email'
|
|
||||||
# section.
|
|
||||||
#
|
|
||||||
# Note that the placeholder must be written '%(app)s', including the
|
|
||||||
# trailing 's'.
|
|
||||||
#
|
|
||||||
# If this is not set, a default value is used.
|
|
||||||
#
|
|
||||||
#renew_email_subject: "Renew your %(app)s account"
|
|
||||||
|
|
||||||
# Directory in which Synapse will try to find templates for the HTML files to
|
|
||||||
# serve to the user when trying to renew an account. If not set, default
|
|
||||||
# templates from within the Synapse package will be used.
|
|
||||||
#
|
|
||||||
# The currently available templates are:
|
|
||||||
#
|
|
||||||
# * account_renewed.html: Displayed to the user after they have successfully
|
|
||||||
# renewed their account.
|
|
||||||
#
|
|
||||||
# * account_previously_renewed.html: Displayed to the user if they attempt to
|
|
||||||
# renew their account with a token that is valid, but that has already
|
|
||||||
# been used. In this case the account is not renewed again.
|
|
||||||
#
|
|
||||||
# * invalid_token.html: Displayed to the user when they try to renew an account
|
|
||||||
# with an unknown or invalid renewal token.
|
|
||||||
#
|
|
||||||
# See https://github.com/matrix-org/synapse/tree/master/synapse/res/templates for
|
|
||||||
# default template contents.
|
|
||||||
#
|
|
||||||
# The file name of some of these templates can be configured below for legacy
|
|
||||||
# reasons.
|
|
||||||
#
|
|
||||||
#template_dir: "res/templates"
|
|
||||||
|
|
||||||
# A custom file name for the 'account_renewed.html' template.
|
|
||||||
#
|
|
||||||
# If not set, the file is assumed to be named "account_renewed.html".
|
|
||||||
#
|
|
||||||
#account_renewed_html_path: "account_renewed.html"
|
|
||||||
|
|
||||||
# A custom file name for the 'invalid_token.html' template.
|
|
||||||
#
|
|
||||||
# If not set, the file is assumed to be named "invalid_token.html".
|
|
||||||
#
|
|
||||||
#invalid_token_html_path: "invalid_token.html"
|
|
||||||
|
|
||||||
|
|
||||||
## Metrics ###
|
## Metrics ###
|
||||||
|
|
||||||
# Enable collection and rendering of performance metrics
|
# Enable collection and rendering of performance metrics
|
||||||
|
|
|
@ -62,6 +62,7 @@ class Auth:
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.state = hs.get_state_handler()
|
self.state = hs.get_state_handler()
|
||||||
|
self._account_validity_handler = hs.get_account_validity_handler()
|
||||||
|
|
||||||
self.token_cache: LruCache[str, Tuple[str, bool]] = LruCache(
|
self.token_cache: LruCache[str, Tuple[str, bool]] = LruCache(
|
||||||
10000, "token_cache"
|
10000, "token_cache"
|
||||||
|
@ -69,9 +70,6 @@ class Auth:
|
||||||
|
|
||||||
self._auth_blocking = AuthBlocking(self.hs)
|
self._auth_blocking = AuthBlocking(self.hs)
|
||||||
|
|
||||||
self._account_validity_enabled = (
|
|
||||||
hs.config.account_validity.account_validity_enabled
|
|
||||||
)
|
|
||||||
self._track_appservice_user_ips = hs.config.track_appservice_user_ips
|
self._track_appservice_user_ips = hs.config.track_appservice_user_ips
|
||||||
self._macaroon_secret_key = hs.config.macaroon_secret_key
|
self._macaroon_secret_key = hs.config.macaroon_secret_key
|
||||||
self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users
|
self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users
|
||||||
|
@ -187,12 +185,17 @@ class Auth:
|
||||||
shadow_banned = user_info.shadow_banned
|
shadow_banned = user_info.shadow_banned
|
||||||
|
|
||||||
# Deny the request if the user account has expired.
|
# Deny the request if the user account has expired.
|
||||||
if self._account_validity_enabled and not allow_expired:
|
if not allow_expired:
|
||||||
if await self.store.is_account_expired(
|
if await self._account_validity_handler.is_user_expired(
|
||||||
user_info.user_id, self.clock.time_msec()
|
user_info.user_id
|
||||||
):
|
):
|
||||||
|
# Raise the error if either an account validity module has determined
|
||||||
|
# the account has expired, or the legacy account validity
|
||||||
|
# implementation is enabled and determined the account has expired
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403, "User account has expired", errcode=Codes.EXPIRED_ACCOUNT
|
403,
|
||||||
|
"User account has expired",
|
||||||
|
errcode=Codes.EXPIRED_ACCOUNT,
|
||||||
)
|
)
|
||||||
|
|
||||||
device_id = user_info.device_id
|
device_id = user_info.device_id
|
||||||
|
|
|
@ -18,6 +18,21 @@ class AccountValidityConfig(Config):
|
||||||
section = "account_validity"
|
section = "account_validity"
|
||||||
|
|
||||||
def read_config(self, config, **kwargs):
|
def read_config(self, config, **kwargs):
|
||||||
|
"""Parses the old account validity config. The config format looks like this:
|
||||||
|
|
||||||
|
account_validity:
|
||||||
|
enabled: true
|
||||||
|
period: 6w
|
||||||
|
renew_at: 1w
|
||||||
|
renew_email_subject: "Renew your %(app)s account"
|
||||||
|
template_dir: "res/templates"
|
||||||
|
account_renewed_html_path: "account_renewed.html"
|
||||||
|
invalid_token_html_path: "invalid_token.html"
|
||||||
|
|
||||||
|
We expect admins to use modules for this feature (which is why it doesn't appear
|
||||||
|
in the sample config file), but we want to keep support for it around for a bit
|
||||||
|
for backwards compatibility.
|
||||||
|
"""
|
||||||
account_validity_config = config.get("account_validity") or {}
|
account_validity_config = config.get("account_validity") or {}
|
||||||
self.account_validity_enabled = account_validity_config.get("enabled", False)
|
self.account_validity_enabled = account_validity_config.get("enabled", False)
|
||||||
self.account_validity_renew_by_email_enabled = (
|
self.account_validity_renew_by_email_enabled = (
|
||||||
|
@ -75,90 +90,3 @@ class AccountValidityConfig(Config):
|
||||||
],
|
],
|
||||||
account_validity_template_dir,
|
account_validity_template_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_config_section(self, **kwargs):
|
|
||||||
return """\
|
|
||||||
## Account Validity ##
|
|
||||||
|
|
||||||
# Optional account validity configuration. This allows for accounts to be denied
|
|
||||||
# any request after a given period.
|
|
||||||
#
|
|
||||||
# Once this feature is enabled, Synapse will look for registered users without an
|
|
||||||
# expiration date at startup and will add one to every account it found using the
|
|
||||||
# current settings at that time.
|
|
||||||
# This means that, if a validity period is set, and Synapse is restarted (it will
|
|
||||||
# then derive an expiration date from the current validity period), and some time
|
|
||||||
# after that the validity period changes and Synapse is restarted, the users'
|
|
||||||
# expiration dates won't be updated unless their account is manually renewed. This
|
|
||||||
# date will be randomly selected within a range [now + period - d ; now + period],
|
|
||||||
# where d is equal to 10% of the validity period.
|
|
||||||
#
|
|
||||||
account_validity:
|
|
||||||
# The account validity feature is disabled by default. Uncomment the
|
|
||||||
# following line to enable it.
|
|
||||||
#
|
|
||||||
#enabled: true
|
|
||||||
|
|
||||||
# The period after which an account is valid after its registration. When
|
|
||||||
# renewing the account, its validity period will be extended by this amount
|
|
||||||
# of time. This parameter is required when using the account validity
|
|
||||||
# feature.
|
|
||||||
#
|
|
||||||
#period: 6w
|
|
||||||
|
|
||||||
# The amount of time before an account's expiry date at which Synapse will
|
|
||||||
# send an email to the account's email address with a renewal link. By
|
|
||||||
# default, no such emails are sent.
|
|
||||||
#
|
|
||||||
# If you enable this setting, you will also need to fill out the 'email' and
|
|
||||||
# 'public_baseurl' configuration sections.
|
|
||||||
#
|
|
||||||
#renew_at: 1w
|
|
||||||
|
|
||||||
# The subject of the email sent out with the renewal link. '%(app)s' can be
|
|
||||||
# used as a placeholder for the 'app_name' parameter from the 'email'
|
|
||||||
# section.
|
|
||||||
#
|
|
||||||
# Note that the placeholder must be written '%(app)s', including the
|
|
||||||
# trailing 's'.
|
|
||||||
#
|
|
||||||
# If this is not set, a default value is used.
|
|
||||||
#
|
|
||||||
#renew_email_subject: "Renew your %(app)s account"
|
|
||||||
|
|
||||||
# Directory in which Synapse will try to find templates for the HTML files to
|
|
||||||
# serve to the user when trying to renew an account. If not set, default
|
|
||||||
# templates from within the Synapse package will be used.
|
|
||||||
#
|
|
||||||
# The currently available templates are:
|
|
||||||
#
|
|
||||||
# * account_renewed.html: Displayed to the user after they have successfully
|
|
||||||
# renewed their account.
|
|
||||||
#
|
|
||||||
# * account_previously_renewed.html: Displayed to the user if they attempt to
|
|
||||||
# renew their account with a token that is valid, but that has already
|
|
||||||
# been used. In this case the account is not renewed again.
|
|
||||||
#
|
|
||||||
# * invalid_token.html: Displayed to the user when they try to renew an account
|
|
||||||
# with an unknown or invalid renewal token.
|
|
||||||
#
|
|
||||||
# See https://github.com/matrix-org/synapse/tree/master/synapse/res/templates for
|
|
||||||
# default template contents.
|
|
||||||
#
|
|
||||||
# The file name of some of these templates can be configured below for legacy
|
|
||||||
# reasons.
|
|
||||||
#
|
|
||||||
#template_dir: "res/templates"
|
|
||||||
|
|
||||||
# A custom file name for the 'account_renewed.html' template.
|
|
||||||
#
|
|
||||||
# If not set, the file is assumed to be named "account_renewed.html".
|
|
||||||
#
|
|
||||||
#account_renewed_html_path: "account_renewed.html"
|
|
||||||
|
|
||||||
# A custom file name for the 'invalid_token.html' template.
|
|
||||||
#
|
|
||||||
# If not set, the file is assumed to be named "invalid_token.html".
|
|
||||||
#
|
|
||||||
#invalid_token_html_path: "invalid_token.html"
|
|
||||||
"""
|
|
||||||
|
|
|
@ -15,9 +15,11 @@
|
||||||
import email.mime.multipart
|
import email.mime.multipart
|
||||||
import email.utils
|
import email.utils
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Tuple
|
||||||
|
|
||||||
from synapse.api.errors import StoreError, SynapseError
|
from twisted.web.http import Request
|
||||||
|
|
||||||
|
from synapse.api.errors import AuthError, StoreError, SynapseError
|
||||||
from synapse.metrics.background_process_metrics import wrap_as_background_process
|
from synapse.metrics.background_process_metrics import wrap_as_background_process
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
from synapse.util import stringutils
|
from synapse.util import stringutils
|
||||||
|
@ -27,6 +29,15 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Types for callbacks to be registered via the module api
|
||||||
|
IS_USER_EXPIRED_CALLBACK = Callable[[str], Awaitable[Optional[bool]]]
|
||||||
|
ON_USER_REGISTRATION_CALLBACK = Callable[[str], Awaitable]
|
||||||
|
# Temporary hooks to allow for a transition from `/_matrix/client` endpoints
|
||||||
|
# to `/_synapse/client/account_validity`. See `register_account_validity_callbacks`.
|
||||||
|
ON_LEGACY_SEND_MAIL_CALLBACK = Callable[[str], Awaitable]
|
||||||
|
ON_LEGACY_RENEW_CALLBACK = Callable[[str], Awaitable[Tuple[bool, bool, int]]]
|
||||||
|
ON_LEGACY_ADMIN_REQUEST = Callable[[Request], Awaitable]
|
||||||
|
|
||||||
|
|
||||||
class AccountValidityHandler:
|
class AccountValidityHandler:
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
@ -70,6 +81,99 @@ class AccountValidityHandler:
|
||||||
if hs.config.run_background_tasks:
|
if hs.config.run_background_tasks:
|
||||||
self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000)
|
self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000)
|
||||||
|
|
||||||
|
self._is_user_expired_callbacks: List[IS_USER_EXPIRED_CALLBACK] = []
|
||||||
|
self._on_user_registration_callbacks: List[ON_USER_REGISTRATION_CALLBACK] = []
|
||||||
|
self._on_legacy_send_mail_callback: Optional[
|
||||||
|
ON_LEGACY_SEND_MAIL_CALLBACK
|
||||||
|
] = None
|
||||||
|
self._on_legacy_renew_callback: Optional[ON_LEGACY_RENEW_CALLBACK] = None
|
||||||
|
|
||||||
|
# The legacy admin requests callback isn't a protected attribute because we need
|
||||||
|
# to access it from the admin servlet, which is outside of this handler.
|
||||||
|
self.on_legacy_admin_request_callback: Optional[ON_LEGACY_ADMIN_REQUEST] = None
|
||||||
|
|
||||||
|
def register_account_validity_callbacks(
|
||||||
|
self,
|
||||||
|
is_user_expired: Optional[IS_USER_EXPIRED_CALLBACK] = None,
|
||||||
|
on_user_registration: Optional[ON_USER_REGISTRATION_CALLBACK] = None,
|
||||||
|
on_legacy_send_mail: Optional[ON_LEGACY_SEND_MAIL_CALLBACK] = None,
|
||||||
|
on_legacy_renew: Optional[ON_LEGACY_RENEW_CALLBACK] = None,
|
||||||
|
on_legacy_admin_request: Optional[ON_LEGACY_ADMIN_REQUEST] = None,
|
||||||
|
):
|
||||||
|
"""Register callbacks from module for each hook."""
|
||||||
|
if is_user_expired is not None:
|
||||||
|
self._is_user_expired_callbacks.append(is_user_expired)
|
||||||
|
|
||||||
|
if on_user_registration is not None:
|
||||||
|
self._on_user_registration_callbacks.append(on_user_registration)
|
||||||
|
|
||||||
|
# The builtin account validity feature exposes 3 endpoints (send_mail, renew, and
|
||||||
|
# an admin one). As part of moving the feature into a module, we need to change
|
||||||
|
# the path from /_matrix/client/unstable/account_validity/... to
|
||||||
|
# /_synapse/client/account_validity, because:
|
||||||
|
#
|
||||||
|
# * the feature isn't part of the Matrix spec thus shouldn't live under /_matrix
|
||||||
|
# * the way we register servlets means that modules can't register resources
|
||||||
|
# under /_matrix/client
|
||||||
|
#
|
||||||
|
# We need to allow for a transition period between the old and new endpoints
|
||||||
|
# in order to allow for clients to update (and for emails to be processed).
|
||||||
|
#
|
||||||
|
# Once the email-account-validity module is loaded, it will take control of account
|
||||||
|
# validity by moving the rows from our `account_validity` table into its own table.
|
||||||
|
#
|
||||||
|
# Therefore, we need to allow modules (in practice just the one implementing the
|
||||||
|
# email-based account validity) to temporarily hook into the legacy endpoints so we
|
||||||
|
# can route the traffic coming into the old endpoints into the module, which is
|
||||||
|
# why we have the following three temporary hooks.
|
||||||
|
if on_legacy_send_mail is not None:
|
||||||
|
if self._on_legacy_send_mail_callback is not None:
|
||||||
|
raise RuntimeError("Tried to register on_legacy_send_mail twice")
|
||||||
|
|
||||||
|
self._on_legacy_send_mail_callback = on_legacy_send_mail
|
||||||
|
|
||||||
|
if on_legacy_renew is not None:
|
||||||
|
if self._on_legacy_renew_callback is not None:
|
||||||
|
raise RuntimeError("Tried to register on_legacy_renew twice")
|
||||||
|
|
||||||
|
self._on_legacy_renew_callback = on_legacy_renew
|
||||||
|
|
||||||
|
if on_legacy_admin_request is not None:
|
||||||
|
if self.on_legacy_admin_request_callback is not None:
|
||||||
|
raise RuntimeError("Tried to register on_legacy_admin_request twice")
|
||||||
|
|
||||||
|
self.on_legacy_admin_request_callback = on_legacy_admin_request
|
||||||
|
|
||||||
|
async def is_user_expired(self, user_id: str) -> bool:
|
||||||
|
"""Checks if a user has expired against third-party modules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user to check the expiry of.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Whether the user has expired.
|
||||||
|
"""
|
||||||
|
for callback in self._is_user_expired_callbacks:
|
||||||
|
expired = await callback(user_id)
|
||||||
|
if expired is not None:
|
||||||
|
return expired
|
||||||
|
|
||||||
|
if self._account_validity_enabled:
|
||||||
|
# If no module could determine whether the user has expired and the legacy
|
||||||
|
# configuration is enabled, fall back to it.
|
||||||
|
return await self.store.is_account_expired(user_id, self.clock.time_msec())
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def on_user_registration(self, user_id: str):
|
||||||
|
"""Tell third-party modules about a user's registration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The ID of the newly registered user.
|
||||||
|
"""
|
||||||
|
for callback in self._on_user_registration_callbacks:
|
||||||
|
await callback(user_id)
|
||||||
|
|
||||||
@wrap_as_background_process("send_renewals")
|
@wrap_as_background_process("send_renewals")
|
||||||
async def _send_renewal_emails(self) -> None:
|
async def _send_renewal_emails(self) -> None:
|
||||||
"""Gets the list of users whose account is expiring in the amount of time
|
"""Gets the list of users whose account is expiring in the amount of time
|
||||||
|
@ -95,6 +199,17 @@ class AccountValidityHandler:
|
||||||
Raises:
|
Raises:
|
||||||
SynapseError if the user is not set to renew.
|
SynapseError if the user is not set to renew.
|
||||||
"""
|
"""
|
||||||
|
# If a module supports sending a renewal email from here, do that, otherwise do
|
||||||
|
# the legacy dance.
|
||||||
|
if self._on_legacy_send_mail_callback is not None:
|
||||||
|
await self._on_legacy_send_mail_callback(user_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._account_validity_renew_by_email_enabled:
|
||||||
|
raise AuthError(
|
||||||
|
403, "Account renewal via email is disabled on this server."
|
||||||
|
)
|
||||||
|
|
||||||
expiration_ts = await self.store.get_expiration_ts_for_user(user_id)
|
expiration_ts = await self.store.get_expiration_ts_for_user(user_id)
|
||||||
|
|
||||||
# If this user isn't set to be expired, raise an error.
|
# If this user isn't set to be expired, raise an error.
|
||||||
|
@ -209,6 +324,10 @@ class AccountValidityHandler:
|
||||||
token is considered stale. A token is stale if the 'token_used_ts_ms' db column
|
token is considered stale. A token is stale if the 'token_used_ts_ms' db column
|
||||||
is non-null.
|
is non-null.
|
||||||
|
|
||||||
|
This method exists to support handling the legacy account validity /renew
|
||||||
|
endpoint. If a module implements the on_legacy_renew callback, then this process
|
||||||
|
is delegated to the module instead.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
renewal_token: Token sent with the renewal request.
|
renewal_token: Token sent with the renewal request.
|
||||||
Returns:
|
Returns:
|
||||||
|
@ -218,6 +337,11 @@ class AccountValidityHandler:
|
||||||
* An int representing the user's expiry timestamp as milliseconds since the
|
* An int representing the user's expiry timestamp as milliseconds since the
|
||||||
epoch, or 0 if the token was invalid.
|
epoch, or 0 if the token was invalid.
|
||||||
"""
|
"""
|
||||||
|
# If a module supports triggering a renew from here, do that, otherwise do the
|
||||||
|
# legacy dance.
|
||||||
|
if self._on_legacy_renew_callback is not None:
|
||||||
|
return await self._on_legacy_renew_callback(renewal_token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
(
|
(
|
||||||
user_id,
|
user_id,
|
||||||
|
|
|
@ -77,6 +77,7 @@ class RegistrationHandler(BaseHandler):
|
||||||
self.identity_handler = self.hs.get_identity_handler()
|
self.identity_handler = self.hs.get_identity_handler()
|
||||||
self.ratelimiter = hs.get_registration_ratelimiter()
|
self.ratelimiter = hs.get_registration_ratelimiter()
|
||||||
self.macaroon_gen = hs.get_macaroon_generator()
|
self.macaroon_gen = hs.get_macaroon_generator()
|
||||||
|
self._account_validity_handler = hs.get_account_validity_handler()
|
||||||
self._server_notices_mxid = hs.config.server_notices_mxid
|
self._server_notices_mxid = hs.config.server_notices_mxid
|
||||||
self._server_name = hs.hostname
|
self._server_name = hs.hostname
|
||||||
|
|
||||||
|
@ -700,6 +701,10 @@ class RegistrationHandler(BaseHandler):
|
||||||
shadow_banned=shadow_banned,
|
shadow_banned=shadow_banned,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Only call the account validity module(s) on the main process, to avoid
|
||||||
|
# repeating e.g. database writes on all of the workers.
|
||||||
|
await self._account_validity_handler.on_user_registration(user_id)
|
||||||
|
|
||||||
async def register_device(
|
async def register_device(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|
|
@ -12,18 +12,42 @@
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
import email.utils
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Generator, Iterable, List, Optional, Tuple
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Generator,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
)
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.web.resource import IResource
|
from twisted.web.resource import IResource
|
||||||
|
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.http.client import SimpleHttpClient
|
from synapse.http.client import SimpleHttpClient
|
||||||
|
from synapse.http.server import (
|
||||||
|
DirectServeHtmlResource,
|
||||||
|
DirectServeJsonResource,
|
||||||
|
respond_with_html,
|
||||||
|
)
|
||||||
|
from synapse.http.servlet import parse_json_object_from_request
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
from synapse.logging.context import make_deferred_yieldable, run_in_background
|
from synapse.logging.context import make_deferred_yieldable, run_in_background
|
||||||
|
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||||
|
from synapse.storage.database import DatabasePool, LoggingTransaction
|
||||||
|
from synapse.storage.databases.main.roommember import ProfileInfo
|
||||||
from synapse.storage.state import StateFilter
|
from synapse.storage.state import StateFilter
|
||||||
from synapse.types import JsonDict, UserID, create_requester
|
from synapse.types import JsonDict, Requester, UserID, create_requester
|
||||||
|
from synapse.util import Clock
|
||||||
|
from synapse.util.caches.descriptors import cached
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
@ -33,7 +57,20 @@ This package defines the 'stable' API which can be used by extension modules whi
|
||||||
are loaded into Synapse.
|
are loaded into Synapse.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__all__ = ["errors", "make_deferred_yieldable", "run_in_background", "ModuleApi"]
|
__all__ = [
|
||||||
|
"errors",
|
||||||
|
"make_deferred_yieldable",
|
||||||
|
"parse_json_object_from_request",
|
||||||
|
"respond_with_html",
|
||||||
|
"run_in_background",
|
||||||
|
"cached",
|
||||||
|
"UserID",
|
||||||
|
"DatabasePool",
|
||||||
|
"LoggingTransaction",
|
||||||
|
"DirectServeHtmlResource",
|
||||||
|
"DirectServeJsonResource",
|
||||||
|
"ModuleApi",
|
||||||
|
]
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -52,12 +89,27 @@ class ModuleApi:
|
||||||
self._server_name = hs.hostname
|
self._server_name = hs.hostname
|
||||||
self._presence_stream = hs.get_event_sources().sources["presence"]
|
self._presence_stream = hs.get_event_sources().sources["presence"]
|
||||||
self._state = hs.get_state_handler()
|
self._state = hs.get_state_handler()
|
||||||
|
self._clock = hs.get_clock() # type: Clock
|
||||||
|
self._send_email_handler = hs.get_send_email_handler()
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_name = self._hs.config.email_app_name
|
||||||
|
|
||||||
|
self._from_string = self._hs.config.email_notif_from % {"app": app_name}
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
# If substitution failed (which can happen if the string contains
|
||||||
|
# placeholders other than just "app", or if the type of the placeholder is
|
||||||
|
# not a string), fall back to the bare strings.
|
||||||
|
self._from_string = self._hs.config.email_notif_from
|
||||||
|
|
||||||
|
self._raw_from = email.utils.parseaddr(self._from_string)[1]
|
||||||
|
|
||||||
# We expose these as properties below in order to attach a helpful docstring.
|
# We expose these as properties below in order to attach a helpful docstring.
|
||||||
self._http_client: SimpleHttpClient = hs.get_simple_http_client()
|
self._http_client: SimpleHttpClient = hs.get_simple_http_client()
|
||||||
self._public_room_list_manager = PublicRoomListManager(hs)
|
self._public_room_list_manager = PublicRoomListManager(hs)
|
||||||
|
|
||||||
self._spam_checker = hs.get_spam_checker()
|
self._spam_checker = hs.get_spam_checker()
|
||||||
|
self._account_validity_handler = hs.get_account_validity_handler()
|
||||||
|
|
||||||
#################################################################################
|
#################################################################################
|
||||||
# The following methods should only be called during the module's initialisation.
|
# The following methods should only be called during the module's initialisation.
|
||||||
|
@ -67,6 +119,11 @@ class ModuleApi:
|
||||||
"""Registers callbacks for spam checking capabilities."""
|
"""Registers callbacks for spam checking capabilities."""
|
||||||
return self._spam_checker.register_callbacks
|
return self._spam_checker.register_callbacks
|
||||||
|
|
||||||
|
@property
|
||||||
|
def register_account_validity_callbacks(self):
|
||||||
|
"""Registers callbacks for account validity capabilities."""
|
||||||
|
return self._account_validity_handler.register_account_validity_callbacks
|
||||||
|
|
||||||
def register_web_resource(self, path: str, resource: IResource):
|
def register_web_resource(self, path: str, resource: IResource):
|
||||||
"""Registers a web resource to be served at the given path.
|
"""Registers a web resource to be served at the given path.
|
||||||
|
|
||||||
|
@ -101,22 +158,56 @@ class ModuleApi:
|
||||||
"""
|
"""
|
||||||
return self._public_room_list_manager
|
return self._public_room_list_manager
|
||||||
|
|
||||||
def get_user_by_req(self, req, allow_guest=False):
|
@property
|
||||||
|
def public_baseurl(self) -> str:
|
||||||
|
"""The configured public base URL for this homeserver."""
|
||||||
|
return self._hs.config.public_baseurl
|
||||||
|
|
||||||
|
@property
|
||||||
|
def email_app_name(self) -> str:
|
||||||
|
"""The application name configured in the homeserver's configuration."""
|
||||||
|
return self._hs.config.email.email_app_name
|
||||||
|
|
||||||
|
async def get_user_by_req(
|
||||||
|
self,
|
||||||
|
req: SynapseRequest,
|
||||||
|
allow_guest: bool = False,
|
||||||
|
allow_expired: bool = False,
|
||||||
|
) -> Requester:
|
||||||
"""Check the access_token provided for a request
|
"""Check the access_token provided for a request
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
req (twisted.web.server.Request): Incoming HTTP request
|
req: Incoming HTTP request
|
||||||
allow_guest (bool): True if guest users should be allowed. If this
|
allow_guest: True if guest users should be allowed. If this
|
||||||
is False, and the access token is for a guest user, an
|
is False, and the access token is for a guest user, an
|
||||||
AuthError will be thrown
|
AuthError will be thrown
|
||||||
|
allow_expired: True if expired users should be allowed. If this
|
||||||
|
is False, and the access token is for an expired user, an
|
||||||
|
AuthError will be thrown
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
twisted.internet.defer.Deferred[synapse.types.Requester]:
|
The requester for this request
|
||||||
the requester for this request
|
|
||||||
Raises:
|
Raises:
|
||||||
synapse.api.errors.AuthError: if no user by that token exists,
|
InvalidClientCredentialsError: if no user by that token exists,
|
||||||
or the token is invalid.
|
or the token is invalid.
|
||||||
"""
|
"""
|
||||||
return self._auth.get_user_by_req(req, allow_guest)
|
return await self._auth.get_user_by_req(
|
||||||
|
req,
|
||||||
|
allow_guest,
|
||||||
|
allow_expired=allow_expired,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def is_user_admin(self, user_id: str) -> bool:
|
||||||
|
"""Checks if a user is a server admin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The Matrix ID of the user to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the user is a server admin, False otherwise.
|
||||||
|
"""
|
||||||
|
return await self._store.is_server_admin(UserID.from_string(user_id))
|
||||||
|
|
||||||
def get_qualified_user_id(self, username):
|
def get_qualified_user_id(self, username):
|
||||||
"""Qualify a user id, if necessary
|
"""Qualify a user id, if necessary
|
||||||
|
@ -134,6 +225,32 @@ class ModuleApi:
|
||||||
return username
|
return username
|
||||||
return UserID(username, self._hs.hostname).to_string()
|
return UserID(username, self._hs.hostname).to_string()
|
||||||
|
|
||||||
|
async def get_profile_for_user(self, localpart: str) -> ProfileInfo:
|
||||||
|
"""Look up the profile info for the user with the given localpart.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
localpart: The localpart to look up profile information for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The profile information (i.e. display name and avatar URL).
|
||||||
|
"""
|
||||||
|
return await self._store.get_profileinfo(localpart)
|
||||||
|
|
||||||
|
async def get_threepids_for_user(self, user_id: str) -> List[Dict[str, str]]:
|
||||||
|
"""Look up the threepids (email addresses and phone numbers) associated with the
|
||||||
|
given Matrix user ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The Matrix user ID to look up threepids for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of threepids, each threepid being represented by a dictionary
|
||||||
|
containing a "medium" key which value is "email" for email addresses and
|
||||||
|
"msisdn" for phone numbers, and an "address" key which value is the
|
||||||
|
threepid's address.
|
||||||
|
"""
|
||||||
|
return await self._store.user_get_threepids(user_id)
|
||||||
|
|
||||||
def check_user_exists(self, user_id):
|
def check_user_exists(self, user_id):
|
||||||
"""Check if user exists.
|
"""Check if user exists.
|
||||||
|
|
||||||
|
@ -464,6 +581,88 @@ class ModuleApi:
|
||||||
presence_events, destination
|
presence_events, destination
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def looping_background_call(
|
||||||
|
self,
|
||||||
|
f: Callable,
|
||||||
|
msec: float,
|
||||||
|
*args,
|
||||||
|
desc: Optional[str] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Wraps a function as a background process and calls it repeatedly.
|
||||||
|
|
||||||
|
Waits `msec` initially before calling `f` for the first time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
f: The function to call repeatedly. f can be either synchronous or
|
||||||
|
asynchronous, and must follow Synapse's logcontext rules.
|
||||||
|
More info about logcontexts is available at
|
||||||
|
https://matrix-org.github.io/synapse/latest/log_contexts.html
|
||||||
|
msec: How long to wait between calls in milliseconds.
|
||||||
|
*args: Positional arguments to pass to function.
|
||||||
|
desc: The background task's description. Default to the function's name.
|
||||||
|
**kwargs: Key arguments to pass to function.
|
||||||
|
"""
|
||||||
|
if desc is None:
|
||||||
|
desc = f.__name__
|
||||||
|
|
||||||
|
if self._hs.config.run_background_tasks:
|
||||||
|
self._clock.looping_call(
|
||||||
|
run_as_background_process,
|
||||||
|
msec,
|
||||||
|
desc,
|
||||||
|
f,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Not running looping call %s as the configuration forbids it",
|
||||||
|
f,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_mail(
|
||||||
|
self,
|
||||||
|
recipient: str,
|
||||||
|
subject: str,
|
||||||
|
html: str,
|
||||||
|
text: str,
|
||||||
|
):
|
||||||
|
"""Send an email on behalf of the homeserver.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recipient: The email address for the recipient.
|
||||||
|
subject: The email's subject.
|
||||||
|
html: The email's HTML content.
|
||||||
|
text: The email's text content.
|
||||||
|
"""
|
||||||
|
await self._send_email_handler.send_email(
|
||||||
|
email_address=recipient,
|
||||||
|
subject=subject,
|
||||||
|
app_name=self.email_app_name,
|
||||||
|
html=html,
|
||||||
|
text=text,
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_templates(
|
||||||
|
self,
|
||||||
|
filenames: List[str],
|
||||||
|
custom_template_directory: Optional[str] = None,
|
||||||
|
) -> List[jinja2.Template]:
|
||||||
|
"""Read and load the content of the template files at the given location.
|
||||||
|
By default, Synapse will look for these templates in its configured template
|
||||||
|
directory, but another directory to search in can be provided.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filenames: The name of the template files to look for.
|
||||||
|
custom_template_directory: An additional directory to look for the files in.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list containing the loaded templates, with the orders matching the one of
|
||||||
|
the filenames parameter.
|
||||||
|
"""
|
||||||
|
return self._hs.config.read_templates(filenames, custom_template_directory)
|
||||||
|
|
||||||
|
|
||||||
class PublicRoomListManager:
|
class PublicRoomListManager:
|
||||||
"""Contains methods for adding to, removing from and querying whether a room
|
"""Contains methods for adding to, removing from and querying whether a room
|
||||||
|
|
|
@ -14,5 +14,9 @@
|
||||||
|
|
||||||
"""Exception types which are exposed as part of the stable module API"""
|
"""Exception types which are exposed as part of the stable module API"""
|
||||||
|
|
||||||
from synapse.api.errors import RedirectException, SynapseError # noqa: F401
|
from synapse.api.errors import ( # noqa: F401
|
||||||
|
InvalidClientCredentialsError,
|
||||||
|
RedirectException,
|
||||||
|
SynapseError,
|
||||||
|
)
|
||||||
from synapse.config._base import ConfigError # noqa: F401
|
from synapse.config._base import ConfigError # noqa: F401
|
||||||
|
|
|
@ -62,10 +62,6 @@ class PusherPool:
|
||||||
self.store = self.hs.get_datastore()
|
self.store = self.hs.get_datastore()
|
||||||
self.clock = self.hs.get_clock()
|
self.clock = self.hs.get_clock()
|
||||||
|
|
||||||
self._account_validity_enabled = (
|
|
||||||
hs.config.account_validity.account_validity_enabled
|
|
||||||
)
|
|
||||||
|
|
||||||
# We shard the handling of push notifications by user ID.
|
# We shard the handling of push notifications by user ID.
|
||||||
self._pusher_shard_config = hs.config.push.pusher_shard_config
|
self._pusher_shard_config = hs.config.push.pusher_shard_config
|
||||||
self._instance_name = hs.get_instance_name()
|
self._instance_name = hs.get_instance_name()
|
||||||
|
@ -89,6 +85,8 @@ class PusherPool:
|
||||||
# map from user id to app_id:pushkey to pusher
|
# map from user id to app_id:pushkey to pusher
|
||||||
self.pushers: Dict[str, Dict[str, Pusher]] = {}
|
self.pushers: Dict[str, Dict[str, Pusher]] = {}
|
||||||
|
|
||||||
|
self._account_validity_handler = hs.get_account_validity_handler()
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
"""Starts the pushers off in a background process."""
|
"""Starts the pushers off in a background process."""
|
||||||
if not self._should_start_pushers:
|
if not self._should_start_pushers:
|
||||||
|
@ -238,10 +236,7 @@ class PusherPool:
|
||||||
|
|
||||||
for u in users_affected:
|
for u in users_affected:
|
||||||
# Don't push if the user account has expired
|
# Don't push if the user account has expired
|
||||||
if self._account_validity_enabled:
|
expired = await self._account_validity_handler.is_user_expired(u)
|
||||||
expired = await self.store.is_account_expired(
|
|
||||||
u, self.clock.time_msec()
|
|
||||||
)
|
|
||||||
if expired:
|
if expired:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -268,10 +263,7 @@ class PusherPool:
|
||||||
|
|
||||||
for u in users_affected:
|
for u in users_affected:
|
||||||
# Don't push if the user account has expired
|
# Don't push if the user account has expired
|
||||||
if self._account_validity_enabled:
|
expired = await self._account_validity_handler.is_user_expired(u)
|
||||||
expired = await self.store.is_account_expired(
|
|
||||||
u, self.clock.time_msec()
|
|
||||||
)
|
|
||||||
if expired:
|
if expired:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -560,10 +560,18 @@ class AccountValidityRenewServlet(RestServlet):
|
||||||
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
|
||||||
await assert_requester_is_admin(self.auth, request)
|
await assert_requester_is_admin(self.auth, request)
|
||||||
|
|
||||||
|
if self.account_activity_handler.on_legacy_admin_request_callback:
|
||||||
|
expiration_ts = await (
|
||||||
|
self.account_activity_handler.on_legacy_admin_request_callback(request)
|
||||||
|
)
|
||||||
|
else:
|
||||||
body = parse_json_object_from_request(request)
|
body = parse_json_object_from_request(request)
|
||||||
|
|
||||||
if "user_id" not in body:
|
if "user_id" not in body:
|
||||||
raise SynapseError(400, "Missing property 'user_id' in the request body")
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Missing property 'user_id' in the request body",
|
||||||
|
)
|
||||||
|
|
||||||
expiration_ts = await self.account_activity_handler.renew_account_for_user(
|
expiration_ts = await self.account_activity_handler.renew_account_for_user(
|
||||||
body["user_id"],
|
body["user_id"],
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from synapse.api.errors import AuthError, SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.http.server import respond_with_html
|
from synapse.http.server import respond_with_html
|
||||||
from synapse.http.servlet import RestServlet
|
from synapse.http.servlet import RestServlet
|
||||||
|
|
||||||
|
@ -92,11 +92,6 @@ class AccountValiditySendMailServlet(RestServlet):
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_POST(self, request):
|
async def on_POST(self, request):
|
||||||
if not self.account_validity_renew_by_email_enabled:
|
|
||||||
raise AuthError(
|
|
||||||
403, "Account renewal via email is disabled on this server."
|
|
||||||
)
|
|
||||||
|
|
||||||
requester = await self.auth.get_user_by_req(request, allow_expired=True)
|
requester = await self.auth.get_user_by_req(request, allow_expired=True)
|
||||||
user_id = requester.user.to_string()
|
user_id = requester.user.to_string()
|
||||||
await self.account_activity_handler.send_renewal_email_to_user(user_id)
|
await self.account_activity_handler.send_renewal_email_to_user(user_id)
|
||||||
|
|
|
@ -168,6 +168,7 @@ class StateTestCase(unittest.TestCase):
|
||||||
"get_state_handler",
|
"get_state_handler",
|
||||||
"get_clock",
|
"get_clock",
|
||||||
"get_state_resolution_handler",
|
"get_state_resolution_handler",
|
||||||
|
"get_account_validity_handler",
|
||||||
"hostname",
|
"hostname",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue