diff --git a/cms/envs/common.py b/cms/envs/common.py index af161418c67856f8b12668bc937d660451119dbd..05a3b84e15c7a9ed65544d035e83af65f3eb2170 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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' diff --git a/cms/envs/production.py b/cms/envs/production.py index e0baad585f1bb35b8141ae18bdc2a226f51234fd..2df0b7f334b2bf56491518bace66178f848a066f 100644 --- a/cms/envs/production.py +++ b/cms/envs/production.py @@ -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') diff --git a/cms/envs/test.py b/cms/envs/test.py index d6bd389537492c85d9e6b694fe8631ad171e4288..3c63dd8dc5f43f962d5ee7adbf7dc17c6c031917 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -305,3 +305,5 @@ PROCTORING_SETTINGS = {} ##### LOGISTRATION RATE LIMIT SETTINGS ##### LOGISTRATION_RATELIMIT_RATE = '5/5m' + +REGISTRATION_VALIDATION_RATELIMIT = '5/minute' diff --git a/lms/envs/common.py b/lms/envs/common.py index 9554ce9e7653ee68c6d5e66476c2f0279c9991dd..d8a8a1a37e4ae9083ad750f9c14fec60e6821c57 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2575,6 +2575,8 @@ REST_FRAMEWORK = { }, } +REGISTRATION_VALIDATION_RATELIMIT = '30/7d' + SWAGGER_SETTINGS = { 'DEFAULT_INFO': 'openedx.core.apidocs.api_info', } diff --git a/lms/envs/production.py b/lms/envs/production.py index 5479e412bf3f76c52e4d1ea6e383fab9aa8386b3..d855dc7d45dfbea44326e5f99904947b64119285 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -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) diff --git a/lms/envs/test.py b/lms/envs/test.py index 3fa29ba538e8b7d3610744d78eb879558808ccf9..f633e68e8c8af76c5c74a7f3f0a2baecc0330402 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -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' diff --git a/openedx/core/djangoapps/user_authn/views/register.py b/openedx/core/djangoapps/user_authn/views/register.py index bfc454f30381b2ea87713ff5b66a2a631dac764e..0d19bdfd4dd8527f07e88688a5f14e978cd5b639 100644 --- a/openedx/core/djangoapps/user_authn/views/register.py +++ b/openedx/core/djangoapps/user_authn/views/register.py @@ -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 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 six import text_type -from ipware.ip import get_ip 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/ diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_register.py b/openedx/core/djangoapps/user_authn/views/tests/test_register.py index 4a0d7f7a54f33fc0ed4394e672565d9e7a2fd2b5..86b044aa216b2bdeef2d6ce9744b48c6a2555d78 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_register.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_register.py @@ -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)