diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index db2b0d1d70..02584c39a5 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -2805,7 +2805,7 @@ class TestInstructorAPILevelsDataDump(SharedModuleStoreTestCase, LoginEnrollment # other users of the lms. with freeze_time(base_time + datetime.timedelta(minutes=1)): response = self.client.post(url, {}) - assert response.status_code == 403 + assert response.status_code == 429 with freeze_time(base_time + datetime.timedelta(minutes=5)): response = self.client.post(url, {}) diff --git a/lms/djangoapps/static_template_view/views.py b/lms/djangoapps/static_template_view/views.py index ffe3a31338..9ce2666bc2 100644 --- a/lms/djangoapps/static_template_view/views.py +++ b/lms/djangoapps/static_template_view/views.py @@ -9,11 +9,13 @@ import mimetypes from django.conf import settings -from django.http import Http404, HttpResponseNotFound, HttpResponseServerError +from django.http import Http404, HttpResponse, HttpResponseNotFound, HttpResponseServerError from django.shortcuts import redirect from django.template import TemplateDoesNotExist from django.utils.safestring import mark_safe from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.defaults import permission_denied +from ratelimit.exceptions import Ratelimited from mako.exceptions import TopLevelLookupException from common.djangoapps.edxmako.shortcuts import render_to_response, render_to_string @@ -92,12 +94,32 @@ def render_press_release(request, slug): return resp +@fix_crum_request +def render_403(request, exception=None): + """ + Render the permission_denied template unless it's a ratelimit exception in which case use the rate limit template. + """ + if isinstance(exception, Ratelimited): + return render_429(request, exception) + + return permission_denied(request, exception) + + @fix_crum_request def render_404(request, exception): # lint-amnesty, pylint: disable=unused-argument request.view_name = '404' return HttpResponseNotFound(render_to_string('static_templates/404.html', {}, request=request)) +@fix_crum_request +def render_429(request, exception=None): # lint-amnesty, pylint: disable=unused-argument + """ + Render the rate limit template as an HttpResponse. + """ + request.view_name = '429' + return HttpResponse(render_to_string('static_templates/429.html', {}, request=request), status=429) + + @fix_crum_request def render_500(request): return HttpResponseServerError(render_to_string('static_templates/server-error.html', {}, request=request)) diff --git a/lms/templates/static_templates/429.html b/lms/templates/static_templates/429.html new file mode 100644 index 0000000000..8f3079f02f --- /dev/null +++ b/lms/templates/static_templates/429.html @@ -0,0 +1,26 @@ +<%page expression_filter="h"/> +<%namespace name='static' file='../static_content.html'/> +<%! +from django.utils.translation import ugettext as _ +from openedx.core.djangolib.markup import HTML, Text +%> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Too Many Requests")} + +
+
+

+ <%block name="pageheader">${page_header or _("Too Many Requests")} +

+

+ <%block name="pagecontent"> + % if page_content: + ${page_content} + % else: + ${Text(_('Your request has been rate-limited. Please try again later.'))} + % endif + +

+
+
diff --git a/lms/urls.py b/lms/urls.py index 24c6e204b8..5e9f0a523b 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -7,6 +7,7 @@ from django.conf import settings from django.conf.urls import include, url from django.conf.urls.static import static from django.contrib.admin import autodiscover as django_autodiscover +from django.urls import path from django.utils.translation import ugettext_lazy as _ from django.views.generic.base import RedirectView from edx_api_doc_tools import make_docs_urls @@ -69,7 +70,9 @@ if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): # Custom error pages # These are used by Django to render these error codes. Do not remove. # pylint: disable=invalid-name +handler403 = static_template_view_views.render_403 handler404 = static_template_view_views.render_404 +handler429 = static_template_view_views.render_429 handler500 = static_template_view_views.render_500 notification_prefs_urls = [ @@ -198,6 +201,10 @@ urlpatterns = [ ), url(r'^api/discounts/', include(('openedx.features.discounts.urls', 'openedx.features.discounts'), namespace='api_discounts')), + path('403', handler403), + path('404', handler404), + path('429', handler429), + path('500', handler500), ] if settings.FEATURES.get('ENABLE_MOBILE_REST_API'): diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py index a2b50573ce..cf74f1ed23 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_logistration.py @@ -137,7 +137,7 @@ class LoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMixin, ModuleSto # then the rate limiter should kick in and give a HttpForbidden response response = self.client.get(login_url) - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 429) # now reset the time to 6 mins from now in future in order to unblock reset_time = datetime.now(UTC) + timedelta(seconds=361)