password_reset: Send email unconditionally.

This was basically rewritten by tabbott, because the code is a lot
cleaner after just rewriting the ZulipPasswordResetForm code to no
longer copy the model of the original Django version.

Fixes #4733.
This commit is contained in:
Umair Khan 2017-08-11 10:55:51 +05:00 committed by Tim Abbott
parent 489feaf139
commit 95ba3e7cbb
4 changed files with 98 additions and 75 deletions

View File

@ -2,8 +2,15 @@
{% block content %} {% block content %}
<p> <p>
{% if attempted_realm %} {% if no_account_in_realm %}
Someone (possibly you) requested a password reset email for {{ email }} on {{ attempted_realm.uri }}, but {{ email }} does not have an active account in {{ attempted_realm.uri }}. However, {{ email }} does have an active account in {{ user.realm.uri }} organization; you can try logging in or resetting your password there. Someone (possibly you) requested a password reset email for {{ email }}
on {{ realm_uri }}, but {{ email }} does not have an
active account in {{ realm_uri }}.
{% if account_exists_another_realm %}
However, {{ email }} does have an active account in the {{ user.realm.uri }}
organization; you can try logging in or resetting your password there.
{% endif %}
{% else %} {% else %}
Psst. Word on the street is that you need a new password, {{ email }}.<br /> Psst. Word on the street is that you need a new password, {{ email }}.<br />
It's all good. Follow the link below and we'll take care of the rest:<br /> It's all good. Follow the link below and we'll take care of the rest:<br />

View File

@ -1,9 +1,13 @@
{% if attempted_realm %} {% if no_account_in_realm %}
Someone (possibly you) requested a password reset email for {{ email }} on Someone (possibly you) requested a password reset email for
{{ attempted_realm.uri }}, but {{ email }} does not {{ email }} on {{ realm_uri }}, but
have an active account in {{ attempted_realm.uri }}. However, {{ email }} does {{ email }} does not have an active account in
have an active account in {{ user.realm.uri }} organization; you {{ realm_uri }}.
can try logging in or resetting your password there. {% if account_exists_another_realm %}
However, {{ email }} does have an active account in
{{ user.realm.uri }} organization; you can try
logging in or resetting your password there.
{% endif %}
{% else %} {% else %}
Psst. Word on the street is that you need a new password, {{ email }}. Psst. Word on the street is that you need a new password, {{ email }}.

View File

@ -183,49 +183,8 @@ class LoggingSetPasswordForm(SetPasswordForm):
return self.user return self.user
class ZulipPasswordResetForm(PasswordResetForm): class ZulipPasswordResetForm(PasswordResetForm):
def get_users(self, email):
# type: (str) -> QuerySet
"""Given an email, return matching user(s) who should receive a reset.
This is modified from the original in that it allows non-bot
users who don't have a usable password to reset their
passwords.
"""
if not email_auth_enabled():
logging.info("Password reset attempted for %s even though password auth is disabled." % (email,))
return []
result = UserProfile.objects.filter(email__iexact=email, is_active=True,
is_bot=False)
if len(result) == 0:
logging.info("Password reset attempted for %s; no active account." % (email,))
return result
def send_mail(self, context, from_email, to_email):
# type: (Dict[str, Any], str, str) -> None
"""
Currently we don't support accounts in multiple subdomains using
a single email address. We override this function so that we do
not send a reset link to an email address if the reset attempt is
done on the subdomain which does not match user.realm.subdomain.
Once we start supporting accounts with the same email in
multiple subdomains, we may be able to refactor this function.
A second reason we override this function is so that we can send
the mail through the functions in zerver.lib.send_email, to match
how we send all other mail in the codebase.
"""
user = get_user_profile_by_email(to_email)
attempted_subdomain = get_subdomain(self.request)
context['attempted_realm'] = False
if not user_matches_subdomain(attempted_subdomain, user):
context['attempted_realm'] = get_realm(attempted_subdomain)
send_email('zerver/emails/password_reset', to_user_id=user.id,
from_name="Zulip Account Security",
from_address=FromAddress.NOREPLY, context=context)
def save(self, def save(self,
domain_override=None, # type: Optional[bool]
subject_template_name='registration/password_reset_subject.txt', # type: Text subject_template_name='registration/password_reset_subject.txt', # type: Text
email_template_name='registration/password_reset_email.html', # type: Text email_template_name='registration/password_reset_email.html', # type: Text
use_https=False, # type: bool use_https=False, # type: bool
@ -237,33 +196,56 @@ class ZulipPasswordResetForm(PasswordResetForm):
): ):
# type: (...) -> None # type: (...) -> None
""" """
Currently we don't support accounts in multiple subdomains using a If the email address has an account in the target realm,
single email addresss. Once we start supporting accounts with the same generates a one-use only link for resetting password and sends
email in multiple subdomains, we may be able to delete or refactor this to the user.
function.
Generates a one-use only link for resetting password and sends to the We send a different email if an associated account does not exist in the
user. database, or an account does exist, but not in the realm.
Note: We ignore the various email template arguments (those Note: We ignore the various email template arguments (those
are an artifact of using Django's password reset framework) are an artifact of using Django's password reset framework)
""" """
setattr(self, 'request', request)
email = self.cleaned_data["email"] email = self.cleaned_data["email"]
users = list(self.get_users(email))
for user in users: subdomain = get_subdomain(request)
realm = get_realm(subdomain)
if realm is None:
raise ValidationError("Invalid realm")
if not email_auth_enabled(realm):
logging.info("Password reset attempted for %s even though password auth is disabled." % (email,))
return
try:
user = get_user_profile_by_email(email)
except UserProfile.DoesNotExist:
user = None
context = { context = {
'email': email, 'email': email,
'uid': urlsafe_base64_encode(force_bytes(user.id)), 'realm_uri': realm.uri,
'user': user, 'user': user,
'token': token_generator.make_token(user),
'protocol': 'https' if use_https else 'http', 'protocol': 'https' if use_https else 'http',
} }
if extra_email_context is not None:
context.update(extra_email_context) if user is not None and user_matches_subdomain(subdomain, user):
self.send_mail(context, from_email, email) context['no_account_in_realm'] = False
context['token'] = token_generator.make_token(user)
context['uid'] = urlsafe_base64_encode(force_bytes(user.id))
send_email('zerver/emails/password_reset', to_user_id=user.id,
from_name="Zulip Account Security",
from_address=FromAddress.NOREPLY, context=context)
else:
context['no_account_in_realm'] = True
if user is not None:
context['account_exists_another_realm'] = True
else:
context['account_exists_another_realm'] = False
send_email('zerver/emails/password_reset', to_email=email,
from_name="Zulip Account Security",
from_address=FromAddress.NOREPLY, context=context)
class CreateUserForm(forms.Form): class CreateUserForm(forms.Form):
full_name = forms.CharField(max_length=100) full_name = forms.CharField(max_length=100)

View File

@ -173,6 +173,7 @@ class PasswordResetTest(ZulipTestCase):
from_email = outbox[0].from_email from_email = outbox[0].from_email
self.assertIn("Zulip Account Security", from_email) self.assertIn("Zulip Account Security", from_email)
self.assertIn(FromAddress.NOREPLY, from_email) self.assertIn(FromAddress.NOREPLY, from_email)
self.assertIn("Psst. Word on the street is that you", outbox[0].body)
# Visit the password reset link. # Visit the password reset link.
password_reset_url = self.get_confirmation_url_from_outbox( password_reset_url = self.get_confirmation_url_from_outbox(
@ -197,7 +198,34 @@ class PasswordResetTest(ZulipTestCase):
# make sure old password no longer works # make sure old password no longer works
self.login(email, password=old_password, fails=True) self.login(email, password=old_password, fails=True)
def test_invalid_subdomain(self): def test_password_reset_for_non_existent_user(self):
# type: () -> None
email = 'nonexisting@mars.com'
with patch('logging.info'):
# start the password reset process by supplying an email address
result = self.client_post('/accounts/password/reset/', {'email': email})
# check the redirect link telling you to check mail for password reset link
self.assertEqual(result.status_code, 302)
self.assertTrue(result["Location"].endswith(
"/accounts/password/reset/done/"))
result = self.client_get(result["Location"])
self.assert_in_response("Check your email to finish the process.", result)
# Check that the password reset email is from a noreply address.
from django.core.mail import outbox
from_email = outbox[0].from_email
self.assertIn("Zulip Account Security", from_email)
self.assertIn(FromAddress.NOREPLY, from_email)
self.assertIn('Someone (possibly you) requested a password',
outbox[0].body)
self.assertNotIn('does have an active account in the zulip.testserver',
outbox[0].body)
def test_wrong_subdomain(self):
# type: () -> None # type: () -> None
email = self.example_email("hamlet") email = self.example_email("hamlet")
string_id = 'hamlet' string_id = 'hamlet'
@ -226,7 +254,9 @@ class PasswordResetTest(ZulipTestCase):
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
message = outbox.pop() message = outbox.pop()
self.assertIn(FromAddress.NOREPLY, message.from_email) self.assertIn(FromAddress.NOREPLY, message.from_email)
self.assertIn("hamlet@zulip.com does not\nhave an active account in http://", self.assertIn('Someone (possibly you) requested a password',
message.body)
self.assertIn("hamlet@zulip.com does not have an active account in\nhttp://zephyr.testserver",
message.body) message.body)
def test_correct_subdomain(self): def test_correct_subdomain(self):