auth: Rate limit username+password authenticate() calls.

This applies rate limiting (through a decorator) of authenticate()
functions in the Email and LDAP backends - because those are the ones
where we check user's password.
The limiting is based on the username that the authentication is
attempted for - more than X attempts in Y minutes to a username is not
permitted.

If the limit is exceeded, RateLimited exception will be raised - this
can be either handled in a custom way by the code that calls
authenticate(), or it will be handled by RateLimitMiddleware and return
a json_error as the response.
This commit is contained in:
Mateusz Mandera 2019-08-01 15:09:27 +02:00 committed by Tim Abbott
parent 335b804510
commit 5f94ea3d54
5 changed files with 240 additions and 34 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 = [

View File

@ -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.