diff --git a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py index 4b5bc538952b..46d6b5232b43 100644 --- a/openedx/core/djangoapps/user_api/accounts/tests/test_views.py +++ b/openedx/core/djangoapps/user_api/accounts/tests/test_views.py @@ -1110,6 +1110,37 @@ def test_patch_invalid_email(self, bad_email): field_errors['email']['developer_message'] assert 'Valid e-mail address required.' == field_errors['email']['user_message'] + @override_settings(SECONDARY_EMAIL_RATE_LIMIT='1/m') + def test_patch_secondary_email_ratelimit(self): + """ + Tests if rate limit is applied on secondary_email patch + """ + client = self.login_client("client", "user") + self.send_patch(client, {"secondary_email": "new_email_01@example.com"}, + expected_status=status.HTTP_200_OK) + self.send_patch(client, {"secondary_email": "new_email_02@example.com"}, + expected_status=status.HTTP_429_TOO_MANY_REQUESTS) + + @override_settings(SECONDARY_EMAIL_RATE_LIMIT='') + def test_ratelimit_is_disabled_on_secondary_email_patch_if_settings_is_empty(self): + """ + Tests rate limit doesn't applied on secondary_email patch if SECONDARY_EMAIL_RATE_LIMIT is empty string or None + """ + client = self.login_client("client", "user") + self.send_patch(client, {"secondary_email": "email_new_01@example.com"}, + expected_status=status.HTTP_200_OK) + self.send_patch(client, {"secondary_email": "email_new_02@example.com"}, + expected_status=status.HTTP_200_OK) + + @override_settings(SECONDARY_EMAIL_RATE_LIMIT='1/d') + def test_ratelimit_is_only_on_secondary_email_change(self): + """ + Tests if rate limit is only applied for secondary_email attribute i.e. when user changes recovery email + """ + client = self.login_client("client", "user") + for i in range(5): + self.send_patch(client, {"name": f"new_name_{i}"}, expected_status=status.HTTP_200_OK) + @mock.patch('common.djangoapps.student.views.management.do_email_change_request') def test_patch_duplicate_email(self, do_email_change_request): """ diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py index 5aab757a491d..0464187b5d7e 100644 --- a/openedx/core/djangoapps/user_api/accounts/views.py +++ b/openedx/core/djangoapps/user_api/accounts/views.py @@ -396,12 +396,17 @@ def partial_update(self, request, username): """ if request.content_type != MergePatchParser.media_type: raise UnsupportedMediaType(request.content_type) - if request.data.get("email") and settings.EMAIL_CHANGE_RATE_LIMIT: - if is_ratelimited( - request=request, group="email_change_rate_limit", key="user", - rate=settings.EMAIL_CHANGE_RATE_LIMIT, increment=True, - ): - return Response({"error": "Too many requests"}, status=status.HTTP_429_TOO_MANY_REQUESTS) + + for key, limit in [ + ('email', settings.EMAIL_CHANGE_RATE_LIMIT), + ('secondary_email', settings.SECONDARY_EMAIL_RATE_LIMIT) + ]: + if request.data.get(key) and limit: + if is_ratelimited( + request=request, group=f"{key}_change_rate_limit", key="user", + rate=limit, increment=True, + ): + return Response({"error": "Too many requests"}, status=status.HTTP_429_TOO_MANY_REQUESTS) try: with transaction.atomic(): diff --git a/openedx/envs/common.py b/openedx/envs/common.py index 151e5f6af51d..c7b387dbebbe 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -830,6 +830,7 @@ def _make_locale_paths(settings): ONE_CLICK_UNSUBSCRIBE_RATE_LIMIT = '100/m' EMAIL_CHANGE_RATE_LIMIT = '' +SECONDARY_EMAIL_RATE_LIMIT = '' LMS_ROOT_URL = None LMS_INTERNAL_ROOT_URL = Derived(lambda settings: settings.LMS_ROOT_URL)