Repository: mozmeao/snippets-service

Issue:
Add caching &/or rate limiting to public jobs list view
Earlier today the snippets admin experienced some performance issues due to automated scanning against the public jobs list view (someone was using [sqlmap](http://sqlmap.org) to probe for SQL injection vulnerabilities). We may may want to use https://github.com/jsocol/django-ratelimit similar to basket to limit the impact of such scanning, and we could potentially add some caching to the views to reduce the DB load from a large number of requests to that public view.


Discussion:
Alternatively, we could require authentication for this view. If we put this behind SSO we could also display the performance data for each job which would be useful for stakeholders without admin access.

Resolving patch:
diff --git a/snippets/base/views.py b/snippets/base/views.py
--- a/snippets/base/views.py
+++ b/snippets/base/views.py
@@ -17,6 +17,7 @@
 import sentry_sdk
 from django_filters.views import FilterView
 from django_statsd.clients import statsd
+from ratelimit.decorators import ratelimit
 
 from snippets.base import util
 from snippets.base.bundles import ASRSnippetBundle, SnippetBundle
@@ -35,8 +36,20 @@ class HomeView(TemplateView):
     template_name = 'base/home.jinja'
 
 
-class JobListView(TemplateView):
-    template_name = 'base/temporarilyDisabled.jinja'
+class JobListView(FilterView):
+    filterset_class = JobFilter
+
+    @ratelimit(rate=settings.RATELIMIT_RATE, block=True,
+               key=lambda g, r: r.META.get('HTTP_X_FORWARDED_FOR', r.META['REMOTE_ADDR']))
+    def get(self, request, **kwargs):
+        return super().get(request, **kwargs)
+
+    @property
+    def template_name(self):
+        if self.request.GET.get('calendar', 'false') == 'true':
+            return 'base/jobs_list_calendar.jinja'
+
+        return 'base/jobs_list_table.jinja'
 
 
 def fetch_snippets(request, **kwargs):
diff --git a/snippets/settings.py b/snippets/settings.py
--- a/snippets/settings.py
+++ b/snippets/settings.py
@@ -389,3 +389,6 @@
 USE_PREGEN_BUNDLES = config('USE_PREGEN_BUNDLES', default=False, cast=bool)
 
 NIGHTLY_INCLUDES_RELEASE = config('NIGHTLY_INCLUDES_RELEASE', default=False, cast=bool)
+
+RATELIMIT_ENABLE = config('RATELIMIT_ENABLE', default=False, cast=bool)
+RATELIMIT_RATE = config('RATELIMIT_RATE', default='10/m')
diff --git a/snippets/urls.py b/snippets/urls.py
--- a/snippets/urls.py
+++ b/snippets/urls.py
@@ -1,17 +1,31 @@
 from django.conf import settings
 from django.contrib import admin
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseForbidden
 from django.views.generic import RedirectView
 from django.views.static import serve as static_serve
+from django.shortcuts import render
 from django.urls import include, path, re_path
 
+import sentry_sdk
+from ratelimit.exceptions import Ratelimited
+
 
 def robots_txt(request):
     permission = 'Allow' if settings.ENGAGE_ROBOTS else 'Disallow'
     return HttpResponse('User-agent: *\n{0}: /'.format(permission), content_type='text/plain')
 
 
+def handler403(request, exception=None):
+    if isinstance(exception, Ratelimited):
+        with sentry_sdk.configure_scope() as scope:
+            scope.level = 'info'
+            scope.set_tag('logger', 'ratelimited')
+            sentry_sdk.capture_message(message='Rate limited')
+        return render(request, template_name='base/ratelimited.jinja', status=429)
+    return HttpResponseForbidden('Forbidden')
+
+
 urlpatterns = [
     path('', include('snippets.base.urls')),
     path('robots.txt', robots_txt),


Test patch: