mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	reset_password: Modify password reset email if email is in wrong realm.
This fixes a confusing issue where a user might try resetting the password for an email account that in part of a different Zulip organization. Is a useful early step towards making Zulip support reusing an email in multiple realms. Fixes: #4557.
This commit is contained in:
		@@ -1,8 +1,15 @@
 | 
				
			|||||||
 | 
					{% if attempted_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.
 | 
				
			||||||
 | 
					{% else %}
 | 
				
			||||||
Psst. Word on the street is that you forgot your password, {{ email }}.
 | 
					Psst. Word on the street is that you forgot your password, {{ email }}.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It's all good. Follow the link below and we'll take care of the rest:
 | 
					It's all good. Follow the link below and we'll take care of the rest:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{{ protocol }}://{{ user.realm.host }}{{ url('django.contrib.auth.views.password_reset_confirm', kwargs=dict(uidb64=uid, token=token)) }}
 | 
					{{ protocol }}://{{ user.realm.host }}{{ url('django.contrib.auth.views.password_reset_confirm', kwargs=dict(uidb64=uid, token=token)) }}
 | 
				
			||||||
 | 
					{% endif %}
 | 
				
			||||||
Thanks,
 | 
					Thanks,
 | 
				
			||||||
Your friends at Zulip HQ
 | 
					Your friends at Zulip HQ
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,7 @@ import logging
 | 
				
			|||||||
import re
 | 
					import re
 | 
				
			||||||
import DNS
 | 
					import DNS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from typing import Any, Callable, List, Optional, Text
 | 
					from typing import Any, Callable, List, Optional, Text, Dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
MIT_VALIDATION_ERROR = u'That user does not exist at MIT or is a ' + \
 | 
					MIT_VALIDATION_ERROR = u'That user does not exist at MIT or is a ' + \
 | 
				
			||||||
                       u'<a href="https://ist.mit.edu/email-lists">mailing list</a>. ' + \
 | 
					                       u'<a href="https://ist.mit.edu/email-lists">mailing list</a>. ' + \
 | 
				
			||||||
@@ -185,6 +185,48 @@ class ZulipPasswordResetForm(PasswordResetForm):
 | 
				
			|||||||
            logging.info("Password reset attempted for %s; no active account." % (email,))
 | 
					            logging.info("Password reset attempted for %s; no active account." % (email,))
 | 
				
			||||||
        return result
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def send_mail(self, subject_template_name, email_template_name,
 | 
				
			||||||
 | 
					                  context, from_email, to_email, html_email_template_name=None):
 | 
				
			||||||
 | 
					        # type: (str, str, Dict[str, Any], str, str, str) -> None
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Currently we don't support accounts in multiple subdomains using
 | 
				
			||||||
 | 
					        a single email addresss. 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 delete or refactor this
 | 
				
			||||||
 | 
					        function.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        user_realm = get_user_profile_by_email(to_email).realm
 | 
				
			||||||
 | 
					        attempted_subdomain = get_subdomain(getattr(self, 'request'))
 | 
				
			||||||
 | 
					        context['attempted_realm'] = False
 | 
				
			||||||
 | 
					        if not check_subdomain(user_realm.subdomain, attempted_subdomain):
 | 
				
			||||||
 | 
					            context['attempted_realm'] = get_realm(attempted_subdomain)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        super(ZulipPasswordResetForm, self).send_mail(
 | 
				
			||||||
 | 
					            subject_template_name,
 | 
				
			||||||
 | 
					            email_template_name,
 | 
				
			||||||
 | 
					            context,
 | 
				
			||||||
 | 
					            from_email,
 | 
				
			||||||
 | 
					            to_email,
 | 
				
			||||||
 | 
					            html_email_template_name=html_email_template_name
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        # type: (*Any, **Any) -> None
 | 
				
			||||||
 | 
					        """Currently we don't support accounts in multiple subdomains using
 | 
				
			||||||
 | 
					        a single email addresss. We override this function so that we can
 | 
				
			||||||
 | 
					        inject request parameter in context. This parameter will be used
 | 
				
			||||||
 | 
					        by send_mail function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Once we start supporting accounts with the same email in
 | 
				
			||||||
 | 
					        multiple subdomains, we may be able to delete or refactor this
 | 
				
			||||||
 | 
					        function.
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        setattr(self, 'request', kwargs.get('request'))
 | 
				
			||||||
 | 
					        super(ZulipPasswordResetForm, self).save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CreateUserForm(forms.Form):
 | 
					class CreateUserForm(forms.Form):
 | 
				
			||||||
    full_name = forms.CharField(max_length=100)
 | 
					    full_name = forms.CharField(max_length=100)
 | 
				
			||||||
    email = forms.EmailField()
 | 
					    email = forms.EmailField()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,8 @@ from zerver.management.commands.deliver_email import send_email_job
 | 
				
			|||||||
from zerver.lib.actions import (
 | 
					from zerver.lib.actions import (
 | 
				
			||||||
    set_default_streams,
 | 
					    set_default_streams,
 | 
				
			||||||
    do_change_is_admin,
 | 
					    do_change_is_admin,
 | 
				
			||||||
    get_stream
 | 
					    get_stream,
 | 
				
			||||||
 | 
					    do_create_realm,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from zerver.lib.initial_password import initial_password
 | 
					from zerver.lib.initial_password import initial_password
 | 
				
			||||||
@@ -151,6 +152,63 @@ 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):
 | 
				
			||||||
 | 
					        # type: () -> None
 | 
				
			||||||
 | 
					        email = 'hamlet@zulip.com'
 | 
				
			||||||
 | 
					        string_id = 'hamlet'
 | 
				
			||||||
 | 
					        name = 'Hamlet'
 | 
				
			||||||
 | 
					        do_create_realm(
 | 
				
			||||||
 | 
					            string_id,
 | 
				
			||||||
 | 
					            name,
 | 
				
			||||||
 | 
					            restricted_to_domain=False,
 | 
				
			||||||
 | 
					            invite_required=False
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with self.settings(REALMS_HAVE_SUBDOMAINS=True):
 | 
				
			||||||
 | 
					            with patch('zerver.forms.get_subdomain', return_value=string_id):
 | 
				
			||||||
 | 
					                # 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        from django.core.mail import outbox
 | 
				
			||||||
 | 
					        self.assertEqual(len(outbox), 1)
 | 
				
			||||||
 | 
					        message = outbox.pop()
 | 
				
			||||||
 | 
					        self.assertIn("hamlet@zulip.com does not\nhave an active account in http://",
 | 
				
			||||||
 | 
					                      message.body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_correct_subdomain(self):
 | 
				
			||||||
 | 
					        # type: () -> None
 | 
				
			||||||
 | 
					        email = 'hamlet@zulip.com'
 | 
				
			||||||
 | 
					        string_id = 'zulip'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with self.settings(REALMS_HAVE_SUBDOMAINS=True):
 | 
				
			||||||
 | 
					            with patch('zerver.forms.get_subdomain', return_value=string_id):
 | 
				
			||||||
 | 
					                # 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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        from django.core.mail import outbox
 | 
				
			||||||
 | 
					        self.assertEqual(len(outbox), 1)
 | 
				
			||||||
 | 
					        message = outbox.pop()
 | 
				
			||||||
 | 
					        self.assertIn("Psst. Word on the street is that you forgot your password,",
 | 
				
			||||||
 | 
					                      message.body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_redirect_endpoints(self):
 | 
					    def test_redirect_endpoints(self):
 | 
				
			||||||
        # type: () -> None
 | 
					        # type: () -> None
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user