From 8a3175f02243cb32a41e451129a7600a9f0b26b7 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Mon, 19 Aug 2019 15:22:30 +0100 Subject: [PATCH 1/8] Remove trusted_third_party_id_servers functionality (#5875) Part of https://github.com/matrix-org/synapse/pull/5835 Removes the concept of a trusted identity server. The original concept of having the homeserver keep a list of trusted identity servers was to mitigate the danger of having a malicious IS handling password reset or registration emails. Since #5835 gives the homeserver the ability to do both of these things itself, as well as the requirement for it to choose an external, trusted identity server if it so chooses, the homeserver no longer needs to constrain which identity servers are chosen (which was traditionally a choice given to the client). Thus, we can safely the functionality of `trusted_third_party_id_servers`. It does need to stay in the config file for the foreseeable though, as it is currently used by a background job for old 3PIDs, which were bound before Synapse tracked which IS a 3PID was bound to. The identity servers in `trusted_third_party_id_servers` are likely candidates to be where a user registered their 3PID, so this is used during the background update. This background job was added in v0.99.4, so we're catering for those still updating from before v0.99.4. --- changelog.d/5875.misc | 1 + contrib/cmdclient/console.py | 2 ++ docs/sample_config.yaml | 8 +++++++ synapse/config/registration.py | 8 +++++++ synapse/handlers/identity.py | 43 +--------------------------------- 5 files changed, 20 insertions(+), 42 deletions(-) create mode 100644 changelog.d/5875.misc diff --git a/changelog.d/5875.misc b/changelog.d/5875.misc new file mode 100644 index 000000000000..e188c28d2f84 --- /dev/null +++ b/changelog.d/5875.misc @@ -0,0 +1 @@ +Deprecate the `trusted_third_party_id_servers` option. \ No newline at end of file diff --git a/contrib/cmdclient/console.py b/contrib/cmdclient/console.py index af8f39c8c279..05743de68397 100755 --- a/contrib/cmdclient/console.py +++ b/contrib/cmdclient/console.py @@ -37,6 +37,8 @@ CONFIG_JSON = "cmdclient_config.json" +# TODO: The concept of trusted identity servers has been deprecated. This option and checks +# should be removed TRUSTED_ID_SERVERS = ["localhost:8001"] diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 0c6be30e513d..c208f7f4bd93 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -890,6 +890,14 @@ uploads_path: "DATADIR/uploads" # Also defines the ID server which will be called when an account is # deactivated (one will be picked arbitrarily). # +# Note: This option is deprecated. Since v0.99.4, Synapse has tracked which identity +# server a 3PID has been bound to. For 3PIDs bound before then, Synapse runs a +# background migration script, informing itself that the identity server all of its +# 3PIDs have been bound to is likely one of the below. +# +# As of Synapse v1.4.0, all other functionality of this option has been deprecated, and +# it is now solely used for the purposes of the background migration script, and can be +# removed once it has run. #trusted_third_party_id_servers: # - matrix.org # - vector.im diff --git a/synapse/config/registration.py b/synapse/config/registration.py index e2bee3c116b4..df3491568c1f 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -257,6 +257,14 @@ def generate_config_section(self, generate_secrets=False, **kwargs): # Also defines the ID server which will be called when an account is # deactivated (one will be picked arbitrarily). # + # Note: This option is deprecated. Since v0.99.4, Synapse has tracked which identity + # server a 3PID has been bound to. For 3PIDs bound before then, Synapse runs a + # background migration script, informing itself that the identity server all of its + # 3PIDs have been bound to is likely one of the below. + # + # As of Synapse v1.4.0, all other functionality of this option has been deprecated, and + # it is now solely used for the purposes of the background migration script, and can be + # removed once it has run. #trusted_third_party_id_servers: # - matrix.org # - vector.im diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index d199521b5878..f342ad1bfb5f 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -23,12 +23,7 @@ from twisted.internet import defer -from synapse.api.errors import ( - CodeMessageException, - Codes, - HttpResponseException, - SynapseError, -) +from synapse.api.errors import CodeMessageException, HttpResponseException, SynapseError from ._base import BaseHandler @@ -42,25 +37,6 @@ def __init__(self, hs): self.http_client = hs.get_simple_http_client() self.federation_http_client = hs.get_http_client() - self.trusted_id_servers = set(hs.config.trusted_third_party_id_servers) - self.trust_any_id_server_just_for_testing_do_not_use = ( - hs.config.use_insecure_ssl_client_just_for_testing_do_not_use - ) - - def _should_trust_id_server(self, id_server): - if id_server not in self.trusted_id_servers: - if self.trust_any_id_server_just_for_testing_do_not_use: - logger.warn( - "Trusting untrustworthy ID server %r even though it isn't" - " in the trusted id list for testing because" - " 'use_insecure_ssl_client_just_for_testing_do_not_use'" - " is set in the config", - id_server, - ) - else: - return False - return True - @defer.inlineCallbacks def threepid_from_creds(self, creds): if "id_server" in creds: @@ -77,13 +53,6 @@ def threepid_from_creds(self, creds): else: raise SynapseError(400, "No client_secret in creds") - if not self._should_trust_id_server(id_server): - logger.warn( - "%s is not a trusted ID server: rejecting 3pid " + "credentials", - id_server, - ) - return None - try: data = yield self.http_client.get_json( "https://%s%s" @@ -230,11 +199,6 @@ def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server): def requestEmailToken( self, id_server, email, client_secret, send_attempt, next_link=None ): - if not self._should_trust_id_server(id_server): - raise SynapseError( - 400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED - ) - params = { "email": email, "client_secret": client_secret, @@ -259,11 +223,6 @@ def requestEmailToken( def requestMsisdnToken( self, id_server, country, phone_number, client_secret, send_attempt, **kwargs ): - if not self._should_trust_id_server(id_server): - raise SynapseError( - 400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED - ) - params = { "country": country, "phone_number": phone_number, From 7739f23995c61666786e30fadce68dd86f90271b Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 21 Aug 2019 18:13:10 +0200 Subject: [PATCH 2/8] Replace trust_identity_server_for_password_resets with account_threepid_delegate (#5876) Replaces the trust_identity_server_for_password_resets boolean option with a account_threepid_delegate str option that defines which identity server to use to handle password resets and registration if the homeserver does not want to or is unable to handle these tasks itself. Having this option being something other than null or "" gives the same indication as True for trust_identity_server_for_password_resets. The domain of the identity server is actually used in #5835 --- changelog.d/5876.misc | 1 + docs/sample_config.yaml | 27 +++--- synapse/config/emailconfig.py | 71 +++++++++----- synapse/config/registration.py | 15 +++ synapse/handlers/auth.py | 5 +- synapse/handlers/identity.py | 67 +++++++++++-- synapse/rest/client/v2_alpha/account.py | 118 +++++++++++++++++------ synapse/rest/client/v2_alpha/register.py | 77 ++++++++++++--- 8 files changed, 290 insertions(+), 91 deletions(-) create mode 100644 changelog.d/5876.misc diff --git a/changelog.d/5876.misc b/changelog.d/5876.misc new file mode 100644 index 000000000000..c1c289d05a26 --- /dev/null +++ b/changelog.d/5876.misc @@ -0,0 +1 @@ +Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegate`. \ No newline at end of file diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index c208f7f4bd93..81b24188050d 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -902,6 +902,20 @@ uploads_path: "DATADIR/uploads" # - matrix.org # - vector.im +# Handle threepid (email/phone etc) registration and password resets +# through a *trusted* identity server. Note that this allows the configured +# identity server to reset passwords for accounts. +# +# If this option is not defined and SMTP options have not been +# configured, registration by email and resetting user passwords via +# email will be disabled +# +# Otherwise, to enable set this option to the reachable domain name, including protocol +# definition, for an identity server +# (e.g "https://matrix.org", "http://localhost:8090") +# +#account_threepid_delegate: "" + # Users who register on this homeserver will automatically be joined # to these rooms # @@ -1163,19 +1177,6 @@ password_config: # # # riot_base_url: "http://localhost/riot" # -# # Enable sending password reset emails via the configured, trusted -# # identity servers -# # -# # IMPORTANT! This will give a malicious or overtaken identity server -# # the ability to reset passwords for your users! Make absolutely sure -# # that you want to do this! It is strongly recommended that password -# # reset emails be sent by the homeserver instead -# # -# # If this option is set to false and SMTP options have not been -# # configured, resetting user passwords via email will be disabled -# # -# #trust_identity_server_for_password_resets: false -# # # Configure the time that a validation email or text message code # # will expire after sending # # diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 36d01a10af70..58c73ff70f0d 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -20,6 +20,7 @@ # This file can't be called email.py because if it is, we cannot: import email.utils import os +from enum import Enum import pkg_resources @@ -74,19 +75,39 @@ def read_config(self, config, **kwargs): "renew_at" ) - email_trust_identity_server_for_password_resets = email_config.get( - "trust_identity_server_for_password_resets", False + self.email_threepid_behaviour = ( + # Have Synapse handle the email sending if account_threepid_delegate + # is not defined + ThreepidBehaviour.REMOTE + if self.account_threepid_delegate + else ThreepidBehaviour.LOCAL ) - self.email_password_reset_behaviour = ( - "remote" if email_trust_identity_server_for_password_resets else "local" - ) - self.password_resets_were_disabled_due_to_email_config = False - if self.email_password_reset_behaviour == "local" and email_config == {}: + # Prior to Synapse v1.4.0, there was another option that defined whether Synapse would + # use an identity server to password reset tokens on its behalf. We now warn the user + # if they have this set and tell them to use the updated option, while using a default + # identity server in the process. + self.using_identity_server_from_trusted_list = False + if config.get("trust_identity_server_for_password_resets", False) is True: + # Use the first entry in self.trusted_third_party_id_servers instead + if self.trusted_third_party_id_servers: + self.account_threepid_delegate = self.trusted_third_party_id_servers[0] + self.using_identity_server_from_trusted_list = True + else: + raise ConfigError( + "Attempted to use an identity server from" + '"trusted_third_party_id_servers" but it is empty.' + ) + + self.local_threepid_emails_disabled_due_to_config = False + if ( + self.email_threepid_behaviour == ThreepidBehaviour.LOCAL + and email_config == {} + ): # We cannot warn the user this has happened here # Instead do so when a user attempts to reset their password - self.password_resets_were_disabled_due_to_email_config = True + self.local_threepid_emails_disabled_due_to_config = True - self.email_password_reset_behaviour = "off" + self.email_threepid_behaviour = ThreepidBehaviour.OFF # Get lifetime of a validation token in milliseconds self.email_validation_token_lifetime = self.parse_duration( @@ -96,7 +117,7 @@ def read_config(self, config, **kwargs): if ( self.email_enable_notifs or account_validity_renewal_enabled - or self.email_password_reset_behaviour == "local" + or self.email_threepid_behaviour == ThreepidBehaviour.LOCAL ): # make sure we can import the required deps import jinja2 @@ -106,7 +127,7 @@ def read_config(self, config, **kwargs): jinja2 bleach - if self.email_password_reset_behaviour == "local": + if self.email_threepid_behaviour == ThreepidBehaviour.LOCAL: required = ["smtp_host", "smtp_port", "notif_from"] missing = [] @@ -239,19 +260,6 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # # # riot_base_url: "http://localhost/riot" # - # # Enable sending password reset emails via the configured, trusted - # # identity servers - # # - # # IMPORTANT! This will give a malicious or overtaken identity server - # # the ability to reset passwords for your users! Make absolutely sure - # # that you want to do this! It is strongly recommended that password - # # reset emails be sent by the homeserver instead - # # - # # If this option is set to false and SMTP options have not been - # # configured, resetting user passwords via email will be disabled - # # - # #trust_identity_server_for_password_resets: false - # # # Configure the time that a validation email or text message code # # will expire after sending # # @@ -289,3 +297,18 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # #password_reset_template_success_html: password_reset_success.html # #password_reset_template_failure_html: password_reset_failure.html """ + + +class ThreepidBehaviour(Enum): + """ + Enum to define the behaviour of Synapse with regards to when it contacts an identity + server for 3pid registration and password resets + + REMOTE = use an external server to send tokens + LOCAL = send tokens ourselves + OFF = disable registration via 3pid and password resets + """ + + REMOTE = "remote" + LOCAL = "local" + OFF = "off" diff --git a/synapse/config/registration.py b/synapse/config/registration.py index df3491568c1f..b9d5e81b1dd5 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -99,6 +99,7 @@ def read_config(self, config, **kwargs): self.trusted_third_party_id_servers = config.get( "trusted_third_party_id_servers", ["matrix.org", "vector.im"] ) + self.account_threepid_delegate = config.get("account_threepid_delegate") self.default_identity_server = config.get("default_identity_server") self.allow_guest_access = config.get("allow_guest_access", False) @@ -269,6 +270,20 @@ def generate_config_section(self, generate_secrets=False, **kwargs): # - matrix.org # - vector.im + # Handle threepid (email/phone etc) registration and password resets + # through a *trusted* identity server. Note that this allows the configured + # identity server to reset passwords for accounts. + # + # If this option is not defined and SMTP options have not been + # configured, registration by email and resetting user passwords via + # email will be disabled + # + # Otherwise, to enable set this option to the reachable domain name, including protocol + # definition, for an identity server + # (e.g "https://matrix.org", "http://localhost:8090") + # + #account_threepid_delegate: "" + # Users who register on this homeserver will automatically be joined # to these rooms # diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 0f3ebf7ef887..091512aa536d 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -38,6 +38,7 @@ UserDeactivatedError, ) from synapse.api.ratelimiting import Ratelimiter +from synapse.config.emailconfig import ThreepidBehaviour from synapse.logging.context import defer_to_thread from synapse.module_api import ModuleApi from synapse.types import UserID @@ -460,10 +461,10 @@ def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs): logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,)) if ( not password_servlet - or self.hs.config.email_password_reset_behaviour == "remote" + or self.hs.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE ): threepid = yield identity_handler.threepid_from_creds(threepid_creds) - elif self.hs.config.email_password_reset_behaviour == "local": + elif self.hs.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: row = yield self.store.get_threepid_validation_session( medium, threepid_creds["client_secret"], diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index f342ad1bfb5f..6bdd1c18c37d 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -36,6 +36,7 @@ def __init__(self, hs): self.http_client = hs.get_simple_http_client() self.federation_http_client = hs.get_http_client() + self.hs = hs @defer.inlineCallbacks def threepid_from_creds(self, creds): @@ -199,19 +200,40 @@ def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server): def requestEmailToken( self, id_server, email, client_secret, send_attempt, next_link=None ): + """ + Request an external server send an email on our behalf for the purposes of threepid + validation. + + Args: + id_server (str): The identity server to proxy to + email (str): The email to send the message to + client_secret (str): The unique client_secret sends by the user + send_attempt (int): Which attempt this is + next_link: A link to redirect the user to once they submit the token + + Returns: + The json response body from the server + """ params = { "email": email, "client_secret": client_secret, "send_attempt": send_attempt, } - if next_link: - params.update({"next_link": next_link}) + params["next_link"] = next_link + + if self.hs.config.using_identity_server_from_trusted_list: + # Warn that a deprecated config option is in use + logger.warn( + 'The config option "trust_identity_server_for_password_resets" ' + 'has been replaced by "account_threepid_delegate". ' + "Please consult the sample config at docs/sample_config.yaml for " + "details and update your config file." + ) try: data = yield self.http_client.post_json_get_json( - "https://%s%s" - % (id_server, "/_matrix/identity/api/v1/validate/email/requestToken"), + id_server + "/_matrix/identity/api/v1/validate/email/requestToken", params, ) return data @@ -221,20 +243,49 @@ def requestEmailToken( @defer.inlineCallbacks def requestMsisdnToken( - self, id_server, country, phone_number, client_secret, send_attempt, **kwargs + self, + id_server, + country, + phone_number, + client_secret, + send_attempt, + next_link=None, ): + """ + Request an external server send an SMS message on our behalf for the purposes of + threepid validation. + Args: + id_server (str): The identity server to proxy to + country (str): The country code of the phone number + phone_number (str): The number to send the message to + client_secret (str): The unique client_secret sends by the user + send_attempt (int): Which attempt this is + next_link: A link to redirect the user to once they submit the token + + Returns: + The json response body from the server + """ params = { "country": country, "phone_number": phone_number, "client_secret": client_secret, "send_attempt": send_attempt, } - params.update(kwargs) + if next_link: + params["next_link"] = next_link + + if self.hs.config.using_identity_server_from_trusted_list: + # Warn that a deprecated config option is in use + logger.warn( + 'The config option "trust_identity_server_for_password_resets" ' + 'has been replaced by "account_threepid_delegate". ' + "Please consult the sample config at docs/sample_config.yaml for " + "details and update your config file." + ) try: data = yield self.http_client.post_json_get_json( - "https://%s%s" - % (id_server, "/_matrix/identity/api/v1/validate/msisdn/requestToken"), + id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken", params, ) return data diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 934ed5d16d1f..2c649259a20b 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -24,6 +24,7 @@ from synapse.api.constants import LoginType from synapse.api.errors import Codes, SynapseError, ThreepidValidationError +from synapse.config.emailconfig import ThreepidBehaviour from synapse.http.server import finish_request from synapse.http.servlet import ( RestServlet, @@ -50,7 +51,7 @@ def __init__(self, hs): self.config = hs.config self.identity_handler = hs.get_handlers().identity_handler - if self.config.email_password_reset_behaviour == "local": + if self.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: from synapse.push.mailer import Mailer, load_jinja2_templates templates = load_jinja2_templates( @@ -67,8 +68,8 @@ def __init__(self, hs): @defer.inlineCallbacks def on_POST(self, request): - if self.config.email_password_reset_behaviour == "off": - if self.config.password_resets_were_disabled_due_to_email_config: + if self.config.email_threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_emails_disabled_due_to_config: logger.warn( "User password resets have been disabled due to lack of email config" ) @@ -93,22 +94,32 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( "email", email ) - if existingUid is None: + if existing_user_id is None: raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) - if self.config.email_password_reset_behaviour == "remote": - if "id_server" not in body: - raise SynapseError(400, "Missing 'id_server' param in body") + if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + # Have the configured identity server handle the request + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, "Password reset by email is not supported on this homeserver" + ) - # Have the identity server handle the password reset flow ret = yield self.identity_handler.requestEmailToken( - body["id_server"], email, client_secret, send_attempt, next_link + self.hs.config.account_threepid_delegate, + email, + client_secret, + send_attempt, + next_link, ) - else: + elif self.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: # Send password reset emails from Synapse sid = yield self.send_password_reset( email, client_secret, send_attempt, next_link @@ -116,6 +127,10 @@ def on_POST(self, request): # Wrap the session id in a JSON object ret = {"sid": sid} + else: + raise SynapseError( + 400, "Password reset by email is not supported on this homeserver" + ) return (200, ret) @@ -127,6 +142,8 @@ def send_password_reset(self, email, client_secret, send_attempt, next_link=None email (str): The user's email address client_secret (str): The provided client secret send_attempt (int): Which send attempt this is + next_link (str|None): The link to redirect the user to upon success. No redirect + occurs if None Returns: The new session_id upon success @@ -202,11 +219,15 @@ def on_POST(self, request): body = parse_json_object_from_request(request) assert_params_in_dict( - body, - ["id_server", "client_secret", "country", "phone_number", "send_attempt"], + body, ["client_secret", "country", "phone_number", "send_attempt"] ) + client_secret = body["client_secret"] + country = body["country"] + phone_number = body["phone_number"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param - msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) + msisdn = phone_number_to_msisdn(country, phone_number) if not check_3pid_allowed(self.hs, "msisdn", msisdn): raise SynapseError( @@ -215,13 +236,37 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn) + existing_user_id = yield self.datastore.get_user_id_by_threepid( + "msisdn", msisdn + ) - if existingUid is None: + if existing_user_id is None: raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND) - ret = yield self.identity_handler.requestMsisdnToken(**body) - return (200, ret) + if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, + "Password reset by phone number is not supported on this homeserver", + ) + + ret = yield self.identity_handler.requestMsisdnToken( + self.config.account_threepid_delegate, + country, + phone_number, + client_secret, + send_attempt, + next_link, + ) + return (200, ret) + + raise SynapseError( + 400, "Password reset by phone number is not supported on this homeserver" + ) class PasswordResetSubmitTokenServlet(RestServlet): @@ -249,8 +294,8 @@ def on_GET(self, request, medium): raise SynapseError( 400, "This medium is currently not supported for password resets" ) - if self.config.email_password_reset_behaviour == "off": - if self.config.password_resets_were_disabled_due_to_email_config: + if self.config.email_threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_emails_disabled_due_to_config: logger.warn( "User password resets have been disabled due to lack of email config" ) @@ -457,7 +502,7 @@ def __init__(self, hs): self.hs = hs super(EmailThreepidRequestTokenRestServlet, self).__init__() self.identity_handler = hs.get_handlers().identity_handler - self.datastore = self.hs.get_datastore() + self.store = self.hs.get_datastore() @defer.inlineCallbacks def on_POST(self, request): @@ -465,6 +510,11 @@ def on_POST(self, request): assert_params_in_dict( body, ["id_server", "client_secret", "email", "send_attempt"] ) + id_server = "https://" + body["id_server"] # Assume https + client_secret = body["client_secret"] + email = body["email"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param if not check_3pid_allowed(self.hs, "email", body["email"]): raise SynapseError( @@ -473,14 +523,16 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.datastore.get_user_id_by_threepid( + existing_user_id = yield self.store.get_user_id_by_threepid( "email", body["email"] ) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) - ret = yield self.identity_handler.requestEmailToken(**body) + ret = yield self.identity_handler.requestEmailToken( + id_server, email, client_secret, send_attempt, next_link + ) return (200, ret) @@ -491,7 +543,7 @@ def __init__(self, hs): self.hs = hs super(MsisdnThreepidRequestTokenRestServlet, self).__init__() self.identity_handler = hs.get_handlers().identity_handler - self.datastore = self.hs.get_datastore() + self.store = self.hs.get_datastore() @defer.inlineCallbacks def on_POST(self, request): @@ -500,8 +552,14 @@ def on_POST(self, request): body, ["id_server", "client_secret", "country", "phone_number", "send_attempt"], ) + id_server = "https://" + body["id_server"] # Assume https + client_secret = body["client_secret"] + country = body["country"] + phone_number = body["phone_number"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param - msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) + msisdn = phone_number_to_msisdn(country, phone_number) if not check_3pid_allowed(self.hs, "msisdn", msisdn): raise SynapseError( @@ -510,12 +568,14 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn) + existing_user_id = yield self.store.get_user_id_by_threepid("msisdn", msisdn) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE) - ret = yield self.identity_handler.requestMsisdnToken(**body) + ret = yield self.identity_handler.requestMsisdnToken( + id_server, country, phone_number, client_secret, send_attempt, next_link + ) return (200, ret) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 05ea1459e356..d866cdb1c7c6 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -31,6 +31,7 @@ SynapseError, UnrecognizedRequestError, ) +from synapse.config.emailconfig import ThreepidBehaviour from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.config.server import is_threepid_reserved from synapse.http.servlet import ( @@ -76,9 +77,13 @@ def __init__(self, hs): def on_POST(self, request): body = parse_json_object_from_request(request) - assert_params_in_dict( - body, ["id_server", "client_secret", "email", "send_attempt"] - ) + assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) + + # Extract params from body + client_secret = body["client_secret"] + email = body["email"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param if not check_3pid_allowed(self.hs, "email", body["email"]): raise SynapseError( @@ -87,14 +92,30 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( "email", body["email"] ) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) - ret = yield self.identity_handler.requestEmailToken(**body) + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, "Registration by email is not supported on this homeserver" + ) + + ret = yield self.identity_handler.requestEmailToken( + self.hs.config.account_threepid_delegate, + email, + client_secret, + send_attempt, + next_link, + ) + return (200, ret) @@ -115,11 +136,15 @@ def on_POST(self, request): body = parse_json_object_from_request(request) assert_params_in_dict( - body, - ["id_server", "client_secret", "country", "phone_number", "send_attempt"], + body, ["client_secret", "country", "phone_number", "send_attempt"] ) + client_secret = body["client_secret"] + country = body["country"] + phone_number = body["phone_number"] + send_attempt = body["send_attempt"] + next_link = body.get("next_link") # Optional param - msisdn = phone_number_to_msisdn(body["country"], body["phone_number"]) + msisdn = phone_number_to_msisdn(country, phone_number) if not check_3pid_allowed(self.hs, "msisdn", msisdn): raise SynapseError( @@ -128,17 +153,39 @@ def on_POST(self, request): Codes.THREEPID_DENIED, ) - existingUid = yield self.hs.get_datastore().get_user_id_by_threepid( + existing_user_id = yield self.hs.get_datastore().get_user_id_by_threepid( "msisdn", msisdn ) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError( 400, "Phone number is already in use", Codes.THREEPID_IN_USE ) - ret = yield self.identity_handler.requestMsisdnToken(**body) - return (200, ret) + if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, + "Registration by phone number is not supported on this homeserver", + ) + + ret = yield self.identity_handler.requestMsisdnToken( + self.config.account_threepid_delegate, + country, + phone_number, + client_secret, + send_attempt, + next_link, + ) + return (200, ret) + + raise SynapseError( + 400, "Registration by phone number is not supported on this homeserver" + ) class UsernameAvailabilityRestServlet(RestServlet): @@ -453,11 +500,11 @@ def on_POST(self, request): medium = auth_result[login_type]["medium"] address = auth_result[login_type]["address"] - existingUid = yield self.store.get_user_id_by_threepid( + existing_user_id = yield self.store.get_user_id_by_threepid( medium, address ) - if existingUid is not None: + if existing_user_id is not None: raise SynapseError( 400, "%s is already in use" % medium, From 8cd8124dfb8dd0486df9ffd4b2c0f982cb058591 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 28 Aug 2019 14:11:38 +0200 Subject: [PATCH 3/8] Add flag in /versions for whether clients should send id_server params (#5868) When a client is registering an account, they need to know whether they should supply an `id_server` param (the contents being the domain of an identity server) to the server in order to specify which `id_server` to send their email from. Beginning from the branch this PR is getting merged into, the homeserver has the capability to send registration emails, as well as instead specify which identity server should send them. There's no longer any need for the client to specify an identity server here. This flag will also be used in other cases, such as password reset and binding 3PIDs, so it's preferable to not just put it in the registration parameters. We will remove this flag in the future when the spec drops support from needing `id_server` in `/register` and other endpoints, and Synapse drops support for older spec versions. --- changelog.d/5868.feature | 1 + synapse/app/client_reader.py | 2 +- synapse/rest/__init__.py | 2 +- synapse/rest/client/versions.py | 24 +++++++++++++++++++++--- 4 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 changelog.d/5868.feature diff --git a/changelog.d/5868.feature b/changelog.d/5868.feature new file mode 100644 index 000000000000..69605c1ae134 --- /dev/null +++ b/changelog.d/5868.feature @@ -0,0 +1 @@ +Add `m.require_identity_server` key to `/versions`'s `unstable_features` section. \ No newline at end of file diff --git a/synapse/app/client_reader.py b/synapse/app/client_reader.py index 721bb5b119f3..7427620d1609 100644 --- a/synapse/app/client_reader.py +++ b/synapse/app/client_reader.py @@ -119,7 +119,7 @@ def _listen_http(self, listener_config): KeyChangesServlet(self).register(resource) VoipRestServlet(self).register(resource) PushRuleRestServlet(self).register(resource) - VersionsRestServlet().register(resource) + VersionsRestServlet(self).register(resource) resources.update({"/_matrix/client": resource}) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 1d20b96d0354..4a1fc2ec2bfe 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -73,7 +73,7 @@ def __init__(self, hs): @staticmethod def register_servlets(client_resource, hs): - versions.register_servlets(client_resource) + versions.register_servlets(hs, client_resource) # Deprecated in r0 initial_sync.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index 0e0919163267..e7f488376f35 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -24,6 +24,10 @@ class VersionsRestServlet(RestServlet): PATTERNS = [re.compile("^/_matrix/client/versions$")] + def __init__(self, hs): + super(VersionsRestServlet, self).__init__() + self.config = hs.config + def on_GET(self, request): return ( 200, @@ -44,10 +48,24 @@ def on_GET(self, request): "r0.5.0", ], # as per MSC1497: - "unstable_features": {"m.lazy_load_members": True}, + "unstable_features": { + "m.lazy_load_members": True, + # Advertise to clients whether they need not include an `id_server` + # parameter during registration or password reset, as Synapse now decides + # itself which identity server to use (or none at all). + # + # This is also used by a client when they wish to bind a 3PID to their + # account, but not bind it to an identity server, the endpoint for which + # also requires `id_server`. If the homeserver is handling 3PID + # verification itself, there is no need to ask the user for `id_server` to + # be supplied. + "m.require_identity_server": ( + self.config.account_threepid_delegate is None + ), + }, }, ) -def register_servlets(http_server): - VersionsRestServlet().register(http_server) +def register_servlets(hs, http_server): + VersionsRestServlet(hs).register(http_server) From 75f7a7b7bb30f3a915bc396a2e7fa4ac58805975 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Fri, 30 Aug 2019 11:00:32 +0100 Subject: [PATCH 4/8] [1/2] Allow homeservers to send registration emails | Sending the email (#5835) ~~Fixes https://github.com/matrix-org/synapse/issues/5833~~ Moved out to ~~https://github.com/matrix-org/synapse/pull/5863~~ Part of fixing https://github.com/matrix-org/synapse/issues/5751 Decouples the activity of sending registration emails and binding them to an identity server. This PR simply sends the registration email, but clicking it does not approve the user for registration. That will come in PR #2. Some of this makes use of existing stuff for sending password reset emails from Synapse. Some work was done to make that stuff even more generic. Upgrade notes: * There is a new top-level config option, `account_threepid_delegate` which defines the address of an identity server that you would like to send registration/password reset emails on your behalf. The option `email.trust_identity_server_for_password_resets` has been replaced by this. If you set `email.trust_identity_server_for_password_resets` in your config to `true`, please remove it and configure `account_threepid_delegate` instead. The [sample config](https://github.com/matrix-org/synapse/blob/anoa/reg_email_sending_email/docs/sample_config.yaml) has information on how to configure it. Note: This PR does not allow homeservers to send emails when simply adding an email to your account. That will come after this and will be blocked on a new MSC. Part [2/2] will be successfully completing the registration step when `/submit_token` is hit with the correct details, and clearing out the `password_servlet` flag stuff, which is no longer needed. Will be a much smaller PR than this one. ~~Requires https://github.com/matrix-org/synapse/pull/5863~~ has been merged into the base branch. ~~Requires https://github.com/matrix-org/synapse/pull/5876~~ has been merged into the base branch. --- UPGRADE.rst | 62 ++++++++ changelog.d/5835.feature | 1 + docs/sample_config.yaml | 11 ++ synapse/config/emailconfig.py | 66 ++++++-- synapse/handlers/auth.py | 4 +- synapse/handlers/identity.py | 79 ++++++++++ synapse/handlers/register.py | 12 -- synapse/push/mailer.py | 34 +++- synapse/res/templates/password_reset.html | 2 +- synapse/res/templates/password_reset.txt | 4 +- .../res/templates/password_reset_failure.html | 4 +- synapse/res/templates/registration.html | 11 ++ synapse/res/templates/registration.txt | 10 ++ .../res/templates/registration_failure.html | 6 + .../res/templates/registration_success.html | 6 + synapse/rest/client/v2_alpha/_base.py | 2 + synapse/rest/client/v2_alpha/account.py | 129 ++++----------- synapse/rest/client/v2_alpha/register.py | 147 ++++++++++++++++-- 18 files changed, 436 insertions(+), 154 deletions(-) create mode 100644 changelog.d/5835.feature create mode 100644 synapse/res/templates/registration.html create mode 100644 synapse/res/templates/registration.txt create mode 100644 synapse/res/templates/registration_failure.html create mode 100644 synapse/res/templates/registration_success.html diff --git a/UPGRADE.rst b/UPGRADE.rst index cf228c7c529b..99e8da4b525f 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -49,6 +49,55 @@ returned by the Client-Server API: # configured on port 443. curl -kv https:///_matrix/client/versions 2>&1 | grep "Server:" +Upgrading to v1.4.0 +=================== + +Config options +-------------- + +**Note: Registration by email address or phone number will not work in this release unless +some config options are changed from their defaults.** + +This is due to Synapse v1.4.0 now defaulting to sending registration and password reset tokens +itself. This is for security reasons as well as putting less reliance on identity servers. +However, currently Synapse only supports sending emails, and does not have support for +phone-based password reset or account registration. If Synapse is configured to handle these on +its own, phone-based password resets and registration will be disabled. For Synapse to send +emails, the ``email`` block of the config must be filled out. If not, then password resets and +registration via email will be disabled entirely. + +This release also deprecates the ``email.trust_identity_server_for_password_resets`` option +and replaces it with ``account_threepid_delegate``. This option defines whether the homeserver +should delegate an external server (typically an `identity server +`_) to handle sending password reset +or registration messages via email or SMS. + +If ``email.trust_identity_server_for_password_resets`` was changed from its default to +``true``, and ``account_threepid_delegate`` is not set to an identity server domain, then the +server handling password resets and registration via third-party addresses will be set to the +first entry in the Synapse config's ``trusted_third_party_id_servers`` entry. If no domains are +configured, Synapse will throw an error on startup. + +If ``email.trust_identity_server_for_password_resets`` is not set to ``true`` and +``account_threepid_delegate`` is not set to a domain, then Synapse will attempt to send +password reset and registration messages itself. + +Email templates +--------------- + +If you have configured a custom template directory with the ``email.template_dir`` option, be +aware that there are new templates regarding registration. ``registration.html`` and +``registration.txt`` have been added and contain the text that is sent to a client upon +registering via email address. + +``registration_success.html`` and ``registration_failure.html`` are templates containing HTML +that will be shown to the user when they click the link in their registration email (if a +redirect URL is not configured), either showing them a success or failure page. + +Synapse will expect these files to exist inside the configured template directory. To view the +default templates, see `synapse/res/templates +`_. + Upgrading to v1.2.0 =================== @@ -132,6 +181,19 @@ server for password resets, set ``trust_identity_server_for_password_resets`` to See the `sample configuration file `_ for more details on these settings. +New email templates +--------------- +Some new templates have been added to the default template directory for the purpose of the +homeserver sending its own password reset emails. If you have configured a custom +``template_dir`` in your Synapse config, these files will need to be added. + +``password_reset.html`` and ``password_reset.txt`` are HTML and plain text templates +respectively that contain the contents of what will be emailed to the user upon attempting to +reset their password via email. ``password_reset_success.html`` and +``password_reset_failure.html`` are HTML files that the content of which (assuming no redirect +URL is set) will be shown to the user after they attempt to click the link in the email sent +to them. + Upgrading to v0.99.0 ==================== diff --git a/changelog.d/5835.feature b/changelog.d/5835.feature new file mode 100644 index 000000000000..3e8bf5068d02 --- /dev/null +++ b/changelog.d/5835.feature @@ -0,0 +1 @@ +Add the ability to send registration emails from the homeserver rather than delegating to an identity server. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index b9e026115e47..8603008ec048 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -1217,11 +1217,22 @@ password_config: # #password_reset_template_html: password_reset.html # #password_reset_template_text: password_reset.txt # +# # Templates for registration emails sent by the homeserver +# # +# #registration_template_html: registration.html +# #registration_template_text: registration.txt +# # # Templates for password reset success and failure pages that a user # # will see after attempting to reset their password # # # #password_reset_template_success_html: password_reset_success.html # #password_reset_template_failure_html: password_reset_failure.html +# +# # Templates for registration success and failure pages that a user +# # will see after attempting to register using an email or phone +# # +# #registration_template_success_html: registration_success.html +# #registration_template_failure_html: registration_failure.html #password_providers: diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index d7b59faa3f26..874166b57938 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -75,7 +75,7 @@ def read_config(self, config, **kwargs): "renew_at" ) - self.email_threepid_behaviour = ( + self.threepid_behaviour = ( # Have Synapse handle the email sending if account_threepid_delegate # is not defined ThreepidBehaviour.REMOTE @@ -87,9 +87,14 @@ def read_config(self, config, **kwargs): # if they have this set and tell them to use the updated option, while using a default # identity server in the process. self.using_identity_server_from_trusted_list = False - if config.get("trust_identity_server_for_password_resets", False) is True: + if ( + not self.account_threepid_delegate + and config.get("trust_identity_server_for_password_resets", False) is True + ): # Use the first entry in self.trusted_third_party_id_servers instead if self.trusted_third_party_id_servers: + # XXX: It's a little confusing that account_threepid_delegate is modifed + # both in RegistrationConfig and here. We should factor this bit out self.account_threepid_delegate = self.trusted_third_party_id_servers[0] self.using_identity_server_from_trusted_list = True else: @@ -98,16 +103,13 @@ def read_config(self, config, **kwargs): '"trusted_third_party_id_servers" but it is empty.' ) - self.local_threepid_emails_disabled_due_to_config = False - if ( - self.email_threepid_behaviour == ThreepidBehaviour.LOCAL - and email_config == {} - ): + self.local_threepid_handling_disabled_due_to_email_config = False + if self.threepid_behaviour == ThreepidBehaviour.LOCAL and email_config == {}: # We cannot warn the user this has happened here # Instead do so when a user attempts to reset their password - self.local_threepid_emails_disabled_due_to_config = True + self.local_threepid_handling_disabled_due_to_email_config = True - self.email_threepid_behaviour = ThreepidBehaviour.OFF + self.threepid_behaviour = ThreepidBehaviour.OFF # Get lifetime of a validation token in milliseconds self.email_validation_token_lifetime = self.parse_duration( @@ -117,7 +119,7 @@ def read_config(self, config, **kwargs): if ( self.email_enable_notifs or account_validity_renewal_enabled - or self.email_threepid_behaviour == ThreepidBehaviour.LOCAL + or self.threepid_behaviour == ThreepidBehaviour.LOCAL ): # make sure we can import the required deps import jinja2 @@ -127,7 +129,7 @@ def read_config(self, config, **kwargs): jinja2 bleach - if self.email_threepid_behaviour == ThreepidBehaviour.LOCAL: + if self.threepid_behaviour == ThreepidBehaviour.LOCAL: required = ["smtp_host", "smtp_port", "notif_from"] missing = [] @@ -146,28 +148,45 @@ def read_config(self, config, **kwargs): % (", ".join(missing),) ) - # Templates for password reset emails + # These email templates have placeholders in them, and thus must be + # parsed using a templating engine during a request self.email_password_reset_template_html = email_config.get( "password_reset_template_html", "password_reset.html" ) self.email_password_reset_template_text = email_config.get( "password_reset_template_text", "password_reset.txt" ) + self.email_registration_template_html = email_config.get( + "registration_template_html", "registration.html" + ) + self.email_registration_template_text = email_config.get( + "registration_template_text", "registration.txt" + ) self.email_password_reset_template_failure_html = email_config.get( "password_reset_template_failure_html", "password_reset_failure.html" ) - # This template does not support any replaceable variables, so we will - # read it from the disk once during setup + self.email_registration_template_failure_html = email_config.get( + "registration_template_failure_html", "registration_failure.html" + ) + + # These templates do not support any placeholder variables, so we + # will read them from disk once during setup email_password_reset_template_success_html = email_config.get( "password_reset_template_success_html", "password_reset_success.html" ) + email_registration_template_success_html = email_config.get( + "registration_template_success_html", "registration_success.html" + ) # Check templates exist for f in [ self.email_password_reset_template_html, self.email_password_reset_template_text, + self.email_registration_template_html, + self.email_registration_template_text, self.email_password_reset_template_failure_html, email_password_reset_template_success_html, + email_registration_template_success_html, ]: p = os.path.join(self.email_template_dir, f) if not os.path.isfile(p): @@ -177,9 +196,15 @@ def read_config(self, config, **kwargs): filepath = os.path.join( self.email_template_dir, email_password_reset_template_success_html ) - self.email_password_reset_template_success_html_content = self.read_file( + self.email_password_reset_template_success_html = self.read_file( filepath, "email.password_reset_template_success_html" ) + filepath = os.path.join( + self.email_template_dir, email_registration_template_success_html + ) + self.email_registration_template_success_html_content = self.read_file( + filepath, "email.registration_template_success_html" + ) if self.email_enable_notifs: required = [ @@ -291,11 +316,22 @@ def generate_config_section(self, config_dir_path, server_name, **kwargs): # #password_reset_template_html: password_reset.html # #password_reset_template_text: password_reset.txt # + # # Templates for registration emails sent by the homeserver + # # + # #registration_template_html: registration.html + # #registration_template_text: registration.txt + # # # Templates for password reset success and failure pages that a user # # will see after attempting to reset their password # # # #password_reset_template_success_html: password_reset_success.html # #password_reset_template_failure_html: password_reset_failure.html + # + # # Templates for registration success and failure pages that a user + # # will see after attempting to register using an email or phone + # # + # #registration_template_success_html: registration_success.html + # #registration_template_failure_html: registration_failure.html """ diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 091512aa536d..a59cd4e7f58c 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -461,10 +461,10 @@ def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs): logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,)) if ( not password_servlet - or self.hs.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE + or self.hs.config.threepid_behaviour == ThreepidBehaviour.REMOTE ): threepid = yield identity_handler.threepid_from_creds(threepid_creds) - elif self.hs.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: + elif self.hs.config.threepid_behaviour == ThreepidBehaviour.LOCAL: row = yield self.store.get_threepid_validation_session( medium, threepid_creds["client_secret"], diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index dc34eb707597..dbd86f670cd1 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -24,6 +24,7 @@ from twisted.internet import defer from synapse.api.errors import CodeMessageException, HttpResponseException, SynapseError +from synapse.util.stringutils import random_string from ._base import BaseHandler @@ -196,6 +197,84 @@ def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server): return changed + @defer.inlineCallbacks + def send_threepid_validation( + self, + email_address, + client_secret, + send_attempt, + send_email_func, + next_link=None, + ): + """Send a threepid validation email for password reset or + registration purposes + + Args: + email_address (str): The user's email address + client_secret (str): The provided client secret + send_attempt (int): Which send attempt this is + send_email_func (func): A function that takes an email address, token, + client_secret and session_id, sends an email + and returns a Deferred. + next_link (str|None): The URL to redirect the user to after validation + + Returns: + The new session_id upon success + + Raises: + SynapseError is an error occurred when sending the email + """ + # Check that this email/client_secret/send_attempt combo is new or + # greater than what we've seen previously + session = yield self.store.get_threepid_validation_session( + "email", client_secret, address=email_address, validated=False + ) + + # Check to see if a session already exists and that it is not yet + # marked as validated + if session and session.get("validated_at") is None: + session_id = session["session_id"] + last_send_attempt = session["last_send_attempt"] + + # Check that the send_attempt is higher than previous attempts + if send_attempt <= last_send_attempt: + # If not, just return a success without sending an email + return session_id + else: + # An non-validated session does not exist yet. + # Generate a session id + session_id = random_string(16) + + # Generate a new validation token + token = random_string(32) + + # Send the mail with the link containing the token, client_secret + # and session_id + try: + yield send_email_func(email_address, token, client_secret, session_id) + except Exception: + logger.exception( + "Error sending threepid validation email to %s", email_address + ) + raise SynapseError(500, "An error was encountered when sending the email") + + token_expires = ( + self.hs.clock.time_msec() + self.hs.config.email_validation_token_lifetime + ) + + yield self.store.start_or_continue_validation_session( + "email", + email_address, + session_id, + client_secret, + send_attempt, + next_link, + token, + token_expires, + ) + + return session_id + @defer.inlineCallbacks def requestEmailToken( self, id_server, email, client_secret, send_attempt, next_link=None diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 4631fab94e39..dce2b7afbfd2 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -414,18 +414,6 @@ def register_email(self, threepidCreds): if not check_3pid_allowed(self.hs, threepid["medium"], threepid["address"]): raise RegistrationError(403, "Third party identifier is not allowed") - @defer.inlineCallbacks - def bind_emails(self, user_id, threepidCreds): - """Links emails with a user ID and informs an identity server. - - Used only by c/s api v1 - """ - - # Now we have a matrix ID, bind it to the threepids we were given - for c in threepidCreds: - # XXX: This should be a deferred list, shouldn't it? - yield self.identity_handler.bind_threepid(c, user_id) - def check_user_id_not_appservice_exclusive(self, user_id, allowed_appservice=None): # don't allow people to register the server notices mxid if self._server_notices_mxid is not None: diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 4245ce26f344..72a38a5d65a0 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -131,14 +131,11 @@ def send_password_reset_mail(self, email_address, token, client_secret, sid): email_address (str): Email address we're sending the password reset to token (str): Unique token generated by the server to verify - password reset email was received + the email was received client_secret (str): Unique token generated by the client to group together multiple email sending attempts sid (str): The generated session ID """ - if email.utils.parseaddr(email_address)[1] == "": - raise RuntimeError("Invalid 'to' email address") - link = ( self.hs.config.public_baseurl + "_matrix/client/unstable/password_reset/email/submit_token" @@ -149,7 +146,34 @@ def send_password_reset_mail(self, email_address, token, client_secret, sid): yield self.send_email( email_address, - "[%s] Password Reset Email" % self.hs.config.server_name, + "[%s] Password Reset" % self.hs.config.server_name, + template_vars, + ) + + @defer.inlineCallbacks + def send_registration_mail(self, email_address, token, client_secret, sid): + """Send an email with a registration confirmation link to a user + + Args: + email_address (str): Email address we're sending the registration + link to + token (str): Unique token generated by the server to verify + the email was received + client_secret (str): Unique token generated by the client to + group together multiple email sending attempts + sid (str): The generated session ID + """ + link = ( + self.hs.config.public_baseurl + + "_matrix/client/unstable/registration/email/submit_token" + "?token=%s&client_secret=%s&sid=%s" % (token, client_secret, sid) + ) + + template_vars = {"link": link} + + yield self.send_email( + email_address, + "[%s] Register your Email Address" % self.hs.config.server_name, template_vars, ) diff --git a/synapse/res/templates/password_reset.html b/synapse/res/templates/password_reset.html index 4fa7b367341a..a197bf872cbb 100644 --- a/synapse/res/templates/password_reset.html +++ b/synapse/res/templates/password_reset.html @@ -4,6 +4,6 @@ {{ link }} -

If this was not you, please disregard this email and contact your server administrator. Thank you.

+

If this was not you, do not click the link above and instead contact your server administrator. Thank you.

diff --git a/synapse/res/templates/password_reset.txt b/synapse/res/templates/password_reset.txt index f0deff59a75f..6aa6527560eb 100644 --- a/synapse/res/templates/password_reset.txt +++ b/synapse/res/templates/password_reset.txt @@ -3,5 +3,5 @@ was you, please click the link below to confirm resetting your password: {{ link }} -If this was not you, please disregard this email and contact your server -administrator. Thank you. +If this was not you, DO NOT click the link above and instead contact your +server administrator. Thank you. diff --git a/synapse/res/templates/password_reset_failure.html b/synapse/res/templates/password_reset_failure.html index 0b132cf8db94..9e3c4446e315 100644 --- a/synapse/res/templates/password_reset_failure.html +++ b/synapse/res/templates/password_reset_failure.html @@ -1,6 +1,8 @@ -

{{ failure_reason }}. Your password has not been reset.

+

The request failed for the following reason: {{ failure_reason }}.

+ +

Your password has not been reset.

diff --git a/synapse/res/templates/registration.html b/synapse/res/templates/registration.html new file mode 100644 index 000000000000..16730a527fce --- /dev/null +++ b/synapse/res/templates/registration.html @@ -0,0 +1,11 @@ + + +

You have asked us to register this email with a new Matrix account. If this was you, please click the link below to confirm your email address:

+ + Verify Your Email Address + +

If this was not you, you can safely disregard this email.

+ +

Thank you.

+ + diff --git a/synapse/res/templates/registration.txt b/synapse/res/templates/registration.txt new file mode 100644 index 000000000000..cb4f16a90ca1 --- /dev/null +++ b/synapse/res/templates/registration.txt @@ -0,0 +1,10 @@ +Hello there, + +You have asked us to register this email with a new Matrix account. If this +was you, please click the link below to confirm your email address: + +{{ link }} + +If this was not you, you can safely disregard this email. + +Thank you. diff --git a/synapse/res/templates/registration_failure.html b/synapse/res/templates/registration_failure.html new file mode 100644 index 000000000000..2833d79c3738 --- /dev/null +++ b/synapse/res/templates/registration_failure.html @@ -0,0 +1,6 @@ + + + +

Validation failed for the following reason: {{ failure_reason }}.

+ + diff --git a/synapse/res/templates/registration_success.html b/synapse/res/templates/registration_success.html new file mode 100644 index 000000000000..fbd6e4018f7d --- /dev/null +++ b/synapse/res/templates/registration_success.html @@ -0,0 +1,6 @@ + + + +

Your email has now been validated, please return to your client. You may now close this window.

+ + diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index e3d59ac3ac5e..8250ae0ae116 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -37,6 +37,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False): SRE_Pattern """ patterns = [] + if unstable: unstable_prefix = CLIENT_API_PREFIX + "/unstable" patterns.append(re.compile("^" + unstable_prefix + path_regex)) @@ -46,6 +47,7 @@ def client_patterns(path_regex, releases=(0,), unstable=True, v1=False): for release in releases: new_prefix = CLIENT_API_PREFIX + "/r%d" % (release,) patterns.append(re.compile("^" + new_prefix + path_regex)) + return patterns diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 2c649259a20b..552ba7cc621a 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -33,7 +33,6 @@ parse_string, ) from synapse.util.msisdn import phone_number_to_msisdn -from synapse.util.stringutils import random_string from synapse.util.threepids import check_3pid_allowed from ._base import client_patterns, interactive_auth_handler @@ -51,7 +50,7 @@ def __init__(self, hs): self.config = hs.config self.identity_handler = hs.get_handlers().identity_handler - if self.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: + if self.config.threepid_behaviour == ThreepidBehaviour.LOCAL: from synapse.push.mailer import Mailer, load_jinja2_templates templates = load_jinja2_templates( @@ -68,8 +67,8 @@ def __init__(self, hs): @defer.inlineCallbacks def on_POST(self, request): - if self.config.email_threepid_behaviour == ThreepidBehaviour.OFF: - if self.config.local_threepid_emails_disabled_due_to_config: + if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warn( "User password resets have been disabled due to lack of email config" ) @@ -101,7 +100,7 @@ def on_POST(self, request): if existing_user_id is None: raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) - if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: # Have the configured identity server handle the request if not self.hs.config.account_threepid_delegate: logger.warn( @@ -119,90 +118,20 @@ def on_POST(self, request): send_attempt, next_link, ) - elif self.config.email_threepid_behaviour == ThreepidBehaviour.LOCAL: + else: # Send password reset emails from Synapse - sid = yield self.send_password_reset( - email, client_secret, send_attempt, next_link + sid = yield self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_password_reset_mail, + next_link, ) # Wrap the session id in a JSON object ret = {"sid": sid} - else: - raise SynapseError( - 400, "Password reset by email is not supported on this homeserver" - ) - - return (200, ret) - - @defer.inlineCallbacks - def send_password_reset(self, email, client_secret, send_attempt, next_link=None): - """Send a password reset email - - Args: - email (str): The user's email address - client_secret (str): The provided client secret - send_attempt (int): Which send attempt this is - next_link (str|None): The link to redirect the user to upon success. No redirect - occurs if None - Returns: - The new session_id upon success - - Raises: - SynapseError is an error occurred when sending the email - """ - # Check that this email/client_secret/send_attempt combo is new or - # greater than what we've seen previously - session = yield self.datastore.get_threepid_validation_session( - "email", client_secret, address=email, validated=False - ) - - # Check to see if a session already exists and that it is not yet - # marked as validated - if session and session.get("validated_at") is None: - session_id = session["session_id"] - last_send_attempt = session["last_send_attempt"] - - # Check that the send_attempt is higher than previous attempts - if send_attempt <= last_send_attempt: - # If not, just return a success without sending an email - return session_id - else: - # An non-validated session does not exist yet. - # Generate a session id - session_id = random_string(16) - - # Generate a new validation token - token = random_string(32) - - # Send the mail with the link containing the token, client_secret - # and session_id - try: - yield self.mailer.send_password_reset_mail( - email, token, client_secret, session_id - ) - except Exception: - logger.exception("Error sending a password reset email to %s", email) - raise SynapseError( - 500, "An error was encountered when sending the password reset email" - ) - - token_expires = ( - self.hs.clock.time_msec() + self.config.email_validation_token_lifetime - ) - - yield self.datastore.start_or_continue_validation_session( - "email", - email, - session_id, - client_secret, - send_attempt, - next_link, - token, - token_expires, - ) - - return session_id + return 200, ret class MsisdnPasswordRequestTokenRestServlet(RestServlet): @@ -243,7 +172,7 @@ def on_POST(self, request): if existing_user_id is None: raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND) - if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: if not self.hs.config.account_threepid_delegate: logger.warn( "No upstream account_threepid_delegate configured on the server to handle " @@ -286,7 +215,7 @@ def __init__(self, hs): self.auth = hs.get_auth() self.config = hs.config self.clock = hs.get_clock() - self.datastore = hs.get_datastore() + self.store = hs.get_datastore() @defer.inlineCallbacks def on_GET(self, request, medium): @@ -294,23 +223,23 @@ def on_GET(self, request, medium): raise SynapseError( 400, "This medium is currently not supported for password resets" ) - if self.config.email_threepid_behaviour == ThreepidBehaviour.OFF: - if self.config.local_threepid_emails_disabled_due_to_config: + if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warn( - "User password resets have been disabled due to lack of email config" + "Password reset emails have been disabled due to lack of an email config" ) raise SynapseError( - 400, "Email-based password resets have been disabled on this server" + 400, "Email-based password resets are disabled on this server" ) - sid = parse_string(request, "sid") - client_secret = parse_string(request, "client_secret") - token = parse_string(request, "token") + sid = parse_string(request, "sid", required=True) + client_secret = parse_string(request, "client_secret", required=True) + token = parse_string(request, "token", required=True) - # Attempt to validate a 3PID sesssion + # Attempt to validate a 3PID session try: # Mark the session as valid - next_link = yield self.datastore.validate_threepid_session( + next_link = yield self.store.validate_threepid_session( sid, client_secret, token, self.clock.time_msec() ) @@ -327,7 +256,7 @@ def on_GET(self, request, medium): return None # Otherwise show the success template - html = self.config.email_password_reset_template_success_html_content + html = self.config.email_password_reset_template_success_html request.setResponseCode(200) except ThreepidValidationError as e: # Show a failure page with a reason @@ -340,7 +269,6 @@ def on_GET(self, request, medium): request.write(html.encode("utf-8")) finish_request(request) - return None def load_jinja2_template(self, template_dir, template_filename, template_vars): """Loads a jinja2 template with variables to insert @@ -499,8 +427,9 @@ class EmailThreepidRequestTokenRestServlet(RestServlet): PATTERNS = client_patterns("/account/3pid/email/requestToken$") def __init__(self, hs): - self.hs = hs super(EmailThreepidRequestTokenRestServlet, self).__init__() + self.hs = hs + self.config = hs.config self.identity_handler = hs.get_handlers().identity_handler self.store = self.hs.get_datastore() @@ -516,7 +445,7 @@ def on_POST(self, request): send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param - if not check_3pid_allowed(self.hs, "email", body["email"]): + if not check_3pid_allowed(self.hs, "email", email): raise SynapseError( 403, "Your email domain is not authorized on this server", @@ -533,7 +462,7 @@ def on_POST(self, request): ret = yield self.identity_handler.requestEmailToken( id_server, email, client_secret, send_attempt, next_link ) - return (200, ret) + return 200, ret class MsisdnThreepidRequestTokenRestServlet(RestServlet): @@ -542,8 +471,8 @@ class MsisdnThreepidRequestTokenRestServlet(RestServlet): def __init__(self, hs): self.hs = hs super(MsisdnThreepidRequestTokenRestServlet, self).__init__() - self.identity_handler = hs.get_handlers().identity_handler self.store = self.hs.get_datastore() + self.identity_handler = hs.get_handlers().identity_handler @defer.inlineCallbacks def on_POST(self, request): diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index ecafee5ae7bb..a5d560516e4e 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -28,11 +28,13 @@ Codes, LimitExceededError, SynapseError, + ThreepidValidationError, UnrecognizedRequestError, ) from synapse.config.emailconfig import ThreepidBehaviour from synapse.config.ratelimiting import FederationRateLimitConfig from synapse.config.server import is_threepid_reserved +from synapse.http.server import finish_request from synapse.http.servlet import ( RestServlet, assert_params_in_dict, @@ -71,9 +73,33 @@ def __init__(self, hs): super(EmailRegisterRequestTokenRestServlet, self).__init__() self.hs = hs self.identity_handler = hs.get_handlers().identity_handler + self.config = hs.config + + if self.hs.config.threepid_behaviour == ThreepidBehaviour.LOCAL: + from synapse.push.mailer import Mailer, load_jinja2_templates + + templates = load_jinja2_templates( + config=hs.config, + template_html_name=hs.config.email_registration_template_html, + template_text_name=hs.config.email_registration_template_text, + ) + self.mailer = Mailer( + hs=self.hs, + app_name=self.hs.config.email_app_name, + template_html=templates[0], + template_text=templates[1], + ) @defer.inlineCallbacks def on_POST(self, request): + if self.hs.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.hs.config.local_threepid_handling_disabled_due_to_email_config: + logger.warn( + "Email registration has been disabled due to lack of email config" + ) + raise SynapseError( + 400, "Email-based registration has been disabled on this server" + ) body = parse_json_object_from_request(request) assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) @@ -84,7 +110,7 @@ def on_POST(self, request): send_attempt = body["send_attempt"] next_link = body.get("next_link") # Optional param - if not check_3pid_allowed(self.hs, "email", body["email"]): + if not check_3pid_allowed(self.hs, "email", email): raise SynapseError( 403, "Your email domain is not authorized to register on this server", @@ -98,24 +124,37 @@ def on_POST(self, request): if existing_user_id is not None: raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) - if not self.hs.config.account_threepid_delegate: - logger.warn( - "No upstream account_threepid_delegate configured on the server to handle " - "this request" + if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: + if not self.hs.config.account_threepid_delegate: + logger.warn( + "No upstream account_threepid_delegate configured on the server to handle " + "this request" + ) + raise SynapseError( + 400, "Registration by email is not supported on this homeserver" + ) + + ret = yield self.identity_handler.requestEmailToken( + self.hs.config.account_threepid_delegate, + email, + client_secret, + send_attempt, + next_link, ) - raise SynapseError( - 400, "Registration by email is not supported on this homeserver" + else: + # Send registration emails from Synapse + sid = yield self.identity_handler.send_threepid_validation( + email, + client_secret, + send_attempt, + self.mailer.send_registration_mail, + next_link, ) - ret = yield self.identity_handler.requestEmailToken( - self.hs.config.account_threepid_delegate, - email, - client_secret, - send_attempt, - next_link, - ) + # Wrap the session id in a JSON object + ret = {"sid": sid} - return (200, ret) + return 200, ret class MsisdnRegisterRequestTokenRestServlet(RestServlet): @@ -161,7 +200,7 @@ def on_POST(self, request): 400, "Phone number is already in use", Codes.THREEPID_IN_USE ) - if self.config.email_threepid_behaviour == ThreepidBehaviour.REMOTE: + if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: if not self.hs.config.account_threepid_delegate: logger.warn( "No upstream account_threepid_delegate configured on the server to handle " @@ -187,6 +226,81 @@ def on_POST(self, request): ) +class RegistrationSubmitTokenServlet(RestServlet): + """Handles registration 3PID validation token submission""" + + PATTERNS = client_patterns( + "/registration/(?P[^/]*)/submit_token$", releases=(), unstable=True + ) + + def __init__(self, hs): + """ + Args: + hs (synapse.server.HomeServer): server + """ + super(RegistrationSubmitTokenServlet, self).__init__() + self.hs = hs + self.auth = hs.get_auth() + self.config = hs.config + self.clock = hs.get_clock() + self.store = hs.get_datastore() + + @defer.inlineCallbacks + def on_GET(self, request, medium): + if medium != "email": + raise SynapseError( + 400, "This medium is currently not supported for registration" + ) + if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.local_threepid_handling_disabled_due_to_email_config: + logger.warn( + "User registration via email has been disabled due to lack of email config" + ) + raise SynapseError( + 400, "Email-based registration is disabled on this server" + ) + + sid = parse_string(request, "sid", required=True) + client_secret = parse_string(request, "client_secret", required=True) + token = parse_string(request, "token", required=True) + + # Attempt to validate a 3PID session + try: + # Mark the session as valid + next_link = yield self.store.validate_threepid_session( + sid, client_secret, token, self.clock.time_msec() + ) + + # Perform a 302 redirect if next_link is set + if next_link: + if next_link.startswith("file:///"): + logger.warn( + "Not redirecting to next_link as it is a local file: address" + ) + else: + request.setResponseCode(302) + request.setHeader("Location", next_link) + finish_request(request) + return None + + # Otherwise show the success template + html = self.config.email_registration_template_success_html_content + + request.setResponseCode(200) + except ThreepidValidationError as e: + # Show a failure page with a reason + html = self.load_jinja2_template( + self.config.email_template_dir, + self.config.email_registration_template_failure_html, + template_vars={"failure_reason": e.msg}, + ) + request.setResponseCode(e.code) + + request.write(html.encode("utf-8")) + finish_request(request) + return None + + class UsernameAvailabilityRestServlet(RestServlet): PATTERNS = client_patterns("/register/available") @@ -601,4 +715,5 @@ def register_servlets(hs, http_server): EmailRegisterRequestTokenRestServlet(hs).register(http_server) MsisdnRegisterRequestTokenRestServlet(hs).register(http_server) UsernameAvailabilityRestServlet(hs).register(http_server) + RegistrationSubmitTokenServlet(hs).register(http_server) RegisterRestServlet(hs).register(http_server) From 891afb57cbdf9867f2848341b29c75d6f35eef5a Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Wed, 4 Sep 2019 15:02:55 +0100 Subject: [PATCH 5/8] Change account_threepid_delegate to a dictionary (#5969) `account_threepid_delegate` was an option added as part of this privacy sprint for the homeserver admin to declare which identity server (or any server handling third-party verification requests) they'd like to use to send email/sms messages on their behalf for the purposes of user registration and password resets. We realized however, that while admins would want to set this option to `""` (allow Synapse to handle email sending itself), some homeservers have users with bound phone numbers, and setting `account_threepid_delegate` to `""` would prevent them from having any phone number verification, since Synapse does not at this time support sending SMS messages. So, seeing as a common use case would be to have Synapse handle email verification, but an external server handle MSISDN verification, we split `account_threepid_delegate` into a dictionary, and called it `account_threepid_delegates` instead. This contains two keys as of present, `email` and `msisdn`. You can then set either to an external server of your choice, or `""` for Synapse to attempt to handle it. --- UPGRADE.rst | 32 +++++++------- changelog.d/{5876.misc => 5876.feature} | 2 +- changelog.d/5969.feature | 1 + docs/sample_config.yaml | 30 ++++++++----- synapse/config/emailconfig.py | 27 +++++++----- synapse/config/registration.py | 35 ++++++++++----- synapse/handlers/auth.py | 4 +- synapse/rest/client/v2_alpha/account.py | 54 +++++++++++------------- synapse/rest/client/v2_alpha/register.py | 53 +++++++++++------------ synapse/rest/client/versions.py | 16 +------ 10 files changed, 132 insertions(+), 122 deletions(-) rename changelog.d/{5876.misc => 5876.feature} (64%) create mode 100644 changelog.d/5969.feature diff --git a/UPGRADE.rst b/UPGRADE.rst index 99e8da4b525f..1ede45d13955 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -66,21 +66,23 @@ its own, phone-based password resets and registration will be disabled. For Syna emails, the ``email`` block of the config must be filled out. If not, then password resets and registration via email will be disabled entirely. -This release also deprecates the ``email.trust_identity_server_for_password_resets`` option -and replaces it with ``account_threepid_delegate``. This option defines whether the homeserver -should delegate an external server (typically an `identity server -`_) to handle sending password reset -or registration messages via email or SMS. - -If ``email.trust_identity_server_for_password_resets`` was changed from its default to -``true``, and ``account_threepid_delegate`` is not set to an identity server domain, then the -server handling password resets and registration via third-party addresses will be set to the -first entry in the Synapse config's ``trusted_third_party_id_servers`` entry. If no domains are -configured, Synapse will throw an error on startup. - -If ``email.trust_identity_server_for_password_resets`` is not set to ``true`` and -``account_threepid_delegate`` is not set to a domain, then Synapse will attempt to send -password reset and registration messages itself. +This release also deprecates the ``email.trust_identity_server_for_password_resets`` option and +replaces it with the ``account_threepid_delegates`` dictionary. This option defines whether the +homeserver should delegate an external server (typically an `identity server +`_) to handle sending password reset or +registration messages via email and SMS. + +Specifically for email, if ``email.trust_identity_server_for_password_resets`` was changed from +its default to ``true``, and ``account_threepid_delegates.email`` is not set, then the server +handling password resets and registration via third-party addresses will be set to the first +entry in the Synapse config's ``trusted_third_party_id_servers`` entry. This is to ensure that +people who set up an external server for handling these tasks before v1.4.0 will not have their +setups mysteriously stop working. However, if no trusted identity server domains are +configured, Synapse will throw an error. + +If ``email.trust_identity_server_for_password_resets`` is not set to ``true`` and a type in +``account_threepid_delegates`` is not set to a domain, then Synapse will attempt to send +password reset and registration messages itself for that type. Email templates --------------- diff --git a/changelog.d/5876.misc b/changelog.d/5876.feature similarity index 64% rename from changelog.d/5876.misc rename to changelog.d/5876.feature index c1c289d05a26..df88193fbd82 100644 --- a/changelog.d/5876.misc +++ b/changelog.d/5876.feature @@ -1 +1 @@ -Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegate`. \ No newline at end of file +Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegates`. \ No newline at end of file diff --git a/changelog.d/5969.feature b/changelog.d/5969.feature new file mode 100644 index 000000000000..cf603fa0c6a5 --- /dev/null +++ b/changelog.d/5969.feature @@ -0,0 +1 @@ +Replace `trust_identity_server_for_password_resets` config option with `account_threepid_delegates`. diff --git a/docs/sample_config.yaml b/docs/sample_config.yaml index 8603008ec048..186cdbedd2de 100644 --- a/docs/sample_config.yaml +++ b/docs/sample_config.yaml @@ -903,19 +903,29 @@ uploads_path: "DATADIR/uploads" # - matrix.org # - vector.im -# Handle threepid (email/phone etc) registration and password resets -# through a *trusted* identity server. Note that this allows the configured -# identity server to reset passwords for accounts. +# Handle threepid (email/phone etc) registration and password resets through a set of +# *trusted* identity servers. Note that this allows the configured identity server to +# reset passwords for accounts! # -# If this option is not defined and SMTP options have not been -# configured, registration by email and resetting user passwords via -# email will be disabled +# Be aware that if `email` is not set, and SMTP options have not been +# configured in the email config block, registration and user password resets via +# email will be globally disabled. # -# Otherwise, to enable set this option to the reachable domain name, including protocol -# definition, for an identity server -# (e.g "https://matrix.org", "http://localhost:8090") +# Additionally, if `msisdn` is not set, registration and password resets via msisdn +# will be disabled regardless. This is due to Synapse currently not supporting any +# method of sending SMS messages on its own. # -#account_threepid_delegate: "" +# To enable using an identity server for operations regarding a particular third-party +# identifier type, set the value to the URL of that identity server as shown in the +# examples below. +# +# Servers handling the these requests must answer the `/requestToken` endpoints defined +# by the Matrix Identity Service API specification: +# https://matrix.org/docs/spec/identity_service/latest +# +account_threepid_delegates: + #email: https://example.com # Delegate email sending to matrix.org + #msisdn: http://localhost:8090 # Delegate SMS sending to this local process # Users who register on this homeserver will automatically be joined # to these rooms diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 874166b57938..e5de768b0ce1 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -75,11 +75,13 @@ def read_config(self, config, **kwargs): "renew_at" ) - self.threepid_behaviour = ( - # Have Synapse handle the email sending if account_threepid_delegate + self.threepid_behaviour_email = ( + # Have Synapse handle the email sending if account_threepid_delegates.email # is not defined + # msisdn is currently always remote while Synapse does not support any method of + # sending SMS messages ThreepidBehaviour.REMOTE - if self.account_threepid_delegate + if self.account_threepid_delegate_email else ThreepidBehaviour.LOCAL ) # Prior to Synapse v1.4.0, there was another option that defined whether Synapse would @@ -88,14 +90,16 @@ def read_config(self, config, **kwargs): # identity server in the process. self.using_identity_server_from_trusted_list = False if ( - not self.account_threepid_delegate + not self.account_threepid_delegate_email and config.get("trust_identity_server_for_password_resets", False) is True ): # Use the first entry in self.trusted_third_party_id_servers instead if self.trusted_third_party_id_servers: - # XXX: It's a little confusing that account_threepid_delegate is modifed + # XXX: It's a little confusing that account_threepid_delegate_email is modified # both in RegistrationConfig and here. We should factor this bit out - self.account_threepid_delegate = self.trusted_third_party_id_servers[0] + self.account_threepid_delegate_email = self.trusted_third_party_id_servers[ + 0 + ] self.using_identity_server_from_trusted_list = True else: raise ConfigError( @@ -104,12 +108,15 @@ def read_config(self, config, **kwargs): ) self.local_threepid_handling_disabled_due_to_email_config = False - if self.threepid_behaviour == ThreepidBehaviour.LOCAL and email_config == {}: + if ( + self.threepid_behaviour_email == ThreepidBehaviour.LOCAL + and email_config == {} + ): # We cannot warn the user this has happened here # Instead do so when a user attempts to reset their password self.local_threepid_handling_disabled_due_to_email_config = True - self.threepid_behaviour = ThreepidBehaviour.OFF + self.threepid_behaviour_email = ThreepidBehaviour.OFF # Get lifetime of a validation token in milliseconds self.email_validation_token_lifetime = self.parse_duration( @@ -119,7 +126,7 @@ def read_config(self, config, **kwargs): if ( self.email_enable_notifs or account_validity_renewal_enabled - or self.threepid_behaviour == ThreepidBehaviour.LOCAL + or self.threepid_behaviour_email == ThreepidBehaviour.LOCAL ): # make sure we can import the required deps import jinja2 @@ -129,7 +136,7 @@ def read_config(self, config, **kwargs): jinja2 bleach - if self.threepid_behaviour == ThreepidBehaviour.LOCAL: + if self.threepid_behaviour_email == ThreepidBehaviour.LOCAL: required = ["smtp_host", "smtp_port", "notif_from"] missing = [] diff --git a/synapse/config/registration.py b/synapse/config/registration.py index b9d5e81b1dd5..9548560edb10 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -99,7 +99,10 @@ def read_config(self, config, **kwargs): self.trusted_third_party_id_servers = config.get( "trusted_third_party_id_servers", ["matrix.org", "vector.im"] ) - self.account_threepid_delegate = config.get("account_threepid_delegate") + account_threepid_delegates = config.get("account_threepid_delegates") or {} + self.account_threepid_delegate_email = account_threepid_delegates.get("email") + self.account_threepid_delegate_msisdn = account_threepid_delegates.get("msisdn") + self.default_identity_server = config.get("default_identity_server") self.allow_guest_access = config.get("allow_guest_access", False) @@ -270,19 +273,29 @@ def generate_config_section(self, generate_secrets=False, **kwargs): # - matrix.org # - vector.im - # Handle threepid (email/phone etc) registration and password resets - # through a *trusted* identity server. Note that this allows the configured - # identity server to reset passwords for accounts. + # Handle threepid (email/phone etc) registration and password resets through a set of + # *trusted* identity servers. Note that this allows the configured identity server to + # reset passwords for accounts! + # + # Be aware that if `email` is not set, and SMTP options have not been + # configured in the email config block, registration and user password resets via + # email will be globally disabled. + # + # Additionally, if `msisdn` is not set, registration and password resets via msisdn + # will be disabled regardless. This is due to Synapse currently not supporting any + # method of sending SMS messages on its own. # - # If this option is not defined and SMTP options have not been - # configured, registration by email and resetting user passwords via - # email will be disabled + # To enable using an identity server for operations regarding a particular third-party + # identifier type, set the value to the URL of that identity server as shown in the + # examples below. # - # Otherwise, to enable set this option to the reachable domain name, including protocol - # definition, for an identity server - # (e.g "https://matrix.org", "http://localhost:8090") + # Servers handling the these requests must answer the `/requestToken` endpoints defined + # by the Matrix Identity Service API specification: + # https://matrix.org/docs/spec/identity_service/latest # - #account_threepid_delegate: "" + account_threepid_delegates: + #email: https://example.com # Delegate email sending to matrix.org + #msisdn: http://localhost:8090 # Delegate SMS sending to this local process # Users who register on this homeserver will automatically be joined # to these rooms diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index a59cd4e7f58c..6231d021dd49 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -461,10 +461,10 @@ def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs): logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,)) if ( not password_servlet - or self.hs.config.threepid_behaviour == ThreepidBehaviour.REMOTE + or self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE ): threepid = yield identity_handler.threepid_from_creds(threepid_creds) - elif self.hs.config.threepid_behaviour == ThreepidBehaviour.LOCAL: + elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: row = yield self.store.get_threepid_validation_session( medium, threepid_creds["client_secret"], diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 552ba7cc621a..4d9f8305ea15 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -50,7 +50,7 @@ def __init__(self, hs): self.config = hs.config self.identity_handler = hs.get_handlers().identity_handler - if self.config.threepid_behaviour == ThreepidBehaviour.LOCAL: + if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: from synapse.push.mailer import Mailer, load_jinja2_templates templates = load_jinja2_templates( @@ -67,7 +67,7 @@ def __init__(self, hs): @defer.inlineCallbacks def on_POST(self, request): - if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warn( "User password resets have been disabled due to lack of email config" @@ -100,19 +100,19 @@ def on_POST(self, request): if existing_user_id is None: raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND) - if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: + if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: # Have the configured identity server handle the request - if not self.hs.config.account_threepid_delegate: + if not self.hs.config.account_threepid_delegate_email: logger.warn( - "No upstream account_threepid_delegate configured on the server to handle " - "this request" + "No upstream email account_threepid_delegate configured on the server to " + "handle this request" ) raise SynapseError( 400, "Password reset by email is not supported on this homeserver" ) ret = yield self.identity_handler.requestEmailToken( - self.hs.config.account_threepid_delegate, + self.hs.config.account_threepid_delegate_email, email, client_secret, send_attempt, @@ -172,31 +172,27 @@ def on_POST(self, request): if existing_user_id is None: raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND) - if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: - if not self.hs.config.account_threepid_delegate: - logger.warn( - "No upstream account_threepid_delegate configured on the server to handle " - "this request" - ) - raise SynapseError( - 400, - "Password reset by phone number is not supported on this homeserver", - ) - - ret = yield self.identity_handler.requestMsisdnToken( - self.config.account_threepid_delegate, - country, - phone_number, - client_secret, - send_attempt, - next_link, + if not self.hs.config.account_threepid_delegate_msisdn: + logger.warn( + "No upstream msisdn account_threepid_delegate configured on the server to " + "handle this request" + ) + raise SynapseError( + 400, + "Password reset by phone number is not supported on this homeserver", ) - return (200, ret) - raise SynapseError( - 400, "Password reset by phone number is not supported on this homeserver" + ret = yield self.identity_handler.requestMsisdnToken( + self.config.account_threepid_delegate_msisdn, + country, + phone_number, + client_secret, + send_attempt, + next_link, ) + return 200, ret + class PasswordResetSubmitTokenServlet(RestServlet): """Handles 3PID validation token submission""" @@ -223,7 +219,7 @@ def on_GET(self, request, medium): raise SynapseError( 400, "This medium is currently not supported for password resets" ) - if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warn( "Password reset emails have been disabled due to lack of an email config" diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index a5d560516e4e..3120c153e91b 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -75,7 +75,7 @@ def __init__(self, hs): self.identity_handler = hs.get_handlers().identity_handler self.config = hs.config - if self.hs.config.threepid_behaviour == ThreepidBehaviour.LOCAL: + if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: from synapse.push.mailer import Mailer, load_jinja2_templates templates = load_jinja2_templates( @@ -92,7 +92,7 @@ def __init__(self, hs): @defer.inlineCallbacks def on_POST(self, request): - if self.hs.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.hs.config.local_threepid_handling_disabled_due_to_email_config: logger.warn( "Email registration has been disabled due to lack of email config" @@ -124,18 +124,18 @@ def on_POST(self, request): if existing_user_id is not None: raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) - if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: - if not self.hs.config.account_threepid_delegate: + if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: + if not self.hs.config.account_threepid_delegate_email: logger.warn( - "No upstream account_threepid_delegate configured on the server to handle " - "this request" + "No upstream email account_threepid_delegate configured on the server to " + "handle this request" ) raise SynapseError( 400, "Registration by email is not supported on this homeserver" ) ret = yield self.identity_handler.requestEmailToken( - self.hs.config.account_threepid_delegate, + self.hs.config.account_threepid_delegate_email, email, client_secret, send_attempt, @@ -200,31 +200,26 @@ def on_POST(self, request): 400, "Phone number is already in use", Codes.THREEPID_IN_USE ) - if self.config.threepid_behaviour == ThreepidBehaviour.REMOTE: - if not self.hs.config.account_threepid_delegate: - logger.warn( - "No upstream account_threepid_delegate configured on the server to handle " - "this request" - ) - raise SynapseError( - 400, - "Registration by phone number is not supported on this homeserver", - ) - - ret = yield self.identity_handler.requestMsisdnToken( - self.config.account_threepid_delegate, - country, - phone_number, - client_secret, - send_attempt, - next_link, + if not self.hs.config.account_threepid_delegate_msisdn: + logger.warn( + "No upstream msisdn account_threepid_delegate configured on the server to " + "handle this request" + ) + raise SynapseError( + 400, "Registration by phone number is not supported on this homeserver" ) - return (200, ret) - raise SynapseError( - 400, "Registration by phone number is not supported on this homeserver" + ret = yield self.identity_handler.requestMsisdnToken( + self.hs.config.account_threepid_delegate_msisdn, + country, + phone_number, + client_secret, + send_attempt, + next_link, ) + return 200, ret + class RegistrationSubmitTokenServlet(RestServlet): """Handles registration 3PID validation token submission""" @@ -251,7 +246,7 @@ def on_GET(self, request, medium): raise SynapseError( 400, "This medium is currently not supported for registration" ) - if self.config.threepid_behaviour == ThreepidBehaviour.OFF: + if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF: if self.config.local_threepid_handling_disabled_due_to_email_config: logger.warn( "User registration via email has been disabled due to lack of email config" diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py index e7f488376f35..0058b6b4590d 100644 --- a/synapse/rest/client/versions.py +++ b/synapse/rest/client/versions.py @@ -48,21 +48,7 @@ def on_GET(self, request): "r0.5.0", ], # as per MSC1497: - "unstable_features": { - "m.lazy_load_members": True, - # Advertise to clients whether they need not include an `id_server` - # parameter during registration or password reset, as Synapse now decides - # itself which identity server to use (or none at all). - # - # This is also used by a client when they wish to bind a 3PID to their - # account, but not bind it to an identity server, the endpoint for which - # also requires `id_server`. If the homeserver is handling 3PID - # verification itself, there is no need to ask the user for `id_server` to - # be supplied. - "m.require_identity_server": ( - self.config.account_threepid_delegate is None - ), - }, + "unstable_features": {"m.lazy_load_members": True}, }, ) From 182fc193a91a7106ddd84e3f0affa25b418cec75 Mon Sep 17 00:00:00 2001 From: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> Date: Thu, 5 Sep 2019 11:51:38 +0100 Subject: [PATCH 6/8] [2/2] Allow homeservers to send registration emails | Accepting the verification (#5940) Fixes: #5751 Fixes: https://github.com/matrix-org/synapse/issues/5928 https://github.com/matrix-org/synapse/pull/5835 allowed Synapse to send registration emails to the user. Now we need to accept them and have it succeed the `m.login.email.identity` registration flow step. `account_threepid_handler` will also be switched from a `str` in the config file to a dictionary which contains entries for `msisdn` and `email`, each with their own `str`. This will let people use an external server to handle `msisdn` registration and password reset requests, while using Synapse for email-based things. And the `password_servlet` hack that was introduced in https://github.com/matrix-org/synapse/pull/5377/files#diff-b8464485d36f6f87caee3f4d82524213R189 to distinguish a registration call from a password reset call will be removed. --- changelog.d/5940.feature | 1 + synapse/handlers/account_validity.py | 12 ++++-- synapse/handlers/auth.py | 31 +++----------- synapse/push/mailer.py | 49 ++++++++++++++++------ synapse/push/pusher.py | 17 +++++--- synapse/rest/client/v2_alpha/account.py | 53 +++++++++--------------- synapse/rest/client/v2_alpha/register.py | 35 +++++++++------- 7 files changed, 105 insertions(+), 93 deletions(-) create mode 100644 changelog.d/5940.feature diff --git a/changelog.d/5940.feature b/changelog.d/5940.feature new file mode 100644 index 000000000000..5b69b97fe794 --- /dev/null +++ b/changelog.d/5940.feature @@ -0,0 +1 @@ +Add the ability to send registration emails from the homeserver rather than delegating to an identity server. \ No newline at end of file diff --git a/synapse/handlers/account_validity.py b/synapse/handlers/account_validity.py index 34574f1a12a6..d04e0fe576e5 100644 --- a/synapse/handlers/account_validity.py +++ b/synapse/handlers/account_validity.py @@ -38,6 +38,7 @@ class AccountValidityHandler(object): def __init__(self, hs): self.hs = hs + self.config = hs.config self.store = self.hs.get_datastore() self.sendmail = self.hs.get_sendmail() self.clock = self.hs.get_clock() @@ -62,9 +63,14 @@ def __init__(self, hs): self._raw_from = email.utils.parseaddr(self._from_string)[1] self._template_html, self._template_text = load_jinja2_templates( - config=self.hs.config, - template_html_name=self.hs.config.email_expiry_template_html, - template_text_name=self.hs.config.email_expiry_template_text, + self.config.email_template_dir, + [ + self.config.email_expiry_template_html, + self.config.email_expiry_template_text, + ], + apply_format_ts_filter=True, + apply_mxc_to_http_filter=True, + public_baseurl=self.config.public_baseurl, ) # Check the renewal emails to send and send them every 30min. diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 6231d021dd49..d3c0486dbe39 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -159,7 +159,7 @@ def validate_user_via_ui_auth(self, requester, request_body, clientip): return params @defer.inlineCallbacks - def check_auth(self, flows, clientdict, clientip, password_servlet=False): + def check_auth(self, flows, clientdict, clientip): """ Takes a dictionary sent by the client in the login / registration protocol and handles the User-Interactive Auth flow. @@ -183,16 +183,6 @@ def check_auth(self, flows, clientdict, clientip, password_servlet=False): clientip (str): The IP address of the client. - password_servlet (bool): Whether the request originated from - PasswordRestServlet. - XXX: This is a temporary hack to distinguish between checking - for threepid validations locally (in the case of password - resets) and using the identity server (in the case of binding - a 3PID during registration). Once we start using the - homeserver for both tasks, this distinction will no longer be - necessary. - - Returns: defer.Deferred[dict, dict, str]: a deferred tuple of (creds, params, session_id). @@ -248,9 +238,7 @@ def check_auth(self, flows, clientdict, clientip, password_servlet=False): if "type" in authdict: login_type = authdict["type"] try: - result = yield self._check_auth_dict( - authdict, clientip, password_servlet=password_servlet - ) + result = yield self._check_auth_dict(authdict, clientip) if result: creds[login_type] = result self._save_session(session) @@ -357,7 +345,7 @@ def get_session_data(self, session_id, key, default=None): return sess.setdefault("serverdict", {}).get(key, default) @defer.inlineCallbacks - def _check_auth_dict(self, authdict, clientip, password_servlet=False): + def _check_auth_dict(self, authdict, clientip): """Attempt to validate the auth dict provided by a client Args: @@ -375,11 +363,7 @@ def _check_auth_dict(self, authdict, clientip, password_servlet=False): login_type = authdict["type"] checker = self.checkers.get(login_type) if checker is not None: - # XXX: Temporary workaround for having Synapse handle password resets - # See AuthHandler.check_auth for further details - res = yield checker( - authdict, clientip=clientip, password_servlet=password_servlet - ) + res = yield checker(authdict, clientip=clientip) return res # build a v1-login-style dict out of the authdict and fall back to the @@ -450,7 +434,7 @@ def _check_terms_auth(self, authdict, **kwargs): return defer.succeed(True) @defer.inlineCallbacks - def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs): + def _check_threepid(self, medium, authdict, **kwargs): if "threepid_creds" not in authdict: raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM) @@ -459,10 +443,7 @@ def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs): identity_handler = self.hs.get_handlers().identity_handler logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,)) - if ( - not password_servlet - or self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE - ): + if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE: threepid = yield identity_handler.threepid_from_creds(threepid_creds) elif self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: row = yield self.store.get_threepid_validation_session( diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 72a38a5d65a0..3dfd52784914 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -629,25 +629,50 @@ def format_ts_filter(value, format): return time.strftime(format, time.localtime(value / 1000)) -def load_jinja2_templates(config, template_html_name, template_text_name): - """Load the jinja2 email templates from disk +def load_jinja2_templates( + template_dir, + template_filenames, + apply_format_ts_filter=False, + apply_mxc_to_http_filter=False, + public_baseurl=None, +): + """Loads and returns one or more jinja2 templates and applies optional filters + + Args: + template_dir (str): The directory where templates are stored + template_filenames (list[str]): A list of template filenames + apply_format_ts_filter (bool): Whether to apply a template filter that formats + timestamps + apply_mxc_to_http_filter (bool): Whether to apply a template filter that converts + mxc urls to http urls + public_baseurl (str|None): The public baseurl of the server. Required for + apply_mxc_to_http_filter to be enabled Returns: - (template_html, template_text) + A list of jinja2 templates corresponding to the given list of filenames, + with order preserved """ - logger.info("loading email templates from '%s'", config.email_template_dir) - loader = jinja2.FileSystemLoader(config.email_template_dir) + logger.info( + "loading email templates %s from '%s'", template_filenames, template_dir + ) + loader = jinja2.FileSystemLoader(template_dir) env = jinja2.Environment(loader=loader) - env.filters["format_ts"] = format_ts_filter - env.filters["mxc_to_http"] = _create_mxc_to_http_filter(config) - template_html = env.get_template(template_html_name) - template_text = env.get_template(template_text_name) + if apply_format_ts_filter: + env.filters["format_ts"] = format_ts_filter + + if apply_mxc_to_http_filter and public_baseurl: + env.filters["mxc_to_http"] = _create_mxc_to_http_filter(public_baseurl) + + templates = [] + for template_filename in template_filenames: + template = env.get_template(template_filename) + templates.append(template) - return template_html, template_text + return templates -def _create_mxc_to_http_filter(config): +def _create_mxc_to_http_filter(public_baseurl): def mxc_to_http_filter(value, width, height, resize_method="crop"): if value[0:6] != "mxc://": return "" @@ -660,7 +685,7 @@ def mxc_to_http_filter(value, width, height, resize_method="crop"): params = {"width": width, "height": height, "method": resize_method} return "%s_matrix/media/v1/thumbnail/%s?%s%s" % ( - config.public_baseurl, + public_baseurl, serverAndMediaId, urllib.parse.urlencode(params), fragment or "", diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index a9c64a9c5401..f277aeb1312d 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -35,6 +35,7 @@ class PusherFactory(object): def __init__(self, hs): self.hs = hs + self.config = hs.config self.pusher_types = {"http": HttpPusher} @@ -42,12 +43,16 @@ def __init__(self, hs): if hs.config.email_enable_notifs: self.mailers = {} # app_name -> Mailer - templates = load_jinja2_templates( - config=hs.config, - template_html_name=hs.config.email_notif_template_html, - template_text_name=hs.config.email_notif_template_text, + self.notif_template_html, self.notif_template_text = load_jinja2_templates( + self.config.email_template_dir, + [ + self.config.email_notif_template_html, + self.config.email_notif_template_text, + ], + apply_format_ts_filter=True, + apply_mxc_to_http_filter=True, + public_baseurl=self.config.public_baseurl, ) - self.notif_template_html, self.notif_template_text = templates self.pusher_types["email"] = self._create_email_pusher @@ -78,6 +83,6 @@ def _app_name_from_pusherdict(self, pusherdict): if "data" in pusherdict and "brand" in pusherdict["data"]: app_name = pusherdict["data"]["brand"] else: - app_name = self.hs.config.email_app_name + app_name = self.config.email_app_name return app_name diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 4d9f8305ea15..31e2f055d755 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -18,8 +18,6 @@ from six.moves import http_client -import jinja2 - from twisted.internet import defer from synapse.api.constants import LoginType @@ -32,6 +30,7 @@ parse_json_object_from_request, parse_string, ) +from synapse.push.mailer import Mailer, load_jinja2_templates from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.threepids import check_3pid_allowed @@ -51,18 +50,21 @@ def __init__(self, hs): self.identity_handler = hs.get_handlers().identity_handler if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: - from synapse.push.mailer import Mailer, load_jinja2_templates - - templates = load_jinja2_templates( - config=hs.config, - template_html_name=hs.config.email_password_reset_template_html, - template_text_name=hs.config.email_password_reset_template_text, + template_html, template_text = load_jinja2_templates( + self.config.email_template_dir, + [ + self.config.email_password_reset_template_html, + self.config.email_password_reset_template_text, + ], + apply_format_ts_filter=True, + apply_mxc_to_http_filter=True, + public_baseurl=self.config.public_baseurl, ) self.mailer = Mailer( hs=self.hs, app_name=self.config.email_app_name, - template_html=templates[0], - template_text=templates[1], + template_html=template_html, + template_text=template_text, ) @defer.inlineCallbacks @@ -215,6 +217,7 @@ def __init__(self, hs): @defer.inlineCallbacks def on_GET(self, request, medium): + # We currently only handle threepid token submissions for email if medium != "email": raise SynapseError( 400, "This medium is currently not supported for password resets" @@ -255,35 +258,20 @@ def on_GET(self, request, medium): html = self.config.email_password_reset_template_success_html request.setResponseCode(200) except ThreepidValidationError as e: + request.setResponseCode(e.code) + # Show a failure page with a reason - html = self.load_jinja2_template( + html_template = load_jinja2_templates( self.config.email_template_dir, - self.config.email_password_reset_template_failure_html, - template_vars={"failure_reason": e.msg}, + [self.config.email_password_reset_template_failure_html], ) - request.setResponseCode(e.code) + + template_vars = {"failure_reason": e.msg} + html = html_template.render(**template_vars) request.write(html.encode("utf-8")) finish_request(request) - def load_jinja2_template(self, template_dir, template_filename, template_vars): - """Loads a jinja2 template with variables to insert - - Args: - template_dir (str): The directory where templates are stored - template_filename (str): The name of the template in the template_dir - template_vars (Dict): Dictionary of keys in the template - alongside their values to insert - - Returns: - str containing the contents of the rendered template - """ - loader = jinja2.FileSystemLoader(template_dir) - env = jinja2.Environment(loader=loader) - - template = env.get_template(template_filename) - return template.render(**template_vars) - @defer.inlineCallbacks def on_POST(self, request, medium): if medium != "email": @@ -340,7 +328,6 @@ def on_POST(self, request): [[LoginType.EMAIL_IDENTITY], [LoginType.MSISDN]], body, self.hs.get_ip_from_request(request), - password_servlet=True, ) if LoginType.EMAIL_IDENTITY in result: diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 3120c153e91b..b6361ac05570 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -41,6 +41,7 @@ parse_json_object_from_request, parse_string, ) +from synapse.push.mailer import load_jinja2_templates from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.ratelimitutils import FederationRateLimiter from synapse.util.threepids import check_3pid_allowed @@ -78,16 +79,21 @@ def __init__(self, hs): if self.hs.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL: from synapse.push.mailer import Mailer, load_jinja2_templates - templates = load_jinja2_templates( - config=hs.config, - template_html_name=hs.config.email_registration_template_html, - template_text_name=hs.config.email_registration_template_text, + template_html, template_text = load_jinja2_templates( + self.config.email_template_dir, + [ + self.config.email_registration_template_html, + self.config.email_registration_template_text, + ], + apply_format_ts_filter=True, + apply_mxc_to_http_filter=True, + public_baseurl=self.config.public_baseurl, ) self.mailer = Mailer( hs=self.hs, - app_name=self.hs.config.email_app_name, - template_html=templates[0], - template_text=templates[1], + app_name=self.config.email_app_name, + template_html=template_html, + template_text=template_text, ) @defer.inlineCallbacks @@ -284,16 +290,19 @@ def on_GET(self, request, medium): request.setResponseCode(200) except ThreepidValidationError as e: # Show a failure page with a reason - html = self.load_jinja2_template( + request.setResponseCode(e.code) + + # Show a failure page with a reason + html_template = load_jinja2_templates( self.config.email_template_dir, - self.config.email_registration_template_failure_html, - template_vars={"failure_reason": e.msg}, + [self.config.email_registration_template_failure_html], ) - request.setResponseCode(e.code) + + template_vars = {"failure_reason": e.msg} + html = html_template.render(**template_vars) request.write(html.encode("utf-8")) finish_request(request) - return None class UsernameAvailabilityRestServlet(RestServlet): @@ -386,7 +395,6 @@ def on_POST(self, request): if kind == b"guest": ret = yield self._do_guest_registration(body, address=client_addr) return ret - return elif kind != b"user": raise UnrecognizedRequestError( "Do not understand membership kind: %s" % (kind,) @@ -436,7 +444,6 @@ def on_POST(self, request): desired_username, access_token, body ) return (200, result) # we throw for non 200 responses - return # for regular registration, downcase the provided username before # attempting to register it. This should mean From 38ae8fdb30f228bc355cdb31e4d28cebbcb23a3c Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 5 Sep 2019 16:06:27 +0100 Subject: [PATCH 7/8] lint --- synapse/handlers/identity.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 4b08e4ea4eba..71b5a8739243 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -23,7 +23,12 @@ from twisted.internet import defer -from synapse.api.errors import CodeMessageException, HttpResponseException, SynapseError +from synapse.api.errors import ( + CodeMessageException, + Codes, + HttpResponseException, + SynapseError, +) from synapse.util.stringutils import random_string from ._base import BaseHandler From 71ad861adfa657f8d08477e22173c186f6cc22ba Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Thu, 5 Sep 2019 16:15:35 +0100 Subject: [PATCH 8/8] UPGRADE.rst fixup --- UPGRADE.rst | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/UPGRADE.rst b/UPGRADE.rst index 1ede45d13955..dddcd75fdac7 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -72,29 +72,28 @@ homeserver should delegate an external server (typically an `identity server `_) to handle sending password reset or registration messages via email and SMS. -Specifically for email, if ``email.trust_identity_server_for_password_resets`` was changed from -its default to ``true``, and ``account_threepid_delegates.email`` is not set, then the server -handling password resets and registration via third-party addresses will be set to the first -entry in the Synapse config's ``trusted_third_party_id_servers`` entry. This is to ensure that -people who set up an external server for handling these tasks before v1.4.0 will not have their -setups mysteriously stop working. However, if no trusted identity server domains are -configured, Synapse will throw an error. - -If ``email.trust_identity_server_for_password_resets`` is not set to ``true`` and a type in -``account_threepid_delegates`` is not set to a domain, then Synapse will attempt to send -password reset and registration messages itself for that type. +If ``email.trust_identity_server_for_password_resets`` is set to ``true``, and +``account_threepid_delegates.email`` is not set, then the first entry in +``trusted_third_party_id_servers`` will be used as the account threepid delegate for email. +This is to ensure compatibility with existing Synapse installs that set up external server +handling for these tasks before v1.4.0. If ``email.trust_identity_server_for_password_resets`` +is ``true`` and no trusted identity server domains are configured, Synapse will throw an error. + +If ``email.trust_identity_server_for_password_resets`` is ``false`` or absent and a threepid +type in ``account_threepid_delegates`` is not set to a domain, then Synapse will attempt to +send password reset and registration messages for that type. Email templates --------------- If you have configured a custom template directory with the ``email.template_dir`` option, be aware that there are new templates regarding registration. ``registration.html`` and -``registration.txt`` have been added and contain the text that is sent to a client upon -registering via email address. +``registration.txt`` have been added and contain the content that is sent to a client upon +registering via an email address. -``registration_success.html`` and ``registration_failure.html`` are templates containing HTML -that will be shown to the user when they click the link in their registration email (if a -redirect URL is not configured), either showing them a success or failure page. +``registration_success.html`` and ``registration_failure.html`` are also new HTML templates +that will be shown to the user when they click the link in their registration emai , either +showing them a success or failure page (assuming a redirect URL is not configured). Synapse will expect these files to exist inside the configured template directory. To view the default templates, see `synapse/res/templates