Ratelimit the registration endpoint
PROD-880
This commit is contained in:
@@ -2287,6 +2287,9 @@ DISABLE_DEPRECATED_SIGNUP_URL = False
|
||||
##### LOGISTRATION RATE LIMIT SETTINGS #####
|
||||
LOGISTRATION_RATELIMIT_RATE = '100/5m'
|
||||
|
||||
##### REGISTRATION RATE LIMIT SETTINGS #####
|
||||
REGISTRATION_VALIDATION_RATELIMIT = '30/7d'
|
||||
|
||||
##### PASSWORD RESET RATE LIMIT SETTINGS #####
|
||||
PASSWORD_RESET_IP_RATE = '1/m'
|
||||
PASSWORD_RESET_EMAIL_RATE = '2/h'
|
||||
|
||||
@@ -267,6 +267,11 @@ COMPREHENSIVE_THEME_LOCALE_PATHS = ENV_TOKENS.get('COMPREHENSIVE_THEME_LOCALE_PA
|
||||
#Timezone overrides
|
||||
TIME_ZONE = ENV_TOKENS.get('CELERY_TIMEZONE', CELERY_TIMEZONE)
|
||||
|
||||
##### REGISTRATION RATE LIMIT SETTINGS #####
|
||||
REGISTRATION_VALIDATION_RATELIMIT = ENV_TOKENS.get(
|
||||
'REGISTRATION_VALIDATION_RATELIMIT', REGISTRATION_VALIDATION_RATELIMIT
|
||||
)
|
||||
|
||||
# Push to LMS overrides
|
||||
GIT_REPO_EXPORT_DIR = ENV_TOKENS.get('GIT_REPO_EXPORT_DIR', '/edx/var/edxapp/export_course_repos')
|
||||
|
||||
|
||||
@@ -305,3 +305,5 @@ PROCTORING_SETTINGS = {}
|
||||
|
||||
##### LOGISTRATION RATE LIMIT SETTINGS #####
|
||||
LOGISTRATION_RATELIMIT_RATE = '5/5m'
|
||||
|
||||
REGISTRATION_VALIDATION_RATELIMIT = '5/minute'
|
||||
|
||||
@@ -2575,6 +2575,8 @@ REST_FRAMEWORK = {
|
||||
},
|
||||
}
|
||||
|
||||
REGISTRATION_VALIDATION_RATELIMIT = '30/7d'
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_INFO': 'openedx.core.apidocs.api_info',
|
||||
}
|
||||
|
||||
@@ -603,6 +603,11 @@ MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = ENV_TOKENS.get(
|
||||
##### LOGISTRATION RATE LIMIT SETTINGS #####
|
||||
LOGISTRATION_RATELIMIT_RATE = ENV_TOKENS.get('LOGISTRATION_RATELIMIT_RATE', LOGISTRATION_RATELIMIT_RATE)
|
||||
|
||||
##### REGISTRATION RATE LIMIT SETTINGS #####
|
||||
REGISTRATION_VALIDATION_RATELIMIT = ENV_TOKENS.get(
|
||||
'REGISTRATION_VALIDATION_RATELIMIT', REGISTRATION_VALIDATION_RATELIMIT
|
||||
)
|
||||
|
||||
#### PASSWORD POLICY SETTINGS #####
|
||||
AUTH_PASSWORD_VALIDATORS = ENV_TOKENS.get("AUTH_PASSWORD_VALIDATORS", AUTH_PASSWORD_VALIDATORS)
|
||||
|
||||
|
||||
@@ -518,11 +518,6 @@ ACTIVATION_EMAIL_FROM_ADDRESS = 'test_activate@edx.org'
|
||||
|
||||
TEMPLATES[0]['OPTIONS']['debug'] = True
|
||||
|
||||
########################### DRF default throttle rates ############################
|
||||
# Increasing rates to enable test cases hitting registration view succesfully.
|
||||
# Lower rate is causing view to get blocked, causing test case failure.
|
||||
REST_FRAMEWORK['DEFAULT_THROTTLE_RATES']['registration_validation'] = '100/minute'
|
||||
|
||||
########################## VIDEO TRANSCRIPTS STORAGE ############################
|
||||
VIDEO_TRANSCRIPTS_SETTINGS = dict(
|
||||
VIDEO_TRANSCRIPTS_MAX_BYTES=3 * 1024 * 1024, # 3 MB
|
||||
@@ -603,3 +598,5 @@ RATELIMIT_RATE = '2/m'
|
||||
|
||||
##### LOGISTRATION RATE LIMIT SETTINGS #####
|
||||
LOGISTRATION_RATELIMIT_RATE = '5/5m'
|
||||
|
||||
REGISTRATION_VALIDATION_RATELIMIT = '5/minute'
|
||||
|
||||
@@ -15,19 +15,20 @@ from django.core.validators import ValidationError
|
||||
from django.db import transaction
|
||||
from django.dispatch import Signal
|
||||
from django.http import HttpResponse, HttpResponseForbidden
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import get_language
|
||||
from django.utils.translation import ugettext as _
|
||||
from pytz import UTC
|
||||
from requests import HTTPError
|
||||
from six import text_type
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from ipware.ip import get_ip
|
||||
from pytz import UTC
|
||||
from ratelimit.decorators import ratelimit
|
||||
from requests import HTTPError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
from six import text_type
|
||||
from social_core.exceptions import AuthAlreadyAssociated, AuthException
|
||||
from social_django import utils as social_utils
|
||||
|
||||
@@ -48,27 +49,27 @@ from openedx.core.djangoapps.user_api.accounts.api import (
|
||||
get_username_existence_validation_error,
|
||||
get_username_validation_error
|
||||
)
|
||||
from openedx.core.djangoapps.user_authn.utils import generate_password, is_registration_api_v1
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangoapps.user_authn.cookies import set_logged_in_cookies
|
||||
from openedx.core.djangoapps.user_authn.utils import generate_password, is_registration_api_v1
|
||||
from openedx.core.djangoapps.user_authn.views.registration_form import (
|
||||
get_registration_extension_form,
|
||||
AccountCreationForm,
|
||||
RegistrationFormFactory
|
||||
RegistrationFormFactory,
|
||||
get_registration_extension_form
|
||||
)
|
||||
from openedx.core.djangoapps.waffle_utils import WaffleFlag, WaffleFlagNamespace
|
||||
from student.helpers import (
|
||||
AccountValidationError,
|
||||
authenticate_new_user,
|
||||
create_or_set_user_attribute_created_on_site,
|
||||
do_create_account,
|
||||
AccountValidationError,
|
||||
do_create_account
|
||||
)
|
||||
from student.models import (
|
||||
RegistrationCookieConfiguration,
|
||||
UserAttribute,
|
||||
create_comments_service_user,
|
||||
email_exists_or_retired,
|
||||
username_exists_or_retired,
|
||||
username_exists_or_retired
|
||||
)
|
||||
from student.views import compose_and_send_activation_email
|
||||
from third_party_auth import pipeline, provider
|
||||
@@ -110,6 +111,7 @@ REGISTRATION_FAILURE_LOGGING_FLAG = WaffleFlag(
|
||||
waffle_namespace=WaffleFlagNamespace(name=u'registration'),
|
||||
flag_name=u'enable_failure_logging',
|
||||
)
|
||||
REAL_IP_KEY = 'openedx.core.djangoapps.util.ratelimit.real_ip'
|
||||
|
||||
|
||||
@transaction.non_atomic_requests
|
||||
@@ -575,19 +577,6 @@ class RegistrationView(APIView):
|
||||
pass
|
||||
|
||||
|
||||
class RegistrationValidationThrottle(AnonRateThrottle):
|
||||
"""
|
||||
Custom throttle rate for /api/user/v1/validation/registration
|
||||
endpoint's use case.
|
||||
"""
|
||||
|
||||
scope = 'registration_validation'
|
||||
|
||||
def get_ident(self, request):
|
||||
client_ip = get_ip(request)
|
||||
return client_ip
|
||||
|
||||
|
||||
# pylint: disable=line-too-long
|
||||
class RegistrationValidationView(APIView):
|
||||
"""
|
||||
@@ -677,7 +666,6 @@ class RegistrationValidationView(APIView):
|
||||
|
||||
# This end-point is available to anonymous users, so no authentication is needed.
|
||||
authentication_classes = []
|
||||
throttle_classes = (RegistrationValidationThrottle,)
|
||||
|
||||
def name_handler(self, request):
|
||||
name = request.data.get('name')
|
||||
@@ -725,6 +713,9 @@ class RegistrationValidationView(APIView):
|
||||
"country": country_handler
|
||||
}
|
||||
|
||||
@method_decorator(
|
||||
ratelimit(key=REAL_IP_KEY, rate=settings.REGISTRATION_VALIDATION_RATELIMIT, method='POST', block=True)
|
||||
)
|
||||
def post(self, request):
|
||||
"""
|
||||
POST /api/user/v1/validation/registration/
|
||||
|
||||
@@ -2,24 +2,23 @@
|
||||
"""Tests for account creation"""
|
||||
|
||||
import json
|
||||
from unittest import skipIf, skipUnless
|
||||
from datetime import datetime
|
||||
from unittest import skipIf, skipUnless
|
||||
|
||||
import ddt
|
||||
import httpretty
|
||||
import mock
|
||||
import six
|
||||
from six.moves import range
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
from django.core.cache import cache
|
||||
from django.test import TransactionTestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from pytz import UTC
|
||||
|
||||
from six.moves import range
|
||||
from social_django.models import Partial, UserSocialAuth
|
||||
|
||||
from openedx.core.djangoapps.site_configuration.helpers import get_value
|
||||
@@ -32,25 +31,24 @@ from openedx.core.djangoapps.user_api.accounts import (
|
||||
EMAIL_MIN_LENGTH,
|
||||
NAME_MAX_LENGTH,
|
||||
REQUIRED_FIELD_CONFIRM_EMAIL_MSG,
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH,
|
||||
USERNAME_BAD_LENGTH_MSG,
|
||||
USERNAME_CONFLICT_MSG,
|
||||
USERNAME_INVALID_CHARS_ASCII,
|
||||
USERNAME_INVALID_CHARS_UNICODE,
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
|
||||
from openedx.core.djangoapps.user_api.accounts.tests import testutils
|
||||
from openedx.core.djangoapps.user_api.accounts.tests.retirement_helpers import ( # pylint: disable=unused-import
|
||||
RetirementTestCase,
|
||||
fake_requested_retirement,
|
||||
setup_retirement_states,
|
||||
setup_retirement_states
|
||||
)
|
||||
from openedx.core.djangoapps.user_api.tests.test_helpers import TestCaseForm
|
||||
from openedx.core.djangoapps.user_api.tests.test_constants import SORTED_COUNTRIES
|
||||
from openedx.core.djangoapps.user_api.tests.test_helpers import TestCaseForm
|
||||
from openedx.core.djangoapps.user_api.tests.test_views import UserAPITestCase
|
||||
from openedx.core.djangoapps.user_authn.views.register import RegistrationValidationThrottle, \
|
||||
REGISTRATION_FAILURE_LOGGING_FLAG
|
||||
from openedx.core.djangoapps.user_authn.views.register import REGISTRATION_FAILURE_LOGGING_FLAG
|
||||
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
|
||||
from openedx.core.lib.api import test_utils
|
||||
@@ -2098,6 +2096,10 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
endpoint_name = 'registration_validation'
|
||||
path = reverse(endpoint_name)
|
||||
|
||||
def setUp(self):
|
||||
super(RegistrationValidationViewTests, self).setUp()
|
||||
cache.clear()
|
||||
|
||||
def get_validation_decision(self, data):
|
||||
response = self.client.post(self.path, data)
|
||||
return response.data.get('validation_decisions', {})
|
||||
@@ -2297,7 +2299,8 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase):
|
||||
to enforce limits; that's why this test needs a "real"
|
||||
default cache (as opposed to the usual-for-tests DummyCache)
|
||||
"""
|
||||
for _ in range(RegistrationValidationThrottle().num_requests):
|
||||
self.request_without_auth('post', self.path)
|
||||
for _ in range(int(settings.REGISTRATION_VALIDATION_RATELIMIT.split('/')[0])):
|
||||
response = self.request_without_auth('post', self.path)
|
||||
self.assertNotEqual(response.status_code, 403)
|
||||
response = self.request_without_auth('post', self.path)
|
||||
self.assertEqual(response.status_code, 429)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
Reference in New Issue
Block a user