2016-04-21 18:34:54 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from django.conf import settings
|
2019-11-01 23:26:49 +01:00
|
|
|
from django.contrib.auth import authenticate
|
2017-06-15 07:15:57 +02:00
|
|
|
from django.core import mail
|
2016-09-13 21:30:18 +02:00
|
|
|
from django.http import HttpResponse
|
2017-05-25 02:29:42 +02:00
|
|
|
from django.test import override_settings
|
2019-10-23 00:15:29 +02:00
|
|
|
from django_auth_ldap.backend import LDAPSearch, _LDAPUser
|
2016-07-25 11:24:36 +02:00
|
|
|
from django.test.client import RequestFactory
|
2018-08-10 00:58:44 +02:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2019-11-01 05:12:11 +01:00
|
|
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
2016-10-17 14:28:23 +02:00
|
|
|
from django.core import signing
|
2018-01-30 06:05:25 +01:00
|
|
|
from django.urls import reverse
|
2019-02-03 09:18:01 +01:00
|
|
|
|
2018-05-18 03:27:47 +02:00
|
|
|
import httpretty
|
2016-04-21 18:34:54 +02:00
|
|
|
|
2019-10-05 01:02:46 +02:00
|
|
|
import ldap
|
2016-10-24 11:38:38 +02:00
|
|
|
import jwt
|
2016-04-21 18:34:54 +02:00
|
|
|
import mock
|
2016-09-13 21:30:18 +02:00
|
|
|
import re
|
2017-10-27 02:45:38 +02:00
|
|
|
import time
|
2018-08-10 00:58:44 +02:00
|
|
|
import datetime
|
2016-04-21 18:34:54 +02:00
|
|
|
|
2017-03-21 18:08:40 +01:00
|
|
|
from zerver.lib.actions import (
|
2019-11-09 00:27:18 +01:00
|
|
|
do_create_user,
|
|
|
|
do_create_realm,
|
2017-03-21 18:08:40 +01:00
|
|
|
do_deactivate_realm,
|
|
|
|
do_deactivate_user,
|
2019-12-10 18:45:36 +01:00
|
|
|
do_invite_users,
|
2017-03-21 18:08:40 +01:00
|
|
|
do_reactivate_realm,
|
|
|
|
do_reactivate_user,
|
2018-03-21 22:05:21 +01:00
|
|
|
ensure_stream,
|
2018-08-14 22:17:23 +02:00
|
|
|
validate_email,
|
2017-03-21 18:08:40 +01:00
|
|
|
)
|
2018-12-12 19:46:37 +01:00
|
|
|
from zerver.lib.avatar import avatar_url
|
2019-06-07 23:36:19 +02:00
|
|
|
from zerver.lib.avatar_hash import user_avatar_path
|
2018-12-13 22:46:37 +01:00
|
|
|
from zerver.lib.dev_ldap_directory import generate_dev_ldap_dir
|
2017-03-19 20:01:01 +01:00
|
|
|
from zerver.lib.mobile_auth_otp import otp_decrypt_api_key
|
2017-05-04 01:13:56 +02:00
|
|
|
from zerver.lib.validator import validate_login_email, \
|
2019-10-22 01:33:44 +02:00
|
|
|
check_bool, check_dict_only, check_list, check_string, Validator
|
2017-04-10 08:06:10 +02:00
|
|
|
from zerver.lib.request import JsonableError
|
2019-07-17 02:29:08 +02:00
|
|
|
from zerver.lib.storage import static_path
|
2018-08-01 10:53:40 +02:00
|
|
|
from zerver.lib.users import get_all_api_keys
|
2019-06-07 23:36:19 +02:00
|
|
|
from zerver.lib.upload import resize_avatar, MEDIUM_AVATAR_SIZE
|
2016-04-21 21:07:43 +02:00
|
|
|
from zerver.lib.initial_password import initial_password
|
2016-11-10 19:30:09 +01:00
|
|
|
from zerver.lib.test_classes import (
|
|
|
|
ZulipTestCase,
|
2016-04-21 18:34:54 +02:00
|
|
|
)
|
|
|
|
from zerver.models import \
|
2019-01-29 13:39:21 +01:00
|
|
|
get_realm, email_to_username, CustomProfileField, CustomProfileFieldValue, \
|
2019-03-17 22:19:53 +01:00
|
|
|
UserProfile, PreregistrationUser, Realm, RealmDomain, get_user, MultiuseInvite, \
|
auth: Use zxcvbn to ensure password strength on server side.
For a long time, we've been only doing the zxcvbn password strength
checks on the browser, which is helpful, but means users could through
hackery (or a bug in the frontend validation code) manage to set a
too-weak password. We fix this by running our password strength
validation on the backend as well, using python-zxcvbn.
In theory, a bug in python-zxcvbn could result in it producing a
different opinion than the frontend version; if so, it'd be a pretty
bad bug in the library, and hopefully we'd hear about it from users,
report upstream, and get it fixed that way. Alternatively, we can
switch to shelling out to node like we do for KaTeX.
Fixes #6880.
2019-11-18 08:11:03 +01:00
|
|
|
clear_supported_auth_backends_cache, PasswordTooWeakError
|
2018-08-10 00:58:44 +02:00
|
|
|
from zerver.signals import JUST_CREATED_THRESHOLD
|
2016-10-26 14:40:14 +02:00
|
|
|
|
2019-02-02 23:53:44 +01:00
|
|
|
from confirmation.models import Confirmation, create_confirmation_link
|
2016-04-21 18:34:54 +02:00
|
|
|
|
|
|
|
from zproject.backends import ZulipDummyBackend, EmailAuthBackend, \
|
2019-02-02 16:51:26 +01:00
|
|
|
GoogleAuthBackend, ZulipRemoteUserBackend, ZulipLDAPAuthBackend, \
|
2016-10-26 13:50:00 +02:00
|
|
|
ZulipLDAPUserPopulator, DevAuthBackend, GitHubAuthBackend, ZulipAuthMixin, \
|
2019-02-02 16:51:26 +01:00
|
|
|
dev_auth_enabled, password_auth_enabled, github_auth_enabled, google_auth_enabled, \
|
2018-05-31 00:12:39 +02:00
|
|
|
require_email_format_usernames, AUTH_BACKEND_NAME_MAP, \
|
2019-11-01 23:26:49 +01:00
|
|
|
ZulipLDAPConfigurationError, ZulipLDAPExceptionNoMatchingLDAPUser, ZulipLDAPExceptionOutsideDomain, \
|
2019-09-04 10:11:25 +02:00
|
|
|
ZulipLDAPException, query_ldap, sync_user_from_ldap, SocialAuthMixin, \
|
2019-10-22 01:33:44 +02:00
|
|
|
PopulateUserLDAPError, SAMLAuthBackend, saml_auth_enabled, email_belongs_to_ldap, \
|
2019-12-08 23:11:25 +01:00
|
|
|
get_external_method_dicts, AzureADAuthBackend, check_password_strength, \
|
2019-11-09 00:27:18 +01:00
|
|
|
ZulipLDAPUser
|
2016-07-25 11:24:36 +02:00
|
|
|
|
2017-04-20 08:25:15 +02:00
|
|
|
from zerver.views.auth import (maybe_send_to_registration,
|
2017-10-27 02:45:38 +02:00
|
|
|
_subdomain_token_salt)
|
2016-10-26 14:40:14 +02:00
|
|
|
|
2019-09-29 06:32:56 +02:00
|
|
|
from onelogin.saml2.auth import OneLogin_Saml2_Auth
|
|
|
|
from onelogin.saml2.response import OneLogin_Saml2_Response
|
2017-03-07 08:32:40 +01:00
|
|
|
from social_core.exceptions import AuthFailed, AuthStateForbidden
|
2017-04-19 19:05:04 +02:00
|
|
|
from social_django.strategy import DjangoStrategy
|
2017-01-21 16:52:59 +01:00
|
|
|
from social_django.storage import BaseDjangoStorage
|
2016-04-21 18:34:54 +02:00
|
|
|
|
2019-09-29 06:32:56 +02:00
|
|
|
import base64
|
2019-10-22 18:23:57 +02:00
|
|
|
import copy
|
2018-05-18 03:27:47 +02:00
|
|
|
import json
|
2017-11-05 05:30:31 +01:00
|
|
|
import urllib
|
2016-04-21 18:34:54 +02:00
|
|
|
import ujson
|
2019-10-16 18:10:40 +02:00
|
|
|
from zerver.lib.test_helpers import load_subdomain_token, \
|
2019-06-07 23:36:19 +02:00
|
|
|
use_s3_backend, create_s3_buckets, get_test_image_file
|
2016-04-21 18:34:54 +02:00
|
|
|
|
2017-04-27 06:46:43 +02:00
|
|
|
class AuthBackendTest(ZulipTestCase):
|
2018-05-11 01:39:38 +02:00
|
|
|
def get_username(self, email_to_username: Optional[Callable[[str], str]]=None) -> str:
|
2017-05-23 20:57:59 +02:00
|
|
|
username = self.example_email('hamlet')
|
2017-03-28 11:11:29 +02:00
|
|
|
if email_to_username is not None:
|
2017-05-23 20:57:59 +02:00
|
|
|
username = email_to_username(self.example_email('hamlet'))
|
2017-03-28 11:11:29 +02:00
|
|
|
|
|
|
|
return username
|
|
|
|
|
2017-11-19 04:02:03 +01:00
|
|
|
def verify_backend(self, backend: Any, good_kwargs: Optional[Dict[str, Any]]=None, bad_kwargs: Optional[Dict[str, Any]]=None) -> None:
|
2019-03-17 22:19:53 +01:00
|
|
|
clear_supported_auth_backends_cache()
|
2017-05-23 20:57:59 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
2017-03-28 11:16:36 +02:00
|
|
|
|
2017-09-15 21:51:45 +02:00
|
|
|
assert good_kwargs is not None
|
2016-04-21 18:34:54 +02:00
|
|
|
|
|
|
|
# If bad_kwargs was specified, verify auth fails in that case
|
|
|
|
if bad_kwargs is not None:
|
2017-03-28 11:16:36 +02:00
|
|
|
self.assertIsNone(backend.authenticate(**bad_kwargs))
|
2016-04-21 18:34:54 +02:00
|
|
|
|
|
|
|
# Verify auth works
|
2017-03-28 11:16:36 +02:00
|
|
|
result = backend.authenticate(**good_kwargs)
|
2016-04-21 18:34:54 +02:00
|
|
|
self.assertEqual(user_profile, result)
|
|
|
|
|
|
|
|
# Verify auth fails with a deactivated user
|
|
|
|
do_deactivate_user(user_profile)
|
2019-04-12 06:24:58 +02:00
|
|
|
result = backend.authenticate(**good_kwargs)
|
|
|
|
if isinstance(backend, SocialAuthMixin):
|
|
|
|
# Returns a redirect to login page with an error.
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-04-17 21:28:57 +02:00
|
|
|
self.assertEqual(result.url, "/login/?is_deactivated=true")
|
2019-04-12 06:24:58 +02:00
|
|
|
else:
|
|
|
|
# Just takes you back to the login page treating as
|
|
|
|
# invalid auth; this is correct because the form will
|
|
|
|
# provide the appropriate validation error for deactivated
|
|
|
|
# account.
|
|
|
|
self.assertIsNone(result)
|
2016-04-21 18:34:54 +02:00
|
|
|
|
|
|
|
# Reactivate the user and verify auth works again
|
|
|
|
do_reactivate_user(user_profile)
|
2017-03-28 11:16:36 +02:00
|
|
|
result = backend.authenticate(**good_kwargs)
|
2016-04-21 18:34:54 +02:00
|
|
|
self.assertEqual(user_profile, result)
|
|
|
|
|
|
|
|
# Verify auth fails with a deactivated realm
|
|
|
|
do_deactivate_realm(user_profile.realm)
|
2017-03-28 11:16:36 +02:00
|
|
|
self.assertIsNone(backend.authenticate(**good_kwargs))
|
2016-04-21 18:34:54 +02:00
|
|
|
|
|
|
|
# Verify auth works again after reactivating the realm
|
|
|
|
do_reactivate_realm(user_profile.realm)
|
2017-03-28 11:16:36 +02:00
|
|
|
result = backend.authenticate(**good_kwargs)
|
2016-04-21 18:34:54 +02:00
|
|
|
self.assertEqual(user_profile, result)
|
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
# ZulipDummyBackend isn't a real backend so the remainder
|
|
|
|
# doesn't make sense for it
|
|
|
|
if isinstance(backend, ZulipDummyBackend):
|
|
|
|
return
|
|
|
|
|
|
|
|
# Verify auth fails if the auth backend is disabled on server
|
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipDummyBackend',)):
|
2019-03-17 22:19:53 +01:00
|
|
|
clear_supported_auth_backends_cache()
|
2017-03-28 11:16:36 +02:00
|
|
|
self.assertIsNone(backend.authenticate(**good_kwargs))
|
2019-03-17 22:19:53 +01:00
|
|
|
clear_supported_auth_backends_cache()
|
2016-11-02 21:41:10 +01:00
|
|
|
|
|
|
|
# Verify auth fails if the auth backend is disabled for the realm
|
|
|
|
for backend_name in AUTH_BACKEND_NAME_MAP.keys():
|
|
|
|
if isinstance(backend, AUTH_BACKEND_NAME_MAP[backend_name]):
|
|
|
|
break
|
|
|
|
|
|
|
|
index = getattr(user_profile.realm.authentication_methods, backend_name).number
|
|
|
|
user_profile.realm.authentication_methods.set_bit(index, False)
|
|
|
|
user_profile.realm.save()
|
2017-11-17 23:14:08 +01:00
|
|
|
if 'realm' in good_kwargs:
|
|
|
|
# Because this test is a little unfaithful to the ordering
|
|
|
|
# (i.e. we fetched the realm object before this function
|
|
|
|
# was called, when in fact it should be fetched after we
|
|
|
|
# changed the allowed authentication methods), we need to
|
|
|
|
# propagate the changes we just made to the actual realm
|
|
|
|
# object in good_kwargs.
|
|
|
|
good_kwargs['realm'] = user_profile.realm
|
2017-03-28 11:16:36 +02:00
|
|
|
self.assertIsNone(backend.authenticate(**good_kwargs))
|
2016-11-02 21:41:10 +01:00
|
|
|
user_profile.realm.authentication_methods.set_bit(index, True)
|
|
|
|
user_profile.realm.save()
|
2016-11-07 00:09:21 +01:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_dummy_backend(self) -> None:
|
2017-10-03 02:29:20 +02:00
|
|
|
realm = get_realm("zulip")
|
2017-03-28 11:16:36 +02:00
|
|
|
username = self.get_username()
|
2016-04-21 18:34:54 +02:00
|
|
|
self.verify_backend(ZulipDummyBackend(),
|
2017-03-28 11:16:36 +02:00
|
|
|
good_kwargs=dict(username=username,
|
2017-10-03 02:29:20 +02:00
|
|
|
realm=realm,
|
2017-03-28 11:16:36 +02:00
|
|
|
use_dummy_backend=True),
|
|
|
|
bad_kwargs=dict(username=username,
|
2017-10-03 02:29:20 +02:00
|
|
|
realm=realm,
|
2017-03-28 11:16:36 +02:00
|
|
|
use_dummy_backend=False))
|
2016-04-21 18:34:54 +02:00
|
|
|
|
2017-11-19 04:02:03 +01:00
|
|
|
def setup_subdomain(self, user_profile: UserProfile) -> None:
|
2016-10-07 13:38:01 +02:00
|
|
|
realm = user_profile.realm
|
2016-10-26 18:13:43 +02:00
|
|
|
realm.string_id = 'zulip'
|
2016-10-07 13:38:01 +02:00
|
|
|
realm.save()
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_email_auth_backend(self) -> None:
|
2017-03-28 11:16:36 +02:00
|
|
|
username = self.get_username()
|
2017-05-23 20:57:59 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
2016-04-21 18:34:54 +02:00
|
|
|
password = "testpassword"
|
|
|
|
user_profile.set_password(password)
|
|
|
|
user_profile.save()
|
|
|
|
|
2017-03-23 07:49:42 +01:00
|
|
|
with mock.patch('zproject.backends.email_auth_enabled',
|
|
|
|
return_value=False), \
|
|
|
|
mock.patch('zproject.backends.password_auth_enabled',
|
|
|
|
return_value=True):
|
|
|
|
return_data = {} # type: Dict[str, bool]
|
2019-05-08 02:18:34 +02:00
|
|
|
user = EmailAuthBackend().authenticate(username=self.example_email('hamlet'),
|
2017-11-17 23:56:45 +01:00
|
|
|
realm=get_realm("zulip"),
|
2017-03-23 07:49:42 +01:00
|
|
|
password=password,
|
|
|
|
return_data=return_data)
|
|
|
|
self.assertEqual(user, None)
|
|
|
|
self.assertTrue(return_data['email_auth_disabled'])
|
|
|
|
|
2016-10-07 13:38:01 +02:00
|
|
|
self.verify_backend(EmailAuthBackend(),
|
|
|
|
good_kwargs=dict(password=password,
|
2017-03-28 11:16:36 +02:00
|
|
|
username=username,
|
2017-11-17 23:56:45 +01:00
|
|
|
realm=get_realm('zulip'),
|
2017-09-15 21:51:45 +02:00
|
|
|
return_data=dict()),
|
|
|
|
bad_kwargs=dict(password=password,
|
|
|
|
username=username,
|
2017-11-17 23:56:45 +01:00
|
|
|
realm=get_realm('zephyr'),
|
|
|
|
return_data=dict()))
|
|
|
|
self.verify_backend(EmailAuthBackend(),
|
|
|
|
good_kwargs=dict(password=password,
|
|
|
|
username=username,
|
|
|
|
realm=get_realm('zulip'),
|
|
|
|
return_data=dict()),
|
|
|
|
bad_kwargs=dict(password=password,
|
|
|
|
username=username,
|
2019-05-05 01:04:48 +02:00
|
|
|
realm=get_realm('zephyr'),
|
2017-09-15 21:51:45 +02:00
|
|
|
return_data=dict()))
|
2016-10-07 13:38:01 +02:00
|
|
|
|
2019-11-18 07:57:36 +01:00
|
|
|
def test_email_auth_backend_empty_password(self) -> None:
|
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
password = "testpassword"
|
|
|
|
user_profile.set_password(password)
|
|
|
|
user_profile.save()
|
|
|
|
|
|
|
|
# First, verify authentication works with the a nonempty
|
|
|
|
# password so we know we've set up the test correctly.
|
|
|
|
self.assertIsNotNone(EmailAuthBackend().authenticate(username=self.example_email('hamlet'),
|
|
|
|
password=password,
|
|
|
|
realm=get_realm("zulip")))
|
|
|
|
|
|
|
|
# Now do the same test with the empty string as the password.
|
|
|
|
password = ""
|
auth: Use zxcvbn to ensure password strength on server side.
For a long time, we've been only doing the zxcvbn password strength
checks on the browser, which is helpful, but means users could through
hackery (or a bug in the frontend validation code) manage to set a
too-weak password. We fix this by running our password strength
validation on the backend as well, using python-zxcvbn.
In theory, a bug in python-zxcvbn could result in it producing a
different opinion than the frontend version; if so, it'd be a pretty
bad bug in the library, and hopefully we'd hear about it from users,
report upstream, and get it fixed that way. Alternatively, we can
switch to shelling out to node like we do for KaTeX.
Fixes #6880.
2019-11-18 08:11:03 +01:00
|
|
|
with self.assertRaises(PasswordTooWeakError):
|
|
|
|
# UserProfile.set_password protects against setting an empty password.
|
|
|
|
user_profile.set_password(password)
|
|
|
|
# We do want to force an empty password for this test, so we bypass the protection
|
|
|
|
# by using Django's version of this method.
|
|
|
|
super(UserProfile, user_profile).set_password(password)
|
2019-11-18 07:57:36 +01:00
|
|
|
user_profile.save()
|
|
|
|
self.assertIsNone(EmailAuthBackend().authenticate(username=self.example_email('hamlet'),
|
|
|
|
password=password,
|
|
|
|
realm=get_realm("zulip")))
|
|
|
|
|
2017-11-20 03:22:57 +01:00
|
|
|
def test_email_auth_backend_disabled_password_auth(self) -> None:
|
2017-05-23 20:57:59 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
2016-04-21 18:34:54 +02:00
|
|
|
password = "testpassword"
|
|
|
|
user_profile.set_password(password)
|
|
|
|
user_profile.save()
|
|
|
|
# Verify if a realm has password auth disabled, correct password is rejected
|
|
|
|
with mock.patch('zproject.backends.password_auth_enabled', return_value=False):
|
2019-05-08 02:18:34 +02:00
|
|
|
self.assertIsNone(EmailAuthBackend().authenticate(username=self.example_email('hamlet'),
|
|
|
|
password=password,
|
2017-11-17 23:56:45 +01:00
|
|
|
realm=get_realm("zulip")))
|
2016-04-21 18:34:54 +02:00
|
|
|
|
2019-04-13 09:37:53 +02:00
|
|
|
def test_login_preview(self) -> None:
|
|
|
|
# Test preview=true displays organization login page
|
|
|
|
# instead of redirecting to app
|
|
|
|
self.login(self.example_email("iago"))
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
result = self.client_get('/login/?preview=true')
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_response(realm.description, result)
|
|
|
|
self.assert_in_response(realm.name, result)
|
|
|
|
self.assert_in_response("Log in to Zulip", result)
|
|
|
|
|
|
|
|
data = dict(description=ujson.dumps("New realm description"),
|
|
|
|
name=ujson.dumps("New Zulip"))
|
|
|
|
result = self.client_patch('/json/realm', data)
|
|
|
|
self.assert_json_success(result)
|
|
|
|
|
|
|
|
result = self.client_get('/login/?preview=true')
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_response("New realm description", result)
|
|
|
|
self.assert_in_response("New Zulip", result)
|
|
|
|
|
|
|
|
result = self.client_get('/login/')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, 'http://zulip.testserver')
|
|
|
|
|
2017-04-27 06:46:43 +02:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipDummyBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_no_backend_enabled(self) -> None:
|
2017-04-27 06:46:43 +02:00
|
|
|
result = self.client_get('/login/')
|
|
|
|
self.assert_in_success_response(["No authentication backends are enabled"], result)
|
|
|
|
|
|
|
|
result = self.client_get('/register/')
|
|
|
|
self.assert_in_success_response(["No authentication backends are enabled"], result)
|
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_any_backend_enabled(self) -> None:
|
2017-04-27 06:46:43 +02:00
|
|
|
|
|
|
|
# testing to avoid false error messages.
|
|
|
|
result = self.client_get('/login/')
|
2018-06-05 08:39:01 +02:00
|
|
|
self.assert_not_in_success_response(["No authentication backends are enabled"], result)
|
2017-04-27 06:46:43 +02:00
|
|
|
|
|
|
|
result = self.client_get('/register/')
|
2018-06-05 08:39:01 +02:00
|
|
|
self.assert_not_in_success_response(["No authentication backends are enabled"], result)
|
2017-04-27 06:46:43 +02:00
|
|
|
|
2019-10-05 03:54:48 +02:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',),
|
|
|
|
LDAP_EMAIL_ATTR="mail")
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_ldap_backend(self) -> None:
|
2019-10-05 03:54:48 +02:00
|
|
|
self.init_default_ldap_database()
|
2017-05-07 19:39:30 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
email = user_profile.email
|
2019-10-18 18:25:51 +02:00
|
|
|
password = self.ldap_password()
|
2016-10-07 13:38:01 +02:00
|
|
|
self.setup_subdomain(user_profile)
|
|
|
|
|
2017-03-28 11:16:36 +02:00
|
|
|
username = self.get_username()
|
2016-04-21 18:34:54 +02:00
|
|
|
backend = ZulipLDAPAuthBackend()
|
|
|
|
|
|
|
|
# Test LDAP auth fails when LDAP server rejects password
|
2019-10-05 03:54:48 +02:00
|
|
|
self.assertIsNone(backend.authenticate(username=email, password="wrongpass", realm=get_realm("zulip")))
|
|
|
|
|
|
|
|
self.verify_backend(backend,
|
|
|
|
bad_kwargs=dict(username=username,
|
|
|
|
password=password,
|
|
|
|
realm=get_realm('zephyr')),
|
|
|
|
good_kwargs=dict(username=username,
|
|
|
|
password=password,
|
|
|
|
realm=get_realm('zulip')))
|
|
|
|
self.verify_backend(backend,
|
|
|
|
bad_kwargs=dict(username=username,
|
|
|
|
password=password,
|
|
|
|
realm=get_realm('zephyr')),
|
|
|
|
good_kwargs=dict(username=username,
|
|
|
|
password=password,
|
|
|
|
realm=get_realm('zulip')))
|
2017-03-23 07:49:42 +01:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_devauth_backend(self) -> None:
|
2017-03-28 11:16:36 +02:00
|
|
|
self.verify_backend(DevAuthBackend(),
|
2017-11-21 21:19:20 +01:00
|
|
|
good_kwargs=dict(dev_auth_username=self.get_username(),
|
|
|
|
realm=get_realm("zulip")),
|
|
|
|
bad_kwargs=dict(dev_auth_username=self.get_username(),
|
2019-05-05 01:04:48 +02:00
|
|
|
realm=get_realm("zephyr")))
|
2016-04-21 18:34:54 +02:00
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_remote_user_backend(self) -> None:
|
2017-03-28 11:16:36 +02:00
|
|
|
username = self.get_username()
|
2016-10-07 13:38:01 +02:00
|
|
|
self.verify_backend(ZulipRemoteUserBackend(),
|
2017-03-28 11:16:36 +02:00
|
|
|
good_kwargs=dict(remote_user=username,
|
2017-11-17 23:14:08 +01:00
|
|
|
realm=get_realm('zulip')),
|
2017-09-15 21:51:45 +02:00
|
|
|
bad_kwargs=dict(remote_user=username,
|
2017-11-17 23:14:08 +01:00
|
|
|
realm=get_realm('zephyr')))
|
|
|
|
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',))
|
2017-12-09 06:57:59 +01:00
|
|
|
def test_remote_user_backend_invalid_realm(self) -> None:
|
2017-11-17 23:14:08 +01:00
|
|
|
username = self.get_username()
|
|
|
|
self.verify_backend(ZulipRemoteUserBackend(),
|
|
|
|
good_kwargs=dict(remote_user=username,
|
|
|
|
realm=get_realm('zulip')),
|
|
|
|
bad_kwargs=dict(remote_user=username,
|
2019-05-05 01:04:48 +02:00
|
|
|
realm=get_realm('zephyr')))
|
2016-04-21 18:34:54 +02:00
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',))
|
2017-09-15 21:51:45 +02:00
|
|
|
@override_settings(SSO_APPEND_DOMAIN='zulip.com')
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_remote_user_backend_sso_append_domain(self) -> None:
|
2017-03-28 11:16:36 +02:00
|
|
|
username = self.get_username(email_to_username)
|
2017-09-15 21:51:45 +02:00
|
|
|
self.verify_backend(ZulipRemoteUserBackend(),
|
|
|
|
good_kwargs=dict(remote_user=username,
|
2017-11-17 23:14:08 +01:00
|
|
|
realm=get_realm("zulip")),
|
2017-09-15 21:51:45 +02:00
|
|
|
bad_kwargs=dict(remote_user=username,
|
2017-11-17 23:14:08 +01:00
|
|
|
realm=get_realm('zephyr')))
|
2016-04-21 21:07:43 +02:00
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.GitHubAuthBackend',
|
|
|
|
'zproject.backends.GoogleAuthBackend'))
|
2019-03-04 21:11:23 +01:00
|
|
|
def test_social_auth_backends(self) -> None:
|
2017-05-08 16:23:43 +02:00
|
|
|
user = self.example_user('hamlet')
|
2018-05-31 00:12:39 +02:00
|
|
|
token_data_dict = {
|
|
|
|
'access_token': 'foobar',
|
|
|
|
'token_type': 'bearer'
|
|
|
|
}
|
2019-03-04 21:11:23 +01:00
|
|
|
github_email_data = [
|
|
|
|
dict(email=user.email,
|
2018-06-07 00:19:06 +02:00
|
|
|
verified=True,
|
|
|
|
primary=True),
|
2018-07-18 23:45:49 +02:00
|
|
|
dict(email="nonprimary@zulip.com",
|
2018-06-07 00:19:06 +02:00
|
|
|
verified=True),
|
|
|
|
dict(email="ignored@example.com",
|
|
|
|
verified=False),
|
|
|
|
]
|
2019-02-02 16:51:26 +01:00
|
|
|
google_email_data = dict(email=user.email,
|
|
|
|
name=user.full_name,
|
|
|
|
email_verified=True)
|
2019-03-04 21:11:23 +01:00
|
|
|
backends_to_test = {
|
2019-02-02 16:51:26 +01:00
|
|
|
'google': {
|
|
|
|
'urls': [
|
|
|
|
{
|
|
|
|
'url': "https://accounts.google.com/o/oauth2/token",
|
|
|
|
'method': httpretty.POST,
|
|
|
|
'status': 200,
|
|
|
|
'body': json.dumps(token_data_dict),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'url': "https://www.googleapis.com/oauth2/v3/userinfo",
|
|
|
|
'method': httpretty.GET,
|
|
|
|
'status': 200,
|
|
|
|
'body': json.dumps(google_email_data),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
'backend': GoogleAuthBackend,
|
|
|
|
},
|
2019-03-04 21:11:23 +01:00
|
|
|
'github': {
|
|
|
|
'urls': [
|
|
|
|
{
|
|
|
|
'url': "https://github.com/login/oauth/access_token",
|
|
|
|
'method': httpretty.POST,
|
|
|
|
'status': 200,
|
|
|
|
'body': json.dumps(token_data_dict),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'url': "https://api.github.com/user",
|
|
|
|
'method': httpretty.GET,
|
|
|
|
'status': 200,
|
|
|
|
'body': json.dumps(dict(email=user.email, name=user.full_name)),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'url': "https://api.github.com/user/emails",
|
|
|
|
'method': httpretty.GET,
|
|
|
|
'status': 200,
|
|
|
|
'body': json.dumps(github_email_data),
|
|
|
|
},
|
|
|
|
],
|
|
|
|
'backend': GitHubAuthBackend,
|
|
|
|
}
|
|
|
|
} # type: Dict[str, Any]
|
2018-05-31 00:12:39 +02:00
|
|
|
|
2019-05-08 02:18:34 +02:00
|
|
|
def patched_authenticate(**kwargs: Any) -> Any:
|
2018-07-18 23:45:49 +02:00
|
|
|
# This is how we pass the subdomain to the authentication
|
|
|
|
# backend in production code, so we need to do this setup
|
|
|
|
# here.
|
2018-05-31 00:12:39 +02:00
|
|
|
if 'subdomain' in kwargs:
|
|
|
|
backend.strategy.session_set("subdomain", kwargs["subdomain"])
|
|
|
|
del kwargs['subdomain']
|
2018-07-18 23:45:49 +02:00
|
|
|
|
|
|
|
# Because we're not simulating the full python-social-auth
|
|
|
|
# pipeline here, we need to provide the user's choice of
|
|
|
|
# which email to select in the partial phase of the
|
|
|
|
# pipeline when we display an email picker for the GitHub
|
|
|
|
# authentication backend. We do that here.
|
|
|
|
def return_email() -> Dict[str, str]:
|
|
|
|
return {'email': user.email}
|
|
|
|
backend.strategy.request_data = return_email
|
|
|
|
|
2019-05-08 02:18:34 +02:00
|
|
|
result = orig_authenticate(backend, **kwargs)
|
2018-05-31 00:12:39 +02:00
|
|
|
return result
|
2019-03-04 21:11:23 +01:00
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
def patched_get_verified_emails(*args: Any, **kwargs: Any) -> Any:
|
|
|
|
return google_email_data['email']
|
|
|
|
|
2019-03-04 21:11:23 +01:00
|
|
|
for backend_name in backends_to_test:
|
2019-03-04 21:13:49 +01:00
|
|
|
httpretty.enable(allow_net_connect=False)
|
2019-03-04 21:11:23 +01:00
|
|
|
urls = backends_to_test[backend_name]['urls'] # type: List[Dict[str, Any]]
|
|
|
|
for details in urls:
|
|
|
|
httpretty.register_uri(
|
|
|
|
details['method'],
|
|
|
|
details['url'],
|
|
|
|
status=details['status'],
|
|
|
|
body=details['body'])
|
|
|
|
backend_class = backends_to_test[backend_name]['backend']
|
|
|
|
backend = backend_class()
|
|
|
|
backend.strategy = DjangoStrategy(storage=BaseDjangoStorage())
|
2019-02-02 16:51:26 +01:00
|
|
|
|
2019-03-04 21:11:23 +01:00
|
|
|
orig_authenticate = backend_class.authenticate
|
|
|
|
backend.authenticate = patched_authenticate
|
2019-02-02 16:51:26 +01:00
|
|
|
orig_get_verified_emails = backend_class.get_verified_emails
|
|
|
|
if backend_name == "google":
|
|
|
|
backend.get_verified_emails = patched_get_verified_emails
|
|
|
|
|
2019-03-04 21:11:23 +01:00
|
|
|
good_kwargs = dict(backend=backend, strategy=backend.strategy,
|
|
|
|
storage=backend.strategy.storage,
|
|
|
|
response=token_data_dict,
|
|
|
|
subdomain='zulip')
|
|
|
|
bad_kwargs = dict(subdomain='acme')
|
|
|
|
with mock.patch('zerver.views.auth.redirect_and_log_into_subdomain',
|
|
|
|
return_value=user):
|
|
|
|
self.verify_backend(backend,
|
|
|
|
good_kwargs=good_kwargs,
|
|
|
|
bad_kwargs=bad_kwargs)
|
|
|
|
bad_kwargs['subdomain'] = "zephyr"
|
|
|
|
self.verify_backend(backend,
|
|
|
|
good_kwargs=good_kwargs,
|
|
|
|
bad_kwargs=bad_kwargs)
|
|
|
|
backend.authenticate = orig_authenticate
|
2019-02-02 16:51:26 +01:00
|
|
|
backend.get_verified_emails = orig_get_verified_emails
|
2019-03-04 21:11:23 +01:00
|
|
|
httpretty.disable()
|
|
|
|
httpretty.reset()
|
2016-07-25 11:24:36 +02:00
|
|
|
|
auth: Use zxcvbn to ensure password strength on server side.
For a long time, we've been only doing the zxcvbn password strength
checks on the browser, which is helpful, but means users could through
hackery (or a bug in the frontend validation code) manage to set a
too-weak password. We fix this by running our password strength
validation on the backend as well, using python-zxcvbn.
In theory, a bug in python-zxcvbn could result in it producing a
different opinion than the frontend version; if so, it'd be a pretty
bad bug in the library, and hopefully we'd hear about it from users,
report upstream, and get it fixed that way. Alternatively, we can
switch to shelling out to node like we do for KaTeX.
Fixes #6880.
2019-11-18 08:11:03 +01:00
|
|
|
class CheckPasswordStrengthTest(ZulipTestCase):
|
|
|
|
def test_check_password_strength(self) -> None:
|
|
|
|
with self.settings(PASSWORD_MIN_LENGTH=0, PASSWORD_MIN_GUESSES=0):
|
|
|
|
# Never allow empty password.
|
|
|
|
self.assertFalse(check_password_strength(''))
|
|
|
|
|
|
|
|
with self.settings(PASSWORD_MIN_LENGTH=6, PASSWORD_MIN_GUESSES=1000):
|
|
|
|
self.assertFalse(check_password_strength(''))
|
|
|
|
self.assertFalse(check_password_strength('short'))
|
|
|
|
# Long enough, but too easy:
|
|
|
|
self.assertFalse(check_password_strength('longer'))
|
|
|
|
# Good password:
|
|
|
|
self.assertTrue(check_password_strength('f657gdGGk9'))
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
class SocialAuthBase(ZulipTestCase):
|
2019-07-22 04:26:47 +02:00
|
|
|
"""This is a base class for testing social-auth backends. These
|
|
|
|
methods are often overriden by subclasses:
|
2019-02-03 09:18:01 +01:00
|
|
|
|
2019-07-22 04:26:47 +02:00
|
|
|
register_extra_endpoints() - If the backend being tested calls some extra
|
|
|
|
endpoints then they can be added here.
|
2019-02-03 09:18:01 +01:00
|
|
|
|
|
|
|
get_account_data_dict() - Return the data returned by the user info endpoint
|
|
|
|
according to the respective backend.
|
|
|
|
"""
|
|
|
|
# Don't run base class tests, make sure to set it to False
|
|
|
|
# in subclass otherwise its tests will not run.
|
|
|
|
__unittest_skip__ = True
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def setUp(self) -> None:
|
2019-10-19 20:47:00 +02:00
|
|
|
super().setUp()
|
2017-05-07 21:25:59 +02:00
|
|
|
self.user_profile = self.example_user('hamlet')
|
|
|
|
self.email = self.user_profile.email
|
2019-09-29 00:55:10 +02:00
|
|
|
self.name = self.user_profile.full_name
|
2019-02-03 09:18:01 +01:00
|
|
|
self.backend = self.BACKEND_CLASS
|
2017-04-19 19:05:04 +02:00
|
|
|
self.backend.strategy = DjangoStrategy(storage=BaseDjangoStorage())
|
2016-07-25 11:24:36 +02:00
|
|
|
self.user_profile.backend = self.backend
|
|
|
|
|
2018-05-31 00:12:39 +02:00
|
|
|
# This is a workaround for the fact that Python social auth
|
|
|
|
# caches the set of authentication backends that are enabled
|
|
|
|
# the first time that `social_django.utils` is imported. See
|
|
|
|
# https://github.com/python-social-auth/social-app-django/pull/162
|
|
|
|
# for details.
|
|
|
|
from social_core.backends.utils import load_backends
|
|
|
|
load_backends(settings.AUTHENTICATION_BACKENDS, force_load=True)
|
2016-10-07 11:10:21 +02:00
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
def register_extra_endpoints(self,
|
|
|
|
account_data_dict: Dict[str, str],
|
|
|
|
**extra_data: Any) -> None:
|
|
|
|
pass
|
|
|
|
|
2019-09-29 02:25:46 +02:00
|
|
|
def prepare_login_url_and_headers(self,
|
|
|
|
subdomain: Optional[str]=None,
|
|
|
|
mobile_flow_otp: Optional[str]=None,
|
|
|
|
is_signup: Optional[str]=None,
|
|
|
|
next: str='',
|
|
|
|
multiuse_object_key: str='',
|
|
|
|
alternative_start_url: Optional[str]=None
|
|
|
|
) -> Tuple[str, Dict[str, Any]]:
|
2019-02-03 09:18:01 +01:00
|
|
|
url = self.LOGIN_URL
|
2019-08-27 05:51:04 +02:00
|
|
|
if alternative_start_url is not None:
|
|
|
|
url = alternative_start_url
|
|
|
|
|
2018-05-18 03:27:47 +02:00
|
|
|
params = {}
|
|
|
|
headers = {}
|
|
|
|
if subdomain is not None:
|
|
|
|
headers['HTTP_HOST'] = subdomain + ".testserver"
|
|
|
|
if mobile_flow_otp is not None:
|
|
|
|
params['mobile_flow_otp'] = mobile_flow_otp
|
|
|
|
headers['HTTP_USER_AGENT'] = "ZulipAndroid"
|
|
|
|
if is_signup is not None:
|
2019-02-03 09:18:01 +01:00
|
|
|
url = self.SIGNUP_URL
|
2018-05-18 03:27:47 +02:00
|
|
|
params['next'] = next
|
2019-02-08 17:09:25 +01:00
|
|
|
params['multiuse_object_key'] = multiuse_object_key
|
2018-05-18 03:27:47 +02:00
|
|
|
if len(params) > 0:
|
2019-04-20 01:00:46 +02:00
|
|
|
url += "?%s" % (urllib.parse.urlencode(params),)
|
2018-05-18 03:27:47 +02:00
|
|
|
|
2019-09-29 02:25:46 +02:00
|
|
|
return url, headers
|
|
|
|
|
|
|
|
def social_auth_test(self, account_data_dict: Dict[str, str],
|
|
|
|
*, subdomain: Optional[str]=None,
|
|
|
|
mobile_flow_otp: Optional[str]=None,
|
|
|
|
is_signup: Optional[str]=None,
|
|
|
|
next: str='',
|
|
|
|
multiuse_object_key: str='',
|
|
|
|
expect_choose_email_screen: bool=False,
|
|
|
|
alternative_start_url: Optional[str]=None,
|
|
|
|
**extra_data: Any) -> HttpResponse:
|
|
|
|
url, headers = self.prepare_login_url_and_headers(
|
|
|
|
subdomain, mobile_flow_otp, is_signup, next, multiuse_object_key, alternative_start_url
|
|
|
|
)
|
|
|
|
|
2018-05-18 03:27:47 +02:00
|
|
|
result = self.client_get(url, **headers)
|
2018-07-10 08:07:23 +02:00
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
expected_result_url_prefix = 'http://testserver/login/%s/' % (self.backend.name,)
|
2018-07-10 08:07:23 +02:00
|
|
|
if settings.SOCIAL_AUTH_SUBDOMAIN is not None:
|
2019-02-03 09:18:01 +01:00
|
|
|
expected_result_url_prefix = ('http://%s.testserver/login/%s/' %
|
|
|
|
(settings.SOCIAL_AUTH_SUBDOMAIN, self.backend.name,))
|
2018-07-10 08:07:23 +02:00
|
|
|
|
|
|
|
if result.status_code != 302 or not result.url.startswith(expected_result_url_prefix):
|
2018-05-18 03:27:47 +02:00
|
|
|
return result
|
|
|
|
|
|
|
|
result = self.client_get(result.url, **headers)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-02-03 09:18:01 +01:00
|
|
|
assert self.AUTHORIZATION_URL in result.url
|
2018-05-18 03:27:47 +02:00
|
|
|
|
|
|
|
self.client.cookies = result.cookies
|
|
|
|
|
|
|
|
# Next, the browser requests result["Location"], and gets
|
2019-02-03 09:18:01 +01:00
|
|
|
# redirected back to the registered redirect uri.
|
2018-05-18 03:27:47 +02:00
|
|
|
|
2018-06-07 00:22:20 +02:00
|
|
|
token_data_dict = {
|
|
|
|
'access_token': 'foobar',
|
|
|
|
'token_type': 'bearer'
|
|
|
|
}
|
2018-07-19 00:12:07 +02:00
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
# We register callbacks for the key URLs on Identity Provider that
|
|
|
|
# auth completion url will call
|
2019-03-04 21:13:49 +01:00
|
|
|
httpretty.enable(allow_net_connect=False)
|
2018-05-18 03:27:47 +02:00
|
|
|
httpretty.register_uri(
|
|
|
|
httpretty.POST,
|
2019-02-03 09:18:01 +01:00
|
|
|
self.ACCESS_TOKEN_URL,
|
2018-05-18 03:27:47 +02:00
|
|
|
match_querystring=False,
|
|
|
|
status=200,
|
|
|
|
body=json.dumps(token_data_dict))
|
|
|
|
httpretty.register_uri(
|
|
|
|
httpretty.GET,
|
2019-02-03 09:18:01 +01:00
|
|
|
self.USER_INFO_URL,
|
2018-05-18 03:27:47 +02:00
|
|
|
status=200,
|
|
|
|
body=json.dumps(account_data_dict)
|
|
|
|
)
|
2019-07-22 04:26:47 +02:00
|
|
|
self.register_extra_endpoints(account_data_dict, **extra_data)
|
2018-05-18 03:27:47 +02:00
|
|
|
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
csrf_state = urllib.parse.parse_qs(parsed_url.query)['state']
|
2019-02-03 09:18:01 +01:00
|
|
|
result = self.client_get(self.AUTH_FINISH_URL,
|
2018-05-18 03:27:47 +02:00
|
|
|
dict(state=csrf_state), **headers)
|
2018-07-18 23:45:49 +02:00
|
|
|
|
|
|
|
if expect_choose_email_screen and result.status_code == 200:
|
|
|
|
# For authentication backends such as GitHub that
|
|
|
|
# successfully authenticate multiple email addresses,
|
|
|
|
# we'll have an additional screen where the user selects
|
|
|
|
# which email address to login using (this screen is a
|
|
|
|
# "partial" state of the python-social-auth pipeline).
|
|
|
|
#
|
|
|
|
# TODO: Generalize this testing code for use with other
|
|
|
|
# authentication backends; for now, we just assert that
|
|
|
|
# it's definitely the GitHub authentication backend.
|
2019-08-02 16:44:05 +02:00
|
|
|
self.assert_in_success_response(["Select account"], result)
|
2018-07-18 23:45:49 +02:00
|
|
|
assert self.AUTH_FINISH_URL == "/complete/github/"
|
|
|
|
|
|
|
|
# Testing hack: When the pipeline goes to the partial
|
|
|
|
# step, the below given URL is called again in the same
|
|
|
|
# test. If the below URL is not registered again as done
|
|
|
|
# below, the URL returns emails from previous tests. e.g
|
|
|
|
# email = 'invalid' may be one of the emails in the list
|
|
|
|
# in a test function followed by it. This is probably a
|
|
|
|
# bug in httpretty.
|
|
|
|
httpretty.disable()
|
|
|
|
httpretty.enable()
|
|
|
|
httpretty.register_uri(
|
|
|
|
httpretty.GET,
|
|
|
|
"https://api.github.com/user/emails",
|
|
|
|
status=200,
|
|
|
|
body=json.dumps(self.email_data)
|
|
|
|
)
|
|
|
|
result = self.client_get(self.AUTH_FINISH_URL,
|
|
|
|
dict(state=csrf_state, email=account_data_dict['email']), **headers)
|
|
|
|
elif self.AUTH_FINISH_URL == "/complete/github/":
|
|
|
|
# We want to be explicit about when we expect a test to
|
|
|
|
# use the "choose email" screen, but of course we should
|
|
|
|
# only check for that screen with the GitHub backend,
|
|
|
|
# because this test code is shared with other
|
|
|
|
# authentication backends that structurally will never use
|
|
|
|
# that screen.
|
|
|
|
assert not expect_choose_email_screen
|
|
|
|
|
2018-05-18 03:27:47 +02:00
|
|
|
httpretty.disable()
|
|
|
|
return result
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_no_key(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
with self.settings(**{self.CLIENT_KEY_SETTING: None}):
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip', next='/user_uploads/image')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, self.CONFIG_ERROR_URL)
|
2018-05-18 03:27:47 +02:00
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_success(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zulip', next='/user_uploads/image')
|
2018-07-10 08:07:23 +02:00
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
2019-09-29 00:55:10 +02:00
|
|
|
self.assertEqual(data['name'], self.name)
|
2018-07-10 08:07:23 +02:00
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
self.assertEqual(data['next'], '/user_uploads/image')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
|
|
|
parsed_url.path)
|
|
|
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
|
|
|
|
|
|
|
@override_settings(SOCIAL_AUTH_SUBDOMAIN=None)
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_when_social_auth_subdomain_is_not_set(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip',
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
next='/user_uploads/image')
|
2018-05-18 03:27:47 +02:00
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
2019-09-29 00:55:10 +02:00
|
|
|
self.assertEqual(data['name'], self.name)
|
2018-05-18 03:27:47 +02:00
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
self.assertEqual(data['next'], '/user_uploads/image')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
|
|
|
parsed_url.path)
|
|
|
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_deactivated_user(self) -> None:
|
2018-05-31 02:20:01 +02:00
|
|
|
user_profile = self.example_user("hamlet")
|
|
|
|
do_deactivate_user(user_profile)
|
2019-02-03 09:18:01 +01:00
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
2018-07-18 23:45:49 +02:00
|
|
|
# We expect to go through the "choose email" screen here,
|
|
|
|
# because there won't be an existing user account we can
|
|
|
|
# auto-select for the user.
|
2019-02-03 09:18:01 +01:00
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zulip')
|
2018-05-31 00:12:39 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-04-17 21:28:57 +02:00
|
|
|
self.assertEqual(result.url, "/login/?is_deactivated=true")
|
2018-05-31 00:12:39 +02:00
|
|
|
# TODO: verify whether we provide a clear error message
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_invalid_realm(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
2018-05-31 00:12:39 +02:00
|
|
|
with mock.patch('zerver.middleware.get_realm', return_value=get_realm("zulip")):
|
|
|
|
# This mock.patch case somewhat hackishly arranges it so
|
|
|
|
# that we switch realms halfway through the test
|
2019-02-03 09:18:01 +01:00
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='invalid', next='/user_uploads/image')
|
2018-05-31 00:12:39 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/accounts/login/?subdomain=1")
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_invalid_email(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email="invalid", name=self.name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zulip', next='/user_uploads/image')
|
2018-05-31 02:20:01 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/login/?next=/user_uploads/image")
|
|
|
|
|
2018-05-18 03:27:47 +02:00
|
|
|
def test_user_cannot_log_into_nonexisting_realm(self) -> None:
|
2019-02-03 09:18:01 +01:00
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='nonexistent')
|
2019-03-12 01:56:52 +01:00
|
|
|
self.assert_in_response("There is no Zulip organization hosted at this subdomain.",
|
|
|
|
result)
|
|
|
|
self.assertEqual(result.status_code, 404)
|
2018-05-18 03:27:47 +02:00
|
|
|
|
|
|
|
def test_user_cannot_log_into_wrong_subdomain(self) -> None:
|
2019-02-03 09:18:01 +01:00
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zephyr')
|
2018-05-18 03:27:47 +02:00
|
|
|
self.assertTrue(result.url.startswith("http://zephyr.testserver/accounts/login/subdomain/"))
|
|
|
|
result = self.client_get(result.url.replace('http://zephyr.testserver', ''),
|
|
|
|
subdomain="zephyr")
|
|
|
|
self.assert_in_success_response(['Your email address, hamlet@zulip.com, is not in one of the domains ',
|
|
|
|
'that are allowed to register for accounts in this organization.'], result)
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_mobile_success(self) -> None:
|
2018-05-18 03:27:47 +02:00
|
|
|
mobile_flow_otp = '1234abcd' * 8
|
2019-02-03 09:18:01 +01:00
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name='Full Name')
|
2018-05-18 03:27:47 +02:00
|
|
|
self.assertEqual(len(mail.outbox), 0)
|
2018-08-10 00:58:44 +02:00
|
|
|
self.user_profile.date_joined = timezone_now() - datetime.timedelta(seconds=JUST_CREATED_THRESHOLD + 1)
|
|
|
|
self.user_profile.save()
|
|
|
|
|
2018-05-18 03:27:47 +02:00
|
|
|
with self.settings(SEND_LOGIN_EMAILS=True):
|
|
|
|
# Verify that the right thing happens with an invalid-format OTP
|
2019-02-03 09:18:01 +01:00
|
|
|
result = self.social_auth_test(account_data_dict, subdomain='zulip',
|
|
|
|
mobile_flow_otp="1234")
|
2018-05-18 03:27:47 +02:00
|
|
|
self.assert_json_error(result, "Invalid OTP")
|
2019-02-03 09:18:01 +01:00
|
|
|
result = self.social_auth_test(account_data_dict, subdomain='zulip',
|
|
|
|
mobile_flow_otp="invalido" * 8)
|
2018-05-18 03:27:47 +02:00
|
|
|
self.assert_json_error(result, "Invalid OTP")
|
|
|
|
|
|
|
|
# Now do it correctly
|
2019-02-03 09:18:01 +01:00
|
|
|
result = self.social_auth_test(account_data_dict, subdomain='zulip',
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
mobile_flow_otp=mobile_flow_otp)
|
2018-05-18 03:27:47 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
redirect_url = result['Location']
|
|
|
|
parsed_url = urllib.parse.urlparse(redirect_url)
|
|
|
|
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
|
|
self.assertEqual(parsed_url.scheme, 'zulip')
|
|
|
|
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
|
|
|
self.assertEqual(query_params["email"], [self.example_email("hamlet")])
|
|
|
|
encrypted_api_key = query_params["otp_encrypted_api_key"][0]
|
2018-08-01 10:53:40 +02:00
|
|
|
hamlet_api_keys = get_all_api_keys(self.example_user('hamlet'))
|
|
|
|
self.assertIn(otp_decrypt_api_key(encrypted_api_key, mobile_flow_otp), hamlet_api_keys)
|
2018-05-18 03:27:47 +02:00
|
|
|
self.assertEqual(len(mail.outbox), 1)
|
|
|
|
self.assertIn('Zulip on Android', mail.outbox[0].body)
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_registration_existing_account(self) -> None:
|
2018-05-31 00:12:39 +02:00
|
|
|
"""If the user already exists, signup flow just logs them in"""
|
|
|
|
email = "hamlet@zulip.com"
|
|
|
|
name = 'Full Name'
|
2019-02-03 09:18:01 +01:00
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zulip', is_signup='1')
|
2018-05-31 00:12:39 +02:00
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
|
|
|
self.assertEqual(data['name'], 'Full Name')
|
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
|
|
|
parsed_url.path)
|
|
|
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
# Name wasn't changed at all
|
|
|
|
self.assertEqual(hamlet.full_name, "King Hamlet")
|
|
|
|
|
2019-11-01 00:33:56 +01:00
|
|
|
def stage_two_of_registration(self, result: HttpResponse, realm: Realm, subdomain: str,
|
|
|
|
email: str, name: str, expected_final_name: str,
|
|
|
|
skip_registration_form: bool) -> None:
|
2018-05-18 03:27:47 +02:00
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], email)
|
|
|
|
self.assertEqual(data['name'], name)
|
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
|
|
|
parsed_url.path)
|
|
|
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
|
|
|
|
|
|
|
result = self.client_get(result.url)
|
|
|
|
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-11-01 00:23:05 +01:00
|
|
|
confirmation = Confirmation.objects.all().last()
|
2018-05-18 03:27:47 +02:00
|
|
|
confirmation_key = confirmation.confirmation_key
|
|
|
|
self.assertIn('do_confirm/' + confirmation_key, result.url)
|
|
|
|
result = self.client_get(result.url)
|
|
|
|
self.assert_in_response('action="/accounts/register/"', result)
|
|
|
|
data = {"from_confirmation": "1",
|
|
|
|
"key": confirmation_key}
|
|
|
|
result = self.client_post('/accounts/register/', data)
|
2019-11-01 00:33:56 +01:00
|
|
|
if not skip_registration_form:
|
2019-11-01 00:00:36 +01:00
|
|
|
self.assert_in_response("We just need you to do one last thing", result)
|
|
|
|
|
|
|
|
# Verify that the user is asked for name but not password
|
|
|
|
self.assert_not_in_success_response(['id_password'], result)
|
|
|
|
self.assert_in_success_response(['id_full_name'], result)
|
|
|
|
# Verify the name field gets correctly pre-populated:
|
2019-11-01 00:23:05 +01:00
|
|
|
self.assert_in_success_response([expected_final_name], result)
|
2019-11-01 00:00:36 +01:00
|
|
|
|
|
|
|
# Click confirm registration button.
|
|
|
|
result = self.client_post(
|
|
|
|
'/accounts/register/',
|
2019-11-01 00:23:05 +01:00
|
|
|
{'full_name': expected_final_name,
|
2019-11-01 00:00:36 +01:00
|
|
|
'key': confirmation_key,
|
|
|
|
'terms': True})
|
2018-05-18 03:27:47 +02:00
|
|
|
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
user_profile = get_user(email, realm)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2019-11-01 00:23:05 +01:00
|
|
|
self.assertEqual(user_profile.full_name, expected_final_name)
|
|
|
|
|
auth: Use zxcvbn to ensure password strength on server side.
For a long time, we've been only doing the zxcvbn password strength
checks on the browser, which is helpful, but means users could through
hackery (or a bug in the frontend validation code) manage to set a
too-weak password. We fix this by running our password strength
validation on the backend as well, using python-zxcvbn.
In theory, a bug in python-zxcvbn could result in it producing a
different opinion than the frontend version; if so, it'd be a pretty
bad bug in the library, and hopefully we'd hear about it from users,
report upstream, and get it fixed that way. Alternatively, we can
switch to shelling out to node like we do for KaTeX.
Fixes #6880.
2019-11-18 08:11:03 +01:00
|
|
|
self.assertFalse(user_profile.has_usable_password())
|
|
|
|
|
2019-11-01 00:23:05 +01:00
|
|
|
@override_settings(TERMS_OF_SERVICE=None)
|
|
|
|
def test_social_auth_registration(self) -> None:
|
|
|
|
"""If the user doesn't exist yet, social auth can be used to register an account"""
|
|
|
|
email = "newuser@zulip.com"
|
|
|
|
name = 'Full Name'
|
2019-11-01 00:33:56 +01:00
|
|
|
subdomain = 'zulip'
|
2019-11-01 00:23:05 +01:00
|
|
|
realm = get_realm("zulip")
|
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
expect_choose_email_screen=True,
|
2019-11-01 00:33:56 +01:00
|
|
|
subdomain=subdomain, is_signup='1')
|
|
|
|
self.stage_two_of_registration(result, realm, subdomain, email, name, name,
|
|
|
|
self.BACKEND_CLASS.full_name_validated)
|
2018-05-18 03:27:47 +02:00
|
|
|
|
2019-12-10 18:45:36 +01:00
|
|
|
@override_settings(TERMS_OF_SERVICE=None)
|
|
|
|
def test_social_auth_registration_invitation_exists(self) -> None:
|
|
|
|
"""
|
|
|
|
This tests the registration flow in the case where an invitation for the user
|
|
|
|
was generated.
|
|
|
|
"""
|
|
|
|
email = "newuser@zulip.com"
|
|
|
|
name = 'Full Name'
|
|
|
|
subdomain = 'zulip'
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
|
|
|
|
iago = self.example_user("iago")
|
|
|
|
do_invite_users(iago, [email], [])
|
|
|
|
|
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
subdomain=subdomain, is_signup='1')
|
|
|
|
self.stage_two_of_registration(result, realm, subdomain, email, name, name,
|
|
|
|
self.BACKEND_CLASS.full_name_validated)
|
|
|
|
|
2019-11-01 00:00:36 +01:00
|
|
|
@override_settings(TERMS_OF_SERVICE=None)
|
2019-02-08 17:09:25 +01:00
|
|
|
def test_social_auth_registration_using_multiuse_invite(self) -> None:
|
|
|
|
"""If the user doesn't exist yet, social auth can be used to register an account"""
|
|
|
|
email = "newuser@zulip.com"
|
|
|
|
name = 'Full Name'
|
2019-11-01 00:33:56 +01:00
|
|
|
subdomain = 'zulip'
|
2019-02-08 17:09:25 +01:00
|
|
|
realm = get_realm("zulip")
|
|
|
|
realm.invite_required = True
|
|
|
|
realm.save()
|
|
|
|
|
|
|
|
stream_names = ["new_stream_1", "new_stream_2"]
|
|
|
|
streams = []
|
|
|
|
for stream_name in set(stream_names):
|
|
|
|
stream = ensure_stream(realm, stream_name)
|
|
|
|
streams.append(stream)
|
|
|
|
|
|
|
|
referrer = self.example_user("hamlet")
|
|
|
|
multiuse_obj = MultiuseInvite.objects.create(realm=realm, referred_by=referrer)
|
|
|
|
multiuse_obj.streams.set(streams)
|
|
|
|
create_confirmation_link(multiuse_obj, realm.host, Confirmation.MULTIUSE_INVITE)
|
|
|
|
multiuse_confirmation = Confirmation.objects.all().last()
|
|
|
|
multiuse_object_key = multiuse_confirmation.confirmation_key
|
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
|
|
|
|
# First, try to signup for closed realm without using an invitation
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-11-01 00:33:56 +01:00
|
|
|
subdomain=subdomain, is_signup='1')
|
2019-02-08 17:09:25 +01:00
|
|
|
result = self.client_get(result.url)
|
|
|
|
# Verify that we're unable to signup, since this is a closed realm
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_success_response(["Sign up"], result)
|
|
|
|
|
2019-11-01 00:33:56 +01:00
|
|
|
result = self.social_auth_test(account_data_dict, subdomain=subdomain, is_signup='1',
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-08 17:09:25 +01:00
|
|
|
multiuse_object_key=multiuse_object_key)
|
2019-11-01 00:33:56 +01:00
|
|
|
self.stage_two_of_registration(result, realm, subdomain, email, name, name,
|
|
|
|
self.BACKEND_CLASS.full_name_validated)
|
2019-02-08 17:09:25 +01:00
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_registration_without_is_signup(self) -> None:
|
|
|
|
"""If `is_signup` is not set then a new account isn't created"""
|
2018-05-31 02:27:43 +02:00
|
|
|
email = "newuser@zulip.com"
|
|
|
|
name = 'Full Name'
|
2019-02-03 09:18:01 +01:00
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zulip')
|
2018-05-31 02:27:43 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], email)
|
|
|
|
self.assertEqual(data['name'], name)
|
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
|
|
|
parsed_url.path)
|
|
|
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
|
|
|
|
|
|
|
result = self.client_get(result.url)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_response("No account found for newuser@zulip.com.", result)
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_registration_without_is_signup_closed_realm(self) -> None:
|
2018-05-31 00:12:39 +02:00
|
|
|
"""If the user doesn't exist yet in closed realm, give an error"""
|
|
|
|
email = "nonexisting@phantom.com"
|
|
|
|
name = 'Full Name'
|
2019-02-03 09:18:01 +01:00
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zulip')
|
2018-05-31 00:12:39 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], email)
|
|
|
|
self.assertEqual(data['name'], name)
|
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
|
|
|
parsed_url.path)
|
|
|
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
2018-05-18 03:27:47 +02:00
|
|
|
|
2018-05-31 00:12:39 +02:00
|
|
|
result = self.client_get(result.url)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_response('action="/register/"', result)
|
|
|
|
self.assert_in_response('Your email address, {}, is not '
|
|
|
|
'in one of the domains that are allowed to register '
|
|
|
|
'for accounts in this organization.'.format(email), result)
|
2018-05-18 03:27:47 +02:00
|
|
|
|
2019-11-01 00:33:56 +01:00
|
|
|
@override_settings(TERMS_OF_SERVICE=None)
|
|
|
|
def test_social_auth_with_ldap_populate_registration_from_confirmation(self) -> None:
|
|
|
|
self.init_default_ldap_database()
|
|
|
|
email = "newuser@zulip.com"
|
|
|
|
name = "Full Name"
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
subdomain = "zulip"
|
|
|
|
ldap_user_attr_map = {'full_name': 'cn'}
|
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
|
|
|
|
backend_path = 'zproject.backends.{}'.format(self.BACKEND_CLASS.__name__)
|
|
|
|
with self.settings(
|
|
|
|
POPULATE_PROFILE_VIA_LDAP=True,
|
|
|
|
LDAP_APPEND_DOMAIN='zulip.com',
|
|
|
|
AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map,
|
|
|
|
AUTHENTICATION_BACKENDS=(backend_path,
|
|
|
|
'zproject.backends.ZulipLDAPUserPopulator',
|
|
|
|
'zproject.backends.ZulipDummyBackend')
|
|
|
|
):
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
subdomain=subdomain, is_signup='1')
|
|
|
|
# Full name should get populated from ldap:
|
|
|
|
self.stage_two_of_registration(result, realm, subdomain, email, name, "New LDAP fullname",
|
|
|
|
skip_registration_form=True)
|
|
|
|
|
2019-11-09 07:01:05 +01:00
|
|
|
# Now try a user that doesn't exist in ldap:
|
|
|
|
email = self.nonreg_email("alice")
|
|
|
|
name = "Alice Social"
|
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
subdomain=subdomain, is_signup='1')
|
|
|
|
# Full name should get populated as provided by the social backend, because
|
|
|
|
# this user isn't in the ldap dictionary:
|
|
|
|
self.stage_two_of_registration(result, realm, subdomain, email, name, name,
|
|
|
|
skip_registration_form=self.BACKEND_CLASS.full_name_validated)
|
|
|
|
|
2019-11-21 14:48:49 +01:00
|
|
|
@override_settings(TERMS_OF_SERVICE=None)
|
|
|
|
def test_social_auth_with_ldap_auth_registration_from_confirmation(self) -> None:
|
|
|
|
"""
|
|
|
|
This test checks that in configurations that use the ldap authentication backend
|
|
|
|
and a social backend, it is possible to create non-ldap users via the social backend.
|
|
|
|
"""
|
|
|
|
self.init_default_ldap_database()
|
|
|
|
email = self.nonreg_email("alice")
|
|
|
|
name = "Alice Social"
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
subdomain = "zulip"
|
|
|
|
ldap_user_attr_map = {'full_name': 'cn'}
|
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
|
|
|
|
backend_path = 'zproject.backends.{}'.format(self.BACKEND_CLASS.__name__)
|
|
|
|
with self.settings(
|
|
|
|
POPULATE_PROFILE_VIA_LDAP=True,
|
|
|
|
LDAP_EMAIL_ATTR='mail',
|
|
|
|
AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map,
|
|
|
|
AUTHENTICATION_BACKENDS=(backend_path,
|
|
|
|
'zproject.backends.ZulipLDAPAuthBackend',
|
|
|
|
'zproject.backends.ZulipDummyBackend')
|
|
|
|
):
|
|
|
|
account_data_dict = self.get_account_data_dict(email=email, name=name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
subdomain=subdomain, is_signup='1')
|
|
|
|
# Full name should get populated as provided by the social backend, because
|
|
|
|
# this user isn't in the ldap dictionary:
|
|
|
|
self.stage_two_of_registration(result, realm, subdomain, email, name, name,
|
|
|
|
skip_registration_form=self.BACKEND_CLASS.full_name_validated)
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_complete(self) -> None:
|
2018-07-03 18:47:20 +02:00
|
|
|
with mock.patch('social_core.backends.oauth.BaseOAuth2.process_error',
|
|
|
|
side_effect=AuthFailed('Not found')):
|
2019-02-03 09:18:01 +01:00
|
|
|
result = self.client_get(reverse('social:complete', args=[self.backend.name]))
|
2018-07-03 18:47:20 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertIn('login', result.url)
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def test_social_auth_complete_when_base_exc_is_raised(self) -> None:
|
2018-07-03 18:47:20 +02:00
|
|
|
with mock.patch('social_core.backends.oauth.BaseOAuth2.auth_complete',
|
|
|
|
side_effect=AuthStateForbidden('State forbidden')), \
|
|
|
|
mock.patch('zproject.backends.logging.warning'):
|
2019-02-03 09:18:01 +01:00
|
|
|
result = self.client_get(reverse('social:complete', args=[self.backend.name]))
|
2018-07-03 18:47:20 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertIn('login', result.url)
|
|
|
|
|
2019-09-29 06:32:56 +02:00
|
|
|
class SAMLAuthBackendTest(SocialAuthBase):
|
|
|
|
__unittest_skip__ = False
|
|
|
|
|
|
|
|
BACKEND_CLASS = SAMLAuthBackend
|
2019-10-22 18:23:57 +02:00
|
|
|
LOGIN_URL = "/accounts/login/social/saml/test_idp"
|
|
|
|
SIGNUP_URL = "/accounts/register/social/saml/test_idp"
|
2019-09-29 06:32:56 +02:00
|
|
|
AUTHORIZATION_URL = "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO"
|
|
|
|
AUTH_FINISH_URL = "/complete/saml/"
|
|
|
|
CONFIG_ERROR_URL = "/config-error/saml"
|
|
|
|
|
|
|
|
# We have to define our own social_auth_test as the flow of SAML authentication
|
|
|
|
# is different from the other social backends.
|
|
|
|
def social_auth_test(self, account_data_dict: Dict[str, str],
|
|
|
|
*, subdomain: Optional[str]=None,
|
|
|
|
mobile_flow_otp: Optional[str]=None,
|
|
|
|
is_signup: Optional[str]=None,
|
|
|
|
next: str='',
|
|
|
|
multiuse_object_key: str='',
|
|
|
|
**extra_data: Any) -> HttpResponse:
|
|
|
|
url, headers = self.prepare_login_url_and_headers(
|
|
|
|
subdomain, mobile_flow_otp, is_signup, next, multiuse_object_key
|
|
|
|
)
|
|
|
|
|
|
|
|
result = self.client_get(url, **headers)
|
|
|
|
|
|
|
|
expected_result_url_prefix = 'http://testserver/login/%s/' % (self.backend.name,)
|
|
|
|
if settings.SOCIAL_AUTH_SUBDOMAIN is not None:
|
|
|
|
expected_result_url_prefix = (
|
|
|
|
'http://%s.testserver/login/%s/' % (settings.SOCIAL_AUTH_SUBDOMAIN, self.backend.name)
|
|
|
|
)
|
|
|
|
|
|
|
|
if result.status_code != 302 or not result.url.startswith(expected_result_url_prefix):
|
|
|
|
return result
|
|
|
|
|
|
|
|
result = self.client_get(result.url, **headers)
|
|
|
|
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
assert self.AUTHORIZATION_URL in result.url
|
|
|
|
assert "samlrequest" in result.url.lower()
|
|
|
|
|
|
|
|
self.client.cookies = result.cookies
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
relay_state = urllib.parse.parse_qs(parsed_url.query)['RelayState'][0]
|
|
|
|
# Make sure params are getting encoded into RelayState:
|
|
|
|
data = SAMLAuthBackend.get_data_from_redis(relay_state)
|
|
|
|
if next:
|
|
|
|
self.assertEqual(data['next'], next)
|
|
|
|
if is_signup:
|
|
|
|
self.assertEqual(data['is_signup'], is_signup)
|
|
|
|
|
|
|
|
saml_response = self.generate_saml_response(**account_data_dict)
|
|
|
|
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
|
|
|
# The mock below is necessary, so that python3-saml accepts our SAMLResponse,
|
|
|
|
# and doesn't verify the cryptographic signatures etc., since generating
|
|
|
|
# a perfectly valid SAMLResponse for the purpose of these tests would be too complex,
|
|
|
|
# and we simply use one loaded from a fixture file.
|
|
|
|
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
|
|
|
result = self.client_post(self.AUTH_FINISH_URL, post_params, **headers)
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
def generate_saml_response(self, email: str, name: str) -> str:
|
|
|
|
"""
|
|
|
|
The samlresponse.txt fixture has a pre-generated SAMLResponse,
|
|
|
|
with {email}, {first_name}, {last_name} placeholders, that can
|
|
|
|
be filled out with the data we want.
|
|
|
|
"""
|
|
|
|
name_parts = name.split(' ')
|
|
|
|
first_name = name_parts[0]
|
|
|
|
last_name = name_parts[1]
|
|
|
|
|
|
|
|
unencoded_saml_response = self.fixture_data("samlresponse.txt", type="saml").format(
|
|
|
|
email=email,
|
|
|
|
first_name=first_name,
|
|
|
|
last_name=last_name
|
|
|
|
)
|
|
|
|
# SAMLResponse needs to be base64-encoded.
|
|
|
|
saml_response = base64.b64encode(unencoded_saml_response.encode()).decode() # type: str
|
|
|
|
|
|
|
|
return saml_response
|
|
|
|
|
|
|
|
def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]:
|
|
|
|
return dict(email=email, name=name)
|
|
|
|
|
|
|
|
def test_social_auth_no_key(self) -> None:
|
|
|
|
"""
|
|
|
|
Since in the case of SAML there isn't a direct equivalent of CLIENT_KEY_SETTING,
|
|
|
|
we override this test, to test for the case where the obligatory
|
|
|
|
SOCIAL_AUTH_SAML_ENABLED_IDPS isn't configured.
|
|
|
|
"""
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
with self.settings(SOCIAL_AUTH_SAML_ENABLED_IDPS=None):
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip', next='/user_uploads/image')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, self.CONFIG_ERROR_URL)
|
|
|
|
|
2019-10-26 01:51:48 +02:00
|
|
|
# Test the signup path too:
|
|
|
|
result = self.social_auth_test(account_data_dict, is_signup='1',
|
|
|
|
subdomain='zulip', next='/user_uploads/image')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, self.CONFIG_ERROR_URL)
|
|
|
|
|
2019-09-29 06:32:56 +02:00
|
|
|
def test_saml_auth_works_without_private_public_keys(self) -> None:
|
|
|
|
with self.settings(SOCIAL_AUTH_SAML_SP_PUBLIC_CERT='', SOCIAL_AUTH_SAML_SP_PRIVATE_KEY=''):
|
|
|
|
self.test_social_auth_success()
|
|
|
|
|
|
|
|
def test_saml_auth_enabled(self) -> None:
|
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.SAMLAuthBackend',)):
|
|
|
|
self.assertTrue(saml_auth_enabled())
|
|
|
|
result = self.client_get("/saml/metadata.xml")
|
|
|
|
self.assert_in_success_response(
|
|
|
|
['entityID="{}"'.format(settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID)], result
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_social_auth_complete(self) -> None:
|
|
|
|
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
|
|
|
with mock.patch.object(OneLogin_Saml2_Auth, 'is_authenticated', return_value=False), \
|
|
|
|
mock.patch('zproject.backends.logging.info') as m:
|
|
|
|
# This mock causes AuthFailed to be raised.
|
|
|
|
saml_response = self.generate_saml_response(self.email, self.name)
|
|
|
|
relay_state = SAMLAuthBackend.put_data_in_redis({"idp": "test_idp"})
|
|
|
|
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
|
|
|
result = self.client_post('/complete/saml/', post_params)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertIn('login', result.url)
|
|
|
|
m.assert_called_with("Authentication failed: SAML login failed: [] (None)")
|
|
|
|
|
|
|
|
def test_social_auth_complete_when_base_exc_is_raised(self) -> None:
|
|
|
|
with mock.patch.object(OneLogin_Saml2_Response, 'is_valid', return_value=True):
|
|
|
|
with mock.patch('social_core.backends.saml.SAMLAuth.auth_complete',
|
|
|
|
side_effect=AuthStateForbidden('State forbidden')), \
|
|
|
|
mock.patch('zproject.backends.logging.warning') as m:
|
|
|
|
saml_response = self.generate_saml_response(self.email, self.name)
|
|
|
|
relay_state = SAMLAuthBackend.put_data_in_redis({"idp": "test_idp"})
|
|
|
|
post_params = {"SAMLResponse": saml_response, "RelayState": relay_state}
|
|
|
|
result = self.client_post('/complete/saml/', post_params)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertIn('login', result.url)
|
|
|
|
m.assert_called_with("Wrong state parameter given.")
|
|
|
|
|
|
|
|
def test_social_auth_complete_bad_params(self) -> None:
|
|
|
|
# Simple GET for /complete/saml without the required parameters.
|
|
|
|
# This tests the auth_complete wrapped in our SAMLAuthBackend,
|
|
|
|
# ensuring it prevents this requests from causing an internal server error.
|
|
|
|
with mock.patch('zproject.backends.logging.info') as m:
|
|
|
|
result = self.client_get('/complete/saml/')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertIn('login', result.url)
|
|
|
|
m.assert_called_with("SAML authentication failed: missing RelayState.")
|
|
|
|
|
|
|
|
# Check that POSTing the RelayState, but with missing SAMLResponse,
|
|
|
|
# doesn't cause errors either:
|
|
|
|
with mock.patch('zproject.backends.logging.info') as m:
|
|
|
|
relay_state = SAMLAuthBackend.put_data_in_redis({"idp": "test_idp"})
|
|
|
|
post_params = {"RelayState": relay_state}
|
|
|
|
result = self.client_post('/complete/saml/', post_params)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertIn('login', result.url)
|
|
|
|
m.assert_called_with(
|
|
|
|
# OneLogin_Saml2_Error exception:
|
|
|
|
"SAML Response not found, Only supported HTTP_POST Binding"
|
|
|
|
)
|
|
|
|
|
|
|
|
with mock.patch('zproject.backends.logging.info') as m:
|
|
|
|
relay_state = SAMLAuthBackend.put_data_in_redis({"idp": "test_idp"})
|
|
|
|
relay_state = relay_state[:-1] # Break the token by removing the last character
|
|
|
|
post_params = {"RelayState": relay_state}
|
|
|
|
result = self.client_post('/complete/saml/', post_params)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertIn('login', result.url)
|
|
|
|
m.assert_called_with("SAML authentication failed: bad RelayState token.")
|
|
|
|
|
|
|
|
def test_social_auth_saml_bad_idp_param_on_login_page(self) -> None:
|
|
|
|
with mock.patch('zproject.backends.logging.info') as m:
|
|
|
|
result = self.client_get('/login/saml/')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual('/login/', result.url)
|
|
|
|
m.assert_called_with("/login/saml/ : Bad idp param.")
|
|
|
|
|
|
|
|
with mock.patch('zproject.backends.logging.info') as m:
|
|
|
|
result = self.client_get('/login/saml/?idp=bad_idp')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual('/login/', result.url)
|
|
|
|
m.assert_called_with("/login/saml/ : Bad idp param.")
|
|
|
|
|
|
|
|
def test_social_auth_invalid_email(self) -> None:
|
|
|
|
"""
|
|
|
|
This test needs an override from the original class. For security reasons,
|
|
|
|
the 'next' and 'mobile_flow_otp' params don't get passed on in the session
|
|
|
|
if the authentication attempt failed. See SAMLAuthBackend.auth_complete for details.
|
|
|
|
"""
|
|
|
|
account_data_dict = self.get_account_data_dict(email="invalid", name=self.name)
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
subdomain='zulip', next='/user_uploads/image')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/login/")
|
|
|
|
|
|
|
|
def test_social_auth_saml_multiple_idps_configured(self) -> None:
|
|
|
|
"""
|
|
|
|
Using multiple IdPs is not supported right now, and having multiple configured
|
|
|
|
should lead to misconfiguration page.
|
|
|
|
"""
|
|
|
|
|
2019-10-22 18:23:57 +02:00
|
|
|
# Setup a new SOCIAL_AUTH_SAML_ENABLED_IDPS dict with two idps.
|
|
|
|
# We deepcopy() dictionaries around for the sake of brevity,
|
|
|
|
# to avoid having to spell them out explicitly here.
|
|
|
|
# The second idp's configuration is a copy of the first one,
|
|
|
|
# with name test_idp2 and altered url.
|
|
|
|
idps_dict = copy.deepcopy(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS)
|
|
|
|
idps_dict['test_idp2'] = copy.deepcopy(idps_dict['test_idp'])
|
|
|
|
idps_dict['test_idp2']['url'] = 'https://idp2.example.com/idp/profile/SAML2/Redirect/SSO'
|
|
|
|
idps_dict['test_idp2']['display_name'] = 'Second Test IdP'
|
|
|
|
|
|
|
|
# Run tests with multiple idps configured:
|
|
|
|
with self.settings(SOCIAL_AUTH_SAML_ENABLED_IDPS=idps_dict):
|
|
|
|
# Go to the login page and check that buttons to log in show up for both IdPs:
|
|
|
|
result = self.client_get('/accounts/login/')
|
|
|
|
self.assert_in_success_response(["Log in with Test IdP"], result)
|
|
|
|
self.assert_in_success_response(["/accounts/login/social/saml/test_idp"], result)
|
|
|
|
self.assert_in_success_response(["Log in with Second Test IdP"], result)
|
|
|
|
self.assert_in_success_response(["/accounts/login/social/saml/test_idp2"], result)
|
|
|
|
|
|
|
|
# Try succesful authentication with the regular idp from all previous tests:
|
|
|
|
self.test_social_auth_success()
|
|
|
|
|
|
|
|
# Now test with the second idp:
|
|
|
|
original_LOGIN_URL = self.LOGIN_URL
|
|
|
|
original_SIGNUP_URL = self.SIGNUP_URL
|
|
|
|
original_AUTHORIZATION_URL = self.AUTHORIZATION_URL
|
|
|
|
self.LOGIN_URL = "/accounts/login/social/saml/test_idp2"
|
|
|
|
self.SIGNUP_URL = "/accounts/register/social/saml/test_idp2"
|
|
|
|
self.AUTHORIZATION_URL = idps_dict['test_idp2']['url']
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.test_social_auth_success()
|
|
|
|
finally:
|
|
|
|
# Restore original values at the end, regardless of what happens
|
|
|
|
# in the block above, to avoid affecting other tests in unpredictable
|
|
|
|
# ways.
|
|
|
|
self.LOGIN_URL = original_LOGIN_URL
|
|
|
|
self.SIGNUP_URL = original_SIGNUP_URL
|
|
|
|
self.AUTHORIZATION_URL = original_AUTHORIZATION_URL
|
|
|
|
|
|
|
|
def test_social_auth_saml_login_bad_idp_arg(self) -> None:
|
|
|
|
for action in ['login', 'register']:
|
|
|
|
result = self.client_get('/accounts/{}/social/saml'.format(action))
|
|
|
|
# Missing idp argument.
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, '/config-error/saml')
|
|
|
|
|
|
|
|
result = self.client_get('/accounts/{}/social/saml/nonexistent_idp'.format(action))
|
|
|
|
# No such IdP is configured.
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, '/config-error/saml')
|
|
|
|
|
|
|
|
result = self.client_get('/accounts/{}/social/saml/'.format(action))
|
|
|
|
# No matching url pattern.
|
|
|
|
self.assertEqual(result.status_code, 404)
|
2019-09-29 06:32:56 +02:00
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
class GitHubAuthBackendTest(SocialAuthBase):
|
|
|
|
__unittest_skip__ = False
|
|
|
|
|
|
|
|
BACKEND_CLASS = GitHubAuthBackend
|
|
|
|
CLIENT_KEY_SETTING = "SOCIAL_AUTH_GITHUB_KEY"
|
|
|
|
LOGIN_URL = "/accounts/login/social/github"
|
|
|
|
SIGNUP_URL = "/accounts/register/social/github"
|
|
|
|
AUTHORIZATION_URL = "https://github.com/login/oauth/authorize"
|
|
|
|
ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
|
|
USER_INFO_URL = "https://api.github.com/user"
|
|
|
|
AUTH_FINISH_URL = "/complete/github/"
|
|
|
|
CONFIG_ERROR_URL = "/config-error/github"
|
|
|
|
|
2019-07-22 04:26:47 +02:00
|
|
|
def register_extra_endpoints(self,
|
|
|
|
account_data_dict: Dict[str, str],
|
|
|
|
**extra_data: Any) -> None:
|
2019-02-03 09:18:01 +01:00
|
|
|
# Keeping a verified email before the primary email makes sure
|
|
|
|
# get_verified_emails puts the primary email at the start of the
|
|
|
|
# email list returned as social_associate_user_helper assumes the
|
|
|
|
# first email as the primary email.
|
|
|
|
email_data = [
|
|
|
|
dict(email="notprimary@example.com",
|
|
|
|
verified=True),
|
|
|
|
dict(email=account_data_dict["email"],
|
|
|
|
verified=True,
|
|
|
|
primary=True),
|
|
|
|
dict(email="ignored@example.com",
|
|
|
|
verified=False),
|
|
|
|
]
|
|
|
|
email_data = extra_data.get("email_data", email_data)
|
|
|
|
|
|
|
|
httpretty.register_uri(
|
|
|
|
httpretty.GET,
|
|
|
|
"https://api.github.com/user/emails",
|
|
|
|
status=200,
|
|
|
|
body=json.dumps(email_data)
|
|
|
|
)
|
|
|
|
|
2018-07-18 23:45:49 +02:00
|
|
|
httpretty.register_uri(
|
|
|
|
httpretty.GET,
|
|
|
|
"https://api.github.com/teams/zulip-webapp/members/None",
|
|
|
|
status=200,
|
|
|
|
body=json.dumps(email_data)
|
|
|
|
)
|
|
|
|
|
|
|
|
self.email_data = email_data
|
|
|
|
|
2019-02-03 09:18:01 +01:00
|
|
|
def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]:
|
|
|
|
return dict(email=email, name=name)
|
|
|
|
|
|
|
|
def test_social_auth_email_not_verified(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
email_data = [
|
|
|
|
dict(email=account_data_dict["email"],
|
|
|
|
verified=False,
|
|
|
|
primary=True),
|
|
|
|
]
|
|
|
|
with mock.patch('logging.warning') as mock_warning:
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip',
|
|
|
|
email_data=email_data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/login/")
|
|
|
|
mock_warning.assert_called_once_with("Social auth (GitHub) failed "
|
|
|
|
"because user has no verified emails")
|
|
|
|
|
|
|
|
@override_settings(SOCIAL_AUTH_GITHUB_TEAM_ID='zulip-webapp')
|
|
|
|
def test_social_auth_github_team_not_member_failed(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
with mock.patch('social_core.backends.github.GithubTeamOAuth2.user_data',
|
|
|
|
side_effect=AuthFailed('Not found')), \
|
|
|
|
mock.patch('logging.info') as mock_info:
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/login/")
|
|
|
|
mock_info.assert_called_once_with("GitHub user is not member of required team")
|
|
|
|
|
|
|
|
@override_settings(SOCIAL_AUTH_GITHUB_TEAM_ID='zulip-webapp')
|
|
|
|
def test_social_auth_github_team_member_success(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
with mock.patch('social_core.backends.github.GithubTeamOAuth2.user_data',
|
|
|
|
return_value=account_data_dict):
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zulip')
|
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
2019-09-29 00:55:10 +02:00
|
|
|
self.assertEqual(data['name'], self.name)
|
2019-02-03 09:18:01 +01:00
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
|
|
|
|
@override_settings(SOCIAL_AUTH_GITHUB_ORG_NAME='Zulip')
|
|
|
|
def test_social_auth_github_organization_not_member_failed(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
with mock.patch('social_core.backends.github.GithubOrganizationOAuth2.user_data',
|
|
|
|
side_effect=AuthFailed('Not found')), \
|
|
|
|
mock.patch('logging.info') as mock_info:
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/login/")
|
|
|
|
mock_info.assert_called_once_with("GitHub user is not member of required organization")
|
|
|
|
|
|
|
|
@override_settings(SOCIAL_AUTH_GITHUB_ORG_NAME='Zulip')
|
|
|
|
def test_social_auth_github_organization_member_success(self) -> None:
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name=self.name)
|
|
|
|
with mock.patch('social_core.backends.github.GithubOrganizationOAuth2.user_data',
|
|
|
|
return_value=account_data_dict):
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
2018-07-18 23:45:49 +02:00
|
|
|
expect_choose_email_screen=True,
|
2019-02-03 09:18:01 +01:00
|
|
|
subdomain='zulip')
|
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
2019-09-29 00:55:10 +02:00
|
|
|
self.assertEqual(data['name'], self.name)
|
2019-02-03 09:18:01 +01:00
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
|
2018-05-31 00:12:39 +02:00
|
|
|
def test_github_auth_enabled(self) -> None:
|
2016-11-01 23:50:35 +01:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.GitHubAuthBackend',)):
|
2018-05-31 00:12:39 +02:00
|
|
|
self.assertTrue(github_auth_enabled())
|
2017-03-07 08:32:40 +01:00
|
|
|
|
2019-07-22 04:26:47 +02:00
|
|
|
def test_github_oauth2_success_non_primary(self) -> None:
|
|
|
|
account_data_dict = dict(email='nonprimary@zulip.com', name="Non Primary")
|
|
|
|
email_data = [
|
|
|
|
dict(email=account_data_dict["email"],
|
|
|
|
verified=True),
|
|
|
|
dict(email='hamlet@zulip.com',
|
|
|
|
verified=True,
|
|
|
|
primary=True),
|
|
|
|
dict(email="ignored@example.com",
|
|
|
|
verified=False),
|
|
|
|
]
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip', email_data=email_data,
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
next='/user_uploads/image')
|
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], 'nonprimary@zulip.com')
|
|
|
|
self.assertEqual(data['name'], 'Non Primary')
|
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
self.assertEqual(data['next'], '/user_uploads/image')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
|
|
|
parsed_url.path)
|
|
|
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
|
|
|
|
|
|
|
def test_github_oauth2_success_single_email(self) -> None:
|
|
|
|
# If the user has a single email associated with its GitHub account,
|
|
|
|
# the choose email screen should not be shown and the first email
|
|
|
|
# should be used for user's signup/login.
|
|
|
|
account_data_dict = dict(email='not-hamlet@zulip.com', name=self.name)
|
|
|
|
email_data = [
|
|
|
|
dict(email='hamlet@zulip.com',
|
|
|
|
verified=True,
|
|
|
|
primary=True),
|
|
|
|
]
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip',
|
|
|
|
email_data=email_data,
|
|
|
|
expect_choose_email_screen=False,
|
|
|
|
next='/user_uploads/image')
|
|
|
|
data = load_subdomain_token(result)
|
|
|
|
self.assertEqual(data['email'], self.example_email("hamlet"))
|
2019-09-29 00:55:10 +02:00
|
|
|
self.assertEqual(data['name'], self.name)
|
2019-07-22 04:26:47 +02:00
|
|
|
self.assertEqual(data['subdomain'], 'zulip')
|
|
|
|
self.assertEqual(data['next'], '/user_uploads/image')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
parsed_url = urllib.parse.urlparse(result.url)
|
|
|
|
uri = "{}://{}{}".format(parsed_url.scheme, parsed_url.netloc,
|
|
|
|
parsed_url.path)
|
|
|
|
self.assertTrue(uri.startswith('http://zulip.testserver/accounts/login/subdomain/'))
|
|
|
|
|
|
|
|
def test_github_oauth2_email_no_reply_dot_github_dot_com(self) -> None:
|
|
|
|
# As emails ending with `noreply.github.com` are excluded from
|
|
|
|
# verified_emails, choosing it as an email should raise a `email
|
|
|
|
# not associated` warning.
|
2019-08-05 15:15:56 +02:00
|
|
|
account_data_dict = dict(email="hamlet@users.noreply.github.com", name=self.name)
|
2019-07-22 04:26:47 +02:00
|
|
|
email_data = [
|
|
|
|
dict(email="notprimary@zulip.com",
|
|
|
|
verified=True),
|
|
|
|
dict(email="hamlet@zulip.com",
|
|
|
|
verified=True,
|
|
|
|
primary=True),
|
|
|
|
dict(email=account_data_dict["email"],
|
|
|
|
verified=True),
|
|
|
|
]
|
|
|
|
with mock.patch('logging.warning') as mock_warning:
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip',
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
email_data=email_data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/login/")
|
|
|
|
mock_warning.assert_called_once_with("Social auth (GitHub) failed because user has no verified"
|
|
|
|
" emails associated with the account")
|
|
|
|
|
|
|
|
def test_github_oauth2_email_not_associated(self) -> None:
|
|
|
|
account_data_dict = dict(email='not-associated@zulip.com', name=self.name)
|
|
|
|
email_data = [
|
|
|
|
dict(email='nonprimary@zulip.com',
|
|
|
|
verified=True,),
|
|
|
|
dict(email='hamlet@zulip.com',
|
|
|
|
verified=True,
|
|
|
|
primary=True),
|
|
|
|
]
|
|
|
|
with mock.patch('logging.warning') as mock_warning:
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip',
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
email_data=email_data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/login/")
|
|
|
|
mock_warning.assert_called_once_with("Social auth (GitHub) failed because user has no verified"
|
|
|
|
" emails associated with the account")
|
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
class GoogleAuthBackendTest(SocialAuthBase):
|
|
|
|
__unittest_skip__ = False
|
2016-10-17 14:28:23 +02:00
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
BACKEND_CLASS = GoogleAuthBackend
|
|
|
|
CLIENT_KEY_SETTING = "SOCIAL_AUTH_GOOGLE_KEY"
|
|
|
|
LOGIN_URL = "/accounts/login/social/google"
|
|
|
|
SIGNUP_URL = "/accounts/register/social/google"
|
|
|
|
AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/auth"
|
|
|
|
ACCESS_TOKEN_URL = "https://accounts.google.com/o/oauth2/token"
|
|
|
|
USER_INFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
|
|
|
|
AUTH_FINISH_URL = "/complete/google/"
|
|
|
|
CONFIG_ERROR_URL = "/config-error/google"
|
2017-09-16 19:34:59 +02:00
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]:
|
|
|
|
return dict(email=email, name=name, email_verified=True)
|
2018-08-10 00:58:44 +02:00
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
def test_social_auth_email_not_verified(self) -> None:
|
|
|
|
account_data_dict = dict(email=self.email, name=self.name)
|
|
|
|
with mock.patch('logging.warning') as mock_warning:
|
|
|
|
result = self.social_auth_test(account_data_dict,
|
|
|
|
subdomain='zulip')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "/login/")
|
|
|
|
mock_warning.assert_called_once_with("Social auth (Google) failed "
|
|
|
|
"because user has no verified emails")
|
2017-03-19 20:01:01 +01:00
|
|
|
|
2019-08-27 05:51:04 +02:00
|
|
|
def test_social_auth_mobile_success_legacy_url(self) -> None:
|
|
|
|
mobile_flow_otp = '1234abcd' * 8
|
|
|
|
account_data_dict = self.get_account_data_dict(email=self.email, name='Full Name')
|
|
|
|
self.assertEqual(len(mail.outbox), 0)
|
|
|
|
self.user_profile.date_joined = timezone_now() - datetime.timedelta(seconds=JUST_CREATED_THRESHOLD + 1)
|
|
|
|
self.user_profile.save()
|
|
|
|
|
|
|
|
with self.settings(SEND_LOGIN_EMAILS=True):
|
|
|
|
# Verify that the right thing happens with an invalid-format OTP
|
|
|
|
result = self.social_auth_test(account_data_dict, subdomain='zulip',
|
|
|
|
alternative_start_url="/accounts/login/google/",
|
|
|
|
mobile_flow_otp="1234")
|
|
|
|
self.assert_json_error(result, "Invalid OTP")
|
|
|
|
result = self.social_auth_test(account_data_dict, subdomain='zulip',
|
|
|
|
alternative_start_url="/accounts/login/google/",
|
|
|
|
mobile_flow_otp="invalido" * 8)
|
|
|
|
self.assert_json_error(result, "Invalid OTP")
|
|
|
|
|
|
|
|
# Now do it correctly
|
|
|
|
result = self.social_auth_test(account_data_dict, subdomain='zulip',
|
|
|
|
expect_choose_email_screen=True,
|
|
|
|
alternative_start_url="/accounts/login/google/",
|
|
|
|
mobile_flow_otp=mobile_flow_otp)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
redirect_url = result['Location']
|
|
|
|
parsed_url = urllib.parse.urlparse(redirect_url)
|
|
|
|
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
|
|
self.assertEqual(parsed_url.scheme, 'zulip')
|
|
|
|
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
|
|
|
self.assertEqual(query_params["email"], [self.example_email("hamlet")])
|
|
|
|
encrypted_api_key = query_params["otp_encrypted_api_key"][0]
|
|
|
|
hamlet_api_keys = get_all_api_keys(self.example_user('hamlet'))
|
|
|
|
self.assertIn(otp_decrypt_api_key(encrypted_api_key, mobile_flow_otp), hamlet_api_keys)
|
|
|
|
self.assertEqual(len(mail.outbox), 1)
|
|
|
|
self.assertIn('Zulip on Android', mail.outbox[0].body)
|
|
|
|
|
2019-02-02 16:51:26 +01:00
|
|
|
def test_google_auth_enabled(self) -> None:
|
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.GoogleAuthBackend',)):
|
|
|
|
self.assertTrue(google_auth_enabled())
|
2017-03-19 20:01:01 +01:00
|
|
|
|
2017-11-19 04:02:03 +01:00
|
|
|
def get_log_into_subdomain(self, data: Dict[str, Any], *, key: Optional[str]=None, subdomain: str='zulip') -> HttpResponse:
|
2017-10-27 02:45:38 +02:00
|
|
|
token = signing.dumps(data, salt=_subdomain_token_salt, key=key)
|
|
|
|
url_path = reverse('zerver.views.auth.log_into_subdomain', args=[token])
|
|
|
|
return self.client_get(url_path, subdomain=subdomain)
|
2017-10-27 02:41:54 +02:00
|
|
|
|
2018-03-12 12:54:50 +01:00
|
|
|
def test_redirect_to_next_url_for_log_into_subdomain(self) -> None:
|
2018-05-11 01:39:38 +02:00
|
|
|
def test_redirect_to_next_url(next: str='') -> HttpResponse:
|
2018-03-12 12:54:50 +01:00
|
|
|
data = {'name': 'Hamlet',
|
|
|
|
'email': self.example_email("hamlet"),
|
|
|
|
'subdomain': 'zulip',
|
|
|
|
'is_signup': False,
|
|
|
|
'next': next}
|
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
with mock.patch(
|
|
|
|
'zerver.views.auth.authenticate_remote_user',
|
2019-05-05 00:40:30 +02:00
|
|
|
return_value=user_profile):
|
2018-03-12 12:54:50 +01:00
|
|
|
with mock.patch('zerver.views.auth.do_login'):
|
|
|
|
result = self.get_log_into_subdomain(data)
|
|
|
|
return result
|
|
|
|
|
|
|
|
res = test_redirect_to_next_url()
|
|
|
|
self.assertEqual(res.status_code, 302)
|
|
|
|
self.assertEqual(res.url, 'http://zulip.testserver')
|
|
|
|
res = test_redirect_to_next_url('/user_uploads/path_to_image')
|
|
|
|
self.assertEqual(res.status_code, 302)
|
|
|
|
self.assertEqual(res.url, 'http://zulip.testserver/user_uploads/path_to_image')
|
|
|
|
|
2018-03-12 14:08:12 +01:00
|
|
|
res = test_redirect_to_next_url('/#narrow/stream/7-test-here')
|
|
|
|
self.assertEqual(res.status_code, 302)
|
|
|
|
self.assertEqual(res.url, 'http://zulip.testserver/#narrow/stream/7-test-here')
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_log_into_subdomain_when_signature_is_bad(self) -> None:
|
2017-10-27 02:41:54 +02:00
|
|
|
data = {'name': 'Full Name',
|
|
|
|
'email': self.example_email("hamlet"),
|
|
|
|
'subdomain': 'zulip',
|
2018-03-10 14:13:30 +01:00
|
|
|
'is_signup': False,
|
|
|
|
'next': ''}
|
2017-10-28 00:54:46 +02:00
|
|
|
with mock.patch('logging.warning') as mock_warning:
|
|
|
|
result = self.get_log_into_subdomain(data, key='nonsense')
|
|
|
|
mock_warning.assert_called_with("Subdomain cookie: Bad signature.")
|
2017-10-27 02:45:38 +02:00
|
|
|
self.assertEqual(result.status_code, 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_log_into_subdomain_when_signature_is_expired(self) -> None:
|
2017-10-27 02:45:38 +02:00
|
|
|
data = {'name': 'Full Name',
|
|
|
|
'email': self.example_email("hamlet"),
|
|
|
|
'subdomain': 'zulip',
|
2018-03-10 14:13:30 +01:00
|
|
|
'is_signup': False,
|
|
|
|
'next': ''}
|
2017-10-27 02:45:38 +02:00
|
|
|
with mock.patch('django.core.signing.time.time', return_value=time.time() - 45):
|
|
|
|
token = signing.dumps(data, salt=_subdomain_token_salt)
|
|
|
|
url_path = reverse('zerver.views.auth.log_into_subdomain', args=[token])
|
2017-10-28 00:54:46 +02:00
|
|
|
with mock.patch('logging.warning') as mock_warning:
|
|
|
|
result = self.client_get(url_path, subdomain='zulip')
|
|
|
|
mock_warning.assert_called_once()
|
2017-10-27 02:41:54 +02:00
|
|
|
self.assertEqual(result.status_code, 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_log_into_subdomain_when_is_signup_is_true(self) -> None:
|
2017-04-20 08:25:15 +02:00
|
|
|
data = {'name': 'Full Name',
|
2017-05-25 01:40:26 +02:00
|
|
|
'email': self.example_email("hamlet"),
|
2017-04-20 08:25:15 +02:00
|
|
|
'subdomain': 'zulip',
|
2018-03-10 14:13:30 +01:00
|
|
|
'is_signup': True,
|
|
|
|
'next': ''}
|
2017-10-27 02:41:54 +02:00
|
|
|
result = self.get_log_into_subdomain(data)
|
2017-09-27 07:01:41 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_response('hamlet@zulip.com already has an account', result)
|
2017-04-20 08:25:15 +02:00
|
|
|
|
2018-04-23 00:12:52 +02:00
|
|
|
def test_log_into_subdomain_when_is_signup_is_true_and_new_user(self) -> None:
|
2017-04-20 08:25:15 +02:00
|
|
|
data = {'name': 'New User Name',
|
|
|
|
'email': 'new@zulip.com',
|
|
|
|
'subdomain': 'zulip',
|
2018-03-10 14:13:30 +01:00
|
|
|
'is_signup': True,
|
|
|
|
'next': ''}
|
2017-10-27 02:41:54 +02:00
|
|
|
result = self.get_log_into_subdomain(data)
|
2017-09-27 07:01:41 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
confirmation = Confirmation.objects.all().first()
|
|
|
|
confirmation_key = confirmation.confirmation_key
|
|
|
|
self.assertIn('do_confirm/' + confirmation_key, result.url)
|
|
|
|
result = self.client_get(result.url)
|
|
|
|
self.assert_in_response('action="/accounts/register/"', result)
|
|
|
|
data = {"from_confirmation": "1",
|
|
|
|
"full_name": data['name'],
|
|
|
|
"key": confirmation_key}
|
|
|
|
result = self.client_post('/accounts/register/', data, subdomain="zulip")
|
2018-06-26 00:33:05 +02:00
|
|
|
self.assert_in_response("We just need you to do one last thing", result)
|
2017-09-27 07:01:41 +02:00
|
|
|
|
|
|
|
# Verify that the user is asked for name but not password
|
|
|
|
self.assert_not_in_success_response(['id_password'], result)
|
|
|
|
self.assert_in_success_response(['id_full_name'], result)
|
2017-08-09 22:09:38 +02:00
|
|
|
|
2018-04-23 00:12:52 +02:00
|
|
|
def test_log_into_subdomain_when_is_signup_is_false_and_new_user(self) -> None:
|
|
|
|
data = {'name': 'New User Name',
|
|
|
|
'email': 'new@zulip.com',
|
|
|
|
'subdomain': 'zulip',
|
|
|
|
'is_signup': False,
|
|
|
|
'next': ''}
|
|
|
|
result = self.get_log_into_subdomain(data)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_response('No account found for', result)
|
2018-04-25 02:59:20 +02:00
|
|
|
self.assert_in_response('new@zulip.com.', result)
|
2018-04-23 00:12:52 +02:00
|
|
|
self.assert_in_response('action="http://zulip.testserver/accounts/do_confirm/', result)
|
|
|
|
|
|
|
|
url = re.findall('action="(http://zulip.testserver/accounts/do_confirm[^"]*)"', result.content.decode('utf-8'))[0]
|
|
|
|
confirmation = Confirmation.objects.all().first()
|
|
|
|
confirmation_key = confirmation.confirmation_key
|
|
|
|
self.assertIn('do_confirm/' + confirmation_key, url)
|
|
|
|
result = self.client_get(url)
|
|
|
|
self.assert_in_response('action="/accounts/register/"', result)
|
|
|
|
data = {"from_confirmation": "1",
|
|
|
|
"full_name": data['name'],
|
|
|
|
"key": confirmation_key}
|
|
|
|
result = self.client_post('/accounts/register/', data, subdomain="zulip")
|
2018-06-26 00:33:05 +02:00
|
|
|
self.assert_in_response("We just need you to do one last thing", result)
|
2018-04-23 00:12:52 +02:00
|
|
|
|
|
|
|
# Verify that the user is asked for name but not password
|
|
|
|
self.assert_not_in_success_response(['id_password'], result)
|
|
|
|
self.assert_in_success_response(['id_full_name'], result)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_log_into_subdomain_when_using_invite_link(self) -> None:
|
2017-09-27 03:34:58 +02:00
|
|
|
data = {'name': 'New User Name',
|
|
|
|
'email': 'new@zulip.com',
|
|
|
|
'subdomain': 'zulip',
|
2018-03-10 14:13:30 +01:00
|
|
|
'is_signup': True,
|
|
|
|
'next': ''}
|
2017-09-27 03:34:58 +02:00
|
|
|
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
realm.invite_required = True
|
|
|
|
realm.save()
|
|
|
|
|
|
|
|
stream_names = ["new_stream_1", "new_stream_2"]
|
|
|
|
streams = []
|
|
|
|
for stream_name in set(stream_names):
|
2018-03-21 22:05:21 +01:00
|
|
|
stream = ensure_stream(realm, stream_name)
|
2017-09-27 03:34:58 +02:00
|
|
|
streams.append(stream)
|
|
|
|
|
|
|
|
# Without the invite link, we can't create an account due to invite_required
|
2017-10-27 02:41:54 +02:00
|
|
|
result = self.get_log_into_subdomain(data)
|
2017-09-27 03:34:58 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_success_response(['Sign up for Zulip'], result)
|
|
|
|
|
|
|
|
# Now confirm an invitation link works
|
|
|
|
referrer = self.example_user("hamlet")
|
|
|
|
multiuse_obj = MultiuseInvite.objects.create(realm=realm, referred_by=referrer)
|
2018-01-31 08:22:07 +01:00
|
|
|
multiuse_obj.streams.set(streams)
|
2019-02-08 17:09:25 +01:00
|
|
|
create_confirmation_link(multiuse_obj, realm.host, Confirmation.MULTIUSE_INVITE)
|
|
|
|
multiuse_confirmation = Confirmation.objects.all().last()
|
|
|
|
multiuse_object_key = multiuse_confirmation.confirmation_key
|
2017-09-27 03:34:58 +02:00
|
|
|
|
2019-02-08 17:09:25 +01:00
|
|
|
data["multiuse_object_key"] = multiuse_object_key
|
2017-10-27 02:45:38 +02:00
|
|
|
result = self.get_log_into_subdomain(data)
|
2017-09-27 03:34:58 +02:00
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
|
|
|
|
confirmation = Confirmation.objects.all().last()
|
|
|
|
confirmation_key = confirmation.confirmation_key
|
|
|
|
self.assertIn('do_confirm/' + confirmation_key, result.url)
|
|
|
|
result = self.client_get(result.url)
|
|
|
|
self.assert_in_response('action="/accounts/register/"', result)
|
|
|
|
data2 = {"from_confirmation": "1",
|
|
|
|
"full_name": data['name'],
|
|
|
|
"key": confirmation_key}
|
|
|
|
result = self.client_post('/accounts/register/', data2, subdomain="zulip")
|
2018-06-26 00:33:05 +02:00
|
|
|
self.assert_in_response("We just need you to do one last thing", result)
|
2017-09-27 03:34:58 +02:00
|
|
|
|
|
|
|
# Verify that the user is asked for name but not password
|
|
|
|
self.assert_not_in_success_response(['id_password'], result)
|
|
|
|
self.assert_in_success_response(['id_full_name'], result)
|
|
|
|
|
|
|
|
# Click confirm registration button.
|
|
|
|
result = self.client_post(
|
|
|
|
'/accounts/register/',
|
|
|
|
{'full_name': 'New User Name',
|
|
|
|
'key': confirmation_key,
|
|
|
|
'terms': True})
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(sorted(self.get_streams('new@zulip.com', realm)), stream_names)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_log_into_subdomain_when_email_is_none(self) -> None:
|
2017-04-18 08:34:29 +02:00
|
|
|
data = {'name': None,
|
|
|
|
'email': None,
|
2017-04-20 08:25:15 +02:00
|
|
|
'subdomain': 'zulip',
|
2018-03-10 14:13:30 +01:00
|
|
|
'is_signup': False,
|
|
|
|
'next': ''}
|
2017-04-18 08:34:29 +02:00
|
|
|
|
2017-09-27 07:01:41 +02:00
|
|
|
with mock.patch('logging.warning'):
|
2017-10-27 02:41:54 +02:00
|
|
|
result = self.get_log_into_subdomain(data)
|
2018-04-23 00:12:52 +02:00
|
|
|
self.assert_in_success_response(["You need an invitation to join this organization."], result)
|
2017-04-18 08:34:29 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_user_cannot_log_into_wrong_subdomain_with_cookie(self) -> None:
|
2016-10-17 14:28:23 +02:00
|
|
|
data = {'name': 'Full Name',
|
2017-05-25 01:40:26 +02:00
|
|
|
'email': self.example_email("hamlet"),
|
2017-09-16 19:34:59 +02:00
|
|
|
'subdomain': 'zephyr'}
|
2017-10-28 00:54:46 +02:00
|
|
|
with mock.patch('logging.warning') as mock_warning:
|
|
|
|
result = self.get_log_into_subdomain(data)
|
|
|
|
mock_warning.assert_called_with("Login attempt on invalid subdomain")
|
2017-09-27 07:01:41 +02:00
|
|
|
self.assertEqual(result.status_code, 400)
|
2016-10-17 14:28:23 +02:00
|
|
|
|
2018-01-11 23:08:53 +01:00
|
|
|
class JSONFetchAPIKeyTest(ZulipTestCase):
|
|
|
|
def setUp(self) -> None:
|
2019-10-19 20:47:00 +02:00
|
|
|
super().setUp()
|
2018-01-11 23:08:53 +01:00
|
|
|
self.user_profile = self.example_user('hamlet')
|
|
|
|
self.email = self.user_profile.email
|
|
|
|
|
|
|
|
def test_success(self) -> None:
|
|
|
|
self.login(self.email)
|
|
|
|
result = self.client_post("/json/fetch_api_key",
|
|
|
|
dict(user_profile=self.user_profile,
|
|
|
|
password=initial_password(self.email)))
|
|
|
|
self.assert_json_success(result)
|
|
|
|
|
|
|
|
def test_not_loggedin(self) -> None:
|
|
|
|
result = self.client_post("/json/fetch_api_key",
|
|
|
|
dict(user_profile=self.user_profile,
|
|
|
|
password=initial_password(self.email)))
|
|
|
|
self.assert_json_error(result,
|
|
|
|
"Not logged in: API authentication or user session required", 401)
|
|
|
|
|
|
|
|
def test_wrong_password(self) -> None:
|
|
|
|
self.login(self.email)
|
|
|
|
result = self.client_post("/json/fetch_api_key",
|
|
|
|
dict(user_profile=self.user_profile,
|
|
|
|
password="wrong"))
|
|
|
|
self.assert_json_error(result, "Your username or password is incorrect.", 400)
|
|
|
|
|
2016-08-23 02:08:42 +02:00
|
|
|
class FetchAPIKeyTest(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def setUp(self) -> None:
|
2019-10-19 20:47:00 +02:00
|
|
|
super().setUp()
|
2017-05-07 21:25:59 +02:00
|
|
|
self.user_profile = self.example_user('hamlet')
|
|
|
|
self.email = self.user_profile.email
|
2016-04-21 21:07:43 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_success(self) -> None:
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/fetch_api_key",
|
2016-04-21 21:07:43 +02:00
|
|
|
dict(username=self.email,
|
|
|
|
password=initial_password(self.email)))
|
|
|
|
self.assert_json_success(result)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_invalid_email(self) -> None:
|
2017-04-07 08:21:29 +02:00
|
|
|
result = self.client_post("/api/v1/fetch_api_key",
|
|
|
|
dict(username='hamlet',
|
|
|
|
password=initial_password(self.email)))
|
|
|
|
self.assert_json_error(result, "Enter a valid email address.", 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_wrong_password(self) -> None:
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/fetch_api_key",
|
2016-04-21 21:07:43 +02:00
|
|
|
dict(username=self.email,
|
|
|
|
password="wrong"))
|
|
|
|
self.assert_json_error(result, "Your username or password is incorrect.", 403)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_password_auth_disabled(self) -> None:
|
2016-04-21 21:07:43 +02:00
|
|
|
with mock.patch('zproject.backends.password_auth_enabled', return_value=False):
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/fetch_api_key",
|
2016-04-21 21:07:43 +02:00
|
|
|
dict(username=self.email,
|
|
|
|
password=initial_password(self.email)))
|
|
|
|
self.assert_json_error_contains(result, "Password auth is disabled", 403)
|
|
|
|
|
2016-11-07 01:41:29 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_ldap_auth_email_auth_disabled_success(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
self.init_default_ldap_database()
|
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2016-11-07 01:41:29 +01:00
|
|
|
result = self.client_post("/api/v1/fetch_api_key",
|
2019-10-16 18:10:40 +02:00
|
|
|
dict(username=self.example_email('hamlet'),
|
2019-10-18 18:25:51 +02:00
|
|
|
password=self.ldap_password()))
|
2016-11-07 01:41:29 +01:00
|
|
|
self.assert_json_success(result)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_inactive_user(self) -> None:
|
2016-04-21 21:07:43 +02:00
|
|
|
do_deactivate_user(self.user_profile)
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/fetch_api_key",
|
2016-04-21 21:07:43 +02:00
|
|
|
dict(username=self.email,
|
|
|
|
password=initial_password(self.email)))
|
|
|
|
self.assert_json_error_contains(result, "Your account has been disabled", 403)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_deactivated_realm(self) -> None:
|
2016-04-21 21:07:43 +02:00
|
|
|
do_deactivate_realm(self.user_profile.realm)
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/fetch_api_key",
|
2016-04-21 21:07:43 +02:00
|
|
|
dict(username=self.email,
|
|
|
|
password=initial_password(self.email)))
|
2018-03-08 01:30:34 +01:00
|
|
|
self.assert_json_error_contains(result, "This organization has been deactivated", 403)
|
2016-06-01 02:28:27 +02:00
|
|
|
|
2016-08-23 02:08:42 +02:00
|
|
|
class DevFetchAPIKeyTest(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def setUp(self) -> None:
|
2019-10-19 20:47:00 +02:00
|
|
|
super().setUp()
|
2017-05-07 21:25:59 +02:00
|
|
|
self.user_profile = self.example_user('hamlet')
|
|
|
|
self.email = self.user_profile.email
|
2016-06-01 02:28:27 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_success(self) -> None:
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/dev_fetch_api_key",
|
2016-06-01 02:28:27 +02:00
|
|
|
dict(username=self.email))
|
|
|
|
self.assert_json_success(result)
|
2017-08-17 08:28:05 +02:00
|
|
|
data = result.json()
|
2016-06-01 02:28:27 +02:00
|
|
|
self.assertEqual(data["email"], self.email)
|
2018-08-01 10:53:40 +02:00
|
|
|
user_api_keys = get_all_api_keys(self.user_profile)
|
|
|
|
self.assertIn(data['api_key'], user_api_keys)
|
2016-06-01 02:28:27 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_invalid_email(self) -> None:
|
2017-04-07 08:21:29 +02:00
|
|
|
email = 'hamlet'
|
|
|
|
result = self.client_post("/api/v1/dev_fetch_api_key",
|
|
|
|
dict(username=email))
|
|
|
|
self.assert_json_error_contains(result, "Enter a valid email address.", 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_unregistered_user(self) -> None:
|
2017-05-22 01:34:21 +02:00
|
|
|
email = 'foo@zulip.com'
|
|
|
|
result = self.client_post("/api/v1/dev_fetch_api_key",
|
|
|
|
dict(username=email))
|
|
|
|
self.assert_json_error_contains(result, "This user is not registered.", 403)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_inactive_user(self) -> None:
|
2016-06-01 02:28:27 +02:00
|
|
|
do_deactivate_user(self.user_profile)
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/dev_fetch_api_key",
|
2016-06-01 02:28:27 +02:00
|
|
|
dict(username=self.email))
|
|
|
|
self.assert_json_error_contains(result, "Your account has been disabled", 403)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_deactivated_realm(self) -> None:
|
2016-06-01 02:28:27 +02:00
|
|
|
do_deactivate_realm(self.user_profile.realm)
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/dev_fetch_api_key",
|
2016-06-01 02:28:27 +02:00
|
|
|
dict(username=self.email))
|
2018-03-08 01:30:34 +01:00
|
|
|
self.assert_json_error_contains(result, "This organization has been deactivated", 403)
|
2016-06-01 02:28:27 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_dev_auth_disabled(self) -> None:
|
2016-10-12 04:50:38 +02:00
|
|
|
with mock.patch('zerver.views.auth.dev_auth_enabled', return_value=False):
|
2016-07-28 00:30:22 +02:00
|
|
|
result = self.client_post("/api/v1/dev_fetch_api_key",
|
2016-06-01 02:28:27 +02:00
|
|
|
dict(username=self.email))
|
|
|
|
self.assert_json_error_contains(result, "Dev environment not enabled.", 400)
|
2016-06-01 02:28:43 +02:00
|
|
|
|
2016-08-23 02:08:42 +02:00
|
|
|
class DevGetEmailsTest(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_success(self) -> None:
|
2018-04-05 21:16:56 +02:00
|
|
|
result = self.client_get("/api/v1/dev_list_users")
|
2016-06-01 02:28:43 +02:00
|
|
|
self.assert_json_success(result)
|
2016-07-12 15:41:45 +02:00
|
|
|
self.assert_in_response("direct_admins", result)
|
|
|
|
self.assert_in_response("direct_users", result)
|
2016-06-01 02:28:43 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_dev_auth_disabled(self) -> None:
|
2016-10-12 04:50:38 +02:00
|
|
|
with mock.patch('zerver.views.auth.dev_auth_enabled', return_value=False):
|
2018-04-05 21:16:56 +02:00
|
|
|
result = self.client_get("/api/v1/dev_list_users")
|
2016-06-01 02:28:43 +02:00
|
|
|
self.assert_json_error_contains(result, "Dev environment not enabled.", 400)
|
2016-06-21 03:32:23 +02:00
|
|
|
|
2019-12-08 23:11:25 +01:00
|
|
|
class ExternalMethodDictsTests(ZulipTestCase):
|
|
|
|
def test_get_external_method_dicts_correctly_sorted(self) -> None:
|
2019-11-02 04:35:39 +01:00
|
|
|
with self.settings(
|
|
|
|
AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',
|
|
|
|
'zproject.backends.GitHubAuthBackend',
|
|
|
|
'zproject.backends.GoogleAuthBackend',
|
2019-12-10 00:42:12 +01:00
|
|
|
'zproject.backends.ZulipRemoteUserBackend',
|
2019-11-02 04:35:39 +01:00
|
|
|
'zproject.backends.SAMLAuthBackend',
|
|
|
|
'zproject.backends.AzureADAuthBackend')
|
|
|
|
):
|
2019-12-08 23:11:25 +01:00
|
|
|
external_auth_methods = get_external_method_dicts()
|
2019-11-02 04:35:39 +01:00
|
|
|
# First backends in the list should be SAML:
|
2019-12-08 23:11:25 +01:00
|
|
|
self.assertIn('saml:', external_auth_methods[0]['name'])
|
2019-11-02 04:35:39 +01:00
|
|
|
self.assertEqual(
|
2019-12-08 23:11:25 +01:00
|
|
|
[social_backend['name'] for social_backend in external_auth_methods[1:]],
|
2019-11-02 04:35:39 +01:00
|
|
|
[social_backend.name for social_backend in sorted(
|
2019-12-10 00:42:12 +01:00
|
|
|
[ZulipRemoteUserBackend, GitHubAuthBackend, AzureADAuthBackend, GoogleAuthBackend],
|
2019-11-02 04:35:39 +01:00
|
|
|
key=lambda x: x.sort_order,
|
|
|
|
reverse=True
|
|
|
|
)]
|
|
|
|
)
|
|
|
|
|
2016-08-23 02:08:42 +02:00
|
|
|
class FetchAuthBackends(ZulipTestCase):
|
2017-11-19 04:02:03 +01:00
|
|
|
def assert_on_error(self, error: Optional[str]) -> None:
|
2017-05-04 01:13:56 +02:00
|
|
|
if error:
|
|
|
|
raise AssertionError(error)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_get_server_settings(self) -> None:
|
2018-02-12 23:12:47 +01:00
|
|
|
def check_result(result: HttpResponse, extra_fields: List[Tuple[str, Validator]]=[]) -> None:
|
2018-12-19 01:35:13 +01:00
|
|
|
authentication_methods_list = [
|
|
|
|
('password', check_bool),
|
|
|
|
]
|
|
|
|
for backend_name_with_case in AUTH_BACKEND_NAME_MAP:
|
|
|
|
authentication_methods_list.append((backend_name_with_case.lower(), check_bool))
|
2019-12-08 23:11:25 +01:00
|
|
|
external_auth_methods = get_external_method_dicts()
|
2018-12-19 01:35:13 +01:00
|
|
|
|
2017-08-25 23:59:49 +02:00
|
|
|
self.assert_json_success(result)
|
2018-02-12 23:12:47 +01:00
|
|
|
checker = check_dict_only([
|
2018-12-19 01:35:13 +01:00
|
|
|
('authentication_methods', check_dict_only(authentication_methods_list)),
|
2019-12-08 23:11:25 +01:00
|
|
|
('external_authentication_methods', check_list(None, length=len(external_auth_methods))),
|
2017-09-15 19:13:48 +02:00
|
|
|
('email_auth_enabled', check_bool),
|
2018-12-06 02:49:34 +01:00
|
|
|
('is_incompatible', check_bool),
|
2017-09-15 19:13:48 +02:00
|
|
|
('require_email_format_usernames', check_bool),
|
2017-08-25 23:59:49 +02:00
|
|
|
('realm_uri', check_string),
|
|
|
|
('zulip_version', check_string),
|
2018-02-12 23:34:59 +01:00
|
|
|
('push_notifications_enabled', check_bool),
|
2017-08-25 23:59:49 +02:00
|
|
|
('msg', check_string),
|
|
|
|
('result', check_string),
|
2018-02-12 23:12:47 +01:00
|
|
|
] + extra_fields)
|
|
|
|
self.assert_on_error(checker("data", result.json()))
|
|
|
|
|
2018-12-06 02:49:34 +01:00
|
|
|
result = self.client_get("/api/v1/server_settings", subdomain="", HTTP_USER_AGENT="")
|
2018-02-12 23:12:47 +01:00
|
|
|
check_result(result)
|
2019-12-08 23:11:25 +01:00
|
|
|
self.assertEqual(result.json()['external_authentication_methods'], get_external_method_dicts())
|
2017-08-25 23:59:49 +02:00
|
|
|
|
2018-12-06 02:49:34 +01:00
|
|
|
result = self.client_get("/api/v1/server_settings", subdomain="", HTTP_USER_AGENT="ZulipInvalid")
|
|
|
|
self.assertTrue(result.json()["is_incompatible"])
|
|
|
|
|
2017-10-03 01:31:20 +02:00
|
|
|
with self.settings(ROOT_DOMAIN_LANDING_PAGE=False):
|
2018-12-06 02:49:34 +01:00
|
|
|
result = self.client_get("/api/v1/server_settings", subdomain="", HTTP_USER_AGENT="")
|
2018-02-12 23:12:47 +01:00
|
|
|
check_result(result)
|
|
|
|
|
|
|
|
with self.settings(ROOT_DOMAIN_LANDING_PAGE=False):
|
2018-12-06 02:49:34 +01:00
|
|
|
result = self.client_get("/api/v1/server_settings", subdomain="zulip", HTTP_USER_AGENT="")
|
2018-02-12 23:12:47 +01:00
|
|
|
check_result(result, [
|
2017-05-04 01:13:56 +02:00
|
|
|
('realm_name', check_string),
|
|
|
|
('realm_description', check_string),
|
|
|
|
('realm_icon', check_string),
|
|
|
|
])
|
|
|
|
|
2019-11-01 05:12:11 +01:00
|
|
|
# Verify invalid subdomain
|
|
|
|
result = self.client_get("/api/v1/server_settings",
|
|
|
|
subdomain="invalid")
|
|
|
|
self.assert_json_error_contains(result, "Invalid subdomain", 400)
|
2018-12-19 01:35:13 +01:00
|
|
|
|
2019-11-01 05:12:11 +01:00
|
|
|
with self.settings(ROOT_DOMAIN_LANDING_PAGE=True):
|
|
|
|
# With ROOT_DOMAIN_LANDING_PAGE, homepage fails
|
|
|
|
result = self.client_get("/api/v1/server_settings",
|
|
|
|
subdomain="")
|
|
|
|
self.assert_json_error_contains(result, "Subdomain required", 400)
|
2017-03-10 06:29:09 +01:00
|
|
|
|
2017-07-13 13:42:57 +02:00
|
|
|
class TestTwoFactor(ZulipTestCase):
|
|
|
|
def test_direct_dev_login_with_2fa(self) -> None:
|
|
|
|
email = self.example_email('hamlet')
|
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
with self.settings(TWO_FACTOR_AUTHENTICATION_ENABLED=True):
|
|
|
|
data = {'direct_email': email}
|
|
|
|
result = self.client_post('/accounts/login/local/', data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2017-07-13 13:42:57 +02:00
|
|
|
# User logs in but when otp device doesn't exist.
|
|
|
|
self.assertNotIn('otp_device_id', self.client.session.keys())
|
|
|
|
|
|
|
|
self.create_default_device(user_profile)
|
|
|
|
|
|
|
|
data = {'direct_email': email}
|
|
|
|
result = self.client_post('/accounts/login/local/', data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2017-07-13 13:42:57 +02:00
|
|
|
# User logs in when otp device exists.
|
|
|
|
self.assertIn('otp_device_id', self.client.session.keys())
|
|
|
|
|
|
|
|
@mock.patch('two_factor.models.totp')
|
|
|
|
def test_two_factor_login_with_ldap(self, mock_totp):
|
|
|
|
# type: (mock.MagicMock) -> None
|
|
|
|
token = 123456
|
|
|
|
email = self.example_email('hamlet')
|
2019-10-18 18:25:51 +02:00
|
|
|
password = self.ldap_password()
|
2017-07-13 13:42:57 +02:00
|
|
|
|
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
user_profile.set_password(password)
|
|
|
|
user_profile.save()
|
|
|
|
self.create_default_device(user_profile)
|
|
|
|
|
|
|
|
def totp(*args, **kwargs):
|
|
|
|
# type: (*Any, **Any) -> int
|
|
|
|
return token
|
|
|
|
|
|
|
|
mock_totp.side_effect = totp
|
|
|
|
|
|
|
|
# Setup LDAP
|
2019-10-16 18:10:40 +02:00
|
|
|
self.init_default_ldap_database()
|
|
|
|
ldap_user_attr_map = {'full_name': 'cn', 'short_name': 'sn'}
|
2017-07-13 13:42:57 +02:00
|
|
|
with self.settings(
|
|
|
|
AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',),
|
|
|
|
TWO_FACTOR_CALL_GATEWAY='two_factor.gateways.fake.Fake',
|
|
|
|
TWO_FACTOR_SMS_GATEWAY='two_factor.gateways.fake.Fake',
|
|
|
|
TWO_FACTOR_AUTHENTICATION_ENABLED=True,
|
|
|
|
POPULATE_PROFILE_VIA_LDAP=True,
|
|
|
|
LDAP_APPEND_DOMAIN='zulip.com',
|
|
|
|
AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map,
|
2019-10-16 18:10:40 +02:00
|
|
|
):
|
2017-07-13 13:42:57 +02:00
|
|
|
first_step_data = {"username": email,
|
|
|
|
"password": password,
|
|
|
|
"two_factor_login_view-current_step": "auth"}
|
|
|
|
result = self.client_post("/accounts/login/", first_step_data)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
|
|
|
|
second_step_data = {"token-otp_token": str(token),
|
|
|
|
"two_factor_login_view-current_step": "token"}
|
|
|
|
result = self.client_post("/accounts/login/", second_step_data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result['Location'], 'http://zulip.testserver')
|
|
|
|
|
|
|
|
# Going to login page should redirect to `realm.uri` if user is
|
|
|
|
# already logged in.
|
|
|
|
result = self.client_get('/accounts/login/')
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result['Location'], 'http://zulip.testserver')
|
|
|
|
|
2016-10-24 08:35:16 +02:00
|
|
|
class TestDevAuthBackend(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success(self) -> None:
|
2017-05-07 19:39:30 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
email = user_profile.email
|
2016-10-24 08:35:16 +02:00
|
|
|
data = {'direct_email': email}
|
|
|
|
result = self.client_post('/accounts/login/local/', data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2016-10-24 08:35:16 +02:00
|
|
|
|
2017-07-13 13:42:57 +02:00
|
|
|
def test_login_success_with_2fa(self) -> None:
|
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
self.create_default_device(user_profile)
|
|
|
|
email = user_profile.email
|
|
|
|
data = {'direct_email': email}
|
|
|
|
with self.settings(TWO_FACTOR_AUTHENTICATION_ENABLED=True):
|
|
|
|
result = self.client_post('/accounts/login/local/', data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, 'http://zulip.testserver')
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2017-07-13 13:42:57 +02:00
|
|
|
self.assertIn('otp_device_id', list(self.client.session.keys()))
|
|
|
|
|
2018-03-12 12:25:50 +01:00
|
|
|
def test_redirect_to_next_url(self) -> None:
|
2018-05-11 01:39:38 +02:00
|
|
|
def do_local_login(formaction: str) -> HttpResponse:
|
2018-03-12 12:25:50 +01:00
|
|
|
user_email = self.example_email('hamlet')
|
|
|
|
data = {'direct_email': user_email}
|
|
|
|
return self.client_post(formaction, data)
|
|
|
|
|
|
|
|
res = do_local_login('/accounts/login/local/')
|
|
|
|
self.assertEqual(res.status_code, 302)
|
|
|
|
self.assertEqual(res.url, 'http://zulip.testserver')
|
|
|
|
|
|
|
|
res = do_local_login('/accounts/login/local/?next=/user_uploads/path_to_image')
|
|
|
|
self.assertEqual(res.status_code, 302)
|
|
|
|
self.assertEqual(res.url, 'http://zulip.testserver/user_uploads/path_to_image')
|
|
|
|
|
2018-03-12 14:08:12 +01:00
|
|
|
# In local Email based authentication we never make browser send the hash
|
|
|
|
# to the backend. Rather we depend upon the browser's behaviour of persisting
|
|
|
|
# hash anchors in between redirect requests. See below stackoverflow conversation
|
|
|
|
# https://stackoverflow.com/questions/5283395/url-hash-is-persisting-between-redirects
|
|
|
|
res = do_local_login('/accounts/login/local/?next=#narrow/stream/7-test-here')
|
|
|
|
self.assertEqual(res.status_code, 302)
|
|
|
|
self.assertEqual(res.url, 'http://zulip.testserver')
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_with_subdomain(self) -> None:
|
2017-05-07 19:39:30 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
email = user_profile.email
|
2017-03-25 20:44:14 +01:00
|
|
|
data = {'direct_email': email}
|
2017-10-03 01:31:20 +02:00
|
|
|
|
|
|
|
result = self.client_post('/accounts/login/local/', data)
|
2017-03-25 20:44:14 +01:00
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2017-03-25 20:44:14 +01:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_choose_realm(self) -> None:
|
2017-09-15 22:03:49 +02:00
|
|
|
result = self.client_post('/devlogin/', subdomain="zulip")
|
2019-05-21 23:37:21 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
2017-09-15 22:03:49 +02:00
|
|
|
self.assert_in_success_response(["Click on a user to log in to Zulip Dev!"], result)
|
|
|
|
self.assert_in_success_response(["iago@zulip.com", "hamlet@zulip.com"], result)
|
|
|
|
|
|
|
|
result = self.client_post('/devlogin/', subdomain="")
|
2019-05-21 23:37:21 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
2017-08-15 00:13:58 +02:00
|
|
|
self.assert_in_success_response(["Click on a user to log in!"], result)
|
|
|
|
self.assert_in_success_response(["iago@zulip.com", "hamlet@zulip.com"], result)
|
|
|
|
self.assert_in_success_response(["starnine@mit.edu", "espuser@mit.edu"], result)
|
|
|
|
|
2019-05-21 23:37:21 +02:00
|
|
|
result = self.client_post('/devlogin/', {'new_realm': 'all_realms'},
|
|
|
|
subdomain="zephyr")
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_success_response(["starnine@mit.edu", "espuser@mit.edu"], result)
|
|
|
|
self.assert_in_success_response(["Click on a user to log in!"], result)
|
|
|
|
self.assert_in_success_response(["iago@zulip.com", "hamlet@zulip.com"], result)
|
|
|
|
|
2017-08-15 00:13:58 +02:00
|
|
|
data = {'new_realm': 'zephyr'}
|
2017-09-15 22:03:49 +02:00
|
|
|
result = self.client_post('/devlogin/', data, subdomain="zulip")
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
self.assertEqual(result.url, "http://zephyr.testserver")
|
2019-05-21 23:37:21 +02:00
|
|
|
|
2017-09-15 22:03:49 +02:00
|
|
|
result = self.client_get('/devlogin/', subdomain="zephyr")
|
2019-05-21 23:37:21 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
2017-08-15 00:13:58 +02:00
|
|
|
self.assert_in_success_response(["starnine@mit.edu", "espuser@mit.edu"], result)
|
|
|
|
self.assert_in_success_response(["Click on a user to log in to MIT!"], result)
|
2017-09-15 22:03:49 +02:00
|
|
|
self.assert_not_in_success_response(["iago@zulip.com", "hamlet@zulip.com"], result)
|
2017-08-15 00:13:58 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_choose_realm_with_subdomains_enabled(self) -> None:
|
2017-08-15 00:13:58 +02:00
|
|
|
with mock.patch('zerver.views.auth.is_subdomain_root_or_alias', return_value=False):
|
|
|
|
with mock.patch('zerver.views.auth.get_realm_from_request', return_value=get_realm('zulip')):
|
2017-10-03 01:31:20 +02:00
|
|
|
result = self.client_get("http://zulip.testserver/devlogin/")
|
|
|
|
self.assert_in_success_response(["iago@zulip.com", "hamlet@zulip.com"], result)
|
|
|
|
self.assert_not_in_success_response(["starnine@mit.edu", "espuser@mit.edu"], result)
|
|
|
|
self.assert_in_success_response(["Click on a user to log in to Zulip Dev!"], result)
|
2017-08-15 00:13:58 +02:00
|
|
|
|
|
|
|
with mock.patch('zerver.views.auth.get_realm_from_request', return_value=get_realm('zephyr')):
|
2017-10-03 01:31:20 +02:00
|
|
|
result = self.client_post("http://zulip.testserver/devlogin/", {'new_realm': 'zephyr'})
|
|
|
|
self.assertEqual(result["Location"], "http://zephyr.testserver")
|
2017-08-15 00:13:58 +02:00
|
|
|
|
2017-10-03 01:31:20 +02:00
|
|
|
result = self.client_get("http://zephyr.testserver/devlogin/")
|
|
|
|
self.assert_not_in_success_response(["iago@zulip.com", "hamlet@zulip.com"], result)
|
|
|
|
self.assert_in_success_response(["starnine@mit.edu", "espuser@mit.edu"], result)
|
|
|
|
self.assert_in_success_response(["Click on a user to log in to MIT!"], result)
|
2017-08-15 00:13:58 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure(self) -> None:
|
2017-05-25 01:40:26 +02:00
|
|
|
email = self.example_email("hamlet")
|
2016-10-24 08:35:16 +02:00
|
|
|
data = {'direct_email': email}
|
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',)):
|
2018-02-21 06:31:53 +01:00
|
|
|
with mock.patch('django.core.handlers.exception.logger'):
|
|
|
|
response = self.client_post('/accounts/login/local/', data)
|
|
|
|
self.assertRedirects(response, reverse('dev_not_supported'))
|
2016-10-24 08:35:16 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_nonexistent_user(self) -> None:
|
2016-10-24 08:35:16 +02:00
|
|
|
email = 'nonexisting@zulip.com'
|
|
|
|
data = {'direct_email': email}
|
2018-02-21 06:31:53 +01:00
|
|
|
with mock.patch('django.core.handlers.exception.logger'):
|
|
|
|
response = self.client_post('/accounts/login/local/', data)
|
|
|
|
self.assertRedirects(response, reverse('dev_not_supported'))
|
2016-10-24 09:09:31 +02:00
|
|
|
|
|
|
|
class TestZulipRemoteUserBackend(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success(self) -> None:
|
2017-05-07 19:39:30 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
email = user_profile.email
|
2016-10-24 09:09:31 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)):
|
|
|
|
result = self.client_post('/accounts/login/sso/', REMOTE_USER=email)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2016-10-24 09:09:31 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success_with_sso_append_domain(self) -> None:
|
2016-10-24 09:09:31 +02:00
|
|
|
username = 'hamlet'
|
2017-05-07 19:39:30 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
2016-10-24 09:09:31 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',),
|
|
|
|
SSO_APPEND_DOMAIN='zulip.com'):
|
|
|
|
result = self.client_post('/accounts/login/sso/', REMOTE_USER=username)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2016-10-24 09:09:31 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure(self) -> None:
|
2017-05-25 01:40:26 +02:00
|
|
|
email = self.example_email("hamlet")
|
2016-10-24 09:09:31 +02:00
|
|
|
result = self.client_post('/accounts/login/sso/', REMOTE_USER=email)
|
2017-06-04 11:36:52 +02:00
|
|
|
self.assertEqual(result.status_code, 200) # This should ideally be not 200.
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2016-10-24 09:09:31 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_nonexisting_user(self) -> None:
|
2016-10-24 09:09:31 +02:00
|
|
|
email = 'nonexisting@zulip.com'
|
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)):
|
|
|
|
result = self.client_post('/accounts/login/sso/', REMOTE_USER=email)
|
2017-04-20 08:25:15 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2017-07-17 17:31:05 +02:00
|
|
|
self.assert_in_response("No account found for", result)
|
2016-10-24 09:09:31 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_invalid_email(self) -> None:
|
2017-04-07 08:21:29 +02:00
|
|
|
email = 'hamlet'
|
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)):
|
|
|
|
result = self.client_post('/accounts/login/sso/', REMOTE_USER=email)
|
|
|
|
self.assert_json_error_contains(result, "Enter a valid email address.", 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_missing_field(self) -> None:
|
2016-10-24 09:09:31 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)):
|
|
|
|
result = self.client_post('/accounts/login/sso/')
|
|
|
|
self.assert_json_error_contains(result, "No REMOTE_USER set.", 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_wrong_subdomain(self) -> None:
|
2017-05-25 01:40:26 +02:00
|
|
|
email = self.example_email("hamlet")
|
2017-10-03 01:31:20 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)):
|
2016-10-24 09:09:31 +02:00
|
|
|
with mock.patch('zerver.views.auth.get_subdomain', return_value='acme'):
|
2017-03-05 04:17:12 +01:00
|
|
|
result = self.client_post('http://testserver:9080/accounts/login/sso/',
|
|
|
|
REMOTE_USER=email)
|
2016-10-24 09:09:31 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2018-04-23 00:12:52 +02:00
|
|
|
self.assert_in_response("You need an invitation to join this organization.", result)
|
2016-10-24 09:09:31 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_empty_subdomain(self) -> None:
|
2017-05-25 01:40:26 +02:00
|
|
|
email = self.example_email("hamlet")
|
2017-10-03 01:31:20 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)):
|
2016-10-24 09:09:31 +02:00
|
|
|
with mock.patch('zerver.views.auth.get_subdomain', return_value=''):
|
2017-03-05 04:17:12 +01:00
|
|
|
result = self.client_post('http://testserver:9080/accounts/login/sso/',
|
|
|
|
REMOTE_USER=email)
|
2016-10-24 09:09:31 +02:00
|
|
|
self.assertEqual(result.status_code, 200)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2018-04-23 00:12:52 +02:00
|
|
|
self.assert_in_response("You need an invitation to join this organization.", result)
|
2016-10-24 09:09:31 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success_under_subdomains(self) -> None:
|
2017-05-07 19:39:30 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
email = user_profile.email
|
2016-10-24 09:09:31 +02:00
|
|
|
with mock.patch('zerver.views.auth.get_subdomain', return_value='zulip'):
|
|
|
|
with self.settings(
|
|
|
|
AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)):
|
|
|
|
result = self.client_post('/accounts/login/sso/', REMOTE_USER=email)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2016-10-24 11:38:38 +02:00
|
|
|
|
2018-02-06 23:29:57 +01:00
|
|
|
@override_settings(SEND_LOGIN_EMAILS=True)
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',))
|
2018-12-10 19:33:52 +01:00
|
|
|
def test_login_mobile_flow_otp_success_email(self) -> None:
|
2018-02-06 23:29:57 +01:00
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
email = user_profile.email
|
2018-08-10 00:58:44 +02:00
|
|
|
user_profile.date_joined = timezone_now() - datetime.timedelta(seconds=61)
|
|
|
|
user_profile.save()
|
2018-02-06 23:29:57 +01:00
|
|
|
mobile_flow_otp = '1234abcd' * 8
|
|
|
|
|
2018-08-10 00:58:44 +02:00
|
|
|
# Verify that the right thing happens with an invalid-format OTP
|
2018-02-06 23:29:57 +01:00
|
|
|
result = self.client_post('/accounts/login/sso/',
|
|
|
|
dict(mobile_flow_otp="1234"),
|
|
|
|
REMOTE_USER=email,
|
|
|
|
HTTP_USER_AGENT = "ZulipAndroid")
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2018-02-06 23:29:57 +01:00
|
|
|
self.assert_json_error_contains(result, "Invalid OTP", 400)
|
|
|
|
|
|
|
|
result = self.client_post('/accounts/login/sso/',
|
|
|
|
dict(mobile_flow_otp="invalido" * 8),
|
|
|
|
REMOTE_USER=email,
|
|
|
|
HTTP_USER_AGENT = "ZulipAndroid")
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2018-02-06 23:29:57 +01:00
|
|
|
self.assert_json_error_contains(result, "Invalid OTP", 400)
|
|
|
|
|
|
|
|
result = self.client_post('/accounts/login/sso/',
|
|
|
|
dict(mobile_flow_otp=mobile_flow_otp),
|
|
|
|
REMOTE_USER=email,
|
|
|
|
HTTP_USER_AGENT = "ZulipAndroid")
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
redirect_url = result['Location']
|
|
|
|
parsed_url = urllib.parse.urlparse(redirect_url)
|
|
|
|
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
|
|
self.assertEqual(parsed_url.scheme, 'zulip')
|
2018-12-10 19:33:52 +01:00
|
|
|
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
|
|
|
self.assertEqual(query_params["email"], [self.example_email("hamlet")])
|
|
|
|
encrypted_api_key = query_params["otp_encrypted_api_key"][0]
|
|
|
|
hamlet_api_keys = get_all_api_keys(self.example_user('hamlet'))
|
|
|
|
self.assertIn(otp_decrypt_api_key(encrypted_api_key, mobile_flow_otp), hamlet_api_keys)
|
|
|
|
self.assertEqual(len(mail.outbox), 1)
|
|
|
|
self.assertIn('Zulip on Android', mail.outbox[0].body)
|
|
|
|
|
|
|
|
@override_settings(SEND_LOGIN_EMAILS=True)
|
|
|
|
@override_settings(SSO_APPEND_DOMAIN="zulip.com")
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',))
|
|
|
|
def test_login_mobile_flow_otp_success_username(self) -> None:
|
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
email = user_profile.email
|
|
|
|
remote_user = email_to_username(email)
|
|
|
|
user_profile.date_joined = timezone_now() - datetime.timedelta(seconds=61)
|
|
|
|
user_profile.save()
|
|
|
|
mobile_flow_otp = '1234abcd' * 8
|
|
|
|
|
|
|
|
# Verify that the right thing happens with an invalid-format OTP
|
|
|
|
result = self.client_post('/accounts/login/sso/',
|
|
|
|
dict(mobile_flow_otp="1234"),
|
|
|
|
REMOTE_USER=remote_user,
|
|
|
|
HTTP_USER_AGENT = "ZulipAndroid")
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2018-12-10 19:33:52 +01:00
|
|
|
self.assert_json_error_contains(result, "Invalid OTP", 400)
|
|
|
|
|
|
|
|
result = self.client_post('/accounts/login/sso/',
|
|
|
|
dict(mobile_flow_otp="invalido" * 8),
|
|
|
|
REMOTE_USER=remote_user,
|
|
|
|
HTTP_USER_AGENT = "ZulipAndroid")
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2018-12-10 19:33:52 +01:00
|
|
|
self.assert_json_error_contains(result, "Invalid OTP", 400)
|
|
|
|
|
|
|
|
result = self.client_post('/accounts/login/sso/',
|
|
|
|
dict(mobile_flow_otp=mobile_flow_otp),
|
|
|
|
REMOTE_USER=remote_user,
|
|
|
|
HTTP_USER_AGENT = "ZulipAndroid")
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
redirect_url = result['Location']
|
|
|
|
parsed_url = urllib.parse.urlparse(redirect_url)
|
|
|
|
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
|
|
self.assertEqual(parsed_url.scheme, 'zulip')
|
2018-02-06 23:29:57 +01:00
|
|
|
self.assertEqual(query_params["realm"], ['http://zulip.testserver'])
|
|
|
|
self.assertEqual(query_params["email"], [self.example_email("hamlet")])
|
|
|
|
encrypted_api_key = query_params["otp_encrypted_api_key"][0]
|
2018-08-01 10:53:40 +02:00
|
|
|
hamlet_api_keys = get_all_api_keys(self.example_user('hamlet'))
|
|
|
|
self.assertIn(otp_decrypt_api_key(encrypted_api_key, mobile_flow_otp), hamlet_api_keys)
|
2018-02-06 23:29:57 +01:00
|
|
|
self.assertEqual(len(mail.outbox), 1)
|
|
|
|
self.assertIn('Zulip on Android', mail.outbox[0].body)
|
|
|
|
|
2018-02-24 22:38:48 +01:00
|
|
|
def test_redirect_to(self) -> None:
|
2018-04-22 23:14:27 +02:00
|
|
|
"""This test verifies the behavior of the redirect_to logic in
|
|
|
|
login_or_register_remote_user."""
|
2018-05-11 01:39:38 +02:00
|
|
|
def test_with_redirect_to_param_set_as_next(next: str='') -> HttpResponse:
|
2018-02-24 22:38:48 +01:00
|
|
|
user_profile = self.example_user('hamlet')
|
|
|
|
email = user_profile.email
|
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipRemoteUserBackend',)):
|
|
|
|
result = self.client_post('/accounts/login/sso/?next=' + next, REMOTE_USER=email)
|
|
|
|
return result
|
|
|
|
|
|
|
|
res = test_with_redirect_to_param_set_as_next()
|
|
|
|
self.assertEqual('http://zulip.testserver', res.url)
|
|
|
|
res = test_with_redirect_to_param_set_as_next('/user_uploads/image_path')
|
|
|
|
self.assertEqual('http://zulip.testserver/user_uploads/image_path', res.url)
|
|
|
|
|
2018-04-22 23:14:27 +02:00
|
|
|
# Third-party domains are rejected and just send you to root domain
|
|
|
|
res = test_with_redirect_to_param_set_as_next('https://rogue.zulip-like.server/login')
|
|
|
|
self.assertEqual('http://zulip.testserver', res.url)
|
|
|
|
|
2018-02-24 22:38:48 +01:00
|
|
|
# In SSO based auth we never make browser send the hash to the backend.
|
|
|
|
# Rather we depend upon the browser's behaviour of persisting hash anchors
|
|
|
|
# in between redirect requests. See below stackoverflow conversation
|
|
|
|
# https://stackoverflow.com/questions/5283395/url-hash-is-persisting-between-redirects
|
|
|
|
res = test_with_redirect_to_param_set_as_next('#narrow/stream/7-test-here')
|
|
|
|
self.assertEqual('http://zulip.testserver', res.url)
|
|
|
|
|
2016-10-24 11:38:38 +02:00
|
|
|
class TestJWTLogin(ZulipTestCase):
|
|
|
|
"""
|
|
|
|
JWT uses ZulipDummyBackend.
|
|
|
|
"""
|
2016-11-29 07:22:02 +01:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success(self) -> None:
|
2016-10-24 11:38:38 +02:00
|
|
|
payload = {'user': 'hamlet', 'realm': 'zulip.com'}
|
2017-08-25 22:02:00 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'zulip': 'key'}):
|
2017-05-25 01:40:26 +02:00
|
|
|
email = self.example_email("hamlet")
|
2017-05-23 20:57:59 +02:00
|
|
|
realm = get_realm('zulip')
|
2017-08-25 22:02:00 +02:00
|
|
|
auth_key = settings.JWT_AUTH_KEYS['zulip']
|
2016-10-24 11:38:38 +02:00
|
|
|
web_token = jwt.encode(payload, auth_key).decode('utf8')
|
|
|
|
|
2017-05-23 20:57:59 +02:00
|
|
|
user_profile = get_user(email, realm)
|
2016-10-24 11:38:38 +02:00
|
|
|
data = {'json_web_token': web_token}
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2016-10-24 11:38:38 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_when_user_is_missing(self) -> None:
|
2016-10-24 11:38:38 +02:00
|
|
|
payload = {'realm': 'zulip.com'}
|
2017-08-25 22:02:00 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'zulip': 'key'}):
|
|
|
|
auth_key = settings.JWT_AUTH_KEYS['zulip']
|
2016-10-24 11:38:38 +02:00
|
|
|
web_token = jwt.encode(payload, auth_key).decode('utf8')
|
|
|
|
data = {'json_web_token': web_token}
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
|
|
|
self.assert_json_error_contains(result, "No user specified in JSON web token claims", 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_when_realm_is_missing(self) -> None:
|
2016-10-24 11:38:38 +02:00
|
|
|
payload = {'user': 'hamlet'}
|
2017-08-25 22:02:00 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'zulip': 'key'}):
|
|
|
|
auth_key = settings.JWT_AUTH_KEYS['zulip']
|
2016-10-24 11:38:38 +02:00
|
|
|
web_token = jwt.encode(payload, auth_key).decode('utf8')
|
|
|
|
data = {'json_web_token': web_token}
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
2018-03-08 02:05:50 +01:00
|
|
|
self.assert_json_error_contains(result, "No organization specified in JSON web token claims", 400)
|
2016-10-24 11:38:38 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_when_key_does_not_exist(self) -> None:
|
2016-10-24 11:38:38 +02:00
|
|
|
data = {'json_web_token': 'not relevant'}
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
|
|
|
self.assert_json_error_contains(result, "Auth key for this subdomain not found.", 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_when_key_is_missing(self) -> None:
|
2017-08-25 22:02:00 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'zulip': 'key'}):
|
2016-10-24 11:38:38 +02:00
|
|
|
result = self.client_post('/accounts/login/jwt/')
|
|
|
|
self.assert_json_error_contains(result, "No JSON web token passed in request", 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_when_bad_token_is_passed(self) -> None:
|
2017-08-25 22:02:00 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'zulip': 'key'}):
|
2016-10-24 11:38:38 +02:00
|
|
|
result = self.client_post('/accounts/login/jwt/')
|
|
|
|
self.assert_json_error_contains(result, "No JSON web token passed in request", 400)
|
|
|
|
data = {'json_web_token': 'bad token'}
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
|
|
|
self.assert_json_error_contains(result, "Bad JSON web token", 400)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_when_user_does_not_exist(self) -> None:
|
2016-10-24 11:38:38 +02:00
|
|
|
payload = {'user': 'nonexisting', 'realm': 'zulip.com'}
|
2017-08-25 22:02:00 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'zulip': 'key'}):
|
|
|
|
auth_key = settings.JWT_AUTH_KEYS['zulip']
|
2016-10-24 11:38:38 +02:00
|
|
|
web_token = jwt.encode(payload, auth_key).decode('utf8')
|
|
|
|
data = {'json_web_token': web_token}
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
2017-06-04 11:36:52 +02:00
|
|
|
self.assertEqual(result.status_code, 200) # This should ideally be not 200.
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2016-10-24 11:38:38 +02:00
|
|
|
|
2017-03-25 20:44:14 +01:00
|
|
|
# The /accounts/login/jwt/ endpoint should also handle the case
|
|
|
|
# where the authentication attempt throws UserProfile.DoesNotExist.
|
|
|
|
with mock.patch(
|
|
|
|
'zerver.views.auth.authenticate',
|
|
|
|
side_effect=UserProfile.DoesNotExist("Do not exist")):
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
2017-06-04 11:36:52 +02:00
|
|
|
self.assertEqual(result.status_code, 200) # This should ideally be not 200.
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2017-03-25 20:44:14 +01:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_wrong_subdomain(self) -> None:
|
2016-10-24 11:38:38 +02:00
|
|
|
payload = {'user': 'hamlet', 'realm': 'zulip.com'}
|
2017-10-03 01:31:20 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'acme': 'key'}):
|
2017-10-28 00:48:13 +02:00
|
|
|
with mock.patch('zerver.views.auth.get_subdomain', return_value='acme'), \
|
|
|
|
mock.patch('logging.warning'):
|
2016-10-24 11:38:38 +02:00
|
|
|
auth_key = settings.JWT_AUTH_KEYS['acme']
|
|
|
|
web_token = jwt.encode(payload, auth_key).decode('utf8')
|
|
|
|
|
|
|
|
data = {'json_web_token': web_token}
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
|
|
|
self.assert_json_error_contains(result, "Wrong subdomain", 400)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2016-10-24 11:38:38 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_empty_subdomain(self) -> None:
|
2016-10-24 11:38:38 +02:00
|
|
|
payload = {'user': 'hamlet', 'realm': 'zulip.com'}
|
2017-10-03 01:31:20 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'': 'key'}):
|
2017-10-28 00:48:13 +02:00
|
|
|
with mock.patch('zerver.views.auth.get_subdomain', return_value=''), \
|
|
|
|
mock.patch('logging.warning'):
|
2016-10-24 11:38:38 +02:00
|
|
|
auth_key = settings.JWT_AUTH_KEYS['']
|
|
|
|
web_token = jwt.encode(payload, auth_key).decode('utf8')
|
|
|
|
|
|
|
|
data = {'json_web_token': web_token}
|
|
|
|
result = self.client_post('/accounts/login/jwt/', data)
|
|
|
|
self.assert_json_error_contains(result, "Wrong subdomain", 400)
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(None)
|
2016-10-24 11:38:38 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success_under_subdomains(self) -> None:
|
2016-10-24 11:38:38 +02:00
|
|
|
payload = {'user': 'hamlet', 'realm': 'zulip.com'}
|
2017-10-03 01:31:20 +02:00
|
|
|
with self.settings(JWT_AUTH_KEYS={'zulip': 'key'}):
|
2016-10-24 11:38:38 +02:00
|
|
|
with mock.patch('zerver.views.auth.get_subdomain', return_value='zulip'):
|
|
|
|
auth_key = settings.JWT_AUTH_KEYS['zulip']
|
|
|
|
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)
|
2017-05-23 20:57:59 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
2019-05-26 22:12:46 +02:00
|
|
|
self.assert_logged_in_user_id(user_profile.id)
|
2016-10-24 14:41:45 +02:00
|
|
|
|
2019-11-01 23:26:49 +01:00
|
|
|
class DjangoToLDAPUsernameTests(ZulipTestCase):
|
|
|
|
def setUp(self) -> None:
|
|
|
|
self.init_default_ldap_database()
|
|
|
|
self.backend = ZulipLDAPAuthBackend()
|
|
|
|
|
|
|
|
def test_django_to_ldap_username_with_append_domain(self) -> None:
|
|
|
|
with self.settings(LDAP_APPEND_DOMAIN="zulip.com"):
|
|
|
|
self.assertEqual(self.backend.django_to_ldap_username("hamlet"), "hamlet")
|
|
|
|
self.assertEqual(self.backend.django_to_ldap_username("hamlet@zulip.com"), "hamlet")
|
|
|
|
with self.assertRaisesRegex(ZulipLDAPExceptionOutsideDomain,
|
|
|
|
'Email hamlet@example.com does not match LDAP domain zulip.com.'):
|
|
|
|
self.backend.django_to_ldap_username("hamlet@example.com")
|
|
|
|
|
|
|
|
self.mock_ldap.directory['uid="hamlet@test",ou=users,dc=zulip,dc=com'] = {
|
|
|
|
"cn": ["King Hamlet"],
|
|
|
|
"uid": ['"hamlet@test"'],
|
|
|
|
}
|
|
|
|
username = self.backend.django_to_ldap_username('"hamlet@test"@zulip.com')
|
|
|
|
self.assertEqual(username, '"hamlet@test"')
|
|
|
|
|
|
|
|
self.mock_ldap.directory['uid="hamlet@test"@zulip,ou=users,dc=zulip,dc=com'] = {
|
|
|
|
"cn": ["King Hamlet"],
|
|
|
|
"uid": ['"hamlet@test"@zulip'],
|
|
|
|
}
|
|
|
|
username = self.backend.django_to_ldap_username('"hamlet@test"@zulip')
|
|
|
|
self.assertEqual(username, '"hamlet@test"@zulip')
|
|
|
|
|
|
|
|
def test_django_to_ldap_username_with_email_search(self) -> None:
|
|
|
|
self.assertEqual(self.backend.django_to_ldap_username("hamlet"),
|
|
|
|
self.ldap_username("hamlet"))
|
|
|
|
self.assertEqual(self.backend.django_to_ldap_username("hamlet@zulip.com"),
|
|
|
|
self.ldap_username("hamlet"))
|
|
|
|
# If there are no matches through the email search, raise exception:
|
|
|
|
with self.assertRaises(ZulipLDAPExceptionNoMatchingLDAPUser):
|
|
|
|
self.backend.django_to_ldap_username("no_such_email@example.com")
|
|
|
|
|
|
|
|
self.assertEqual(self.backend.django_to_ldap_username("aaron@zulip.com"),
|
|
|
|
self.ldap_username("aaron"))
|
|
|
|
|
|
|
|
with mock.patch("zproject.backends.logging.warning") as mock_warn:
|
|
|
|
with self.assertRaises(ZulipLDAPExceptionNoMatchingLDAPUser):
|
|
|
|
self.backend.django_to_ldap_username("shared_email@zulip.com")
|
|
|
|
mock_warn.assert_called_with("Multiple users with email shared_email@zulip.com found in LDAP.")
|
|
|
|
|
|
|
|
# Test on a weird case of a user whose uid is an email and his actual "mail"
|
|
|
|
# attribute is a different email address:
|
|
|
|
self.mock_ldap.directory['uid=some_user@organization_a.com,ou=users,dc=zulip,dc=com'] = {
|
|
|
|
"cn": ["Some User"],
|
|
|
|
"uid": ['some_user@organization_a.com'],
|
|
|
|
"mail": ["some_user@contactaddress.com"]
|
|
|
|
}
|
|
|
|
self.assertEqual(self.backend.django_to_ldap_username("some_user@contactaddress.com"),
|
|
|
|
"some_user@organization_a.com")
|
|
|
|
self.assertEqual(self.backend.django_to_ldap_username("some_user@organization_a.com"),
|
|
|
|
"some_user@organization_a.com")
|
|
|
|
|
|
|
|
# Configure email search for emails in the uid attribute:
|
|
|
|
with self.settings(AUTH_LDAP_REVERSE_EMAIL_SEARCH=LDAPSearch("ou=users,dc=zulip,dc=com",
|
|
|
|
ldap.SCOPE_ONELEVEL,
|
|
|
|
"(uid=%(email)s)")):
|
|
|
|
self.assertEqual(self.backend.django_to_ldap_username("newuser_email_as_uid@zulip.com"),
|
|
|
|
"newuser_email_as_uid@zulip.com")
|
|
|
|
|
|
|
|
self.mock_ldap.directory['uid="hamlet@test"@zulip.com",ou=users,dc=zulip,dc=com'] = {
|
|
|
|
"cn": ["King Hamlet"],
|
|
|
|
"uid": ['"hamlet@test"@zulip.com'],
|
|
|
|
}
|
|
|
|
username = self.backend.django_to_ldap_username('"hamlet@test"@zulip.com')
|
|
|
|
self.assertEqual(username, '"hamlet@test"@zulip.com')
|
|
|
|
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',
|
|
|
|
'zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_authenticate_to_ldap_via_email(self) -> None:
|
|
|
|
"""
|
|
|
|
With AUTH_LDAP_REVERSE_EMAIL_SEARCH configured, django_to_ldap_username
|
|
|
|
should be able to translate an email to ldap username,
|
|
|
|
and thus it should be possible to authenticate through user_profile.email.
|
|
|
|
"""
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
user_profile = self.example_user("hamlet")
|
|
|
|
password = "testpassword"
|
|
|
|
user_profile.set_password(password)
|
|
|
|
user_profile.save()
|
|
|
|
|
|
|
|
with self.settings(LDAP_EMAIL_ATTR='mail'):
|
|
|
|
self.assertEqual(
|
|
|
|
authenticate(username=user_profile.email, password=self.ldap_password(), realm=realm),
|
|
|
|
user_profile)
|
|
|
|
|
|
|
|
@override_settings(LDAP_EMAIL_ATTR='mail', LDAP_DEACTIVATE_NON_MATCHING_USERS=True)
|
|
|
|
def test_sync_user_from_ldap_with_email_attr(self) -> None:
|
|
|
|
"""In LDAP configurations with LDAP_EMAIL_ATTR configured and
|
|
|
|
LDAP_DEACTIVATE_NON_MATCHING_USERS set, a possible failure
|
|
|
|
mode if django_to_ldap_username isn't configured correctly is
|
|
|
|
all LDAP users having their accounts deactivated. Before the
|
|
|
|
introduction of AUTH_LDAP_REVERSE_EMAIL_SEARCH, this would happen
|
|
|
|
even in valid LDAP configurations using LDAP_EMAIL_ATTR.
|
|
|
|
|
|
|
|
This test confirms that such a failure mode doesn't happen with
|
|
|
|
a valid LDAP configuration.
|
|
|
|
"""
|
|
|
|
|
|
|
|
user_profile = self.example_user("hamlet")
|
|
|
|
with self.settings():
|
|
|
|
sync_user_from_ldap(user_profile, mock.Mock())
|
|
|
|
# Syncing didn't deactivate the user:
|
|
|
|
self.assertTrue(user_profile.is_active)
|
|
|
|
|
2019-01-12 17:15:14 +01:00
|
|
|
class ZulipLDAPTestCase(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def setUp(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
super().setUp()
|
|
|
|
|
|
|
|
self.init_default_ldap_database()
|
|
|
|
|
2017-05-07 19:39:30 +02:00
|
|
|
user_profile = self.example_user('hamlet')
|
2016-10-24 14:41:45 +02:00
|
|
|
self.setup_subdomain(user_profile)
|
|
|
|
self.backend = ZulipLDAPAuthBackend()
|
2019-10-16 18:10:40 +02:00
|
|
|
|
2019-01-16 09:59:01 +01:00
|
|
|
# Internally `_realm` and `_prereg_user` attributes are automatically set
|
|
|
|
# by the `authenticate()` method. But for testing the `get_or_build_user()`
|
|
|
|
# method separately, we need to set them manually.
|
2017-01-22 11:21:27 +01:00
|
|
|
self.backend._realm = get_realm('zulip')
|
2019-01-16 09:59:01 +01:00
|
|
|
self.backend._prereg_user = None
|
2016-10-24 14:41:45 +02:00
|
|
|
|
2017-11-19 04:02:03 +01:00
|
|
|
def setup_subdomain(self, user_profile: UserProfile) -> None:
|
2016-10-24 14:41:45 +02:00
|
|
|
realm = user_profile.realm
|
2016-10-26 18:13:43 +02:00
|
|
|
realm.string_id = 'zulip'
|
2016-10-24 14:41:45 +02:00
|
|
|
realm.save()
|
|
|
|
|
2019-01-12 17:15:14 +01:00
|
|
|
class TestLDAP(ZulipLDAPTestCase):
|
2018-08-03 20:05:19 +02:00
|
|
|
def test_generate_dev_ldap_dir(self) -> None:
|
2018-08-18 02:35:19 +02:00
|
|
|
ldap_dir = generate_dev_ldap_dir('A', 10)
|
2018-08-18 04:22:37 +02:00
|
|
|
self.assertEqual(len(ldap_dir), 10)
|
|
|
|
regex = re.compile(r'(uid\=)+[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+(\,ou\=users\,dc\=zulip\,dc\=com)')
|
2019-01-29 14:49:53 +01:00
|
|
|
common_attrs = ['cn', 'userPassword', 'phoneNumber', 'birthDate']
|
2018-08-18 04:22:37 +02:00
|
|
|
for key, value in ldap_dir.items():
|
|
|
|
self.assertTrue(regex.match(key))
|
2019-11-07 06:57:09 +01:00
|
|
|
self.assertCountEqual(list(value.keys()), common_attrs + ['uid', 'thumbnailPhoto', 'userAccountControl'])
|
2018-08-03 20:05:19 +02:00
|
|
|
|
2018-08-18 02:35:19 +02:00
|
|
|
ldap_dir = generate_dev_ldap_dir('b', 9)
|
2018-08-18 04:22:37 +02:00
|
|
|
self.assertEqual(len(ldap_dir), 9)
|
|
|
|
regex = re.compile(r'(uid\=)+[a-zA-Z0-9_.+-]+(\,ou\=users\,dc\=zulip\,dc\=com)')
|
|
|
|
for key, value in ldap_dir.items():
|
|
|
|
self.assertTrue(regex.match(key))
|
2019-11-07 06:57:09 +01:00
|
|
|
self.assertCountEqual(list(value.keys()), common_attrs + ['uid', 'jpegPhoto'])
|
2018-08-03 20:05:19 +02:00
|
|
|
|
2018-08-18 02:35:19 +02:00
|
|
|
ldap_dir = generate_dev_ldap_dir('c', 8)
|
2018-08-18 04:22:37 +02:00
|
|
|
self.assertEqual(len(ldap_dir), 8)
|
|
|
|
regex = re.compile(r'(uid\=)+[a-zA-Z0-9_.+-]+(\,ou\=users\,dc\=zulip\,dc\=com)')
|
|
|
|
for key, value in ldap_dir.items():
|
|
|
|
self.assertTrue(regex.match(key))
|
2019-11-07 06:57:09 +01:00
|
|
|
self.assertCountEqual(list(value.keys()), common_attrs + ['uid', 'email'])
|
2018-08-03 20:05:19 +02:00
|
|
|
|
2019-01-16 08:36:27 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2019-11-07 06:57:09 +01:00
|
|
|
def test_dev_ldap_fail_login(self) -> None:
|
2019-01-16 08:36:27 +01:00
|
|
|
# Tests that login with a substring of password fails. We had a bug in
|
|
|
|
# dev LDAP environment that allowed login via password substrings.
|
|
|
|
self.mock_ldap.directory = generate_dev_ldap_dir('B', 8)
|
|
|
|
with self.settings(
|
2019-11-07 06:57:09 +01:00
|
|
|
AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=zulip,dc=com",
|
|
|
|
ldap.SCOPE_ONELEVEL, "(uid=%(user)s)"),
|
|
|
|
AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch("ou=users,dc=zulip,dc=com",
|
|
|
|
ldap.SCOPE_ONELEVEL, "(email=%(email)s)"),
|
|
|
|
LDAP_APPEND_DOMAIN='zulip.com'
|
|
|
|
):
|
2019-05-08 02:18:34 +02:00
|
|
|
user_profile = self.backend.authenticate(username='ldapuser1', password='dapu',
|
2019-01-16 08:36:27 +01:00
|
|
|
realm=get_realm('zulip'))
|
|
|
|
|
|
|
|
assert(user_profile is None)
|
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username=self.example_email("hamlet"), password=self.ldap_password(),
|
2017-11-17 23:56:45 +01:00
|
|
|
realm=get_realm('zulip'))
|
2017-05-24 04:21:29 +02:00
|
|
|
|
|
|
|
assert(user_profile is not None)
|
2017-05-25 01:40:26 +02:00
|
|
|
self.assertEqual(user_profile.email, self.example_email("hamlet"))
|
2016-10-24 14:41:45 +02:00
|
|
|
|
2019-01-09 17:58:39 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_login_success_with_username(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username="hamlet", password=self.ldap_password(),
|
2019-01-09 17:58:39 +01:00
|
|
|
realm=get_realm('zulip'))
|
|
|
|
|
|
|
|
assert(user_profile is not None)
|
2019-10-16 18:10:40 +02:00
|
|
|
self.assertEqual(user_profile, self.example_user("hamlet"))
|
2019-01-09 17:58:39 +01:00
|
|
|
|
2017-09-10 17:25:24 +02:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success_with_email_attr(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_EMAIL_ATTR='mail'):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username=self.ldap_username("aaron"),
|
|
|
|
password=self.ldap_password(),
|
2017-11-17 23:56:45 +01:00
|
|
|
realm=get_realm('zulip'))
|
2017-09-10 17:25:24 +02:00
|
|
|
|
|
|
|
assert (user_profile is not None)
|
2019-10-16 18:10:40 +02:00
|
|
|
self.assertEqual(user_profile, self.example_user("aaron"))
|
2017-09-10 17:25:24 +02:00
|
|
|
|
2019-10-05 01:02:46 +02:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',
|
|
|
|
'zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_email_and_ldap_backends_together(self) -> None:
|
|
|
|
with self.settings(
|
|
|
|
LDAP_EMAIL_ATTR='mail',
|
|
|
|
AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch("ou=users,dc=zulip,dc=com",
|
|
|
|
ldap.SCOPE_ONELEVEL,
|
|
|
|
"(mail=%(email)s)"),
|
|
|
|
AUTH_LDAP_USERNAME_ATTR = "uid"
|
|
|
|
):
|
|
|
|
realm = get_realm('zulip')
|
|
|
|
self.assertEqual(email_belongs_to_ldap(realm, self.example_email("aaron")), True)
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = ZulipLDAPAuthBackend().authenticate(username=self.ldap_username("aaron"),
|
|
|
|
password=self.ldap_password(),
|
2019-10-05 01:02:46 +02:00
|
|
|
realm=realm)
|
|
|
|
self.assertEqual(user_profile, self.example_user("aaron"))
|
|
|
|
|
|
|
|
othello = self.example_user('othello')
|
|
|
|
password = "testpassword"
|
|
|
|
othello.set_password(password)
|
|
|
|
othello.save()
|
|
|
|
|
|
|
|
self.assertEqual(email_belongs_to_ldap(realm, othello.email), False)
|
|
|
|
user_profile = EmailAuthBackend().authenticate(username=othello.email, password=password,
|
|
|
|
realm=realm)
|
|
|
|
self.assertEqual(user_profile, othello)
|
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_wrong_password(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2019-05-08 02:18:34 +02:00
|
|
|
user = self.backend.authenticate(username=self.example_email("hamlet"), password='wrong',
|
2019-05-05 01:04:48 +02:00
|
|
|
realm=get_realm('zulip'))
|
2017-06-21 10:12:56 +02:00
|
|
|
self.assertIs(user, None)
|
2016-10-24 14:41:45 +02:00
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_nonexistent_user(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2019-10-18 18:25:51 +02:00
|
|
|
user = self.backend.authenticate(username='nonexistent@zulip.com', password=self.ldap_password(),
|
2019-05-05 01:04:48 +02:00
|
|
|
realm=get_realm('zulip'))
|
2017-06-21 10:12:56 +02:00
|
|
|
self.assertIs(user, None)
|
2016-10-24 14:41:45 +02:00
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_ldap_permissions(self) -> None:
|
2016-10-24 14:41:45 +02:00
|
|
|
backend = self.backend
|
|
|
|
self.assertFalse(backend.has_perm(None, None))
|
|
|
|
self.assertFalse(backend.has_module_perms(None, None))
|
|
|
|
self.assertTrue(backend.get_all_permissions(None, None) == set())
|
|
|
|
self.assertTrue(backend.get_group_permissions(None, None) == set())
|
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2019-10-06 00:32:25 +02:00
|
|
|
def test_user_email_from_ldapuser_with_append_domain(self) -> None:
|
2016-10-24 14:41:45 +02:00
|
|
|
backend = self.backend
|
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2019-10-06 00:32:25 +02:00
|
|
|
username = backend.user_email_from_ldapuser('this_argument_is_ignored',
|
|
|
|
_LDAPUser(self.backend, username='"hamlet@test"'))
|
2016-10-24 14:41:45 +02:00
|
|
|
self.assertEqual(username, '"hamlet@test"@zulip.com')
|
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2018-05-22 08:33:56 +02:00
|
|
|
def test_get_or_build_user_when_user_exists(self) -> None:
|
2017-11-05 11:49:43 +01:00
|
|
|
class _LDAPUser:
|
2016-10-24 14:41:45 +02:00
|
|
|
attrs = {'fn': ['Full Name'], 'sn': ['Short Name']}
|
|
|
|
|
|
|
|
backend = self.backend
|
2017-05-25 01:40:26 +02:00
|
|
|
email = self.example_email("hamlet")
|
2018-05-22 08:33:56 +02:00
|
|
|
user_profile, created = backend.get_or_build_user(str(email), _LDAPUser())
|
2016-10-24 14:41:45 +02:00
|
|
|
self.assertFalse(created)
|
|
|
|
self.assertEqual(user_profile.email, email)
|
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2018-05-22 08:33:56 +02:00
|
|
|
def test_get_or_build_user_when_user_does_not_exist(self) -> None:
|
2017-11-05 11:49:43 +01:00
|
|
|
class _LDAPUser:
|
2016-10-24 14:41:45 +02:00
|
|
|
attrs = {'fn': ['Full Name'], 'sn': ['Short Name']}
|
|
|
|
|
|
|
|
ldap_user_attr_map = {'full_name': 'fn', 'short_name': 'sn'}
|
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map):
|
|
|
|
backend = self.backend
|
2019-10-16 18:10:40 +02:00
|
|
|
email = 'newuser@zulip.com'
|
2018-05-22 08:33:56 +02:00
|
|
|
user_profile, created = backend.get_or_build_user(email, _LDAPUser())
|
2016-10-24 14:41:45 +02:00
|
|
|
self.assertTrue(created)
|
|
|
|
self.assertEqual(user_profile.email, email)
|
|
|
|
self.assertEqual(user_profile.full_name, 'Full Name')
|
|
|
|
|
2017-02-08 05:04:14 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2018-05-22 08:33:56 +02:00
|
|
|
def test_get_or_build_user_when_user_has_invalid_name(self) -> None:
|
2017-11-05 11:49:43 +01:00
|
|
|
class _LDAPUser:
|
2017-02-08 05:04:14 +01:00
|
|
|
attrs = {'fn': ['<invalid name>'], 'sn': ['Short Name']}
|
|
|
|
|
|
|
|
ldap_user_attr_map = {'full_name': 'fn', 'short_name': 'sn'}
|
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map):
|
|
|
|
backend = self.backend
|
|
|
|
email = 'nonexisting@zulip.com'
|
|
|
|
with self.assertRaisesRegex(Exception, "Invalid characters in name!"):
|
2018-05-22 08:33:56 +02:00
|
|
|
backend.get_or_build_user(email, _LDAPUser())
|
2017-02-08 05:04:14 +01:00
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2018-05-22 08:33:56 +02:00
|
|
|
def test_get_or_build_user_when_realm_is_deactivated(self) -> None:
|
2017-11-05 11:49:43 +01:00
|
|
|
class _LDAPUser:
|
2016-10-24 14:41:45 +02:00
|
|
|
attrs = {'fn': ['Full Name'], 'sn': ['Short Name']}
|
|
|
|
|
|
|
|
ldap_user_attr_map = {'full_name': 'fn', 'short_name': 'sn'}
|
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map):
|
|
|
|
backend = self.backend
|
|
|
|
email = 'nonexisting@zulip.com'
|
2017-01-22 11:21:27 +01:00
|
|
|
do_deactivate_realm(backend._realm)
|
2016-12-16 02:11:42 +01:00
|
|
|
with self.assertRaisesRegex(Exception, 'Realm has been deactivated'):
|
2018-05-22 08:33:56 +02:00
|
|
|
backend.get_or_build_user(email, _LDAPUser())
|
2016-10-24 14:41:45 +02:00
|
|
|
|
2017-09-10 17:25:24 +02:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2018-05-22 08:33:56 +02:00
|
|
|
def test_get_or_build_user_when_ldap_has_no_email_attr(self) -> None:
|
2017-11-05 11:49:43 +01:00
|
|
|
class _LDAPUser:
|
2017-09-10 17:25:24 +02:00
|
|
|
attrs = {'fn': ['Full Name'], 'sn': ['Short Name']}
|
|
|
|
|
|
|
|
nonexisting_attr = 'email'
|
|
|
|
with self.settings(LDAP_EMAIL_ATTR=nonexisting_attr):
|
|
|
|
backend = self.backend
|
|
|
|
email = 'nonexisting@zulip.com'
|
|
|
|
with self.assertRaisesRegex(Exception, 'LDAP user doesn\'t have the needed email attribute'):
|
2018-05-22 08:33:56 +02:00
|
|
|
backend.get_or_build_user(email, _LDAPUser())
|
2017-09-10 17:25:24 +02:00
|
|
|
|
2019-03-10 08:57:19 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_get_or_build_user_email(self) -> None:
|
|
|
|
class _LDAPUser:
|
|
|
|
attrs = {'fn': ['Test User']}
|
|
|
|
|
|
|
|
ldap_user_attr_map = {'full_name': 'fn'}
|
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map):
|
|
|
|
realm = self.backend._realm
|
|
|
|
realm.emails_restricted_to_domains = False
|
|
|
|
realm.disallow_disposable_email_addresses = True
|
|
|
|
realm.save()
|
|
|
|
|
|
|
|
email = 'spam@mailnator.com'
|
|
|
|
with self.assertRaisesRegex(ZulipLDAPException, 'Email validation failed.'):
|
|
|
|
self.backend.get_or_build_user(email, _LDAPUser())
|
|
|
|
|
|
|
|
realm.emails_restricted_to_domains = True
|
|
|
|
realm.save(update_fields=['emails_restricted_to_domains'])
|
|
|
|
|
|
|
|
email = 'spam+spam@mailnator.com'
|
|
|
|
with self.assertRaisesRegex(ZulipLDAPException, 'Email validation failed.'):
|
|
|
|
self.backend.get_or_build_user(email, _LDAPUser())
|
|
|
|
|
|
|
|
email = 'spam@acme.com'
|
|
|
|
with self.assertRaisesRegex(ZulipLDAPException, "This email domain isn't allowed in this organization."):
|
|
|
|
self.backend.get_or_build_user(email, _LDAPUser())
|
|
|
|
|
2019-01-10 18:25:34 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_get_or_build_user_when_ldap_has_no_full_name_mapping(self) -> None:
|
|
|
|
class _LDAPUser:
|
|
|
|
attrs = {'fn': ['Full Name'], 'sn': ['Short Name']}
|
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={}):
|
|
|
|
backend = self.backend
|
|
|
|
email = 'nonexisting@zulip.com'
|
|
|
|
with self.assertRaisesRegex(Exception, "Missing required mapping for user's full name"):
|
|
|
|
backend.get_or_build_user(email, _LDAPUser())
|
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_when_domain_does_not_match(self) -> None:
|
2017-09-27 21:30:53 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='acme.com'):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username=self.example_email("hamlet"),
|
|
|
|
password=self.ldap_password(),
|
2019-05-05 01:04:48 +02:00
|
|
|
realm=get_realm('zulip'))
|
2017-09-27 21:30:53 +02:00
|
|
|
self.assertIs(user_profile, None)
|
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2019-03-04 13:16:00 +01:00
|
|
|
def test_login_success_with_different_subdomain(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
ldap_user_attr_map = {'full_name': 'cn', 'short_name': 'sn'}
|
2019-03-10 08:57:19 +01:00
|
|
|
|
|
|
|
Realm.objects.create(string_id='acme')
|
2016-10-24 14:41:45 +02:00
|
|
|
with self.settings(
|
|
|
|
LDAP_APPEND_DOMAIN='zulip.com',
|
2019-03-04 13:16:00 +01:00
|
|
|
AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username=self.example_email('hamlet'),
|
|
|
|
password=self.ldap_password(),
|
2019-03-10 08:57:19 +01:00
|
|
|
realm=get_realm('acme'))
|
2019-03-04 13:16:00 +01:00
|
|
|
self.assertEqual(user_profile.email, self.example_email('hamlet'))
|
2016-10-24 14:41:45 +02:00
|
|
|
|
2016-11-07 00:09:21 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_success_with_valid_subdomain(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username=self.example_email("hamlet"),
|
|
|
|
password=self.ldap_password(),
|
2017-11-17 23:56:45 +01:00
|
|
|
realm=get_realm('zulip'))
|
2017-05-24 04:21:29 +02:00
|
|
|
assert(user_profile is not None)
|
2017-05-25 01:40:26 +02:00
|
|
|
self.assertEqual(user_profile.email, self.example_email("hamlet"))
|
2016-10-26 12:46:51 +02:00
|
|
|
|
2017-11-18 01:07:20 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_login_failure_due_to_deactivated_user(self) -> None:
|
2017-11-18 01:07:20 +01:00
|
|
|
user_profile = self.example_user("hamlet")
|
|
|
|
do_deactivate_user(user_profile)
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username=self.example_email("hamlet"),
|
|
|
|
password=self.ldap_password(),
|
2017-11-18 01:07:20 +01:00
|
|
|
realm=get_realm('zulip'))
|
|
|
|
self.assertIs(user_profile, None)
|
|
|
|
|
2017-01-22 11:21:27 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2018-12-12 19:46:37 +01:00
|
|
|
@override_settings(AUTH_LDAP_USER_ATTR_MAP={
|
|
|
|
"full_name": "cn",
|
2019-10-16 18:10:40 +02:00
|
|
|
"avatar": "jpegPhoto",
|
2018-12-12 19:46:37 +01:00
|
|
|
})
|
2019-03-10 08:57:19 +01:00
|
|
|
def test_login_success_when_user_does_not_exist_with_valid_subdomain(self) -> None:
|
|
|
|
RealmDomain.objects.create(realm=self.backend._realm, domain='acme.com')
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='acme.com'):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username='newuser@acme.com',
|
|
|
|
password=self.ldap_password(),
|
2017-11-17 23:56:45 +01:00
|
|
|
realm=get_realm('zulip'))
|
2017-05-24 04:21:29 +02:00
|
|
|
assert(user_profile is not None)
|
2019-10-16 18:10:40 +02:00
|
|
|
self.assertEqual(user_profile.email, 'newuser@acme.com')
|
|
|
|
self.assertEqual(user_profile.full_name, 'New LDAP fullname')
|
2017-01-22 11:21:27 +01:00
|
|
|
self.assertEqual(user_profile.realm.string_id, 'zulip')
|
|
|
|
|
2018-12-12 19:46:37 +01:00
|
|
|
# Verify avatar gets created
|
|
|
|
self.assertEqual(user_profile.avatar_source, UserProfile.AVATAR_FROM_USER)
|
|
|
|
result = self.client_get(avatar_url(user_profile))
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
|
2019-01-10 18:25:34 +01:00
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_login_success_when_user_does_not_exist_with_split_full_name_mapping(self) -> None:
|
|
|
|
with self.settings(
|
2019-03-10 08:57:19 +01:00
|
|
|
LDAP_APPEND_DOMAIN='zulip.com',
|
2019-10-16 18:10:40 +02:00
|
|
|
AUTH_LDAP_USER_ATTR_MAP={'first_name': 'sn', 'last_name': 'cn'}):
|
2019-10-18 18:25:51 +02:00
|
|
|
user_profile = self.backend.authenticate(username='newuser_splitname@zulip.com',
|
|
|
|
password=self.ldap_password(),
|
2019-01-10 18:25:34 +01:00
|
|
|
realm=get_realm('zulip'))
|
|
|
|
assert(user_profile is not None)
|
2019-10-16 18:10:40 +02:00
|
|
|
self.assertEqual(user_profile.email, 'newuser_splitname@zulip.com')
|
|
|
|
self.assertEqual(user_profile.full_name, 'First Last')
|
2019-01-10 18:25:34 +01:00
|
|
|
self.assertEqual(user_profile.realm.string_id, 'zulip')
|
|
|
|
|
2019-01-12 17:15:14 +01:00
|
|
|
class TestZulipLDAPUserPopulator(ZulipLDAPTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_authenticate(self) -> None:
|
2016-10-26 12:46:51 +02:00
|
|
|
backend = ZulipLDAPUserPopulator()
|
2019-10-18 18:25:51 +02:00
|
|
|
result = backend.authenticate(username=self.example_email("hamlet"), password=self.ldap_password(),
|
2019-05-05 01:04:48 +02:00
|
|
|
realm=get_realm('zulip'))
|
2016-10-26 12:46:51 +02:00
|
|
|
self.assertIs(result, None)
|
2016-10-26 12:39:09 +02:00
|
|
|
|
2019-01-12 17:15:14 +01:00
|
|
|
def perform_ldap_sync(self, user_profile: UserProfile) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com'):
|
2019-08-26 21:13:23 +02:00
|
|
|
result = sync_user_from_ldap(user_profile, mock.Mock())
|
2019-01-12 17:15:14 +01:00
|
|
|
self.assertTrue(result)
|
|
|
|
|
2019-09-04 10:11:25 +02:00
|
|
|
@mock.patch("zproject.backends.do_deactivate_user")
|
2019-10-25 02:26:05 +02:00
|
|
|
def test_ldaperror_doesnt_deactivate_user(self, mock_deactivate: mock.MagicMock) -> None:
|
2019-09-04 10:11:25 +02:00
|
|
|
"""
|
|
|
|
This is a test for a bug where failure to connect to LDAP in sync_user_from_ldap
|
|
|
|
(e.g. due to invalid credentials) would cause the user to be deactivated if
|
|
|
|
LDAP_DEACTIVATE_NON_MATCHING_USERS was True.
|
|
|
|
Details: https://github.com/zulip/zulip/issues/13130
|
|
|
|
"""
|
|
|
|
with self.settings(
|
|
|
|
LDAP_DEACTIVATE_NON_MATCHING_USERS=True,
|
|
|
|
LDAP_APPEND_DOMAIN='zulip.com',
|
|
|
|
AUTH_LDAP_BIND_PASSWORD='wrongpass'):
|
2019-10-25 02:26:05 +02:00
|
|
|
with self.assertRaises(ldap.INVALID_CREDENTIALS):
|
|
|
|
sync_user_from_ldap(self.example_user('hamlet'), mock.Mock())
|
|
|
|
mock_deactivate.assert_not_called()
|
|
|
|
|
|
|
|
# Make sure other types of LDAPError won't cause deactivation either:
|
|
|
|
with mock.patch.object(_LDAPUser, '_get_or_create_user', side_effect=ldap.LDAPError):
|
2019-09-04 10:11:25 +02:00
|
|
|
with self.assertRaises(PopulateUserLDAPError):
|
2019-08-26 21:13:23 +02:00
|
|
|
sync_user_from_ldap(self.example_user('hamlet'), mock.Mock())
|
2019-09-04 10:11:25 +02:00
|
|
|
mock_deactivate.assert_not_called()
|
|
|
|
|
2019-11-09 00:27:18 +01:00
|
|
|
@override_settings(LDAP_EMAIL_ATTR="mail")
|
2019-10-25 02:26:05 +02:00
|
|
|
def test_populate_user_returns_none(self) -> None:
|
2019-11-09 00:27:18 +01:00
|
|
|
with mock.patch.object(ZulipLDAPUser, 'populate_user', return_value=None):
|
2019-10-25 02:26:05 +02:00
|
|
|
with self.assertRaises(PopulateUserLDAPError):
|
|
|
|
sync_user_from_ldap(self.example_user('hamlet'), mock.Mock())
|
|
|
|
|
2019-01-12 17:15:14 +01:00
|
|
|
def test_update_full_name(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
self.change_ldap_user_attr('hamlet', 'cn', 'New Name')
|
2019-01-12 17:15:14 +01:00
|
|
|
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
self.assertEqual(hamlet.full_name, 'New Name')
|
|
|
|
|
|
|
|
def test_update_split_full_name(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
self.change_ldap_user_attr('hamlet', 'cn', 'Name')
|
|
|
|
self.change_ldap_user_attr('hamlet', 'sn', 'Full')
|
2019-01-12 17:15:14 +01:00
|
|
|
|
2019-10-16 18:10:40 +02:00
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'first_name': 'sn',
|
|
|
|
'last_name': 'cn'}):
|
2019-01-12 17:15:14 +01:00
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
self.assertEqual(hamlet.full_name, 'Full Name')
|
|
|
|
|
|
|
|
def test_same_full_name(self) -> None:
|
|
|
|
with mock.patch('zerver.lib.actions.do_change_full_name') as fn:
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
fn.assert_not_called()
|
|
|
|
|
|
|
|
def test_too_short_name(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
self.change_ldap_user_attr('hamlet', 'cn', 'a')
|
2019-01-12 17:15:14 +01:00
|
|
|
|
|
|
|
with self.assertRaises(ZulipLDAPException):
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
|
|
|
|
def test_deactivate_user(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
self.change_ldap_user_attr('hamlet', 'userAccountControl', '2')
|
2019-01-12 17:15:14 +01:00
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
|
|
|
'userAccountControl': 'userAccountControl'}):
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
self.assertFalse(hamlet.is_active)
|
|
|
|
|
2019-08-23 22:02:30 +02:00
|
|
|
@mock.patch("zproject.backends.ZulipLDAPAuthBackendBase.sync_full_name_from_ldap")
|
|
|
|
def test_dont_sync_disabled_ldap_user(self, fake_sync: mock.MagicMock) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
self.change_ldap_user_attr('hamlet', 'userAccountControl', '2')
|
2019-08-23 22:02:30 +02:00
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
|
|
|
'userAccountControl': 'userAccountControl'}):
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
fake_sync.assert_not_called()
|
|
|
|
|
2019-01-12 17:15:14 +01:00
|
|
|
def test_reactivate_user(self) -> None:
|
|
|
|
do_deactivate_user(self.example_user('hamlet'))
|
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
|
|
|
'userAccountControl': 'userAccountControl'}):
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
self.assertTrue(hamlet.is_active)
|
|
|
|
|
2019-11-09 00:27:18 +01:00
|
|
|
def test_user_in_multiple_realms(self) -> None:
|
|
|
|
test_realm = do_create_realm('test', 'test', False)
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
hamlet2 = do_create_user(hamlet.email, None, test_realm, hamlet.full_name, hamlet.short_name)
|
|
|
|
|
|
|
|
self.change_ldap_user_attr('hamlet', 'cn', 'Second Hamlet')
|
|
|
|
expected_call_args = [hamlet2, 'Second Hamlet', None]
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn'}):
|
|
|
|
with mock.patch('zerver.lib.actions.do_change_full_name') as f:
|
|
|
|
self.perform_ldap_sync(hamlet2)
|
|
|
|
f.assert_called_once_with(*expected_call_args)
|
|
|
|
|
|
|
|
# Get the updated model and make sure the full name is changed correctly:
|
|
|
|
hamlet2 = get_user(hamlet.email, test_realm)
|
|
|
|
self.assertEqual(hamlet2.full_name, "Second Hamlet")
|
|
|
|
# Now get the original hamlet and make he still has his name unchanged:
|
|
|
|
hamlet = get_user(hamlet.email, get_realm("zulip"))
|
|
|
|
self.assertEqual(hamlet.full_name, "King Hamlet")
|
|
|
|
|
2019-08-26 21:13:23 +02:00
|
|
|
def test_user_not_found_in_ldap(self) -> None:
|
|
|
|
with self.settings(
|
|
|
|
LDAP_DEACTIVATE_NON_MATCHING_USERS=False,
|
2019-10-16 18:10:40 +02:00
|
|
|
LDAP_APPEND_DOMAIN='zulip.com'):
|
|
|
|
othello = self.example_user("othello") # othello isn't in our test directory
|
2019-08-26 21:13:23 +02:00
|
|
|
mock_logger = mock.MagicMock()
|
2019-10-16 18:10:40 +02:00
|
|
|
result = sync_user_from_ldap(othello, mock_logger)
|
|
|
|
mock_logger.warning.assert_called_once_with("Did not find %s in LDAP." % (othello.email,))
|
2019-08-26 21:13:23 +02:00
|
|
|
self.assertFalse(result)
|
|
|
|
|
2019-10-16 18:10:40 +02:00
|
|
|
do_deactivate_user(othello)
|
2019-08-26 21:13:23 +02:00
|
|
|
mock_logger = mock.MagicMock()
|
2019-10-16 18:10:40 +02:00
|
|
|
result = sync_user_from_ldap(othello, mock_logger)
|
2019-08-26 21:13:23 +02:00
|
|
|
self.assertEqual(mock_logger.method_calls, []) # In this case the logger shouldn't be used.
|
|
|
|
self.assertFalse(result)
|
|
|
|
|
2019-01-12 17:15:14 +01:00
|
|
|
def test_update_user_avatar(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
# Hamlet has jpegPhoto set in our test directory by default.
|
2019-01-12 17:15:14 +01:00
|
|
|
with mock.patch('zerver.lib.upload.upload_avatar_image') as fn, \
|
|
|
|
self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
2019-10-16 18:10:40 +02:00
|
|
|
'avatar': 'jpegPhoto'}):
|
2019-01-12 17:15:14 +01:00
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
fn.assert_called_once()
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
self.assertEqual(hamlet.avatar_source, UserProfile.AVATAR_FROM_USER)
|
2019-06-28 00:10:58 +02:00
|
|
|
|
|
|
|
# Verify that the next time we do an LDAP sync, we don't
|
|
|
|
# end up updating this user's avatar again if the LDAP
|
|
|
|
# data hasn't changed.
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
fn.assert_called_once()
|
|
|
|
|
2019-10-16 18:10:40 +02:00
|
|
|
# Now verify that if we do change the jpegPhoto image, we
|
2019-06-28 00:10:58 +02:00
|
|
|
# will upload a new avatar.
|
2019-10-16 18:10:40 +02:00
|
|
|
self.change_ldap_user_attr('hamlet', 'jpegPhoto', static_path("images/logo/zulip-icon-512x512.png"),
|
|
|
|
binary=True)
|
2019-06-28 00:10:58 +02:00
|
|
|
with mock.patch('zerver.lib.upload.upload_avatar_image') as fn, \
|
|
|
|
self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
2019-10-16 18:10:40 +02:00
|
|
|
'avatar': 'jpegPhoto'}):
|
2019-06-28 00:10:58 +02:00
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
fn.assert_called_once()
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
self.assertEqual(hamlet.avatar_source, UserProfile.AVATAR_FROM_USER)
|
2019-01-12 17:15:14 +01:00
|
|
|
|
2019-06-07 23:36:19 +02:00
|
|
|
@use_s3_backend
|
|
|
|
def test_update_user_avatar_for_s3(self) -> None:
|
|
|
|
bucket = create_s3_buckets(settings.S3_AVATAR_BUCKET)[0]
|
2019-10-16 18:10:40 +02:00
|
|
|
test_image_file = get_test_image_file('img.png').name
|
|
|
|
with open(test_image_file, 'rb') as f:
|
2019-07-14 21:37:08 +02:00
|
|
|
test_image_data = f.read()
|
2019-10-16 18:10:40 +02:00
|
|
|
self.change_ldap_user_attr('hamlet', 'jpegPhoto', test_image_data)
|
2019-06-07 23:36:19 +02:00
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
2019-10-16 18:10:40 +02:00
|
|
|
'avatar': 'jpegPhoto'}):
|
2019-06-07 23:36:19 +02:00
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
path_id = user_avatar_path(hamlet)
|
|
|
|
original_image_path_id = path_id + ".original"
|
|
|
|
medium_path_id = path_id + "-medium.png"
|
|
|
|
|
|
|
|
original_image_key = bucket.get_key(original_image_path_id)
|
|
|
|
medium_image_key = bucket.get_key(medium_path_id)
|
|
|
|
|
|
|
|
image_data = original_image_key.get_contents_as_string()
|
|
|
|
self.assertEqual(image_data, test_image_data)
|
|
|
|
|
|
|
|
test_medium_image_data = resize_avatar(test_image_data, MEDIUM_AVATAR_SIZE)
|
|
|
|
medium_image_data = medium_image_key.get_contents_as_string()
|
|
|
|
self.assertEqual(medium_image_data, test_medium_image_data)
|
|
|
|
|
2019-10-16 18:10:40 +02:00
|
|
|
# Try to use invalid data as the image:
|
|
|
|
self.change_ldap_user_attr('hamlet', 'jpegPhoto', b'00' + test_image_data)
|
2019-06-07 23:36:19 +02:00
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
2019-10-16 18:10:40 +02:00
|
|
|
'avatar': 'jpegPhoto'}):
|
2019-06-07 23:36:19 +02:00
|
|
|
with mock.patch('logging.warning') as mock_warning:
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
mock_warning.assert_called_once_with(
|
2019-10-16 18:10:40 +02:00
|
|
|
'Could not parse jpegPhoto field for user %s' % (hamlet.id,))
|
2019-06-07 23:36:19 +02:00
|
|
|
|
2019-01-13 13:53:52 +01:00
|
|
|
def test_deactivate_non_matching_users(self) -> None:
|
|
|
|
with self.settings(LDAP_APPEND_DOMAIN='zulip.com',
|
|
|
|
LDAP_DEACTIVATE_NON_MATCHING_USERS=True):
|
2019-10-16 18:10:40 +02:00
|
|
|
# othello isn't in our test directory
|
|
|
|
result = sync_user_from_ldap(self.example_user('othello'), mock.Mock())
|
2019-01-13 13:53:52 +01:00
|
|
|
|
2019-08-26 21:13:23 +02:00
|
|
|
self.assertTrue(result)
|
2019-10-16 18:10:40 +02:00
|
|
|
othello = self.example_user('othello')
|
|
|
|
self.assertFalse(othello.is_active)
|
2019-01-13 13:53:52 +01:00
|
|
|
|
2019-01-29 13:39:21 +01:00
|
|
|
def test_update_custom_profile_field(self) -> None:
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
2019-10-16 18:10:40 +02:00
|
|
|
'custom_profile_field__phone_number': 'homePhone',
|
2019-01-29 13:39:21 +01:00
|
|
|
'custom_profile_field__birthday': 'birthDate'}):
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
test_data = [
|
|
|
|
{
|
|
|
|
'field_name': 'Phone number',
|
|
|
|
'expected_value': '123456789',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'field_name': 'Birthday',
|
|
|
|
'expected_value': '1900-09-08',
|
|
|
|
},
|
|
|
|
]
|
|
|
|
for test_case in test_data:
|
|
|
|
field = CustomProfileField.objects.get(realm=hamlet.realm, name=test_case['field_name'])
|
|
|
|
field_value = CustomProfileFieldValue.objects.get(user_profile=hamlet, field=field).value
|
|
|
|
self.assertEqual(field_value, test_case['expected_value'])
|
|
|
|
|
|
|
|
def test_update_non_existent_profile_field(self) -> None:
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
2019-10-16 18:10:40 +02:00
|
|
|
'custom_profile_field__non_existent': 'homePhone'}):
|
2019-01-29 13:39:21 +01:00
|
|
|
with self.assertRaisesRegex(ZulipLDAPException, 'Custom profile field with name non_existent not found'):
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
|
|
|
|
def test_update_custom_profile_field_invalid_data(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
self.change_ldap_user_attr('hamlet', 'birthDate', '9999')
|
|
|
|
|
2019-01-29 13:39:21 +01:00
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
|
|
|
'custom_profile_field__birthday': 'birthDate'}):
|
|
|
|
with self.assertRaisesRegex(ZulipLDAPException, 'Invalid data for birthday field'):
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
|
|
|
|
def test_update_custom_profile_field_no_mapping(self) -> None:
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
no_op_field = CustomProfileField.objects.get(realm=hamlet.realm, name='Phone number')
|
|
|
|
expected_value = CustomProfileFieldValue.objects.get(user_profile=hamlet, field=no_op_field).value
|
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
|
|
|
'custom_profile_field__birthday': 'birthDate'}):
|
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
|
|
|
|
actual_value = CustomProfileFieldValue.objects.get(user_profile=hamlet, field=no_op_field).value
|
|
|
|
self.assertEqual(actual_value, expected_value)
|
|
|
|
|
|
|
|
def test_update_custom_profile_field_no_update(self) -> None:
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
phone_number_field = CustomProfileField.objects.get(realm=hamlet.realm, name='Phone number')
|
|
|
|
birthday_field = CustomProfileField.objects.get(realm=hamlet.realm, name='Birthday')
|
|
|
|
phone_number_field_value = CustomProfileFieldValue.objects.get(user_profile=hamlet,
|
|
|
|
field=phone_number_field)
|
2019-10-16 18:10:40 +02:00
|
|
|
phone_number_field_value.value = '123456789'
|
2019-01-29 13:39:21 +01:00
|
|
|
phone_number_field_value.save(update_fields=['value'])
|
|
|
|
expected_call_args = [hamlet, [
|
|
|
|
{
|
|
|
|
'id': birthday_field.id,
|
2019-10-16 18:10:40 +02:00
|
|
|
'value': '1900-09-08',
|
2019-01-29 13:39:21 +01:00
|
|
|
},
|
|
|
|
]]
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
|
|
|
'custom_profile_field__birthday': 'birthDate',
|
2019-10-16 18:10:40 +02:00
|
|
|
'custom_profile_field__phone_number': 'homePhone'}):
|
2019-10-01 04:22:50 +02:00
|
|
|
with mock.patch('zproject.backends.do_update_user_custom_profile_data_if_changed') as f:
|
2019-01-29 13:39:21 +01:00
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
f.assert_called_once_with(*expected_call_args)
|
|
|
|
|
2019-03-05 09:40:40 +01:00
|
|
|
def test_update_custom_profile_field_not_present_in_ldap(self) -> None:
|
|
|
|
hamlet = self.example_user('hamlet')
|
|
|
|
no_op_field = CustomProfileField.objects.get(realm=hamlet.realm, name='Birthday')
|
|
|
|
expected_value = CustomProfileFieldValue.objects.get(user_profile=hamlet, field=no_op_field).value
|
|
|
|
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
2019-10-16 18:10:40 +02:00
|
|
|
'custom_profile_field__birthday': 'nonExistantAttr'}):
|
2019-03-05 09:40:40 +01:00
|
|
|
self.perform_ldap_sync(self.example_user('hamlet'))
|
|
|
|
|
|
|
|
actual_value = CustomProfileFieldValue.objects.get(user_profile=hamlet, field=no_op_field).value
|
|
|
|
self.assertEqual(actual_value, expected_value)
|
|
|
|
|
2019-03-09 08:32:06 +01:00
|
|
|
class TestQueryLDAP(ZulipLDAPTestCase):
|
|
|
|
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',))
|
|
|
|
def test_ldap_not_configured(self) -> None:
|
|
|
|
values = query_ldap(self.example_email('hamlet'))
|
|
|
|
self.assertEqual(values, ['LDAP backend not configured on this server.'])
|
|
|
|
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_user_not_present(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
# othello doesn't have an entry in our test directory
|
|
|
|
values = query_ldap(self.example_email('othello'))
|
2019-03-09 08:32:06 +01:00
|
|
|
self.assertEqual(values, ['No such user found'])
|
|
|
|
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_normal_query(self) -> None:
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
|
|
|
'short_name': 'sn',
|
2019-10-23 00:15:29 +02:00
|
|
|
'avatar': 'jpegPhoto',
|
2019-03-09 08:32:06 +01:00
|
|
|
'custom_profile_field__birthday': 'birthDate',
|
2019-10-23 00:15:29 +02:00
|
|
|
'custom_profile_field__phone_number': 'nonExistentAttr'
|
|
|
|
}):
|
2019-03-09 08:32:06 +01:00
|
|
|
values = query_ldap(self.example_email('hamlet'))
|
2019-10-23 00:15:29 +02:00
|
|
|
self.assertEqual(len(values), 5)
|
2019-03-09 08:32:06 +01:00
|
|
|
self.assertIn('full_name: King Hamlet', values)
|
|
|
|
self.assertIn('short_name: Hamlet', values)
|
|
|
|
self.assertIn('avatar: (An avatar image file)', values)
|
2019-10-23 00:15:29 +02:00
|
|
|
self.assertIn('custom_profile_field__birthday: 1900-09-08', values)
|
2019-03-09 08:32:06 +01:00
|
|
|
self.assertIn('custom_profile_field__phone_number: LDAP field not present', values)
|
|
|
|
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
|
|
|
def test_query_email_attr(self) -> None:
|
|
|
|
with self.settings(AUTH_LDAP_USER_ATTR_MAP={'full_name': 'cn',
|
|
|
|
'short_name': 'sn'},
|
2019-10-23 00:15:29 +02:00
|
|
|
LDAP_EMAIL_ATTR='mail'):
|
|
|
|
# This will look up the user by email in our test dictionary,
|
|
|
|
# should successfully find hamlet's ldap entry.
|
2019-03-09 08:32:06 +01:00
|
|
|
values = query_ldap(self.example_email('hamlet'))
|
|
|
|
self.assertEqual(len(values), 3)
|
|
|
|
self.assertIn('full_name: King Hamlet', values)
|
|
|
|
self.assertIn('short_name: Hamlet', values)
|
2019-10-23 00:15:29 +02:00
|
|
|
self.assertIn('email: hamlet@zulip.com', values)
|
2019-03-09 08:32:06 +01:00
|
|
|
|
2016-10-26 12:39:09 +02:00
|
|
|
class TestZulipAuthMixin(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_get_user(self) -> None:
|
2016-10-26 12:39:09 +02:00
|
|
|
backend = ZulipAuthMixin()
|
|
|
|
result = backend.get_user(11111)
|
|
|
|
self.assertIs(result, None)
|
2016-10-26 13:50:00 +02:00
|
|
|
|
|
|
|
class TestPasswordAuthEnabled(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_password_auth_enabled_for_ldap(self) -> None:
|
2016-10-26 13:50:00 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)):
|
2017-01-08 20:24:05 +01:00
|
|
|
realm = Realm.objects.get(string_id='zulip')
|
2016-10-26 13:50:00 +02:00
|
|
|
self.assertTrue(password_auth_enabled(realm))
|
2016-10-26 14:40:14 +02:00
|
|
|
|
2017-09-15 16:59:03 +02:00
|
|
|
class TestRequireEmailFormatUsernames(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_require_email_format_usernames_for_ldap_with_append_domain(
|
|
|
|
self) -> None:
|
2017-09-15 16:59:03 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',),
|
|
|
|
LDAP_APPEND_DOMAIN="zulip.com"):
|
|
|
|
realm = Realm.objects.get(string_id='zulip')
|
|
|
|
self.assertFalse(require_email_format_usernames(realm))
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_require_email_format_usernames_for_ldap_with_email_attr(
|
|
|
|
self) -> None:
|
2017-09-15 16:59:03 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',),
|
|
|
|
LDAP_EMAIL_ATTR="email"):
|
|
|
|
realm = Realm.objects.get(string_id='zulip')
|
|
|
|
self.assertFalse(require_email_format_usernames(realm))
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_require_email_format_usernames_for_email_only(self) -> None:
|
2017-09-15 16:59:03 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',)):
|
|
|
|
realm = Realm.objects.get(string_id='zulip')
|
|
|
|
self.assertTrue(require_email_format_usernames(realm))
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_require_email_format_usernames_for_email_and_ldap_with_email_attr(
|
|
|
|
self) -> None:
|
2017-09-15 16:59:03 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',
|
|
|
|
'zproject.backends.ZulipLDAPAuthBackend'),
|
|
|
|
LDAP_EMAIL_ATTR="email"):
|
|
|
|
realm = Realm.objects.get(string_id='zulip')
|
|
|
|
self.assertFalse(require_email_format_usernames(realm))
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_require_email_format_usernames_for_email_and_ldap_with_append_email(
|
|
|
|
self) -> None:
|
2017-09-15 16:59:03 +02:00
|
|
|
with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',
|
|
|
|
'zproject.backends.ZulipLDAPAuthBackend'),
|
|
|
|
LDAP_APPEND_DOMAIN="zulip.com"):
|
|
|
|
realm = Realm.objects.get(string_id='zulip')
|
|
|
|
self.assertFalse(require_email_format_usernames(realm))
|
|
|
|
|
2016-10-26 14:40:14 +02:00
|
|
|
class TestMaybeSendToRegistration(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_sso_only_when_preregistration_user_does_not_exist(self) -> None:
|
2016-10-26 14:40:14 +02:00
|
|
|
rf = RequestFactory()
|
|
|
|
request = rf.get('/')
|
|
|
|
request.session = {}
|
|
|
|
request.user = None
|
|
|
|
|
|
|
|
# Creating a mock Django form in order to keep the test simple.
|
|
|
|
# This form will be returned by the create_hompage_form function
|
|
|
|
# and will always be valid so that the code that we want to test
|
|
|
|
# actually runs.
|
2017-11-05 11:49:43 +01:00
|
|
|
class Form:
|
2017-11-19 04:02:03 +01:00
|
|
|
def is_valid(self) -> bool:
|
2016-10-26 14:40:14 +02:00
|
|
|
return True
|
|
|
|
|
2019-12-10 18:45:36 +01:00
|
|
|
with mock.patch('zerver.views.auth.HomepageForm', return_value=Form()):
|
|
|
|
self.assertEqual(PreregistrationUser.objects.all().count(), 0)
|
|
|
|
result = maybe_send_to_registration(request, self.example_email("hamlet"), is_signup=True)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
confirmation = Confirmation.objects.all().first()
|
|
|
|
confirmation_key = confirmation.confirmation_key
|
|
|
|
self.assertIn('do_confirm/' + confirmation_key, result.url)
|
|
|
|
self.assertEqual(PreregistrationUser.objects.all().count(), 1)
|
2016-10-26 14:40:14 +02:00
|
|
|
|
2016-12-01 08:54:21 +01:00
|
|
|
result = self.client_get(result.url)
|
|
|
|
self.assert_in_response('action="/accounts/register/"', result)
|
|
|
|
self.assert_in_response('value="{0}" name="key"'.format(confirmation_key), result)
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_sso_only_when_preregistration_user_exists(self) -> None:
|
2016-10-26 14:40:14 +02:00
|
|
|
rf = RequestFactory()
|
|
|
|
request = rf.get('/')
|
|
|
|
request.session = {}
|
|
|
|
request.user = None
|
|
|
|
|
|
|
|
# Creating a mock Django form in order to keep the test simple.
|
|
|
|
# This form will be returned by the create_hompage_form function
|
|
|
|
# and will always be valid so that the code that we want to test
|
|
|
|
# actually runs.
|
2017-11-05 11:49:43 +01:00
|
|
|
class Form:
|
2017-11-19 04:02:03 +01:00
|
|
|
def is_valid(self) -> bool:
|
2016-10-26 14:40:14 +02:00
|
|
|
return True
|
|
|
|
|
2017-05-25 01:40:26 +02:00
|
|
|
email = self.example_email("hamlet")
|
2016-10-26 14:40:14 +02:00
|
|
|
user = PreregistrationUser(email=email)
|
|
|
|
user.save()
|
|
|
|
|
2019-12-10 18:45:36 +01:00
|
|
|
with mock.patch('zerver.views.auth.HomepageForm', return_value=Form()):
|
|
|
|
self.assertEqual(PreregistrationUser.objects.all().count(), 1)
|
|
|
|
result = maybe_send_to_registration(request, email, is_signup=True)
|
|
|
|
self.assertEqual(result.status_code, 302)
|
|
|
|
confirmation = Confirmation.objects.all().first()
|
|
|
|
confirmation_key = confirmation.confirmation_key
|
|
|
|
self.assertIn('do_confirm/' + confirmation_key, result.url)
|
|
|
|
self.assertEqual(PreregistrationUser.objects.all().count(), 1)
|
2016-11-02 21:51:56 +01:00
|
|
|
|
|
|
|
class TestAdminSetBackends(ZulipTestCase):
|
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_change_enabled_backends(self) -> None:
|
2016-11-02 21:51:56 +01:00
|
|
|
# Log in as admin
|
2017-05-25 01:44:04 +02:00
|
|
|
self.login(self.example_email("iago"))
|
2016-11-02 21:51:56 +01:00
|
|
|
result = self.client_patch("/json/realm", {
|
|
|
|
'authentication_methods': ujson.dumps({u'Email': False, u'Dev': True})})
|
|
|
|
self.assert_json_success(result)
|
2017-01-04 05:30:48 +01:00
|
|
|
realm = get_realm('zulip')
|
2016-11-02 21:51:56 +01:00
|
|
|
self.assertFalse(password_auth_enabled(realm))
|
2016-11-07 21:20:55 +01:00
|
|
|
self.assertTrue(dev_auth_enabled(realm))
|
2016-11-02 21:51:56 +01:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_disable_all_backends(self) -> None:
|
2016-11-02 21:51:56 +01:00
|
|
|
# Log in as admin
|
2017-05-25 01:44:04 +02:00
|
|
|
self.login(self.example_email("iago"))
|
2016-11-02 21:51:56 +01:00
|
|
|
result = self.client_patch("/json/realm", {
|
2016-12-02 00:08:34 +01:00
|
|
|
'authentication_methods': ujson.dumps({u'Email': False, u'Dev': False})})
|
2017-07-25 02:28:33 +02:00
|
|
|
self.assert_json_error(result, 'At least one authentication method must be enabled.')
|
2017-01-04 05:30:48 +01:00
|
|
|
realm = get_realm('zulip')
|
2016-11-02 21:51:56 +01:00
|
|
|
self.assertTrue(password_auth_enabled(realm))
|
2016-11-07 21:20:55 +01:00
|
|
|
self.assertTrue(dev_auth_enabled(realm))
|
2016-11-02 21:51:56 +01:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_supported_backends_only_updated(self) -> None:
|
2016-11-02 21:51:56 +01:00
|
|
|
# Log in as admin
|
2017-05-25 01:44:04 +02:00
|
|
|
self.login(self.example_email("iago"))
|
2016-11-02 21:51:56 +01:00
|
|
|
# Set some supported and unsupported backends
|
|
|
|
result = self.client_patch("/json/realm", {
|
2016-12-02 00:08:34 +01:00
|
|
|
'authentication_methods': ujson.dumps({u'Email': False, u'Dev': True, u'GitHub': False})})
|
2016-11-02 21:51:56 +01:00
|
|
|
self.assert_json_success(result)
|
2017-01-04 05:30:48 +01:00
|
|
|
realm = get_realm('zulip')
|
2016-11-02 21:51:56 +01:00
|
|
|
# Check that unsupported backend is not enabled
|
|
|
|
self.assertFalse(github_auth_enabled(realm))
|
2016-11-07 21:20:55 +01:00
|
|
|
self.assertTrue(dev_auth_enabled(realm))
|
2016-11-02 21:51:56 +01:00
|
|
|
self.assertFalse(password_auth_enabled(realm))
|
2017-04-10 08:06:10 +02:00
|
|
|
|
2018-08-14 22:17:23 +02:00
|
|
|
class EmailValidatorTestCase(ZulipTestCase):
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_valid_email(self) -> None:
|
2017-05-25 01:40:26 +02:00
|
|
|
validate_login_email(self.example_email("hamlet"))
|
2017-04-10 08:06:10 +02:00
|
|
|
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_invalid_email(self) -> None:
|
2017-04-10 08:06:10 +02:00
|
|
|
with self.assertRaises(JsonableError):
|
|
|
|
validate_login_email(u'hamlet')
|
2017-04-20 08:25:15 +02:00
|
|
|
|
2018-08-14 22:17:23 +02:00
|
|
|
def test_validate_email(self) -> None:
|
|
|
|
inviter = self.example_user('hamlet')
|
|
|
|
cordelia = self.example_user('cordelia')
|
|
|
|
|
|
|
|
error, _ = validate_email(inviter, 'fred+5555@zulip.com')
|
|
|
|
self.assertIn('containing + are not allowed', error)
|
|
|
|
|
|
|
|
_, error = validate_email(inviter, cordelia.email)
|
|
|
|
self.assertEqual(error, 'Already has an account.')
|
|
|
|
|
|
|
|
cordelia.is_active = False
|
|
|
|
cordelia.save()
|
|
|
|
|
|
|
|
_, error = validate_email(inviter, cordelia.email)
|
2019-02-14 13:59:23 +01:00
|
|
|
self.assertEqual(error, 'Account has been deactivated.')
|
2018-08-14 22:17:23 +02:00
|
|
|
|
|
|
|
_, error = validate_email(inviter, 'fred-is-fine@zulip.com')
|
|
|
|
self.assertEqual(error, None)
|
|
|
|
|
2017-09-22 10:58:12 +02:00
|
|
|
class LDAPBackendTest(ZulipTestCase):
|
|
|
|
@override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',))
|
2017-11-17 10:47:43 +01:00
|
|
|
def test_non_existing_realm(self) -> None:
|
2019-10-16 18:10:40 +02:00
|
|
|
self.init_default_ldap_database()
|
2017-09-22 10:58:12 +02:00
|
|
|
email = self.example_email('hamlet')
|
|
|
|
data = {'username': email, 'password': initial_password(email)}
|
|
|
|
error_type = ZulipLDAPAuthBackend.REALM_IS_NONE_ERROR
|
|
|
|
error = ZulipLDAPConfigurationError('Realm is None', error_type)
|
2018-05-22 08:33:56 +02:00
|
|
|
with mock.patch('zproject.backends.ZulipLDAPAuthBackend.get_or_build_user',
|
2017-09-22 10:58:12 +02:00
|
|
|
side_effect=error), \
|
|
|
|
mock.patch('django_auth_ldap.backend._LDAPUser._authenticate_user_dn'):
|
|
|
|
response = self.client_post('/login/', data)
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
self.assertEqual(response.url, reverse('ldap_error_realm_is_none'))
|
|
|
|
response = self.client_get(response.url)
|
|
|
|
self.assert_in_response('You are trying to login using LDAP '
|
|
|
|
'without creating an',
|
|
|
|
response)
|