mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-28 15:08:49 +03:00
Port "Allow users to click account renewal links multiple times without hitting an 'Invalid Token' page #74" from synapse-dinsic (#9832)
This attempts to be a direct port of https://github.com/matrix-org/synapse-dinsic/pull/74 to mainline. There was some fiddling required to deal with the changes that have been made to mainline since (mainly dealing with the split of `RegistrationWorkerStore` from `RegistrationStore`, and the changes made to `self.make_request` in test code).
This commit is contained in:
parent
e694a598f8
commit
71f0623de9
18 changed files with 496 additions and 263 deletions
23
UPGRADE.rst
23
UPGRADE.rst
|
@ -85,6 +85,29 @@ for example:
|
||||||
wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
wget https://packages.matrix.org/debian/pool/main/m/matrix-synapse-py3/matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||||
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
dpkg -i matrix-synapse-py3_1.3.0+stretch1_amd64.deb
|
||||||
|
|
||||||
|
Upgrading to v1.33.0
|
||||||
|
====================
|
||||||
|
|
||||||
|
Account Validity HTML templates can now display a user's expiration date
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
This may affect you if you have enabled the account validity feature, and have made use of a
|
||||||
|
custom HTML template specified by the ``account_validity.template_dir`` or ``account_validity.account_renewed_html_path``
|
||||||
|
Synapse config options.
|
||||||
|
|
||||||
|
The template can now accept an ``expiration_ts`` variable, which represents the unix timestamp in milliseconds for the
|
||||||
|
future date of which their account has been renewed until. See the
|
||||||
|
`default template <https://github.com/matrix-org/synapse/blob/release-v1.33.0/synapse/res/templates/account_renewed.html>`_
|
||||||
|
for an example of usage.
|
||||||
|
|
||||||
|
ALso note that a new HTML template, ``account_previously_renewed.html``, has been added. This is is shown to users
|
||||||
|
when they attempt to renew their account with a valid renewal token that has already been used before. The default
|
||||||
|
template contents can been found
|
||||||
|
`here <https://github.com/matrix-org/synapse/blob/release-v1.33.0/synapse/res/templates/account_previously_renewed.html>`_,
|
||||||
|
and can also accept an ``expiration_ts`` variable. This template replaces the error message users would previously see
|
||||||
|
upon attempting to use a valid renewal token more than once.
|
||||||
|
|
||||||
|
|
||||||
Upgrading to v1.32.0
|
Upgrading to v1.32.0
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
|
1
changelog.d/9832.feature
Normal file
1
changelog.d/9832.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Don't return an error when a user attempts to renew their account multiple times with the same token. Instead, state when their account is set to expire. This change concerns the optional account validity feature.
|
|
@ -1175,69 +1175,6 @@ url_preview_accept_language:
|
||||||
#
|
#
|
||||||
#enable_registration: false
|
#enable_registration: false
|
||||||
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
#template_dir: "res/templates"
|
|
||||||
|
|
||||||
# File within 'template_dir' giving the HTML to be displayed to the user after
|
|
||||||
# they successfully renewed their account. If not set, default text is used.
|
|
||||||
#
|
|
||||||
#account_renewed_html_path: "account_renewed.html"
|
|
||||||
|
|
||||||
# File within 'template_dir' giving the HTML to be displayed when the user
|
|
||||||
# tries to renew an account with an invalid renewal token. If not set,
|
|
||||||
# default text is used.
|
|
||||||
#
|
|
||||||
#invalid_token_html_path: "invalid_token.html"
|
|
||||||
|
|
||||||
# Time that a user's session remains valid for, after they log in.
|
# Time that a user's session remains valid for, after they log in.
|
||||||
#
|
#
|
||||||
# Note that this is not currently compatible with guest logins.
|
# Note that this is not currently compatible with guest logins.
|
||||||
|
@ -1432,6 +1369,91 @@ 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
|
||||||
|
|
|
@ -79,7 +79,9 @@ class Auth:
|
||||||
|
|
||||||
self._auth_blocking = AuthBlocking(self.hs)
|
self._auth_blocking = AuthBlocking(self.hs)
|
||||||
|
|
||||||
self._account_validity = hs.config.account_validity
|
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
|
||||||
|
|
||||||
|
@ -222,7 +224,7 @@ 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 self._account_validity_enabled and not allow_expired:
|
||||||
if await self.store.is_account_expired(
|
if await self.store.is_account_expired(
|
||||||
user_info.user_id, self.clock.time_msec()
|
user_info.user_id, self.clock.time_msec()
|
||||||
):
|
):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from typing import Any, Iterable, List, Optional
|
from typing import Any, Iterable, List, Optional
|
||||||
|
|
||||||
from synapse.config import (
|
from synapse.config import (
|
||||||
|
account_validity,
|
||||||
api,
|
api,
|
||||||
appservice,
|
appservice,
|
||||||
auth,
|
auth,
|
||||||
|
@ -59,6 +60,7 @@ class RootConfig:
|
||||||
captcha: captcha.CaptchaConfig
|
captcha: captcha.CaptchaConfig
|
||||||
voip: voip.VoipConfig
|
voip: voip.VoipConfig
|
||||||
registration: registration.RegistrationConfig
|
registration: registration.RegistrationConfig
|
||||||
|
account_validity: account_validity.AccountValidityConfig
|
||||||
metrics: metrics.MetricsConfig
|
metrics: metrics.MetricsConfig
|
||||||
api: api.ApiConfig
|
api: api.ApiConfig
|
||||||
appservice: appservice.AppServiceConfig
|
appservice: appservice.AppServiceConfig
|
||||||
|
|
165
synapse/config/account_validity.py
Normal file
165
synapse/config/account_validity.py
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
# -*- 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, ConfigError
|
||||||
|
|
||||||
|
|
||||||
|
class AccountValidityConfig(Config):
|
||||||
|
section = "account_validity"
|
||||||
|
|
||||||
|
def read_config(self, config, **kwargs):
|
||||||
|
account_validity_config = config.get("account_validity") or {}
|
||||||
|
self.account_validity_enabled = account_validity_config.get("enabled", False)
|
||||||
|
self.account_validity_renew_by_email_enabled = (
|
||||||
|
"renew_at" in account_validity_config
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.account_validity_enabled:
|
||||||
|
if "period" in account_validity_config:
|
||||||
|
self.account_validity_period = self.parse_duration(
|
||||||
|
account_validity_config["period"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ConfigError("'period' is required when using account validity")
|
||||||
|
|
||||||
|
if "renew_at" in account_validity_config:
|
||||||
|
self.account_validity_renew_at = self.parse_duration(
|
||||||
|
account_validity_config["renew_at"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if "renew_email_subject" in account_validity_config:
|
||||||
|
self.account_validity_renew_email_subject = account_validity_config[
|
||||||
|
"renew_email_subject"
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.account_validity_renew_email_subject = "Renew your %(app)s account"
|
||||||
|
|
||||||
|
self.account_validity_startup_job_max_delta = (
|
||||||
|
self.account_validity_period * 10.0 / 100.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.account_validity_renew_by_email_enabled:
|
||||||
|
if not self.public_baseurl:
|
||||||
|
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
|
||||||
|
|
||||||
|
# Load account validity templates.
|
||||||
|
account_validity_template_dir = account_validity_config.get("template_dir")
|
||||||
|
|
||||||
|
account_renewed_template_filename = account_validity_config.get(
|
||||||
|
"account_renewed_html_path", "account_renewed.html"
|
||||||
|
)
|
||||||
|
invalid_token_template_filename = account_validity_config.get(
|
||||||
|
"invalid_token_html_path", "invalid_token.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read and store template content
|
||||||
|
(
|
||||||
|
self.account_validity_account_renewed_template,
|
||||||
|
self.account_validity_account_previously_renewed_template,
|
||||||
|
self.account_validity_invalid_token_template,
|
||||||
|
) = self.read_templates(
|
||||||
|
[
|
||||||
|
account_renewed_template_filename,
|
||||||
|
"account_previously_renewed.html",
|
||||||
|
invalid_token_template_filename,
|
||||||
|
],
|
||||||
|
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"
|
||||||
|
"""
|
|
@ -299,7 +299,7 @@ class EmailConfig(Config):
|
||||||
"client_base_url", email_config.get("riot_base_url", None)
|
"client_base_url", email_config.get("riot_base_url", None)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.account_validity.renew_by_email_enabled:
|
if self.account_validity_renew_by_email_enabled:
|
||||||
expiry_template_html = email_config.get(
|
expiry_template_html = email_config.get(
|
||||||
"expiry_template_html", "notice_expiry.html"
|
"expiry_template_html", "notice_expiry.html"
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
# 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.
|
||||||
|
|
||||||
from ._base import RootConfig
|
from ._base import RootConfig
|
||||||
|
from .account_validity import AccountValidityConfig
|
||||||
from .api import ApiConfig
|
from .api import ApiConfig
|
||||||
from .appservice import AppServiceConfig
|
from .appservice import AppServiceConfig
|
||||||
from .auth import AuthConfig
|
from .auth import AuthConfig
|
||||||
|
@ -68,6 +68,7 @@ class HomeServerConfig(RootConfig):
|
||||||
CaptchaConfig,
|
CaptchaConfig,
|
||||||
VoipConfig,
|
VoipConfig,
|
||||||
RegistrationConfig,
|
RegistrationConfig,
|
||||||
|
AccountValidityConfig,
|
||||||
MetricsConfig,
|
MetricsConfig,
|
||||||
ApiConfig,
|
ApiConfig,
|
||||||
AppServiceConfig,
|
AppServiceConfig,
|
||||||
|
|
|
@ -12,74 +12,12 @@
|
||||||
# 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 os
|
|
||||||
|
|
||||||
import pkg_resources
|
|
||||||
|
|
||||||
from synapse.api.constants import RoomCreationPreset
|
from synapse.api.constants import RoomCreationPreset
|
||||||
from synapse.config._base import Config, ConfigError
|
from synapse.config._base import Config, ConfigError
|
||||||
from synapse.types import RoomAlias, UserID
|
from synapse.types import RoomAlias, UserID
|
||||||
from synapse.util.stringutils import random_string_with_symbols, strtobool
|
from synapse.util.stringutils import random_string_with_symbols, strtobool
|
||||||
|
|
||||||
|
|
||||||
class AccountValidityConfig(Config):
|
|
||||||
section = "accountvalidity"
|
|
||||||
|
|
||||||
def __init__(self, config, synapse_config):
|
|
||||||
if config is None:
|
|
||||||
return
|
|
||||||
super().__init__()
|
|
||||||
self.enabled = config.get("enabled", False)
|
|
||||||
self.renew_by_email_enabled = "renew_at" in config
|
|
||||||
|
|
||||||
if self.enabled:
|
|
||||||
if "period" in config:
|
|
||||||
self.period = self.parse_duration(config["period"])
|
|
||||||
else:
|
|
||||||
raise ConfigError("'period' is required when using account validity")
|
|
||||||
|
|
||||||
if "renew_at" in config:
|
|
||||||
self.renew_at = self.parse_duration(config["renew_at"])
|
|
||||||
|
|
||||||
if "renew_email_subject" in config:
|
|
||||||
self.renew_email_subject = config["renew_email_subject"]
|
|
||||||
else:
|
|
||||||
self.renew_email_subject = "Renew your %(app)s account"
|
|
||||||
|
|
||||||
self.startup_job_max_delta = self.period * 10.0 / 100.0
|
|
||||||
|
|
||||||
if self.renew_by_email_enabled:
|
|
||||||
if "public_baseurl" not in synapse_config:
|
|
||||||
raise ConfigError("Can't send renewal emails without 'public_baseurl'")
|
|
||||||
|
|
||||||
template_dir = config.get("template_dir")
|
|
||||||
|
|
||||||
if not template_dir:
|
|
||||||
template_dir = pkg_resources.resource_filename("synapse", "res/templates")
|
|
||||||
|
|
||||||
if "account_renewed_html_path" in config:
|
|
||||||
file_path = os.path.join(template_dir, config["account_renewed_html_path"])
|
|
||||||
|
|
||||||
self.account_renewed_html_content = self.read_file(
|
|
||||||
file_path, "account_validity.account_renewed_html_path"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.account_renewed_html_content = (
|
|
||||||
"<html><body>Your account has been successfully renewed.</body><html>"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "invalid_token_html_path" in config:
|
|
||||||
file_path = os.path.join(template_dir, config["invalid_token_html_path"])
|
|
||||||
|
|
||||||
self.invalid_token_html_content = self.read_file(
|
|
||||||
file_path, "account_validity.invalid_token_html_path"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.invalid_token_html_content = (
|
|
||||||
"<html><body>Invalid renewal token.</body><html>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RegistrationConfig(Config):
|
class RegistrationConfig(Config):
|
||||||
section = "registration"
|
section = "registration"
|
||||||
|
|
||||||
|
@ -92,10 +30,6 @@ class RegistrationConfig(Config):
|
||||||
str(config["disable_registration"])
|
str(config["disable_registration"])
|
||||||
)
|
)
|
||||||
|
|
||||||
self.account_validity = AccountValidityConfig(
|
|
||||||
config.get("account_validity") or {}, config
|
|
||||||
)
|
|
||||||
|
|
||||||
self.registrations_require_3pid = config.get("registrations_require_3pid", [])
|
self.registrations_require_3pid = config.get("registrations_require_3pid", [])
|
||||||
self.allowed_local_3pids = config.get("allowed_local_3pids", [])
|
self.allowed_local_3pids = config.get("allowed_local_3pids", [])
|
||||||
self.enable_3pid_lookup = config.get("enable_3pid_lookup", True)
|
self.enable_3pid_lookup = config.get("enable_3pid_lookup", True)
|
||||||
|
@ -207,69 +141,6 @@ class RegistrationConfig(Config):
|
||||||
#
|
#
|
||||||
#enable_registration: false
|
#enable_registration: false
|
||||||
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
#template_dir: "res/templates"
|
|
||||||
|
|
||||||
# File within 'template_dir' giving the HTML to be displayed to the user after
|
|
||||||
# they successfully renewed their account. If not set, default text is used.
|
|
||||||
#
|
|
||||||
#account_renewed_html_path: "account_renewed.html"
|
|
||||||
|
|
||||||
# File within 'template_dir' giving the HTML to be displayed when the user
|
|
||||||
# tries to renew an account with an invalid renewal token. If not set,
|
|
||||||
# default text is used.
|
|
||||||
#
|
|
||||||
#invalid_token_html_path: "invalid_token.html"
|
|
||||||
|
|
||||||
# Time that a user's session remains valid for, after they log in.
|
# Time that a user's session remains valid for, after they log in.
|
||||||
#
|
#
|
||||||
# Note that this is not currently compatible with guest logins.
|
# Note that this is not currently compatible with guest logins.
|
||||||
|
|
|
@ -17,7 +17,7 @@ import email.utils
|
||||||
import logging
|
import logging
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from typing import TYPE_CHECKING, List, Optional
|
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||||
|
|
||||||
from synapse.api.errors import StoreError, SynapseError
|
from synapse.api.errors import StoreError, SynapseError
|
||||||
from synapse.logging.context import make_deferred_yieldable
|
from synapse.logging.context import make_deferred_yieldable
|
||||||
|
@ -39,28 +39,44 @@ class AccountValidityHandler:
|
||||||
self.sendmail = self.hs.get_sendmail()
|
self.sendmail = self.hs.get_sendmail()
|
||||||
self.clock = self.hs.get_clock()
|
self.clock = self.hs.get_clock()
|
||||||
|
|
||||||
self._account_validity = self.hs.config.account_validity
|
self._account_validity_enabled = (
|
||||||
|
hs.config.account_validity.account_validity_enabled
|
||||||
|
)
|
||||||
|
self._account_validity_renew_by_email_enabled = (
|
||||||
|
hs.config.account_validity.account_validity_renew_by_email_enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
self._account_validity_period = None
|
||||||
|
if self._account_validity_enabled:
|
||||||
|
self._account_validity_period = (
|
||||||
|
hs.config.account_validity.account_validity_period
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self._account_validity.enabled
|
self._account_validity_enabled
|
||||||
and self._account_validity.renew_by_email_enabled
|
and self._account_validity_renew_by_email_enabled
|
||||||
):
|
):
|
||||||
# Don't do email-specific configuration if renewal by email is disabled.
|
# Don't do email-specific configuration if renewal by email is disabled.
|
||||||
self._template_html = self.config.account_validity_template_html
|
self._template_html = (
|
||||||
self._template_text = self.config.account_validity_template_text
|
hs.config.account_validity.account_validity_template_html
|
||||||
|
)
|
||||||
|
self._template_text = (
|
||||||
|
hs.config.account_validity.account_validity_template_text
|
||||||
|
)
|
||||||
|
account_validity_renew_email_subject = (
|
||||||
|
hs.config.account_validity.account_validity_renew_email_subject
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app_name = self.hs.config.email_app_name
|
app_name = hs.config.email_app_name
|
||||||
|
|
||||||
self._subject = self._account_validity.renew_email_subject % {
|
self._subject = account_validity_renew_email_subject % {"app": app_name}
|
||||||
"app": app_name
|
|
||||||
}
|
|
||||||
|
|
||||||
self._from_string = self.hs.config.email_notif_from % {"app": app_name}
|
self._from_string = hs.config.email_notif_from % {"app": app_name}
|
||||||
except Exception:
|
except Exception:
|
||||||
# If substitution failed, fall back to the bare strings.
|
# If substitution failed, fall back to the bare strings.
|
||||||
self._subject = self._account_validity.renew_email_subject
|
self._subject = account_validity_renew_email_subject
|
||||||
self._from_string = self.hs.config.email_notif_from
|
self._from_string = hs.config.email_notif_from
|
||||||
|
|
||||||
self._raw_from = email.utils.parseaddr(self._from_string)[1]
|
self._raw_from = email.utils.parseaddr(self._from_string)[1]
|
||||||
|
|
||||||
|
@ -220,50 +236,87 @@ class AccountValidityHandler:
|
||||||
attempts += 1
|
attempts += 1
|
||||||
raise StoreError(500, "Couldn't generate a unique string as refresh string.")
|
raise StoreError(500, "Couldn't generate a unique string as refresh string.")
|
||||||
|
|
||||||
async def renew_account(self, renewal_token: str) -> bool:
|
async def renew_account(self, renewal_token: str) -> Tuple[bool, bool, int]:
|
||||||
"""Renews the account attached to a given renewal token by pushing back the
|
"""Renews the account attached to a given renewal token by pushing back the
|
||||||
expiration date by the current validity period in the server's configuration.
|
expiration date by the current validity period in the server's configuration.
|
||||||
|
|
||||||
|
If it turns out that the token is valid but has already been used, then the
|
||||||
|
token is considered stale. A token is stale if the 'token_used_ts_ms' db column
|
||||||
|
is non-null.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
renewal_token: Token sent with the renewal request.
|
renewal_token: Token sent with the renewal request.
|
||||||
Returns:
|
Returns:
|
||||||
Whether the provided token is valid.
|
A tuple containing:
|
||||||
|
* A bool representing whether the token is valid and unused.
|
||||||
|
* A bool which is `True` if the token is valid, but stale.
|
||||||
|
* An int representing the user's expiry timestamp as milliseconds since the
|
||||||
|
epoch, or 0 if the token was invalid.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
user_id = await self.store.get_user_from_renewal_token(renewal_token)
|
(
|
||||||
|
user_id,
|
||||||
|
current_expiration_ts,
|
||||||
|
token_used_ts,
|
||||||
|
) = await self.store.get_user_from_renewal_token(renewal_token)
|
||||||
except StoreError:
|
except StoreError:
|
||||||
return False
|
return False, False, 0
|
||||||
|
|
||||||
|
# Check whether this token has already been used.
|
||||||
|
if token_used_ts:
|
||||||
|
logger.info(
|
||||||
|
"User '%s' attempted to use previously used token '%s' to renew account",
|
||||||
|
user_id,
|
||||||
|
renewal_token,
|
||||||
|
)
|
||||||
|
return False, True, current_expiration_ts
|
||||||
|
|
||||||
logger.debug("Renewing an account for user %s", user_id)
|
logger.debug("Renewing an account for user %s", user_id)
|
||||||
await self.renew_account_for_user(user_id)
|
|
||||||
|
|
||||||
return True
|
# Renew the account. Pass the renewal_token here so that it is not cleared.
|
||||||
|
# We want to keep the token around in case the user attempts to renew their
|
||||||
|
# account with the same token twice (clicking the email link twice).
|
||||||
|
#
|
||||||
|
# In that case, the token will be accepted, but the account's expiration ts
|
||||||
|
# will remain unchanged.
|
||||||
|
new_expiration_ts = await self.renew_account_for_user(
|
||||||
|
user_id, renewal_token=renewal_token
|
||||||
|
)
|
||||||
|
|
||||||
|
return True, False, new_expiration_ts
|
||||||
|
|
||||||
async def renew_account_for_user(
|
async def renew_account_for_user(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
expiration_ts: Optional[int] = None,
|
expiration_ts: Optional[int] = None,
|
||||||
email_sent: bool = False,
|
email_sent: bool = False,
|
||||||
|
renewal_token: Optional[str] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Renews the account attached to a given user by pushing back the
|
"""Renews the account attached to a given user by pushing back the
|
||||||
expiration date by the current validity period in the server's
|
expiration date by the current validity period in the server's
|
||||||
configuration.
|
configuration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
renewal_token: Token sent with the renewal request.
|
user_id: The ID of the user to renew.
|
||||||
expiration_ts: New expiration date. Defaults to now + validity period.
|
expiration_ts: New expiration date. Defaults to now + validity period.
|
||||||
email_sen: Whether an email has been sent for this validity period.
|
email_sent: Whether an email has been sent for this validity period.
|
||||||
Defaults to False.
|
renewal_token: Token sent with the renewal request. The user's token
|
||||||
|
will be cleared if this is None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
New expiration date for this account, as a timestamp in
|
New expiration date for this account, as a timestamp in
|
||||||
milliseconds since epoch.
|
milliseconds since epoch.
|
||||||
"""
|
"""
|
||||||
|
now = self.clock.time_msec()
|
||||||
if expiration_ts is None:
|
if expiration_ts is None:
|
||||||
expiration_ts = self.clock.time_msec() + self._account_validity.period
|
expiration_ts = now + self._account_validity_period
|
||||||
|
|
||||||
await self.store.set_account_validity_for_user(
|
await self.store.set_account_validity_for_user(
|
||||||
user_id=user_id, expiration_ts=expiration_ts, email_sent=email_sent
|
user_id=user_id,
|
||||||
|
expiration_ts=expiration_ts,
|
||||||
|
email_sent=email_sent,
|
||||||
|
renewal_token=renewal_token,
|
||||||
|
token_used_ts=now,
|
||||||
)
|
)
|
||||||
|
|
||||||
return expiration_ts
|
return expiration_ts
|
||||||
|
|
|
@ -49,7 +49,9 @@ class DeactivateAccountHandler(BaseHandler):
|
||||||
if hs.config.run_background_tasks:
|
if hs.config.run_background_tasks:
|
||||||
hs.get_reactor().callWhenRunning(self._start_user_parting)
|
hs.get_reactor().callWhenRunning(self._start_user_parting)
|
||||||
|
|
||||||
self._account_validity_enabled = hs.config.account_validity.enabled
|
self._account_validity_enabled = (
|
||||||
|
hs.config.account_validity.account_validity_enabled
|
||||||
|
)
|
||||||
|
|
||||||
async def deactivate_account(
|
async def deactivate_account(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -62,7 +62,9 @@ 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 = hs.config.account_validity
|
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
|
||||||
|
@ -236,7 +238,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:
|
if self._account_validity_enabled:
|
||||||
expired = await self.store.is_account_expired(
|
expired = await self.store.is_account_expired(
|
||||||
u, self.clock.time_msec()
|
u, self.clock.time_msec()
|
||||||
)
|
)
|
||||||
|
@ -266,7 +268,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:
|
if self._account_validity_enabled:
|
||||||
expired = await self.store.is_account_expired(
|
expired = await self.store.is_account_expired(
|
||||||
u, self.clock.time_msec()
|
u, self.clock.time_msec()
|
||||||
)
|
)
|
||||||
|
|
1
synapse/res/templates/account_previously_renewed.html
Normal file
1
synapse/res/templates/account_previously_renewed.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<html><body>Your account is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}.</body><html>
|
|
@ -1 +1 @@
|
||||||
<html><body>Your account has been successfully renewed.</body><html>
|
<html><body>Your account has been successfully renewed and is valid until {{ expiration_ts|format_ts("%d-%m-%Y") }}.</body><html>
|
||||||
|
|
|
@ -36,24 +36,40 @@ class AccountValidityRenewServlet(RestServlet):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.account_activity_handler = hs.get_account_validity_handler()
|
self.account_activity_handler = hs.get_account_validity_handler()
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.success_html = hs.config.account_validity.account_renewed_html_content
|
self.account_renewed_template = (
|
||||||
self.failure_html = hs.config.account_validity.invalid_token_html_content
|
hs.config.account_validity.account_validity_account_renewed_template
|
||||||
|
)
|
||||||
|
self.account_previously_renewed_template = (
|
||||||
|
hs.config.account_validity.account_validity_account_previously_renewed_template
|
||||||
|
)
|
||||||
|
self.invalid_token_template = (
|
||||||
|
hs.config.account_validity.account_validity_invalid_token_template
|
||||||
|
)
|
||||||
|
|
||||||
async def on_GET(self, request):
|
async def on_GET(self, request):
|
||||||
if b"token" not in request.args:
|
if b"token" not in request.args:
|
||||||
raise SynapseError(400, "Missing renewal token")
|
raise SynapseError(400, "Missing renewal token")
|
||||||
renewal_token = request.args[b"token"][0]
|
renewal_token = request.args[b"token"][0]
|
||||||
|
|
||||||
token_valid = await self.account_activity_handler.renew_account(
|
(
|
||||||
|
token_valid,
|
||||||
|
token_stale,
|
||||||
|
expiration_ts,
|
||||||
|
) = await self.account_activity_handler.renew_account(
|
||||||
renewal_token.decode("utf8")
|
renewal_token.decode("utf8")
|
||||||
)
|
)
|
||||||
|
|
||||||
if token_valid:
|
if token_valid:
|
||||||
status_code = 200
|
status_code = 200
|
||||||
response = self.success_html
|
response = self.account_renewed_template.render(expiration_ts=expiration_ts)
|
||||||
|
elif token_stale:
|
||||||
|
status_code = 200
|
||||||
|
response = self.account_previously_renewed_template.render(
|
||||||
|
expiration_ts=expiration_ts
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
status_code = 404
|
status_code = 404
|
||||||
response = self.failure_html
|
response = self.invalid_token_template.render(expiration_ts=expiration_ts)
|
||||||
|
|
||||||
respond_with_html(request, status_code, response)
|
respond_with_html(request, status_code, response)
|
||||||
|
|
||||||
|
@ -71,10 +87,12 @@ class AccountValiditySendMailServlet(RestServlet):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.account_activity_handler = hs.get_account_validity_handler()
|
self.account_activity_handler = hs.get_account_validity_handler()
|
||||||
self.auth = hs.get_auth()
|
self.auth = hs.get_auth()
|
||||||
self.account_validity = self.hs.config.account_validity
|
self.account_validity_renew_by_email_enabled = (
|
||||||
|
hs.config.account_validity.account_validity_renew_by_email_enabled
|
||||||
|
)
|
||||||
|
|
||||||
async def on_POST(self, request):
|
async def on_POST(self, request):
|
||||||
if not self.account_validity.renew_by_email_enabled:
|
if not self.account_validity_renew_by_email_enabled:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403, "Account renewal via email is disabled on this server."
|
403, "Account renewal via email is disabled on this server."
|
||||||
)
|
)
|
||||||
|
|
|
@ -91,12 +91,24 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
id_column=None,
|
id_column=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._account_validity = hs.config.account_validity
|
self._account_validity_enabled = (
|
||||||
if hs.config.run_background_tasks and self._account_validity.enabled:
|
hs.config.account_validity.account_validity_enabled
|
||||||
self._clock.call_later(
|
)
|
||||||
0.0,
|
self._account_validity_period = None
|
||||||
self._set_expiration_date_when_missing,
|
self._account_validity_startup_job_max_delta = None
|
||||||
|
if self._account_validity_enabled:
|
||||||
|
self._account_validity_period = (
|
||||||
|
hs.config.account_validity.account_validity_period
|
||||||
)
|
)
|
||||||
|
self._account_validity_startup_job_max_delta = (
|
||||||
|
hs.config.account_validity.account_validity_startup_job_max_delta
|
||||||
|
)
|
||||||
|
|
||||||
|
if hs.config.run_background_tasks:
|
||||||
|
self._clock.call_later(
|
||||||
|
0.0,
|
||||||
|
self._set_expiration_date_when_missing,
|
||||||
|
)
|
||||||
|
|
||||||
# Create a background job for culling expired 3PID validity tokens
|
# Create a background job for culling expired 3PID validity tokens
|
||||||
if hs.config.run_background_tasks:
|
if hs.config.run_background_tasks:
|
||||||
|
@ -194,6 +206,7 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
expiration_ts: int,
|
expiration_ts: int,
|
||||||
email_sent: bool,
|
email_sent: bool,
|
||||||
renewal_token: Optional[str] = None,
|
renewal_token: Optional[str] = None,
|
||||||
|
token_used_ts: Optional[int] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Updates the account validity properties of the given account, with the
|
"""Updates the account validity properties of the given account, with the
|
||||||
given values.
|
given values.
|
||||||
|
@ -207,6 +220,8 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
period.
|
period.
|
||||||
renewal_token: Renewal token the user can use to extend the validity
|
renewal_token: Renewal token the user can use to extend the validity
|
||||||
of their account. Defaults to no token.
|
of their account. Defaults to no token.
|
||||||
|
token_used_ts: A timestamp of when the current token was used to renew
|
||||||
|
the account.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_account_validity_for_user_txn(txn):
|
def set_account_validity_for_user_txn(txn):
|
||||||
|
@ -218,6 +233,7 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
"expiration_ts_ms": expiration_ts,
|
"expiration_ts_ms": expiration_ts,
|
||||||
"email_sent": email_sent,
|
"email_sent": email_sent,
|
||||||
"renewal_token": renewal_token,
|
"renewal_token": renewal_token,
|
||||||
|
"token_used_ts_ms": token_used_ts,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self._invalidate_cache_and_stream(
|
self._invalidate_cache_and_stream(
|
||||||
|
@ -231,7 +247,7 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
async def set_renewal_token_for_user(
|
async def set_renewal_token_for_user(
|
||||||
self, user_id: str, renewal_token: str
|
self, user_id: str, renewal_token: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Defines a renewal token for a given user.
|
"""Defines a renewal token for a given user, and clears the token_used timestamp.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: ID of the user to set the renewal token for.
|
user_id: ID of the user to set the renewal token for.
|
||||||
|
@ -244,26 +260,40 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
await self.db_pool.simple_update_one(
|
await self.db_pool.simple_update_one(
|
||||||
table="account_validity",
|
table="account_validity",
|
||||||
keyvalues={"user_id": user_id},
|
keyvalues={"user_id": user_id},
|
||||||
updatevalues={"renewal_token": renewal_token},
|
updatevalues={"renewal_token": renewal_token, "token_used_ts_ms": None},
|
||||||
desc="set_renewal_token_for_user",
|
desc="set_renewal_token_for_user",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_user_from_renewal_token(self, renewal_token: str) -> str:
|
async def get_user_from_renewal_token(
|
||||||
"""Get a user ID from a renewal token.
|
self, renewal_token: str
|
||||||
|
) -> Tuple[str, int, Optional[int]]:
|
||||||
|
"""Get a user ID and renewal status from a renewal token.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
renewal_token: The renewal token to perform the lookup with.
|
renewal_token: The renewal token to perform the lookup with.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The ID of the user to which the token belongs.
|
A tuple of containing the following values:
|
||||||
|
* The ID of a user to which the token belongs.
|
||||||
|
* An int representing the user's expiry timestamp as milliseconds since the
|
||||||
|
epoch, or 0 if the token was invalid.
|
||||||
|
* An optional int representing the timestamp of when the user renewed their
|
||||||
|
account timestamp as milliseconds since the epoch. None if the account
|
||||||
|
has not been renewed using the current token yet.
|
||||||
"""
|
"""
|
||||||
return await self.db_pool.simple_select_one_onecol(
|
ret_dict = await self.db_pool.simple_select_one(
|
||||||
table="account_validity",
|
table="account_validity",
|
||||||
keyvalues={"renewal_token": renewal_token},
|
keyvalues={"renewal_token": renewal_token},
|
||||||
retcol="user_id",
|
retcols=["user_id", "expiration_ts_ms", "token_used_ts_ms"],
|
||||||
desc="get_user_from_renewal_token",
|
desc="get_user_from_renewal_token",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
ret_dict["user_id"],
|
||||||
|
ret_dict["expiration_ts_ms"],
|
||||||
|
ret_dict["token_used_ts_ms"],
|
||||||
|
)
|
||||||
|
|
||||||
async def get_renewal_token_for_user(self, user_id: str) -> str:
|
async def get_renewal_token_for_user(self, user_id: str) -> str:
|
||||||
"""Get the renewal token associated with a given user ID.
|
"""Get the renewal token associated with a given user ID.
|
||||||
|
|
||||||
|
@ -302,7 +332,7 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
"get_users_expiring_soon",
|
"get_users_expiring_soon",
|
||||||
select_users_txn,
|
select_users_txn,
|
||||||
self._clock.time_msec(),
|
self._clock.time_msec(),
|
||||||
self.config.account_validity.renew_at,
|
self.config.account_validity_renew_at,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def set_renewal_mail_status(self, user_id: str, email_sent: bool) -> None:
|
async def set_renewal_mail_status(self, user_id: str, email_sent: bool) -> None:
|
||||||
|
@ -964,11 +994,11 @@ class RegistrationWorkerStore(CacheInvalidationWorkerStore):
|
||||||
delta equal to 10% of the validity period.
|
delta equal to 10% of the validity period.
|
||||||
"""
|
"""
|
||||||
now_ms = self._clock.time_msec()
|
now_ms = self._clock.time_msec()
|
||||||
expiration_ts = now_ms + self._account_validity.period
|
expiration_ts = now_ms + self._account_validity_period
|
||||||
|
|
||||||
if use_delta:
|
if use_delta:
|
||||||
expiration_ts = self.rand.randrange(
|
expiration_ts = self.rand.randrange(
|
||||||
expiration_ts - self._account_validity.startup_job_max_delta,
|
expiration_ts - self._account_validity_startup_job_max_delta,
|
||||||
expiration_ts,
|
expiration_ts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1412,7 +1442,7 @@ class RegistrationStore(StatsStore, RegistrationBackgroundUpdateStore):
|
||||||
except self.database_engine.module.IntegrityError:
|
except self.database_engine.module.IntegrityError:
|
||||||
raise StoreError(400, "User ID already taken.", errcode=Codes.USER_IN_USE)
|
raise StoreError(400, "User ID already taken.", errcode=Codes.USER_IN_USE)
|
||||||
|
|
||||||
if self._account_validity.enabled:
|
if self._account_validity_enabled:
|
||||||
self.set_expiration_date_for_user_txn(txn, user_id)
|
self.set_expiration_date_for_user_txn(txn, user_id)
|
||||||
|
|
||||||
if create_profile_with_displayname:
|
if create_profile_with_displayname:
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- Track when users renew their account using the value of the 'renewal_token' column.
|
||||||
|
-- This field should be set to NULL after a fresh token is generated.
|
||||||
|
ALTER TABLE account_validity ADD token_used_ts_ms BIGINT;
|
|
@ -492,8 +492,8 @@ class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
(user_id, tok) = self.create_user()
|
(user_id, tok) = self.create_user()
|
||||||
|
|
||||||
# Move 6 days forward. This should trigger a renewal email to be sent.
|
# Move 5 days forward. This should trigger a renewal email to be sent.
|
||||||
self.reactor.advance(datetime.timedelta(days=6).total_seconds())
|
self.reactor.advance(datetime.timedelta(days=5).total_seconds())
|
||||||
self.assertEqual(len(self.email_attempts), 1)
|
self.assertEqual(len(self.email_attempts), 1)
|
||||||
|
|
||||||
# Retrieving the URL from the email is too much pain for now, so we
|
# Retrieving the URL from the email is too much pain for now, so we
|
||||||
|
@ -504,14 +504,32 @@ class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertEquals(channel.result["code"], b"200", channel.result)
|
self.assertEquals(channel.result["code"], b"200", channel.result)
|
||||||
|
|
||||||
# Check that we're getting HTML back.
|
# Check that we're getting HTML back.
|
||||||
content_type = None
|
content_type = channel.headers.getRawHeaders(b"Content-Type")
|
||||||
for header in channel.result.get("headers", []):
|
self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result)
|
||||||
if header[0] == b"Content-Type":
|
|
||||||
content_type = header[1]
|
|
||||||
self.assertEqual(content_type, b"text/html; charset=utf-8", channel.result)
|
|
||||||
|
|
||||||
# Check that the HTML we're getting is the one we expect on a successful renewal.
|
# Check that the HTML we're getting is the one we expect on a successful renewal.
|
||||||
expected_html = self.hs.config.account_validity.account_renewed_html_content
|
expiration_ts = self.get_success(self.store.get_expiration_ts_for_user(user_id))
|
||||||
|
expected_html = self.hs.config.account_validity.account_validity_account_renewed_template.render(
|
||||||
|
expiration_ts=expiration_ts
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
channel.result["body"], expected_html.encode("utf8"), channel.result
|
||||||
|
)
|
||||||
|
|
||||||
|
# Move 1 day forward. Try to renew with the same token again.
|
||||||
|
url = "/_matrix/client/unstable/account_validity/renew?token=%s" % renewal_token
|
||||||
|
channel = self.make_request(b"GET", url)
|
||||||
|
self.assertEquals(channel.result["code"], b"200", channel.result)
|
||||||
|
|
||||||
|
# Check that we're getting HTML back.
|
||||||
|
content_type = channel.headers.getRawHeaders(b"Content-Type")
|
||||||
|
self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result)
|
||||||
|
|
||||||
|
# Check that the HTML we're getting is the one we expect when reusing a
|
||||||
|
# token. The account expiration date should not have changed.
|
||||||
|
expected_html = self.hs.config.account_validity.account_validity_account_previously_renewed_template.render(
|
||||||
|
expiration_ts=expiration_ts
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
channel.result["body"], expected_html.encode("utf8"), channel.result
|
channel.result["body"], expected_html.encode("utf8"), channel.result
|
||||||
)
|
)
|
||||||
|
@ -531,15 +549,14 @@ class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertEquals(channel.result["code"], b"404", channel.result)
|
self.assertEquals(channel.result["code"], b"404", channel.result)
|
||||||
|
|
||||||
# Check that we're getting HTML back.
|
# Check that we're getting HTML back.
|
||||||
content_type = None
|
content_type = channel.headers.getRawHeaders(b"Content-Type")
|
||||||
for header in channel.result.get("headers", []):
|
self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result)
|
||||||
if header[0] == b"Content-Type":
|
|
||||||
content_type = header[1]
|
|
||||||
self.assertEqual(content_type, b"text/html; charset=utf-8", channel.result)
|
|
||||||
|
|
||||||
# Check that the HTML we're getting is the one we expect when using an
|
# Check that the HTML we're getting is the one we expect when using an
|
||||||
# invalid/unknown token.
|
# invalid/unknown token.
|
||||||
expected_html = self.hs.config.account_validity.invalid_token_html_content
|
expected_html = (
|
||||||
|
self.hs.config.account_validity.account_validity_invalid_token_template.render()
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
channel.result["body"], expected_html.encode("utf8"), channel.result
|
channel.result["body"], expected_html.encode("utf8"), channel.result
|
||||||
)
|
)
|
||||||
|
@ -647,7 +664,12 @@ class AccountValidityBackgroundJobTestCase(unittest.HomeserverTestCase):
|
||||||
config["account_validity"] = {"enabled": False}
|
config["account_validity"] = {"enabled": False}
|
||||||
|
|
||||||
self.hs = self.setup_test_homeserver(config=config)
|
self.hs = self.setup_test_homeserver(config=config)
|
||||||
self.hs.config.account_validity.period = self.validity_period
|
|
||||||
|
# We need to set these directly, instead of in the homeserver config dict above.
|
||||||
|
# This is due to account validity-related config options not being read by
|
||||||
|
# Synapse when account_validity.enabled is False.
|
||||||
|
self.hs.get_datastore()._account_validity_period = self.validity_period
|
||||||
|
self.hs.get_datastore()._account_validity_startup_job_max_delta = self.max_delta
|
||||||
|
|
||||||
self.store = self.hs.get_datastore()
|
self.store = self.hs.get_datastore()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue