feat: Add a 403 and 429 handler.

See context here: https://django-ratelimit.readthedocs.io/en/latest/cookbook/429.html#context

For now we continue to fall back to django's default 403 handler for 403
but provide a new 429 template that we use for ratelimit exceptions.

This commit also updates a logistration test that relied on the old 403
behavior of django-ratelimit instead of the newly added 429 behavior.
This commit is contained in:
Feanil Patel
2021-02-04 11:11:07 -05:00
parent 54505b82c4
commit cd3e4353b1
5 changed files with 52 additions and 3 deletions

View File

@@ -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, {})

View File

@@ -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))

View File

@@ -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>
<main id="main" aria-label="Content" tabindex="-1">
<section class="outside-app">
<h1>
<%block name="pageheader">${page_header or _("Too Many Requests")}</%block>
</h1>
<p>
<%block name="pagecontent">
% if page_content:
${page_content}
% else:
${Text(_('Your request has been rate-limited. Please try again later.'))}
% endif
</%block>
</p>
</section>
</main>

View File

@@ -69,6 +69,7 @@ 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
handler500 = static_template_view_views.render_500

View File

@@ -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)