diff --git a/templates/zerver/confirm_continue_registration.html b/templates/zerver/confirm_continue_registration.html new file mode 100644 index 0000000000..0b94eec37e --- /dev/null +++ b/templates/zerver/confirm_continue_registration.html @@ -0,0 +1,51 @@ +{% extends "zerver/portico_signup.html" %} + +{% block portico_content %} +
+
+
+
+
+

+

{{ _("Account not found!") }}
+

+ {% if invalid_email %} + {# If the email address is invalid, we can't send the user #} + {# to the preregistered user code path. #} +

+ {% trans %} + Please click the following button if you wish to register. + {% endtrans %} +

+ Register + {% else %} +

+ {% trans %} + You attempted to login using {{ email }}, but {{ email }} does + not have an account in this organization. If you'd like, you can + try to register a new account with this email address. + {% endtrans %} +

+ {# TODO: Ideally, this should use whatever auth #} + {# method the user had used to get here, not just #} + {# send an email. #} +
+ {{ csrf_input }} + + +
+ {% endif %} +
+
+
+
+ +
+ +{% endblock %} diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index 0d8cbe0ec5..bff6e6180f 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -3,6 +3,7 @@ from django.conf import settings from django.http import HttpResponse from django.test import TestCase, override_settings from django_auth_ldap.backend import _LDAPUser +from django.contrib.auth import authenticate from django.test.client import RequestFactory from typing import Any, Callable, Dict, List, Optional, Text from builtins import object @@ -31,6 +32,7 @@ from zerver.lib.sessions import get_session_dict_user from zerver.lib.test_classes import ( ZulipTestCase, ) +from zerver.lib.test_helpers import POSTRequestMock from zerver.models import \ get_realm, get_user_profile_by_email, email_to_username, UserProfile, \ PreregistrationUser, Realm @@ -43,7 +45,8 @@ from zproject.backends import ZulipDummyBackend, EmailAuthBackend, \ dev_auth_enabled, password_auth_enabled, github_auth_enabled, \ SocialAuthMixin, AUTH_BACKEND_NAME_MAP -from zerver.views.auth import maybe_send_to_registration +from zerver.views.auth import (maybe_send_to_registration, + login_or_register_remote_user) from version import ZULIP_VERSION from social_core.exceptions import AuthFailed, AuthStateForbidden @@ -429,7 +432,7 @@ class GitHubAuthBackendTest(ZulipTestCase): def do_auth(self, *args, **kwargs): # type: (*Any, **Any) -> UserProfile with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.GitHubAuthBackend',)): - return self.backend.authenticate(**kwargs) + return authenticate(**kwargs) def test_github_auth_enabled(self): # type: () -> None @@ -598,6 +601,8 @@ class GitHubAuthBackendTest(ZulipTestCase): request.session = {} request.user = self.user_profile self.backend.strategy.request = request + session_data = {'subdomain': False, 'is_signup': '1'} + self.backend.strategy.session_get = lambda k: session_data.get(k) def do_auth(*args, **kwargs): # type: (*Any, **Any) -> UserProfile @@ -615,6 +620,59 @@ class GitHubAuthBackendTest(ZulipTestCase): 'correspond to any existing ' 'organization.'.format(email), result) + def test_github_backend_existing_user(self): + # type: () -> None + rf = RequestFactory() + request = rf.get('/complete') + request.session = {} + request.user = self.user_profile + self.backend.strategy.request = request + session_data = {'subdomain': False, 'is_signup': '1'} + self.backend.strategy.session_get = lambda k: session_data.get(k) + + def do_auth(*args, **kwargs): + # type: (*Any, **Any) -> UserProfile + return_data = kwargs['return_data'] + return_data['valid_attestation'] = True + return None + + with mock.patch('social_core.backends.github.GithubOAuth2.do_auth', + side_effect=do_auth): + email = 'hamlet@zulip.com' + response = dict(email=email, name='Hamlet') + result = self.backend.do_auth(response=response) + self.assert_in_response('action="/register/"', result) + self.assert_in_response('hamlet@zulip.com is already active', + result) + + def test_github_backend_new_user_when_is_signup_is_false(self): + # type: () -> None + rf = RequestFactory() + request = rf.get('/complete') + request.session = {} + request.user = self.user_profile + self.backend.strategy.request = request + session_data = {'subdomain': False, 'is_signup': '0'} + self.backend.strategy.session_get = lambda k: session_data.get(k) + + def do_auth(*args, **kwargs): + # type: (*Any, **Any) -> UserProfile + return_data = kwargs['return_data'] + return_data['valid_attestation'] = True + return None + + with mock.patch('social_core.backends.github.GithubOAuth2.do_auth', + side_effect=do_auth): + email = 'nonexisting@phantom.com' + response = dict(email=email, name='Ghost') + result = self.backend.do_auth(response=response) + self.assert_in_response( + 'action="/register/"', result) + self.assert_in_response('You attempted to login using ' + 'nonexisting@phantom.com, but ' + 'nonexisting@phantom.com does', + result) + def test_login_url(self): # type: () -> None result = self.client_get('/accounts/login/social/github') @@ -661,7 +719,8 @@ class GitHubAuthBackendTest(ZulipTestCase): result = self.client_get(reverse('social:complete', args=['github']), info={'state': 'state'}) self.assertEqual(result.status_code, 200) - self.assertIn("Sign up for Zulip", result.content.decode('utf8')) + self.assert_in_response("Please click the following button " + "if you wish to register.", result) self.assertEqual(mock_get_email_address.call_count, 2) utils.BACKENDS = settings.AUTHENTICATION_BACKENDS @@ -719,7 +778,7 @@ class GoogleOAuthTest(ZulipTestCase): class GoogleSubdomainLoginTest(GoogleOAuthTest): def get_signed_subdomain_cookie(self, data): - # type: (Dict[str, str]) -> Dict[str, str] + # type: (Dict[str, Any]) -> Dict[str, str] key = 'subdomain.signature' salt = key + 'zerver.views.auth' value = ujson.dumps(data) @@ -789,7 +848,8 @@ class GoogleSubdomainLoginTest(GoogleOAuthTest): # type: () -> None data = {'name': 'Full Name', 'email': 'hamlet@zulip.com', - 'subdomain': 'zulip'} + 'subdomain': 'zulip', + 'is_signup': False} self.client.cookies = SimpleCookie(self.get_signed_subdomain_cookie(data)) with mock.patch('zerver.views.auth.get_subdomain', return_value='zulip'): @@ -807,18 +867,48 @@ class GoogleSubdomainLoginTest(GoogleOAuthTest): self.assertEqual(result.status_code, 302) self.assertTrue(result['Location'].endswith, '?subdomain=1') + def test_log_into_subdomain_when_is_signup_is_true(self): + # type: () -> None + data = {'name': 'Full Name', + 'email': 'hamlet@zulip.com', + 'subdomain': 'zulip', + 'is_signup': True} + + self.client.cookies = SimpleCookie(self.get_signed_subdomain_cookie(data)) + with mock.patch('zerver.views.auth.get_subdomain', return_value='zulip'): + result = self.client_get('/accounts/login/subdomain/') + self.assertEqual(result.status_code, 200) + self.assert_in_response('hamlet@zulip.com is already active', result) + + def test_log_into_subdomain_when_is_signup_is_true_and_new_user(self): + # type: () -> None + data = {'name': 'New User Name', + 'email': 'new@zulip.com', + 'subdomain': 'zulip', + 'is_signup': True} + + self.client.cookies = SimpleCookie(self.get_signed_subdomain_cookie(data)) + with mock.patch('zerver.views.auth.get_subdomain', return_value='zulip'): + result = self.client_get('/accounts/login/subdomain/') + self.assertEqual(result.status_code, 302) + confirmation = Confirmation.objects.all().first() + confirmation_key = confirmation.confirmation_key + self.assertIn('do_confirm/' + confirmation_key, result.url) + def test_log_into_subdomain_when_email_is_none(self): # type: () -> None data = {'name': None, 'email': None, - 'subdomain': 'zulip'} + 'subdomain': 'zulip', + 'is_signup': False} self.client.cookies = SimpleCookie(self.get_signed_subdomain_cookie(data)) with mock.patch('zerver.views.auth.get_subdomain', return_value='zulip'), \ mock.patch('logging.warning'): result = self.client_get('/accounts/login/subdomain/') self.assertEqual(result.status_code, 200) - self.assertIn("Sign up for Zulip", result.content.decode('utf8')) + self.assert_in_response("Please click the following button if you " + "wish to register", result) def test_user_cannot_log_into_nonexisting_realm(self): # type: () -> None @@ -881,16 +971,19 @@ class GoogleSubdomainLoginTest(GoogleOAuthTest): self.assertEqual(uri, 'http://zulip.testserver/accounts/login/subdomain/') result = self.client_get(result.url) - result = self.client_get(result.url) # Call the confirmation url. + self.assert_in_response( + "You attempted to login using newuser@zulip.com, " + "but newuser@zulip.com does", result) + # Click confirm registraton button. + result = self.client_post('/register/', + {'email': email}) + self.assertEqual(result.status_code, 302) + self.client_get(result.url) + assert Confirmation.objects.all().count() == 1 + confirmation = Confirmation.objects.all().first() + url = Confirmation.objects.get_activation_url(confirmation.confirmation_key) + result = self.client_get(url) key_match = re.search('value="(?P[0-9a-f]+)" name="key"', result.content.decode("utf-8")) - name_match = re.search('value="(?P[^"]+)" name="full_name"', result.content.decode("utf-8")) - - # This goes through a brief stop on a page that auto-submits via JS - result = self.client_post('/accounts/register/', - {'full_name': name_match.group("name"), - 'key': key_match.group("key"), - 'from_confirmation': "1"}) - self.assertEqual(result.status_code, 200) result = self.client_post('/accounts/register/', {'full_name': "New User", 'password': 'test_password', @@ -924,18 +1017,19 @@ class GoogleLoginTest(GoogleOAuthTest): value=email)]) account_response = ResponseMock(200, account_data) result = self.google_oauth2_test(token_response, account_response) + self.assert_in_response( + "You attempted to login using newuser@zulip.com, " + "but newuser@zulip.com does", result) + # Click confirm registraton button. + result = self.client_post('/register/', + {'email': email}) self.assertEqual(result.status_code, 302) - - result = self.client_get(result.url) + self.client_get(result.url) + assert Confirmation.objects.all().count() == 1 + confirmation = Confirmation.objects.all().first() + url = Confirmation.objects.get_activation_url(confirmation.confirmation_key) + result = self.client_get(url) key_match = re.search('value="(?P[0-9a-f]+)" name="key"', result.content.decode("utf-8")) - name_match = re.search('value="(?P[^"]+)" name="full_name"', result.content.decode("utf-8")) - - # This goes through a brief stop on a page that auto-submits via JS - result = self.client_post('/accounts/register/', - {'full_name': name_match.group("name"), - 'key': key_match.group("key"), - 'from_confirmation': "1"}) - self.assertEqual(result.status_code, 200) result = self.client_post('/accounts/register/', {'full_name': "New User", 'password': 'test_password', @@ -1436,8 +1530,9 @@ class TestZulipRemoteUserBackend(ZulipTestCase): email = 'nonexisting@zulip.com' with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)): result = self.client_post('/accounts/login/sso/', REMOTE_USER=email) - self.assertEqual(result.status_code, 302) + self.assertEqual(result.status_code, 200) self.assertIs(get_session_dict_user(self.client.session), None) + self.assert_in_response("You attempted to login using", result) def test_login_failure_due_to_invalid_email(self): # type: () -> None @@ -1462,7 +1557,7 @@ class TestZulipRemoteUserBackend(ZulipTestCase): REMOTE_USER=email) self.assertEqual(result.status_code, 200) self.assertIs(get_session_dict_user(self.client.session), None) - self.assertIn(b"Sign up for Zulip", result.content) + self.assert_in_response("You attempted to login using", result) def test_login_failure_due_to_empty_subdomain(self): # type: () -> None @@ -1474,7 +1569,7 @@ class TestZulipRemoteUserBackend(ZulipTestCase): REMOTE_USER=email) self.assertEqual(result.status_code, 200) self.assertIs(get_session_dict_user(self.client.session), None) - self.assertIn(b"Sign up for Zulip", result.content) + self.assert_in_response("You attempted to login using", result) def test_login_success_under_subdomains(self): # type: () -> None @@ -1556,7 +1651,7 @@ class TestJWTLogin(ZulipTestCase): web_token = jwt.encode(payload, auth_key).decode('utf8') data = {'json_web_token': web_token} result = self.client_post('/accounts/login/jwt/', data) - self.assertEqual(result.status_code, 302) # This should ideally be not 200. + self.assertEqual(result.status_code, 200) # This should ideally be not 200. self.assertIs(get_session_dict_user(self.client.session), None) # The /accounts/login/jwt/ endpoint should also handle the case @@ -1565,7 +1660,7 @@ class TestJWTLogin(ZulipTestCase): 'zerver.views.auth.authenticate', side_effect=UserProfile.DoesNotExist("Do not exist")): result = self.client_post('/accounts/login/jwt/', data) - self.assertEqual(result.status_code, 302) # This should ideally be not 200. + self.assertEqual(result.status_code, 200) # This should ideally be not 200. self.assertIs(get_session_dict_user(self.client.session), None) def test_login_failure_due_to_wrong_subdomain(self): @@ -1989,3 +2084,19 @@ class LoginEmailValidatorTestCase(TestCase): # type: () -> None with self.assertRaises(JsonableError): validate_login_email(u'hamlet') + +class LoginOrRegisterRemoteUserTestCase(ZulipTestCase): + def test_invalid_subdomain(self): + # type: () -> None + email = 'hamlet@zulip.com' + full_name = 'Hamlet' + invalid_subdomain = True + user_profile = get_user_profile_by_email(email) + request = POSTRequestMock({}, user_profile) + response = login_or_register_remote_user( + request, + email, + user_profile, + full_name=full_name, + invalid_subdomain=invalid_subdomain) + self.assertIn('/accounts/login/?subdomain=1', response.url) diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 5954d1e7a3..52eef31049 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -17,9 +17,11 @@ from zilencer.models import Deployment from zerver.forms import HomepageForm, WRONG_SUBDOMAIN_ERROR from zerver.lib.actions import do_change_password +from zerver.views.auth import login_or_register_remote_user from zerver.views.invite import get_invitee_emails_set from zerver.views.registration import confirmation_key, \ redirect_and_log_into_subdomain, send_registration_completion_email + from zerver.models import ( get_realm, get_prereg_user_by_email, get_user_profile_by_email, get_unique_open_realm, completely_open, @@ -45,7 +47,7 @@ from zerver.lib.mobile_auth_otp import xor_hex_strings, ascii_to_hex, \ from zerver.lib.notifications import enqueue_welcome_emails, \ one_click_unsubscribe_link from zerver.lib.test_helpers import find_pattern_in_email, find_key_by_email, queries_captured, \ - HostRequestMock, unsign_subdomain_cookie + HostRequestMock, unsign_subdomain_cookie, POSTRequestMock from zerver.lib.test_classes import ( ZulipTestCase, ) @@ -1160,6 +1162,39 @@ class UserSignUpTest(ZulipTestCase): 'from_confirmation': '1'}) self.assert_in_success_response(["You're almost there."], result) + def test_signup_with_full_name(self): + # type: () -> None + """ + Check if signing up without a full name redirects to a registration + form. + """ + email = "newguy@zulip.com" + password = "newpassword" + + result = self.client_post('/accounts/home/', {'email': email}) + self.assertEqual(result.status_code, 302) + self.assertTrue(result["Location"].endswith( + "/accounts/send_confirm/%s" % (email,))) + result = self.client_get(result["Location"]) + self.assert_in_response("Check your email so we can get started.", result) + + # Visit the confirmation link. + confirmation_url = self.get_confirmation_url_from_outbox(email) + result = self.client_get(confirmation_url) + self.assertEqual(result.status_code, 200) + + result = self.client_post( + '/accounts/register/', + {'password': password, + 'realm_name': 'Zulip Test', + 'realm_subdomain': 'zuliptest', + 'key': find_key_by_email(email), + 'realm_org_type': Realm.COMMUNITY, + 'terms': True, + 'full_name': "New Guy", + 'from_confirmation': '1'}) + self.assert_in_success_response(["You're almost there."], result) + def test_signup_invalid_subdomain(self): # type: () -> None """ @@ -1765,3 +1800,96 @@ class MobileAuthOTPTest(ZulipTestCase): decryped = otp_decrypt_api_key(result, otp) self.assertEqual(decryped, hamlet.api_key) + +class LoginOrAskForRegistrationTestCase(ZulipTestCase): + def test_confirm(self): + # type: () -> None + request = POSTRequestMock({}, None) + email = 'new@zulip.com' + user_profile = None # type: Optional[UserProfile] + full_name = 'New User' + invalid_subdomain = False + response = login_or_register_remote_user( + request, + email, + user_profile, + full_name=full_name, + invalid_subdomain=invalid_subdomain) + self.assert_in_response('You attempted to login using new@zulip.com, ' + 'but new@zulip.com does', + response) + + def test_invalid_subdomain(self): + # type: () -> None + request = POSTRequestMock({}, None) + email = 'new@zulip.com' + user_profile = None # type: Optional[UserProfile] + full_name = 'New User' + invalid_subdomain = True + response = login_or_register_remote_user( + request, + email, + user_profile, + full_name=full_name, + invalid_subdomain=invalid_subdomain) + self.assertEqual(response.status_code, 302) + self.assertIn('/accounts/login/?subdomain=1', response.url) + + def test_invalid_email(self): + # type: () -> None + request = POSTRequestMock({}, None) + email = None # type: Optional[Text] + user_profile = None # type: Optional[UserProfile] + full_name = 'New User' + invalid_subdomain = False + response = login_or_register_remote_user( + request, + email, + user_profile, + full_name=full_name, + invalid_subdomain=invalid_subdomain) + self.assert_in_response('Please click the following button if ' + 'you wish to register', response) + + def test_login_under_subdomains(self): + # type: () -> None + request = POSTRequestMock({}, None) + setattr(request, 'session', self.client.session) + email = 'hamlet@zulip.com' + user_profile = get_user_profile_by_email(email) + user_profile.backend = 'zproject.backends.GitHubAuthBackend' + full_name = 'Hamlet' + invalid_subdomain = False + with self.settings(REALMS_HAVE_SUBDOMAINS=True): + response = login_or_register_remote_user( + request, + email, + user_profile, + full_name=full_name, + invalid_subdomain=invalid_subdomain) + user_id = get_session_dict_user(getattr(request, 'session')) + self.assertEqual(user_id, user_profile.id) + self.assertEqual(response.status_code, 302) + self.assertIn('http://zulip.testserver', response.url) + + def test_login_without_subdomains(self): + # type: () -> None + request = POSTRequestMock({}, None) + setattr(request, 'session', self.client.session) + setattr(request, 'get_host', lambda: 'localhost') + email = 'hamlet@zulip.com' + user_profile = get_user_profile_by_email(email) + user_profile.backend = 'zproject.backends.GitHubAuthBackend' + full_name = 'Hamlet' + invalid_subdomain = False + with self.settings(REALMS_HAVE_SUBDOMAINS=False): + response = login_or_register_remote_user( + request, + email, + user_profile, + full_name=full_name, + invalid_subdomain=invalid_subdomain) + user_id = get_session_dict_user(getattr(request, 'session')) + self.assertEqual(user_id, user_profile.id) + self.assertEqual(response.status_code, 302) + self.assertIn('http://localhost', response.url) diff --git a/zerver/views/auth.py b/zerver/views/auth.py index eac72793e0..8bca4d8774 100644 --- a/zerver/views/auth.py +++ b/zerver/views/auth.py @@ -85,7 +85,7 @@ def redirect_to_subdomain_login_url(): def login_or_register_remote_user(request, remote_username, user_profile, full_name='', invalid_subdomain=False, mobile_flow_otp=None, is_signup=False): - # type: (HttpRequest, Text, UserProfile, Text, bool, Optional[str]) -> HttpResponse + # type: (HttpRequest, Text, UserProfile, Text, bool, Optional[str], bool) -> HttpResponse if invalid_subdomain: # Show login page with an error message return redirect_to_subdomain_login_url()