diff --git a/lms/envs/common.py b/lms/envs/common.py index c28c6640854b974938936cb9b1aa28bfd97fb56b..2946023bd08248b265146ad0b1ed8267dd9fa16f 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2333,6 +2333,7 @@ REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES': { 'user': '60/minute', 'service_user': '120/minute', + 'registration_validation': '30/minute', }, } diff --git a/lms/envs/test.py b/lms/envs/test.py index d7251f0bc3239dc500f62796b62c1aaf5ac5b935..38d9a8bce8d254649912bace5a8b028d952a4e8a 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -574,6 +574,11 @@ 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 diff --git a/openedx/core/djangoapps/user_api/validation/tests/test_views.py b/openedx/core/djangoapps/user_api/validation/tests/test_views.py index 892d1c1b5a46dd0e3b5eea1e28c43c45282c1bcf..c9e02b81625afdfc5d8d5a6b79fd1c4194255f6d 100644 --- a/openedx/core/djangoapps/user_api/validation/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/validation/tests/test_views.py @@ -9,11 +9,13 @@ import ddt from django.conf import settings from django.contrib.auth.models import User from django.urls import reverse +from django.test.utils import override_settings from six import text_type from openedx.core.djangoapps.user_api import accounts from openedx.core.djangoapps.user_api.accounts.tests import testutils from openedx.core.lib.api import test_utils +from openedx.core.djangoapps.user_api.validation.views import RegistrationValidationThrottle from util.password_policy_validators import password_max_length, password_min_length @@ -199,3 +201,24 @@ class RegistrationValidationViewTests(test_utils.ApiTestCase): {"username": "somephrase", "password": "somephrase"}, {"username": "", "password": u"Password cannot be the same as the username."} ) + + @override_settings( + CACHES={ + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'registration_proxy', + } + } + ) + def test_rate_limiting_registration_view(self): + """ + Confirm rate limits work as expected for registration + end point /api/user/v1/validation/registration/. Note + that drf's rate limiting makes use of the default cache + 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) + response = self.request_without_auth('post', self.path) + self.assertEqual(response.status_code, 429) diff --git a/openedx/core/djangoapps/user_api/validation/views.py b/openedx/core/djangoapps/user_api/validation/views.py index 94fd3a4baf3989cd0376fbc51744ed1f4f3943e0..e5a5a41fedddba3d391cad142318db73772182f7 100644 --- a/openedx/core/djangoapps/user_api/validation/views.py +++ b/openedx/core/djangoapps/user_api/validation/views.py @@ -5,6 +5,7 @@ An API for client-side validation of (potential) user data. from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.throttling import AnonRateThrottle from openedx.core.djangoapps.user_api.accounts.api import ( get_email_validation_error, @@ -16,6 +17,20 @@ from openedx.core.djangoapps.user_api.accounts.api import ( get_username_validation_error, get_username_existence_validation_error ) +from ipware.ip import get_ip + + +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 class RegistrationValidationView(APIView): @@ -106,6 +121,7 @@ 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')