diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index 2e49b2c9967d806354b5483aa957a3b138b34820..db2b0d1d707c841cddded6c60bf9ba0bf8b1dbd6 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -25,6 +25,7 @@ from django.test import RequestFactory, TestCase from django.urls import reverse as django_reverse from django.utils.translation import ugettext as _ from edx_when.api import get_dates_for_course, get_overrides_for_user, set_date_for_block +from freezegun import freeze_time from mock import Mock, NonCallableMock, patch from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.locator import UsageKey @@ -2781,8 +2782,11 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment """ Test the CSV output for the anonymized user ids. """ + base_time = datetime.datetime.now(UTC) url = reverse('get_anon_ids', kwargs={'course_id': text_type(self.course.id)}) - response = self.client.post(url, {}) + with freeze_time(base_time): + response = self.client.post(url, {}) + self.assertEqual(response['Content-Type'], 'text/csv') body = response.content.decode("utf-8").replace('\r', '') self.assertTrue(body.startswith( @@ -2794,6 +2798,19 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment ) self.assertIn("attachment; filename=org", response['Content-Disposition']) + # Test rate-limiting + # The get_anon_ids view is computationally intensive and its execution time can vary + # depending on the number of enrollments in a course. We are rate limiting it to + # prevent too many concurrent calls which could result in a denial of service for + # other users of the lms. + with freeze_time(base_time + datetime.timedelta(minutes=1)): + response = self.client.post(url, {}) + assert response.status_code == 403 + + with freeze_time(base_time + datetime.timedelta(minutes=5)): + response = self.client.post(url, {}) + assert response.status_code == 200 + @patch('lms.djangoapps.instructor_task.models.logger.error') @patch.dict(settings.GRADES_DOWNLOAD, {'STORAGE_TYPE': 's3', 'ROOT_PATH': 'tmp/edx-s3/grades'}) def test_list_report_downloads_error(self, mock_error): diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 3f09cacf2a61d5539128d2777bc168905993b8fc..26574c5e80128f2f03fc294380d6250dd4da8d14 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -34,6 +34,7 @@ from edx_rest_framework_extensions.auth.session.authentication import SessionAut from edx_when.api import get_date_for_block from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey, UsageKey +from ratelimit.decorators import ratelimit from rest_framework import status from rest_framework.permissions import IsAuthenticated, IsAdminUser from rest_framework.response import Response @@ -1381,6 +1382,7 @@ def get_proctored_exam_results(request, course_id): @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) +@ratelimit(key="user", rate="1/5m", block=True) @require_course_permission(permissions.CAN_RESEARCH) def get_anon_ids(request, course_id): """