Compare commits
55 commits
develop
...
anoa/hs_pa
Author | SHA1 | Date | |
---|---|---|---|
|
ad4e2ed2ca | ||
|
5d6e3a2c83 | ||
|
22e4dfa7f0 | ||
|
96f43fe81d | ||
|
78a8992661 | ||
|
2796afc188 | ||
|
9e743c7069 | ||
|
3b0a477db3 | ||
|
25af3f96c6 | ||
|
1460f14c66 | ||
|
6d7890401b | ||
|
1fd217c7cb | ||
|
8dba4bab44 | ||
|
fa2794405d | ||
|
828cdbbcd8 | ||
|
f7395bbd0a | ||
|
1a796cbd38 | ||
|
7168dee695 | ||
|
92090d32d4 | ||
|
cd4f4a2ab4 | ||
|
a37a2f13cf | ||
|
3478213392 | ||
|
9d5f75f3d8 | ||
|
ddc219578b | ||
|
1dcf4eb344 | ||
|
d8d198fbd3 | ||
|
ec781af214 | ||
|
6efb301e05 | ||
|
12ed769fbf | ||
|
78ca92a9b2 | ||
|
6a9588cc60 | ||
|
efa1a56552 | ||
|
a4c0907b84 | ||
|
f522cde541 | ||
|
79bc66883f | ||
|
70b161decc | ||
|
4c406f5afc | ||
|
c9573ca069 | ||
|
91eac880a1 | ||
|
fe0af298ff | ||
|
639471582c | ||
|
6d2d3c9fd3 | ||
|
177f02459a | ||
|
752dbeea70 | ||
|
a862f2adc4 | ||
|
a0e2a103a6 | ||
|
24f31dfb59 | ||
|
62e1ec098c | ||
|
354d749ae1 | ||
|
309943f2ef | ||
|
899219c48c | ||
|
094c351f1d | ||
|
ed35302cd1 | ||
|
9567c60ffa | ||
|
dbdebc2c6f |
1
changelog.d/5377.feature
Normal file
1
changelog.d/5377.feature
Normal file
|
@ -0,0 +1 @@
|
|||
Add ability to perform password reset via email without trusting the identity server.
|
|
@ -1018,10 +1018,8 @@ password_config:
|
|||
|
||||
|
||||
|
||||
# Enable sending emails for notification events or expiry notices
|
||||
# Defining a custom URL for Riot is only needed if email notifications
|
||||
# should contain links to a self-hosted installation of Riot; when set
|
||||
# the "app_name" setting is ignored.
|
||||
# Enable sending emails for password resets, notification events or
|
||||
# account expiry notices
|
||||
#
|
||||
# If your SMTP server requires authentication, the optional smtp_user &
|
||||
# smtp_pass variables should be used
|
||||
|
@ -1029,22 +1027,64 @@ password_config:
|
|||
#email:
|
||||
# enable_notifs: false
|
||||
# smtp_host: "localhost"
|
||||
# smtp_port: 25
|
||||
# smtp_port: 25 # SSL: 465, STARTTLS: 587
|
||||
# smtp_user: "exampleusername"
|
||||
# smtp_pass: "examplepassword"
|
||||
# require_transport_security: False
|
||||
# notif_from: "Your Friendly %(app)s Home Server <noreply@example.com>"
|
||||
# app_name: Matrix
|
||||
# # if template_dir is unset, uses the example templates that are part of
|
||||
# # the Synapse distribution.
|
||||
#
|
||||
# # Enable email notifications by default
|
||||
# notif_for_new_users: True
|
||||
#
|
||||
# # Defining a custom URL for Riot is only needed if email notifications
|
||||
# # should contain links to a self-hosted installation of Riot; when set
|
||||
# # the "app_name" setting is ignored
|
||||
# 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
|
||||
# #
|
||||
# # This is currently used for password resets
|
||||
# #validation_token_lifetime: 1h
|
||||
#
|
||||
# # Template directory. All template files should be stored within this
|
||||
# # directory
|
||||
# #
|
||||
# #template_dir: res/templates
|
||||
#
|
||||
# # Templates for email notifications
|
||||
# #
|
||||
# notif_template_html: notif_mail.html
|
||||
# notif_template_text: notif_mail.txt
|
||||
# # Templates for account expiry notices.
|
||||
#
|
||||
# # Templates for account expiry notices
|
||||
# #
|
||||
# expiry_template_html: notice_expiry.html
|
||||
# expiry_template_text: notice_expiry.txt
|
||||
# notif_for_new_users: True
|
||||
# riot_base_url: "http://localhost/riot"
|
||||
#
|
||||
# # Templates for password reset emails sent by the homeserver
|
||||
# #
|
||||
# #password_reset_template_html: password_reset.html
|
||||
# #password_reset_template_text: password_reset.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
|
||||
|
||||
|
||||
#password_providers:
|
||||
|
|
|
@ -339,6 +339,15 @@ class UnsupportedRoomVersionError(SynapseError):
|
|||
)
|
||||
|
||||
|
||||
class ThreepidValidationError(SynapseError):
|
||||
"""An error raised when there was a problem authorising an event."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "errcode" not in kwargs:
|
||||
kwargs["errcode"] = Codes.FORBIDDEN
|
||||
super(ThreepidValidationError, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class IncompatibleRoomVersionError(SynapseError):
|
||||
"""A server is trying to join a room whose version it does not support.
|
||||
|
||||
|
|
|
@ -176,6 +176,7 @@ class SynapseHomeServer(HomeServer):
|
|||
|
||||
resources.update({
|
||||
"/_matrix/client/api/v1": client_resource,
|
||||
"/_synapse/password_reset": client_resource,
|
||||
"/_matrix/client/r0": client_resource,
|
||||
"/_matrix/client/unstable": client_resource,
|
||||
"/_matrix/client/v2_alpha": client_resource,
|
||||
|
|
|
@ -50,6 +50,11 @@ class EmailConfig(Config):
|
|||
else:
|
||||
self.email_app_name = "Matrix"
|
||||
|
||||
# TODO: Rename notif_from to something more generic, or have a separate
|
||||
# from for password resets, message notifications, etc?
|
||||
# Currently the email section is a bit bogged down with settings for
|
||||
# multiple functions. Would be good to split it out into separate
|
||||
# sections and only put the common ones under email:
|
||||
self.email_notif_from = email_config.get("notif_from", None)
|
||||
if self.email_notif_from is not None:
|
||||
# make sure it's valid
|
||||
|
@ -74,7 +79,28 @@ class EmailConfig(Config):
|
|||
"account_validity", {},
|
||||
).get("renew_at")
|
||||
|
||||
if self.email_enable_notifs or account_validity_renewal_enabled:
|
||||
email_trust_identity_server_for_password_resets = email_config.get(
|
||||
"trust_identity_server_for_password_resets", False,
|
||||
)
|
||||
self.email_password_reset_behaviour = (
|
||||
"remote" if email_trust_identity_server_for_password_resets else "local"
|
||||
)
|
||||
if self.email_password_reset_behaviour == "local" and email_config == {}:
|
||||
logger.warn(
|
||||
"User password resets have been disabled due to lack of email config"
|
||||
)
|
||||
self.email_password_reset_behaviour = "off"
|
||||
|
||||
# Get lifetime of a validation token in milliseconds
|
||||
self.email_validation_token_lifetime = self.parse_duration(
|
||||
email_config.get("validation_token_lifetime", "1h")
|
||||
)
|
||||
|
||||
if (
|
||||
self.email_enable_notifs
|
||||
or account_validity_renewal_enabled
|
||||
or self.email_password_reset_behaviour == "local"
|
||||
):
|
||||
# make sure we can import the required deps
|
||||
import jinja2
|
||||
import bleach
|
||||
|
@ -82,6 +108,67 @@ class EmailConfig(Config):
|
|||
jinja2
|
||||
bleach
|
||||
|
||||
if self.email_password_reset_behaviour == "local":
|
||||
required = [
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"notif_from",
|
||||
]
|
||||
|
||||
missing = []
|
||||
for k in required:
|
||||
if k not in email_config:
|
||||
missing.append(k)
|
||||
|
||||
if (len(missing) > 0):
|
||||
raise RuntimeError(
|
||||
"email.password_reset_behaviour is set to 'local' "
|
||||
"but required keys are missing: %s" %
|
||||
(", ".join(["email." + k for k in missing]),)
|
||||
)
|
||||
|
||||
# Templates for password reset emails
|
||||
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_password_reset_failure_template = email_config.get(
|
||||
"password_reset_failure_template", "password_reset_failure.html",
|
||||
)
|
||||
# This template does not support any replaceable variables, so we will
|
||||
# read it from the disk once during setup
|
||||
email_password_reset_success_template = email_config.get(
|
||||
"password_reset_success_template", "password_reset_success.html",
|
||||
)
|
||||
|
||||
# Check templates exist
|
||||
for f in [self.email_password_reset_template_html,
|
||||
self.email_password_reset_template_text,
|
||||
self.email_password_reset_failure_template,
|
||||
email_password_reset_success_template]:
|
||||
p = os.path.join(self.email_template_dir, f)
|
||||
if not os.path.isfile(p):
|
||||
raise ConfigError("Unable to find template file %s" % (p, ))
|
||||
|
||||
# Retrieve content of web templates
|
||||
filepath = os.path.join(
|
||||
self.email_template_dir,
|
||||
email_password_reset_success_template,
|
||||
)
|
||||
self.email_password_reset_success_html_content = self.read_file(
|
||||
filepath,
|
||||
"email.password_reset_template_success_html",
|
||||
)
|
||||
|
||||
if config.get("public_baseurl") is None:
|
||||
raise RuntimeError(
|
||||
"email.password_reset_behaviour is set to 'local' but no "
|
||||
"public_baseurl is set. This is necessary to generate password "
|
||||
"reset links"
|
||||
)
|
||||
|
||||
if self.email_enable_notifs:
|
||||
required = [
|
||||
"smtp_host",
|
||||
|
@ -121,10 +208,6 @@ class EmailConfig(Config):
|
|||
self.email_riot_base_url = email_config.get(
|
||||
"riot_base_url", None
|
||||
)
|
||||
else:
|
||||
self.email_enable_notifs = False
|
||||
# Not much point setting defaults for the rest: it would be an
|
||||
# error for them to be used.
|
||||
|
||||
if account_validity_renewal_enabled:
|
||||
self.email_expiry_template_html = email_config.get(
|
||||
|
@ -141,10 +224,8 @@ class EmailConfig(Config):
|
|||
|
||||
def default_config(self, config_dir_path, server_name, **kwargs):
|
||||
return """
|
||||
# Enable sending emails for notification events or expiry notices
|
||||
# Defining a custom URL for Riot is only needed if email notifications
|
||||
# should contain links to a self-hosted installation of Riot; when set
|
||||
# the "app_name" setting is ignored.
|
||||
# Enable sending emails for password resets, notification events or
|
||||
# account expiry notices
|
||||
#
|
||||
# If your SMTP server requires authentication, the optional smtp_user &
|
||||
# smtp_pass variables should be used
|
||||
|
@ -152,20 +233,62 @@ class EmailConfig(Config):
|
|||
#email:
|
||||
# enable_notifs: false
|
||||
# smtp_host: "localhost"
|
||||
# smtp_port: 25
|
||||
# smtp_port: 25 # SSL: 465, STARTTLS: 587
|
||||
# smtp_user: "exampleusername"
|
||||
# smtp_pass: "examplepassword"
|
||||
# require_transport_security: False
|
||||
# notif_from: "Your Friendly %(app)s Home Server <noreply@example.com>"
|
||||
# app_name: Matrix
|
||||
# # if template_dir is unset, uses the example templates that are part of
|
||||
# # the Synapse distribution.
|
||||
#
|
||||
# # Enable email notifications by default
|
||||
# notif_for_new_users: True
|
||||
#
|
||||
# # Defining a custom URL for Riot is only needed if email notifications
|
||||
# # should contain links to a self-hosted installation of Riot; when set
|
||||
# # the "app_name" setting is ignored
|
||||
# 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
|
||||
# #
|
||||
# # This is currently used for password resets
|
||||
# #validation_token_lifetime: 1h
|
||||
#
|
||||
# # Template directory. All template files should be stored within this
|
||||
# # directory
|
||||
# #
|
||||
# #template_dir: res/templates
|
||||
#
|
||||
# # Templates for email notifications
|
||||
# #
|
||||
# notif_template_html: notif_mail.html
|
||||
# notif_template_text: notif_mail.txt
|
||||
# # Templates for account expiry notices.
|
||||
#
|
||||
# # Templates for account expiry notices
|
||||
# #
|
||||
# expiry_template_html: notice_expiry.html
|
||||
# expiry_template_text: notice_expiry.txt
|
||||
# notif_for_new_users: True
|
||||
# riot_base_url: "http://localhost/riot"
|
||||
#
|
||||
# # Templates for password reset emails sent by the homeserver
|
||||
# #
|
||||
# #password_reset_template_html: password_reset.html
|
||||
# #password_reset_template_text: password_reset.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
|
||||
"""
|
||||
|
|
|
@ -107,7 +107,7 @@ class TlsConfig(Config):
|
|||
certs = []
|
||||
for ca_file in custom_ca_list:
|
||||
logger.debug("Reading custom CA certificate file: %s", ca_file)
|
||||
content = self.read_file(ca_file)
|
||||
content = self.read_file(ca_file, "federation_custom_ca_list")
|
||||
|
||||
# Parse the CA certificates
|
||||
try:
|
||||
|
|
|
@ -162,7 +162,7 @@ class AuthHandler(BaseHandler):
|
|||
defer.returnValue(params)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_auth(self, flows, clientdict, clientip):
|
||||
def check_auth(self, flows, clientdict, clientip, password_servlet=False):
|
||||
"""
|
||||
Takes a dictionary sent by the client in the login / registration
|
||||
protocol and handles the User-Interactive Auth flow.
|
||||
|
@ -186,6 +186,16 @@ class AuthHandler(BaseHandler):
|
|||
|
||||
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).
|
||||
|
@ -241,7 +251,9 @@ class AuthHandler(BaseHandler):
|
|||
if 'type' in authdict:
|
||||
login_type = authdict['type']
|
||||
try:
|
||||
result = yield self._check_auth_dict(authdict, clientip)
|
||||
result = yield self._check_auth_dict(
|
||||
authdict, clientip, password_servlet=password_servlet,
|
||||
)
|
||||
if result:
|
||||
creds[login_type] = result
|
||||
self._save_session(session)
|
||||
|
@ -351,7 +363,7 @@ class AuthHandler(BaseHandler):
|
|||
return sess.setdefault('serverdict', {}).get(key, default)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _check_auth_dict(self, authdict, clientip):
|
||||
def _check_auth_dict(self, authdict, clientip, password_servlet=False):
|
||||
"""Attempt to validate the auth dict provided by a client
|
||||
|
||||
Args:
|
||||
|
@ -369,7 +381,13 @@ class AuthHandler(BaseHandler):
|
|||
login_type = authdict['type']
|
||||
checker = self.checkers.get(login_type)
|
||||
if checker is not None:
|
||||
res = yield checker(authdict, clientip)
|
||||
# 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,
|
||||
)
|
||||
defer.returnValue(res)
|
||||
|
||||
# build a v1-login-style dict out of the authdict and fall back to the
|
||||
|
@ -383,7 +401,7 @@ class AuthHandler(BaseHandler):
|
|||
defer.returnValue(canonical_id)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _check_recaptcha(self, authdict, clientip):
|
||||
def _check_recaptcha(self, authdict, clientip, **kwargs):
|
||||
try:
|
||||
user_response = authdict["response"]
|
||||
except KeyError:
|
||||
|
@ -429,20 +447,20 @@ class AuthHandler(BaseHandler):
|
|||
defer.returnValue(True)
|
||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
||||
|
||||
def _check_email_identity(self, authdict, _):
|
||||
return self._check_threepid('email', authdict)
|
||||
def _check_email_identity(self, authdict, **kwargs):
|
||||
return self._check_threepid('email', authdict, **kwargs)
|
||||
|
||||
def _check_msisdn(self, authdict, _):
|
||||
def _check_msisdn(self, authdict, **kwargs):
|
||||
return self._check_threepid('msisdn', authdict)
|
||||
|
||||
def _check_dummy_auth(self, authdict, _):
|
||||
def _check_dummy_auth(self, authdict, **kwargs):
|
||||
return defer.succeed(True)
|
||||
|
||||
def _check_terms_auth(self, authdict, _):
|
||||
def _check_terms_auth(self, authdict, **kwargs):
|
||||
return defer.succeed(True)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _check_threepid(self, medium, authdict):
|
||||
def _check_threepid(self, medium, authdict, password_servlet=False, **kwargs):
|
||||
if 'threepid_creds' not in authdict:
|
||||
raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
|
||||
|
||||
|
@ -451,7 +469,29 @@ class AuthHandler(BaseHandler):
|
|||
identity_handler = self.hs.get_handlers().identity_handler
|
||||
|
||||
logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
|
||||
threepid = yield identity_handler.threepid_from_creds(threepid_creds)
|
||||
if (
|
||||
not password_servlet
|
||||
or self.hs.config.email_password_reset_behaviour == "remote"
|
||||
):
|
||||
threepid = yield identity_handler.threepid_from_creds(threepid_creds)
|
||||
elif self.hs.config.email_password_reset_behaviour == "local":
|
||||
row = yield self.store.get_threepid_validation_session(
|
||||
medium,
|
||||
threepid_creds["client_secret"],
|
||||
sid=threepid_creds["sid"],
|
||||
)
|
||||
|
||||
threepid = {
|
||||
"medium": row["medium"],
|
||||
"address": row["address"],
|
||||
"validated_at": row["validated_at"],
|
||||
} if row else None
|
||||
|
||||
if row:
|
||||
# Valid threepid returned, delete from the db
|
||||
yield self.store.delete_threepid_session(threepid_creds["sid"])
|
||||
else:
|
||||
raise SynapseError(400, "Password resets are not enabled on this homeserver")
|
||||
|
||||
if not threepid:
|
||||
raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
|
||||
|
|
|
@ -247,7 +247,14 @@ class IdentityHandler(BaseHandler):
|
|||
defer.returnValue(changed)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def requestEmailToken(self, id_server, email, client_secret, send_attempt, **kwargs):
|
||||
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,
|
||||
|
@ -259,7 +266,9 @@ class IdentityHandler(BaseHandler):
|
|||
'client_secret': client_secret,
|
||||
'send_attempt': send_attempt,
|
||||
}
|
||||
params.update(kwargs)
|
||||
|
||||
if next_link:
|
||||
params.update({'next_link': next_link})
|
||||
|
||||
try:
|
||||
data = yield self.http_client.post_json_get_json(
|
||||
|
|
|
@ -80,10 +80,10 @@ ALLOWED_ATTRS = {
|
|||
|
||||
|
||||
class Mailer(object):
|
||||
def __init__(self, hs, app_name, notif_template_html, notif_template_text):
|
||||
def __init__(self, hs, app_name, template_html, template_text):
|
||||
self.hs = hs
|
||||
self.notif_template_html = notif_template_html
|
||||
self.notif_template_text = notif_template_text
|
||||
self.template_html = template_html
|
||||
self.template_text = template_text
|
||||
|
||||
self.sendmail = self.hs.get_sendmail()
|
||||
self.store = self.hs.get_datastore()
|
||||
|
@ -93,22 +93,49 @@ class Mailer(object):
|
|||
|
||||
logger.info("Created Mailer for app_name %s" % app_name)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_password_reset_mail(
|
||||
self,
|
||||
email_address,
|
||||
token,
|
||||
client_secret,
|
||||
sid,
|
||||
):
|
||||
"""Send an email with a password reset link to a user
|
||||
|
||||
Args:
|
||||
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
|
||||
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 +
|
||||
"_synapse/password_reset/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] Password Reset Email" % self.hs.config.server_name,
|
||||
template_vars,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_notification_mail(self, app_id, user_id, email_address,
|
||||
push_actions, reason):
|
||||
try:
|
||||
from_string = self.hs.config.email_notif_from % {
|
||||
"app": self.app_name
|
||||
}
|
||||
except TypeError:
|
||||
from_string = self.hs.config.email_notif_from
|
||||
|
||||
raw_from = email.utils.parseaddr(from_string)[1]
|
||||
raw_to = email.utils.parseaddr(email_address)[1]
|
||||
|
||||
if raw_to == '':
|
||||
raise RuntimeError("Invalid 'to' address")
|
||||
|
||||
"""Send email regarding a user's room notifications"""
|
||||
rooms_in_order = deduped_ordered_list(
|
||||
[pa['room_id'] for pa in push_actions]
|
||||
)
|
||||
|
@ -176,14 +203,36 @@ class Mailer(object):
|
|||
"reason": reason,
|
||||
}
|
||||
|
||||
html_text = self.notif_template_html.render(**template_vars)
|
||||
yield self.send_email(
|
||||
email_address,
|
||||
"[%s] %s" % (self.app_name, summary_text),
|
||||
template_vars,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_email(self, email_address, subject, template_vars):
|
||||
"""Send an email with the given information and template text"""
|
||||
try:
|
||||
from_string = self.hs.config.email_notif_from % {
|
||||
"app": self.app_name
|
||||
}
|
||||
except TypeError:
|
||||
from_string = self.hs.config.email_notif_from
|
||||
|
||||
raw_from = email.utils.parseaddr(from_string)[1]
|
||||
raw_to = email.utils.parseaddr(email_address)[1]
|
||||
|
||||
if raw_to == '':
|
||||
raise RuntimeError("Invalid 'to' address")
|
||||
|
||||
html_text = self.template_html.render(**template_vars)
|
||||
html_part = MIMEText(html_text, "html", "utf8")
|
||||
|
||||
plain_text = self.notif_template_text.render(**template_vars)
|
||||
plain_text = self.template_text.render(**template_vars)
|
||||
text_part = MIMEText(plain_text, "plain", "utf8")
|
||||
|
||||
multipart_msg = MIMEMultipart('alternative')
|
||||
multipart_msg['Subject'] = "[%s] %s" % (self.app_name, summary_text)
|
||||
multipart_msg['Subject'] = subject
|
||||
multipart_msg['From'] = from_string
|
||||
multipart_msg['To'] = email_address
|
||||
multipart_msg['Date'] = email.utils.formatdate()
|
||||
|
|
|
@ -70,8 +70,8 @@ class PusherFactory(object):
|
|||
mailer = Mailer(
|
||||
hs=self.hs,
|
||||
app_name=app_name,
|
||||
notif_template_html=self.notif_template_html,
|
||||
notif_template_text=self.notif_template_text,
|
||||
template_html=self.notif_template_html,
|
||||
template_text=self.notif_template_text,
|
||||
)
|
||||
self.mailers[app_name] = mailer
|
||||
return EmailPusher(self.hs, pusherdict, mailer)
|
||||
|
|
|
@ -77,7 +77,7 @@ REQUIREMENTS = [
|
|||
]
|
||||
|
||||
CONDITIONAL_REQUIREMENTS = {
|
||||
"email.enable_notifs": ["Jinja2>=2.9", "bleach>=1.4.2"],
|
||||
"email": ["Jinja2>=2.9", "bleach>=1.4.2"],
|
||||
"matrix-synapse-ldap3": ["matrix-synapse-ldap3>=0.1"],
|
||||
|
||||
# we use execute_batch, which arrived in psycopg 2.7.
|
||||
|
|
9
synapse/res/templates/password_reset.html
Normal file
9
synapse/res/templates/password_reset.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<html>
|
||||
<body>
|
||||
<p>A password reset request has been received for your Matrix account. If this was you, please click the link below to confirm resetting your password:</p>
|
||||
|
||||
<a href="{{ link }}">{{ link }}</a>
|
||||
|
||||
<p>If this was not you, please disregard this email and contact your server administrator. Thank you.</p>
|
||||
</body>
|
||||
</html>
|
7
synapse/res/templates/password_reset.txt
Normal file
7
synapse/res/templates/password_reset.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
A password reset request has been received for your Matrix account. If this
|
||||
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.
|
6
synapse/res/templates/password_reset_failure.html
Normal file
6
synapse/res/templates/password_reset_failure.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>{{ failure_reason }}. Your password has not been reset.</p>
|
||||
</body>
|
||||
</html>
|
6
synapse/res/templates/password_reset_success.html
Normal file
6
synapse/res/templates/password_reset_success.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<p>Your password was successfully reset. You may now close this window.</p>
|
||||
</body>
|
||||
</html>
|
|
@ -15,19 +15,25 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
import re
|
||||
|
||||
from six.moves import http_client
|
||||
|
||||
import jinja2
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import LoginType
|
||||
from synapse.api.errors import Codes, SynapseError
|
||||
from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
|
||||
from synapse.http.server import finish_request
|
||||
from synapse.http.servlet import (
|
||||
RestServlet,
|
||||
assert_params_in_dict,
|
||||
parse_json_object_from_request,
|
||||
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
|
||||
|
@ -41,17 +47,42 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
|
|||
def __init__(self, hs):
|
||||
super(EmailPasswordRequestTokenRestServlet, self).__init__()
|
||||
self.hs = hs
|
||||
self.datastore = hs.get_datastore()
|
||||
self.config = hs.config
|
||||
self.identity_handler = hs.get_handlers().identity_handler
|
||||
|
||||
if self.config.email_password_reset_behaviour == "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,
|
||||
)
|
||||
self.mailer = Mailer(
|
||||
hs=self.hs,
|
||||
app_name=self.config.email_app_name,
|
||||
template_html=templates[0],
|
||||
template_text=templates[1],
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
if self.config.email_password_reset_behaviour == "off":
|
||||
raise SynapseError(400, "Password resets have been disabled on this server")
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
assert_params_in_dict(body, [
|
||||
'id_server', 'client_secret', 'email', 'send_attempt'
|
||||
'client_secret', 'email', 'send_attempt'
|
||||
])
|
||||
|
||||
if not check_3pid_allowed(self.hs, "email", body['email']):
|
||||
# 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", email):
|
||||
raise SynapseError(
|
||||
403,
|
||||
"Your email domain is not authorized on this server",
|
||||
|
@ -59,15 +90,100 @@ class EmailPasswordRequestTokenRestServlet(RestServlet):
|
|||
)
|
||||
|
||||
existingUid = yield self.hs.get_datastore().get_user_id_by_threepid(
|
||||
'email', body['email']
|
||||
'email', email,
|
||||
)
|
||||
|
||||
if existingUid is None:
|
||||
raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND)
|
||||
|
||||
ret = yield self.identity_handler.requestEmailToken(**body)
|
||||
if self.config.email_password_reset_behaviour == "remote":
|
||||
if 'id_server' not in body:
|
||||
raise SynapseError(400, "Missing 'id_server' param in body")
|
||||
|
||||
# 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,
|
||||
)
|
||||
else:
|
||||
# Send password reset emails from Synapse
|
||||
sid = yield self.send_password_reset(
|
||||
email, client_secret, send_attempt, next_link,
|
||||
)
|
||||
|
||||
# Wrap the session id in a JSON object
|
||||
ret = {"sid": sid}
|
||||
|
||||
defer.returnValue((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
|
||||
|
||||
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
|
||||
defer.returnValue(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,
|
||||
)
|
||||
|
||||
defer.returnValue(session_id)
|
||||
|
||||
|
||||
class MsisdnPasswordRequestTokenRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/password/msisdn/requestToken$")
|
||||
|
@ -80,6 +196,9 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
if not self.config.email_password_reset_behaviour == "off":
|
||||
raise SynapseError(400, "Password resets have been disabled on this server")
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
|
||||
assert_params_in_dict(body, [
|
||||
|
@ -107,6 +226,118 @@ class MsisdnPasswordRequestTokenRestServlet(RestServlet):
|
|||
defer.returnValue((200, ret))
|
||||
|
||||
|
||||
class PasswordResetSubmitTokenServlet(RestServlet):
|
||||
"""Handles 3PID validation token submission"""
|
||||
PATTERNS = [
|
||||
re.compile("^/_synapse/password_reset/(?P<medium>[^/]*)/submit_token/*$"),
|
||||
]
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
super(PasswordResetSubmitTokenServlet, self).__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.config = hs.config
|
||||
self.clock = hs.get_clock()
|
||||
self.datastore = hs.get_datastore()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, medium):
|
||||
if medium != "email":
|
||||
raise SynapseError(
|
||||
400,
|
||||
"This medium is currently not supported for password resets",
|
||||
)
|
||||
|
||||
sid = parse_string(request, "sid")
|
||||
client_secret = parse_string(request, "client_secret")
|
||||
token = parse_string(request, "token")
|
||||
|
||||
# Attempt to validate a 3PID sesssion
|
||||
try:
|
||||
# Mark the session as valid
|
||||
next_link = yield self.datastore.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)
|
||||
defer.returnValue(None)
|
||||
|
||||
# Otherwise show the success template
|
||||
html = self.config.email_password_reset_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_password_reset_failure_template,
|
||||
template_vars={
|
||||
"failure_reason": e.msg,
|
||||
}
|
||||
)
|
||||
request.setResponseCode(e.code)
|
||||
|
||||
request.write(html.encode('utf-8'))
|
||||
finish_request(request)
|
||||
defer.returnValue(None)
|
||||
|
||||
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":
|
||||
raise SynapseError(
|
||||
400,
|
||||
"This medium is currently not supported for password resets",
|
||||
)
|
||||
|
||||
body = parse_json_object_from_request(request)
|
||||
assert_params_in_dict(body, [
|
||||
'sid', 'client_secret', 'token',
|
||||
])
|
||||
|
||||
valid, _ = yield self.datastore.validate_threepid_validation_token(
|
||||
body['sid'],
|
||||
body['client_secret'],
|
||||
body['token'],
|
||||
self.clock.time_msec(),
|
||||
)
|
||||
response_code = 200 if valid else 400
|
||||
|
||||
defer.returnValue((response_code, {"success": valid}))
|
||||
|
||||
|
||||
class PasswordRestServlet(RestServlet):
|
||||
PATTERNS = client_patterns("/account/password$")
|
||||
|
||||
|
@ -144,6 +375,7 @@ class PasswordRestServlet(RestServlet):
|
|||
result, params, _ = yield self.auth_handler.check_auth(
|
||||
[[LoginType.EMAIL_IDENTITY], [LoginType.MSISDN]],
|
||||
body, self.hs.get_ip_from_request(request),
|
||||
password_servlet=True,
|
||||
)
|
||||
|
||||
if LoginType.EMAIL_IDENTITY in result:
|
||||
|
@ -417,6 +649,7 @@ class WhoamiRestServlet(RestServlet):
|
|||
def register_servlets(hs, http_server):
|
||||
EmailPasswordRequestTokenRestServlet(hs).register(http_server)
|
||||
MsisdnPasswordRequestTokenRestServlet(hs).register(http_server)
|
||||
PasswordResetSubmitTokenServlet(hs).register(http_server)
|
||||
PasswordRestServlet(hs).register(http_server)
|
||||
DeactivateAccountRestServlet(hs).register(http_server)
|
||||
EmailThreepidRequestTokenRestServlet(hs).register(http_server)
|
||||
|
|
|
@ -588,6 +588,10 @@ class SQLBaseStore(object):
|
|||
Args:
|
||||
table : string giving the table name
|
||||
values : dict of new column names and values for them
|
||||
or_ignore : bool stating whether an exception should be raised
|
||||
when a conflicting row already exists. If True, False will be
|
||||
returned by the function instead
|
||||
desc : string giving a description of the transaction
|
||||
|
||||
Returns:
|
||||
bool: Whether the row was inserted or not. Only useful when
|
||||
|
@ -1228,8 +1232,8 @@ class SQLBaseStore(object):
|
|||
)
|
||||
|
||||
txn.execute(select_sql, list(keyvalues.values()))
|
||||
|
||||
row = txn.fetchone()
|
||||
|
||||
if not row:
|
||||
if allow_none:
|
||||
return None
|
||||
|
|
|
@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
# Remember to update this number every time a change is made to database
|
||||
# schema files, so the users will be informed on server restarts.
|
||||
SCHEMA_VERSION = 54
|
||||
SCHEMA_VERSION = 55
|
||||
|
||||
dir_path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
|
|
@ -17,17 +17,20 @@
|
|||
|
||||
import re
|
||||
|
||||
from six import iterkeys
|
||||
from six.moves import range
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import UserTypes
|
||||
from synapse.api.errors import Codes, StoreError
|
||||
from synapse.api.errors import Codes, StoreError, ThreepidValidationError
|
||||
from synapse.storage import background_updates
|
||||
from synapse.storage._base import SQLBaseStore
|
||||
from synapse.types import UserID
|
||||
from synapse.util.caches.descriptors import cached, cachedInlineCallbacks
|
||||
|
||||
THIRTY_MINUTES_IN_MS = 30 * 60 * 1000
|
||||
|
||||
|
||||
class RegistrationWorkerStore(SQLBaseStore):
|
||||
def __init__(self, db_conn, hs):
|
||||
|
@ -422,7 +425,7 @@ class RegistrationWorkerStore(SQLBaseStore):
|
|||
defer.returnValue(None)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_user_id_by_threepid(self, medium, address):
|
||||
def get_user_id_by_threepid(self, medium, address, require_verified=False):
|
||||
"""Returns user id from threepid
|
||||
|
||||
Args:
|
||||
|
@ -595,6 +598,11 @@ class RegistrationStore(
|
|||
"user_threepids_grandfather", self._bg_user_threepids_grandfather,
|
||||
)
|
||||
|
||||
# Create a background job for culling expired 3PID validity tokens
|
||||
hs.get_clock().looping_call(
|
||||
self.cull_expired_threepid_validation_tokens, THIRTY_MINUTES_IN_MS,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def add_access_token_to_user(self, user_id, token, device_id=None):
|
||||
"""Adds an access token for the given user.
|
||||
|
@ -963,7 +971,6 @@ class RegistrationStore(
|
|||
We do this by grandfathering in existing user threepids assuming that
|
||||
they used one of the server configured trusted identity servers.
|
||||
"""
|
||||
|
||||
id_servers = set(self.config.trusted_third_party_id_servers)
|
||||
|
||||
def _bg_user_threepids_grandfather_txn(txn):
|
||||
|
@ -984,3 +991,280 @@ class RegistrationStore(
|
|||
yield self._end_background_update("user_threepids_grandfather")
|
||||
|
||||
defer.returnValue(1)
|
||||
|
||||
def get_threepid_validation_session(
|
||||
self,
|
||||
medium,
|
||||
client_secret,
|
||||
address=None,
|
||||
sid=None,
|
||||
validated=None,
|
||||
):
|
||||
"""Gets a session_id and last_send_attempt (if available) for a
|
||||
client_secret/medium/(address|session_id) combo
|
||||
|
||||
Args:
|
||||
medium (str|None): The medium of the 3PID
|
||||
address (str|None): The address of the 3PID
|
||||
sid (str|None): The ID of the validation session
|
||||
client_secret (str|None): A unique string provided by the client to
|
||||
help identify this validation attempt
|
||||
validated (bool|None): Whether sessions should be filtered by
|
||||
whether they have been validated already or not. None to
|
||||
perform no filtering
|
||||
|
||||
Returns:
|
||||
deferred {str, int}|None: A dict containing the
|
||||
latest session_id and send_attempt count for this 3PID.
|
||||
Otherwise None if there hasn't been a previous attempt
|
||||
"""
|
||||
keyvalues = {
|
||||
"medium": medium,
|
||||
"client_secret": client_secret,
|
||||
}
|
||||
if address:
|
||||
keyvalues["address"] = address
|
||||
if sid:
|
||||
keyvalues["session_id"] = sid
|
||||
|
||||
assert(address or sid)
|
||||
|
||||
def get_threepid_validation_session_txn(txn):
|
||||
sql = """
|
||||
SELECT address, session_id, medium, client_secret,
|
||||
last_send_attempt, validated_at
|
||||
FROM threepid_validation_session WHERE %s
|
||||
""" % (" AND ".join("%s = ?" % k for k in iterkeys(keyvalues)),)
|
||||
|
||||
if validated is not None:
|
||||
sql += " AND validated_at IS " + ("NOT NULL" if validated else "NULL")
|
||||
|
||||
sql += " LIMIT 1"
|
||||
|
||||
txn.execute(sql, list(keyvalues.values()))
|
||||
rows = self.cursor_to_dict(txn)
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
return rows[0]
|
||||
|
||||
return self.runInteraction(
|
||||
"get_threepid_validation_session",
|
||||
get_threepid_validation_session_txn,
|
||||
)
|
||||
|
||||
def validate_threepid_session(
|
||||
self,
|
||||
session_id,
|
||||
client_secret,
|
||||
token,
|
||||
current_ts,
|
||||
):
|
||||
"""Attempt to validate a threepid session using a token
|
||||
|
||||
Args:
|
||||
session_id (str): The id of a validation session
|
||||
client_secret (str): A unique string provided by the client to
|
||||
help identify this validation attempt
|
||||
token (str): A validation token
|
||||
current_ts (int): The current unix time in milliseconds. Used for
|
||||
checking token expiry status
|
||||
|
||||
Returns:
|
||||
deferred str|None: A str representing a link to redirect the user
|
||||
to if there is one.
|
||||
"""
|
||||
# Insert everything into a transaction in order to run atomically
|
||||
def validate_threepid_session_txn(txn):
|
||||
row = self._simple_select_one_txn(
|
||||
txn,
|
||||
table="threepid_validation_session",
|
||||
keyvalues={"session_id": session_id},
|
||||
retcols=["client_secret", "validated_at"],
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
if not row:
|
||||
raise ThreepidValidationError(400, "Unknown session_id")
|
||||
retrieved_client_secret = row["client_secret"]
|
||||
validated_at = row["validated_at"]
|
||||
|
||||
if retrieved_client_secret != client_secret:
|
||||
raise ThreepidValidationError(
|
||||
400, "This client_secret does not match the provided session_id",
|
||||
)
|
||||
|
||||
row = self._simple_select_one_txn(
|
||||
txn,
|
||||
table="threepid_validation_token",
|
||||
keyvalues={"session_id": session_id, "token": token},
|
||||
retcols=["expires", "next_link"],
|
||||
allow_none=True,
|
||||
)
|
||||
|
||||
if not row:
|
||||
raise ThreepidValidationError(
|
||||
400, "Validation token not found or has expired",
|
||||
)
|
||||
expires = row["expires"]
|
||||
next_link = row["next_link"]
|
||||
|
||||
# If the session is already validated, no need to revalidate
|
||||
if validated_at:
|
||||
return next_link
|
||||
|
||||
if expires <= current_ts:
|
||||
raise ThreepidValidationError(
|
||||
400, "This token has expired. Please request a new one",
|
||||
)
|
||||
|
||||
# Looks good. Validate the session
|
||||
self._simple_update_txn(
|
||||
txn,
|
||||
table="threepid_validation_session",
|
||||
keyvalues={"session_id": session_id},
|
||||
updatevalues={"validated_at": self.clock.time_msec()},
|
||||
)
|
||||
|
||||
return next_link
|
||||
|
||||
# Return next_link if it exists
|
||||
return self.runInteraction(
|
||||
"validate_threepid_session_txn",
|
||||
validate_threepid_session_txn,
|
||||
)
|
||||
|
||||
def upsert_threepid_validation_session(
|
||||
self,
|
||||
medium,
|
||||
address,
|
||||
client_secret,
|
||||
send_attempt,
|
||||
session_id,
|
||||
validated_at=None,
|
||||
):
|
||||
"""Upsert a threepid validation session
|
||||
Args:
|
||||
medium (str): The medium of the 3PID
|
||||
address (str): The address of the 3PID
|
||||
client_secret (str): A unique string provided by the client to
|
||||
help identify this validation attempt
|
||||
send_attempt (int): The latest send_attempt on this session
|
||||
session_id (str): The id of this validation session
|
||||
validated_at (int|None): The unix timestamp in milliseconds of
|
||||
when the session was marked as valid
|
||||
"""
|
||||
insertion_values = {
|
||||
"medium": medium,
|
||||
"address": address,
|
||||
"client_secret": client_secret,
|
||||
}
|
||||
|
||||
if validated_at:
|
||||
insertion_values["validated_at"] = validated_at
|
||||
|
||||
return self._simple_upsert(
|
||||
table="threepid_validation_session",
|
||||
keyvalues={"session_id": session_id},
|
||||
values={"last_send_attempt": send_attempt},
|
||||
insertion_values=insertion_values,
|
||||
desc="upsert_threepid_validation_session",
|
||||
)
|
||||
|
||||
def start_or_continue_validation_session(
|
||||
self,
|
||||
medium,
|
||||
address,
|
||||
session_id,
|
||||
client_secret,
|
||||
send_attempt,
|
||||
next_link,
|
||||
token,
|
||||
token_expires,
|
||||
):
|
||||
"""Creates a new threepid validation session if it does not already
|
||||
exist and associates a new validation token with it
|
||||
|
||||
Args:
|
||||
medium (str): The medium of the 3PID
|
||||
address (str): The address of the 3PID
|
||||
session_id (str): The id of this validation session
|
||||
client_secret (str): A unique string provided by the client to
|
||||
help identify this validation attempt
|
||||
send_attempt (int): The latest send_attempt on this session
|
||||
next_link (str|None): The link to redirect the user to upon
|
||||
successful validation
|
||||
token (str): The validation token
|
||||
token_expires (int): The timestamp for which after the token
|
||||
will no longer be valid
|
||||
"""
|
||||
def start_or_continue_validation_session_txn(txn):
|
||||
# Create or update a validation session
|
||||
self._simple_upsert_txn(
|
||||
txn,
|
||||
table="threepid_validation_session",
|
||||
keyvalues={"session_id": session_id},
|
||||
values={"last_send_attempt": send_attempt},
|
||||
insertion_values={
|
||||
"medium": medium,
|
||||
"address": address,
|
||||
"client_secret": client_secret,
|
||||
},
|
||||
)
|
||||
|
||||
# Create a new validation token with this session ID
|
||||
self._simple_insert_txn(
|
||||
txn,
|
||||
table="threepid_validation_token",
|
||||
values={
|
||||
"session_id": session_id,
|
||||
"token": token,
|
||||
"next_link": next_link,
|
||||
"expires": token_expires,
|
||||
},
|
||||
)
|
||||
|
||||
return self.runInteraction(
|
||||
"start_or_continue_validation_session",
|
||||
start_or_continue_validation_session_txn,
|
||||
)
|
||||
|
||||
def cull_expired_threepid_validation_tokens(self):
|
||||
"""Remove threepid validation tokens with expiry dates that have passed"""
|
||||
def cull_expired_threepid_validation_tokens_txn(txn, ts):
|
||||
sql = """
|
||||
DELETE FROM threepid_validation_token WHERE
|
||||
expires < ?
|
||||
"""
|
||||
return txn.execute(sql, (ts,))
|
||||
|
||||
return self.runInteraction(
|
||||
"cull_expired_threepid_validation_tokens",
|
||||
cull_expired_threepid_validation_tokens_txn,
|
||||
self.clock.time_msec(),
|
||||
)
|
||||
|
||||
def delete_threepid_session(self, session_id):
|
||||
"""Removes a threepid validation session from the database. This can
|
||||
be done after validation has been performed and whatever action was
|
||||
waiting on it has been carried out
|
||||
|
||||
Args:
|
||||
session_id (str): The ID of the session to delete
|
||||
"""
|
||||
def delete_threepid_session_txn(txn):
|
||||
self._simple_delete_txn(
|
||||
txn,
|
||||
table="threepid_validation_token",
|
||||
keyvalues={"session_id": session_id},
|
||||
)
|
||||
self._simple_delete_txn(
|
||||
txn,
|
||||
table="threepid_validation_session",
|
||||
keyvalues={"session_id": session_id},
|
||||
)
|
||||
|
||||
return self.runInteraction(
|
||||
"delete_threepid_session",
|
||||
delete_threepid_session_txn,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/* Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
CREATE TABLE IF NOT EXISTS threepid_validation_session (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
medium TEXT NOT NULL,
|
||||
address TEXT NOT NULL,
|
||||
client_secret TEXT NOT NULL,
|
||||
last_send_attempt BIGINT NOT NULL,
|
||||
validated_at BIGINT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS threepid_validation_token (
|
||||
token TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
next_link TEXT,
|
||||
expires BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX threepid_validation_token_session_id ON threepid_validation_token(session_id);
|
|
@ -131,7 +131,6 @@ def default_config(name, parse=False):
|
|||
"password_providers": [],
|
||||
"worker_replication_url": "",
|
||||
"worker_app": None,
|
||||
"email_enable_notifs": False,
|
||||
"block_non_admin_invites": False,
|
||||
"federation_domain_whitelist": None,
|
||||
"filter_timeline_limit": 5000,
|
||||
|
|
Loading…
Reference in a new issue