Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Send password reset from HS: Sending the email #5345

Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
dbdebc2
Ability to send password reset emails
anoadragon453 May 24, 2019
9567c60
Merge branch 'develop' into anoa/hs_password_reset_sending_email
anoadragon453 Jun 4, 2019
ed35302
Fix validation token lifetime email_ prefix
anoadragon453 Jun 4, 2019
094c351
Add changelog
anoadragon453 Jun 4, 2019
899219c
Update manifest to include txt/html template files
anoadragon453 Jun 5, 2019
309943f
Update db
anoadragon453 Jun 5, 2019
354d749
mark jinja2 and bleach as required dependencies
anoadragon453 Jun 5, 2019
62e1ec0
Add email settings to default unit test config
anoadragon453 Jun 5, 2019
a0e2a10
Update unit test template dir
anoadragon453 Jun 5, 2019
a862f2a
gen sample config
anoadragon453 Jun 5, 2019
752dbee
Merge branch 'anoa/feature_hs_password_resets' into anoa/hs_password_…
anoadragon453 Jun 5, 2019
177f024
Add html5lib as a required dep
anoadragon453 Jun 5, 2019
6d2d3c9
Modify check for smtp settings to be kinder to CI
anoadragon453 Jun 5, 2019
6394715
silly linting rules
anoadragon453 Jun 5, 2019
fe0af29
Correct html5lib dep version number
anoadragon453 Jun 5, 2019
91eac88
one more time
anoadragon453 Jun 5, 2019
c9573ca
Change template_dir to originate from synapse root dir
anoadragon453 Jun 5, 2019
4c406f5
Revert "Modify check for smtp settings to be kinder to CI"
anoadragon453 Jun 5, 2019
70b161d
Move templates. New option to disable password resets
anoadragon453 Jun 5, 2019
79bc668
Update templates and make password reset option work
anoadragon453 Jun 5, 2019
f522cde
Change jinja2 and bleach back to opt deps
anoadragon453 Jun 5, 2019
a4c0907
Update email condition requirement
anoadragon453 Jun 5, 2019
efa1a56
Only import jinja2/bleach if we need it
anoadragon453 Jun 5, 2019
6a9588c
Update sample config
anoadragon453 Jun 5, 2019
78ca92a
Revert manifest changes for new res directory
anoadragon453 Jun 5, 2019
12ed769
Remove public_baseurl from unittest config
anoadragon453 Jun 5, 2019
6efb301
infer ability to reset password from email config
anoadragon453 Jun 5, 2019
3478213
Address review comments
anoadragon453 Jun 6, 2019
a37a2f1
regen sample config
anoadragon453 Jun 6, 2019
cd4f4a2
test for ci
anoadragon453 Jun 6, 2019
92090d3
Remove CI test
anoadragon453 Jun 6, 2019
7168dee
fix bug?
anoadragon453 Jun 6, 2019
828cdbb
Run bg update on the master process
anoadragon453 Jun 6, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/5345.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ability to perform password reset via email without trusting the identity server.
57 changes: 46 additions & 11 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1018,33 +1018,68 @@ 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
#
#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 sending email notifications for new chat messages
# #
# enable_notifs: False
#
# # 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"
#
# # Disable 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
# #
# #enable_password_reset_from_is: False
#
# # Configure the time in seconds that a validation email or text
# # message code will expire after sending
# #
# # This is currently used for password resets
# #validation_token_lifetime: 900 # 15 minutes
#
# # 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


#password_providers:
Expand Down
131 changes: 119 additions & 12 deletions synapse/config/emailconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,75 @@ def read_config(self, config):
"account_validity", {},
).get("renew_at")

if self.email_enable_notifs or account_validity_renewal_enabled:
self.email_enable_password_reset_from_is = email_config.get(
"enable_password_reset_from_is", False,
)
self.enable_password_resets = (
self.email_enable_password_reset_from_is
or (not self.email_enable_password_reset_from_is and email_config != {})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just self.enable_password_resets = self.email_enable_password_reset_from_is or email_config != {}?

)
if email_config == {} and not self.email_enable_password_reset_from_is:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if email_config == {} and not self.email_enable_password_reset_from_is:
if not self.enable_password_resets:

logger.warn(
"User password resets have been disabled due to lack of email config."
)

self.email_validation_token_lifetime = email_config.get(
"validation_token_lifetime", 15 * 60,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use self.parse_duration so that you can say 15m in the config. We probably want to keep this valid for, like, 1h at least, since email can be quite slow at times.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should be in milliseconds for consistency

)

if (
self.email_enable_notifs
or account_validity_renewal_enabled
or (self.enable_password_resets
and self.email_enable_password_reset_from_is)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
and self.email_enable_password_reset_from_is)
and not self.email_enable_password_reset_from_is)

No?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

):
# make sure we can import the required deps
import jinja2
import bleach
# prevent unused warnings
jinja2
bleach

if self.enable_password_resets and not self.email_enable_password_reset_from_is:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think at this point we change enable_password_resets to ``enable_local_passwords_resets`, as all this boolean logic between the two is confusing me

required = [
"smtp_host",
"smtp_port",
"notif_from",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably call this something else....

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but, backwards compatibility.

]

missing = []
for k in required:
if k not in email_config:
missing.append(k)

if (len(missing) > 0):
raise RuntimeError(
"email.enable_password_reset_from_is is False "
"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",
)

# Check templates exist
for f in [self.email_password_reset_template_html,
self.email_password_reset_template_text]:
p = os.path.join(self.email_template_dir, f)
if not os.path.isfile(p):
raise ConfigError("Unable to find template file %s" % (p, ))

if config.get("public_baseurl") is None:
raise RuntimeError(
"email.enable_password_reset_from_is is False but no "
"public_baseurl is set"
)

if self.email_enable_notifs:
required = [
"smtp_host",
Expand Down Expand Up @@ -139,33 +200,79 @@ def read_config(self, config):
if not os.path.isfile(p):
raise ConfigError("Unable to find email template file %s" % (p, ))

def _get_template_content(self, template_dir, path):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's already a self.read_file helper function

fullpath = os.path.join(template_dir, path)

try:
with open(fullpath) as f:
return f.read()
except Exception as e:
raise ConfigError(
"Unable to read content of template: %s - %s", fullpath, e,
)

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
#
#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 sending email notifications for new chat messages
# #
# enable_notifs: False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not document these stuff in this PR please, its a bit confusing

#
# # 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"
#
# # Disable 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
# #
# #enable_password_reset_from_is: False
#
# # Configure the time in seconds that a validation email or text
# # message code will expire after sending
# #
# # This is currently used for password resets
# #validation_token_lifetime: 900 # 15 minutes
#
# # 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
"""
85 changes: 67 additions & 18 deletions synapse/push/mailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@


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()
Expand All @@ -94,21 +94,48 @@ def __init__(self, hs, app_name, notif_template_html, notif_template_text):
logger.info("Created Mailer for app_name %s" % app_name)

@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
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 +
"_matrix/identity/api/v1/validate/email/submitToken"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make it hard/impossible to run an identity server on the same domain as the HS. Given this should be an opaque link(?), let's just have something like _synapse/password_reset/email/submit_token

"?token=%s&client_secret=%s&sid=%s" %
(token, client_secret, sid)
)

raw_from = email.utils.parseaddr(from_string)[1]
raw_to = email.utils.parseaddr(email_address)[1]
template_vars = {
"link": link,
}

if raw_to == '':
raise RuntimeError("Invalid 'to' address")
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):
"""Send email regarding a user's room notifications"""
rooms_in_order = deduped_ordered_list(
[pa['room_id'] for pa in push_actions]
)
Expand Down Expand Up @@ -176,14 +203,36 @@ def _fetch_room_state(room_id):
"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()
Expand Down
4 changes: 2 additions & 2 deletions synapse/push/pusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ def _create_email_pusher(self, _hs, pusherdict):
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)
Expand Down
2 changes: 1 addition & 1 deletion synapse/python_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
]

CONDITIONAL_REQUIREMENTS = {
"email.enable_notifs": ["Jinja2>=2.9", "bleach>=1.4.2"],
"email": ["Jinja2>=2.9", "bleach>=1.4.2"],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed as multiple options (all relating to email) now require these dependencies. If some documentation lists the different pip [...] options, we'll need to update them.

"matrix-synapse-ldap3": ["matrix-synapse-ldap3>=0.1"],

# we use execute_batch, which arrived in psycopg 2.7.
Expand Down
9 changes: 9 additions & 0 deletions synapse/res/templates/password_reset.html
Original file line number Diff line number Diff line change
@@ -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>
Loading