diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index a3354ffbd7..b9dae88245 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -2,7 +2,7 @@ from django.conf import settings from django.contrib.auth import authenticate from django.core import mail -from django.http import HttpResponse +from django.http import HttpResponse, HttpRequest from django.test import override_settings from django_auth_ldap.backend import LDAPSearch, _LDAPUser from django.test.client import RequestFactory @@ -33,12 +33,15 @@ from zerver.lib.actions import ( from zerver.lib.avatar import avatar_url from zerver.lib.avatar_hash import user_avatar_path from zerver.lib.dev_ldap_directory import generate_dev_ldap_dir +from zerver.lib.exceptions import RateLimited from zerver.lib.mobile_auth_otp import otp_decrypt_api_key from zerver.lib.validator import validate_login_email, \ check_bool, check_dict_only, check_list, check_string, Validator +from zerver.lib.rate_limiter import add_ratelimit_rule, remove_ratelimit_rule, clear_history from zerver.lib.request import JsonableError from zerver.lib.storage import static_path from zerver.lib.users import get_all_api_keys +from zerver.lib.utils import generate_random_token from zerver.lib.upload import resize_avatar, MEDIUM_AVATAR_SIZE from zerver.lib.utils import generate_random_token from zerver.lib.initial_password import initial_password @@ -62,7 +65,7 @@ from zproject.backends import ZulipDummyBackend, EmailAuthBackend, \ ZulipLDAPException, query_ldap, sync_user_from_ldap, SocialAuthMixin, \ PopulateUserLDAPError, SAMLAuthBackend, saml_auth_enabled, email_belongs_to_ldap, \ get_external_method_dicts, AzureADAuthBackend, check_password_strength, \ - ZulipLDAPUser + ZulipLDAPUser, RateLimitedAuthenticationByUsername from zerver.views.auth import (maybe_send_to_registration, store_login_data, LOGIN_TOKEN_LENGTH) @@ -190,7 +193,8 @@ class AuthBackendTest(ZulipTestCase): mock.patch('zproject.backends.password_auth_enabled', return_value=True): return_data = {} # type: Dict[str, bool] - user = EmailAuthBackend().authenticate(username=self.example_email('hamlet'), + user = EmailAuthBackend().authenticate(request=mock.MagicMock(), + username=self.example_email('hamlet'), realm=get_realm("zulip"), password=password, return_data=return_data) @@ -198,20 +202,24 @@ class AuthBackendTest(ZulipTestCase): self.assertTrue(return_data['email_auth_disabled']) self.verify_backend(EmailAuthBackend(), - good_kwargs=dict(password=password, + good_kwargs=dict(request=mock.MagicMock(), + password=password, username=username, realm=get_realm('zulip'), return_data=dict()), - bad_kwargs=dict(password=password, + bad_kwargs=dict(request=mock.MagicMock(), + password=password, username=username, realm=get_realm('zephyr'), return_data=dict())) self.verify_backend(EmailAuthBackend(), - good_kwargs=dict(password=password, + good_kwargs=dict(request=mock.MagicMock(), + password=password, username=username, realm=get_realm('zulip'), return_data=dict()), - bad_kwargs=dict(password=password, + bad_kwargs=dict(request=mock.MagicMock(), + password=password, username=username, realm=get_realm('zephyr'), return_data=dict())) @@ -224,7 +232,8 @@ class AuthBackendTest(ZulipTestCase): # 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'), + self.assertIsNotNone(EmailAuthBackend().authenticate(request=mock.MagicMock(), + username=self.example_email('hamlet'), password=password, realm=get_realm("zulip"))) @@ -237,7 +246,8 @@ class AuthBackendTest(ZulipTestCase): # by using Django's version of this method. super(UserProfile, user_profile).set_password(password) user_profile.save() - self.assertIsNone(EmailAuthBackend().authenticate(username=self.example_email('hamlet'), + self.assertIsNone(EmailAuthBackend().authenticate(request=mock.MagicMock(), + username=self.example_email('hamlet'), password=password, realm=get_realm("zulip"))) @@ -248,7 +258,8 @@ class AuthBackendTest(ZulipTestCase): 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): - self.assertIsNone(EmailAuthBackend().authenticate(username=self.example_email('hamlet'), + self.assertIsNone(EmailAuthBackend().authenticate(request=mock.MagicMock(), + username=self.example_email('hamlet'), password=password, realm=get_realm("zulip"))) @@ -308,20 +319,25 @@ class AuthBackendTest(ZulipTestCase): backend = ZulipLDAPAuthBackend() # Test LDAP auth fails when LDAP server rejects password - self.assertIsNone(backend.authenticate(username=email, password="wrongpass", realm=get_realm("zulip"))) + self.assertIsNone(backend.authenticate(request=mock.MagicMock(), username=email, + password="wrongpass", realm=get_realm("zulip"))) self.verify_backend(backend, - bad_kwargs=dict(username=username, + bad_kwargs=dict(request=mock.MagicMock(), + username=username, password=password, realm=get_realm('zephyr')), - good_kwargs=dict(username=username, + good_kwargs=dict(request=mock.MagicMock(), + username=username, password=password, realm=get_realm('zulip'))) self.verify_backend(backend, - bad_kwargs=dict(username=username, + bad_kwargs=dict(request=mock.MagicMock(), + username=username, password=password, realm=get_realm('zephyr')), - good_kwargs=dict(username=username, + good_kwargs=dict(request=mock.MagicMock(), + username=username, password=password, realm=get_realm('zulip'))) @@ -459,6 +475,115 @@ class AuthBackendTest(ZulipTestCase): backend.authenticate = orig_authenticate backend.get_verified_emails = orig_get_verified_emails +class RateLimitAuthenticationTests(ZulipTestCase): + @override_settings(RATE_LIMITING_AUTHENTICATE=True) + def do_test_auth_rate_limiting(self, + attempt_authentication_func: Callable[[HttpRequest, str, str], + Optional[UserProfile]], + username: str, correct_password: str, wrong_password: str, + expected_user_profile: UserProfile) -> None: + # We have to mock RateLimitedAuthenticationByUsername.key_fragment to avoid key collisions + # if tests run in parallel. + original_key_fragment_method = RateLimitedAuthenticationByUsername.key_fragment + salt = generate_random_token(32) + + def _mock_key_fragment(self: RateLimitedAuthenticationByUsername) -> str: + return "{}:{}".format(salt, original_key_fragment_method(self)) + + def attempt_authentication(username: str, password: str) -> Optional[UserProfile]: + request = HttpRequest() + return attempt_authentication_func(request, username, password) + + add_ratelimit_rule(10, 2, domain='authenticate') + with mock.patch.object(RateLimitedAuthenticationByUsername, 'key_fragment', new=_mock_key_fragment): + try: + start_time = time.time() + with mock.patch('time.time', return_value=start_time): + self.assertIsNone(attempt_authentication(username, wrong_password)) + self.assertIsNone(attempt_authentication(username, wrong_password)) + # 2 failed attempts is the limit, so the next ones should get blocked, + # even with the correct password. + with self.assertRaises(RateLimited): + attempt_authentication(username, correct_password) + with self.assertRaises(RateLimited): + attempt_authentication(username, wrong_password) + + # After enough time passes, more authentication attempts can be made: + with mock.patch('time.time', return_value=start_time + 11.0): + self.assertIsNone(attempt_authentication(username, wrong_password)) + + self.assertEqual(attempt_authentication(username, correct_password), expected_user_profile) # Correct password + # A correct login attempt should reset the rate limits for this user profile, + # so the next two attempts shouldn't get limited: + self.assertIsNone(attempt_authentication(username, wrong_password)) + self.assertIsNone(attempt_authentication(username, wrong_password)) + # But the third attempt goes over the limit: + with self.assertRaises(RateLimited): + attempt_authentication(username, wrong_password) + finally: + # Clean up to avoid affecting other tests. + clear_history(RateLimitedAuthenticationByUsername(username)) + remove_ratelimit_rule(10, 2, domain='authenticate') + + def test_email_auth_backend_user_based_rate_limiting(self) -> None: + user_profile = self.example_user('hamlet') + password = "testpassword" + user_profile.set_password(password) + user_profile.save() + + def attempt_authentication(request: HttpRequest, username: str, password: str) -> Optional[UserProfile]: + return EmailAuthBackend().authenticate(request=request, + username=username, + realm=get_realm("zulip"), + password=password, + return_data=dict()) + + self.do_test_auth_rate_limiting(attempt_authentication, user_profile.email, password, 'wrong_password', + user_profile) + + @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',), + LDAP_EMAIL_ATTR="mail") + def test_ldap_backend_user_based_rate_limiting(self) -> None: + self.init_default_ldap_database() + user_profile = self.example_user('hamlet') + password = self.ldap_password() + + def attempt_authentication(request: HttpRequest, username: str, password: str) -> Optional[UserProfile]: + return ZulipLDAPAuthBackend().authenticate(request=request, + username=username, + realm=get_realm("zulip"), + password=password, + return_data=dict()) + + self.do_test_auth_rate_limiting(attempt_authentication, user_profile.email, password, 'wrong_password', + user_profile) + + @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend', + 'zproject.backends.ZulipLDAPAuthBackend'), + LDAP_EMAIL_ATTR="mail") + def test_email_and_ldap_backends_user_based_rate_limiting(self) -> None: + self.init_default_ldap_database() + user_profile = self.example_user('hamlet') + ldap_password = self.ldap_password() + + email_password = "email_password" + user_profile.set_password(email_password) + user_profile.save() + + def attempt_authentication(request: HttpRequest, username: str, password: str) -> Optional[UserProfile]: + return authenticate(request=request, + username=username, + realm=get_realm("zulip"), + password=password, + return_data=dict()) + + self.do_test_auth_rate_limiting(attempt_authentication, user_profile.email, + email_password, 'wrong_password', + user_profile) + self.do_test_auth_rate_limiting(attempt_authentication, user_profile.email, + ldap_password, 'wrong_password', + user_profile) + class CheckPasswordStrengthTest(ZulipTestCase): def test_check_password_strength(self) -> None: with self.settings(PASSWORD_MIN_LENGTH=0, PASSWORD_MIN_GUESSES=0): @@ -2698,7 +2823,8 @@ class DjangoToLDAPUsernameTests(ZulipTestCase): with self.settings(LDAP_EMAIL_ATTR='mail'): self.assertEqual( - authenticate(username=user_profile.email, password=self.ldap_password(), realm=realm), + authenticate(request=mock.MagicMock(), username=user_profile.email, + password=self.ldap_password(), realm=realm), user_profile) @override_settings(LDAP_EMAIL_ATTR='mail', LDAP_DEACTIVATE_NON_MATCHING_USERS=True) @@ -2777,7 +2903,8 @@ class TestLDAP(ZulipLDAPTestCase): ldap.SCOPE_ONELEVEL, "(email=%(email)s)"), LDAP_APPEND_DOMAIN='zulip.com' ): - user_profile = self.backend.authenticate(username='ldapuser1', password='dapu', + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username='ldapuser1', password='dapu', realm=get_realm('zulip')) assert(user_profile is None) @@ -2785,7 +2912,9 @@ class TestLDAP(ZulipLDAPTestCase): @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_login_success(self) -> None: with self.settings(LDAP_APPEND_DOMAIN='zulip.com'): - user_profile = self.backend.authenticate(username=self.example_email("hamlet"), password=self.ldap_password(), + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username=self.example_email("hamlet"), + password=self.ldap_password(), realm=get_realm('zulip')) assert(user_profile is not None) @@ -2794,7 +2923,8 @@ class TestLDAP(ZulipLDAPTestCase): @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_login_success_with_username(self) -> None: with self.settings(LDAP_APPEND_DOMAIN='zulip.com'): - user_profile = self.backend.authenticate(username="hamlet", password=self.ldap_password(), + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username="hamlet", password=self.ldap_password(), realm=get_realm('zulip')) assert(user_profile is not None) @@ -2803,7 +2933,8 @@ class TestLDAP(ZulipLDAPTestCase): @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_login_success_with_email_attr(self) -> None: with self.settings(LDAP_EMAIL_ATTR='mail'): - user_profile = self.backend.authenticate(username=self.ldap_username("aaron"), + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username=self.ldap_username("aaron"), password=self.ldap_password(), realm=get_realm('zulip')) @@ -2822,7 +2953,8 @@ class TestLDAP(ZulipLDAPTestCase): ): realm = get_realm('zulip') self.assertEqual(email_belongs_to_ldap(realm, self.example_email("aaron")), True) - user_profile = ZulipLDAPAuthBackend().authenticate(username=self.ldap_username("aaron"), + user_profile = ZulipLDAPAuthBackend().authenticate(request=mock.MagicMock(), + username=self.ldap_username("aaron"), password=self.ldap_password(), realm=realm) self.assertEqual(user_profile, self.example_user("aaron")) @@ -2833,21 +2965,24 @@ class TestLDAP(ZulipLDAPTestCase): othello.save() self.assertEqual(email_belongs_to_ldap(realm, othello.email), False) - user_profile = EmailAuthBackend().authenticate(username=othello.email, password=password, + user_profile = EmailAuthBackend().authenticate(request=mock.MagicMock(), + username=othello.email, password=password, realm=realm) self.assertEqual(user_profile, othello) @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_login_failure_due_to_wrong_password(self) -> None: with self.settings(LDAP_APPEND_DOMAIN='zulip.com'): - user = self.backend.authenticate(username=self.example_email("hamlet"), password='wrong', + user = self.backend.authenticate(request=mock.MagicMock(), + username=self.example_email("hamlet"), password='wrong', realm=get_realm('zulip')) self.assertIs(user, None) @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_login_failure_due_to_nonexistent_user(self) -> None: with self.settings(LDAP_APPEND_DOMAIN='zulip.com'): - user = self.backend.authenticate(username='nonexistent@zulip.com', password=self.ldap_password(), + user = self.backend.authenticate(request=mock.MagicMock(), + username='nonexistent@zulip.com', password=self.ldap_password(), realm=get_realm('zulip')) self.assertIs(user, None) @@ -2974,7 +3109,8 @@ class TestLDAP(ZulipLDAPTestCase): @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_login_failure_when_domain_does_not_match(self) -> None: with self.settings(LDAP_APPEND_DOMAIN='acme.com'): - user_profile = self.backend.authenticate(username=self.example_email("hamlet"), + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username=self.example_email("hamlet"), password=self.ldap_password(), realm=get_realm('zulip')) self.assertIs(user_profile, None) @@ -2987,7 +3123,8 @@ class TestLDAP(ZulipLDAPTestCase): with self.settings( LDAP_APPEND_DOMAIN='zulip.com', AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map): - user_profile = self.backend.authenticate(username=self.example_email('hamlet'), + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username=self.example_email('hamlet'), password=self.ldap_password(), realm=get_realm('acme')) self.assertEqual(user_profile.email, self.example_email('hamlet')) @@ -2995,7 +3132,8 @@ class TestLDAP(ZulipLDAPTestCase): @override_settings(AUTHENTICATION_BACKENDS=('zproject.backends.ZulipLDAPAuthBackend',)) def test_login_success_with_valid_subdomain(self) -> None: with self.settings(LDAP_APPEND_DOMAIN='zulip.com'): - user_profile = self.backend.authenticate(username=self.example_email("hamlet"), + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username=self.example_email("hamlet"), password=self.ldap_password(), realm=get_realm('zulip')) assert(user_profile is not None) @@ -3006,7 +3144,8 @@ class TestLDAP(ZulipLDAPTestCase): user_profile = self.example_user("hamlet") do_deactivate_user(user_profile) with self.settings(LDAP_APPEND_DOMAIN='zulip.com'): - user_profile = self.backend.authenticate(username=self.example_email("hamlet"), + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username=self.example_email("hamlet"), password=self.ldap_password(), realm=get_realm('zulip')) self.assertIs(user_profile, None) @@ -3019,7 +3158,8 @@ class TestLDAP(ZulipLDAPTestCase): def test_login_success_when_user_does_not_exist_with_valid_subdomain(self) -> None: RealmDomain.objects.create(realm=self.backend._realm, domain='acme.com') with self.settings(LDAP_APPEND_DOMAIN='acme.com'): - user_profile = self.backend.authenticate(username='newuser@acme.com', + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username='newuser@acme.com', password=self.ldap_password(), realm=get_realm('zulip')) assert(user_profile is not None) @@ -3037,7 +3177,8 @@ class TestLDAP(ZulipLDAPTestCase): with self.settings( LDAP_APPEND_DOMAIN='zulip.com', AUTH_LDAP_USER_ATTR_MAP={'first_name': 'sn', 'last_name': 'cn'}): - user_profile = self.backend.authenticate(username='newuser_splitname@zulip.com', + user_profile = self.backend.authenticate(request=mock.MagicMock(), + username='newuser_splitname@zulip.com', password=self.ldap_password(), realm=get_realm('zulip')) assert(user_profile is not None) diff --git a/zproject/backends.py b/zproject/backends.py index 7a3c12c37f..e2afb8972f 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -16,19 +16,21 @@ import copy import logging import magic from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union from typing_extensions import TypedDict from zxcvbn import zxcvbn from django_auth_ldap.backend import LDAPBackend, LDAPReverseEmailSearch, \ _LDAPUser, ldap_error +from decorator import decorator + from django.contrib.auth import get_backends from django.contrib.auth.backends import RemoteUserBackend from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import validate_email from django.dispatch import receiver, Signal -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse, HttpResponseRedirect, HttpRequest from django.shortcuts import render from django.urls import reverse from django.utils.translation import ugettext as _ @@ -43,12 +45,14 @@ from social_core.backends.saml import SAMLAuth from social_core.pipeline.partial import partial from social_core.exceptions import AuthFailed, SocialAuthBaseException +from zerver.decorator import client_is_exempt_from_rate_limiting from zerver.lib.actions import do_create_user, do_reactivate_user, do_deactivate_user, \ do_update_user_custom_profile_data_if_changed, validate_email_for_realm from zerver.lib.avatar import is_avatar_new, avatar_url from zerver.lib.avatar_hash import user_avatar_content_hash from zerver.lib.dev_ldap_directory import init_fakeldap from zerver.lib.mobile_auth_otp import is_valid_otp +from zerver.lib.rate_limiter import clear_history, rate_limit_request_by_entity, RateLimitedObject from zerver.lib.request import JsonableError from zerver.lib.users import check_full_name, validate_user_custom_profile_field from zerver.lib.redis_utils import get_redis_client, get_dict_from_redis, put_dict_in_redis @@ -164,6 +168,60 @@ def common_get_active_user(email: str, realm: Realm, return user_profile +AuthFuncT = TypeVar('AuthFuncT', bound=Callable[..., Optional[UserProfile]]) +rate_limiting_rules = settings.RATE_LIMITING_RULES['authenticate'] + +class RateLimitedAuthenticationByUsername(RateLimitedObject): + def __init__(self, username: str) -> None: + self.username = username + + def __str__(self) -> str: + return "Username: {}".format(self.username) + + def key_fragment(self) -> str: + return "{}:{}".format(type(self), self.username) + + def rules(self) -> List[Tuple[int, int]]: + return rate_limiting_rules + +def rate_limit_authentication_by_username(request: HttpRequest, username: str) -> None: + entity = RateLimitedAuthenticationByUsername(username) + rate_limit_request_by_entity(request, entity) + +def auth_rate_limiting_already_applied(request: HttpRequest) -> bool: + return hasattr(request, '_ratelimit') and 'RateLimitedAuthenticationByUsername' in request._ratelimit + +# Django's authentication mechanism uses introspection on the various authenticate() functions +# defined by backends, so we need a decorator that doesn't break function signatures. +# @decorator does this for us. +# The usual @wraps from functools breaks signatures, so it can't be used here. +@decorator +def rate_limit_auth(auth_func: AuthFuncT, *args: Any, **kwargs: Any) -> Optional[UserProfile]: + if not settings.RATE_LIMITING_AUTHENTICATE: + return auth_func(*args, **kwargs) + + request = kwargs['request'] + username = kwargs['username'] + if not hasattr(request, 'client') or not client_is_exempt_from_rate_limiting(request): + # Django cycles through enabled authentication backends until one succeeds, + # or all of them fail. If multiple backends are tried like this, we only want + # to execute rate_limit_authentication_* once, on the first attempt: + if auth_rate_limiting_already_applied(request): + pass + else: + # Apply rate limiting. If this request is above the limit, + # RateLimited will be raised, interrupting the authentication process. + # From there, the code calling authenticate() can either catch the exception + # and handle it on its own, or it will be processed by RateLimitMiddleware. + rate_limit_authentication_by_username(request, username) + + result = auth_func(*args, **kwargs) + if result is not None: + # Authentication succeeded, clear the rate-limiting record. + clear_history(RateLimitedAuthenticationByUsername(username)) + + return result + class ZulipAuthMixin: """This common mixin is used to override Django's default behavior for looking up a logged-in user by ID to use a version that fetches @@ -219,7 +277,8 @@ class EmailAuthBackend(ZulipAuthMixin): Allows a user to sign in using an email/password pair. """ - def authenticate(self, *, username: str, password: str, + @rate_limit_auth + def authenticate(self, *, request: HttpRequest, username: str, password: str, realm: Realm, return_data: Optional[Dict[str, Any]]=None) -> Optional[UserProfile]: """ Authenticate a user based on email address as the user name. """ @@ -528,7 +587,8 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend): class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase): REALM_IS_NONE_ERROR = 1 - def authenticate(self, *, username: str, password: str, realm: Realm, + @rate_limit_auth + def authenticate(self, *, request: HttpRequest, username: str, password: str, realm: Realm, prereg_user: Optional[PreregistrationUser]=None, return_data: Optional[Dict[str, Any]]=None) -> Optional[UserProfile]: self._realm = realm diff --git a/zproject/default_settings.py b/zproject/default_settings.py index d36e1306ef..fc133f957a 100644 --- a/zproject/default_settings.py +++ b/zproject/default_settings.py @@ -134,6 +134,7 @@ PUSH_NOTIFICATION_BOUNCER_URL = None # type: Optional[str] PUSH_NOTIFICATION_REDACT_CONTENT = False SUBMIT_USAGE_STATISTICS = True RATE_LIMITING = True +RATE_LIMITING_AUTHENTICATE = True SEND_LOGIN_EMAILS = True EMBEDDED_BOTS_ENABLED = False diff --git a/zproject/settings.py b/zproject/settings.py index d679ead5fd..af33a06bb1 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -357,6 +357,9 @@ RATE_LIMITING_RULES = { 'all': [ (60, 200), # 200 requests max every minute ], + 'authenticate': [ + (1800, 5), # 5 login attempts within 30 minutes + ], } RATE_LIMITING_MIRROR_REALM_RULES = [ diff --git a/zproject/test_settings.py b/zproject/test_settings.py index 35f9ae631f..b5434e5db0 100644 --- a/zproject/test_settings.py +++ b/zproject/test_settings.py @@ -81,6 +81,7 @@ AUTH_LDAP_REVERSE_EMAIL_SEARCH = LDAPSearch("ou=users,dc=zulip,dc=com", TEST_SUITE = True RATE_LIMITING = False +RATE_LIMITING_AUTHENTICATE = False # Don't use rabbitmq from the test suite -- the user_profile_ids for # any generated queue elements won't match those being used by the # real app.