From 978b83d231ede1831074c0fc7a99e9ef23925b1e Mon Sep 17 00:00:00 2001
From: Tanner Prestegard <tanner.prestegard@ligo.org>
Date: Wed, 6 Feb 2019 09:31:59 -0600
Subject: [PATCH] Update throttles and add anonymous throttles

All throttles now use a database-backed cache since that is the
only way to do centralized throttling (important for production
deployment with multiple workers). We also add default throttles
for anonymous users for the entire API.
---
 config/settings/base.py             | 10 ++++++++-
 gracedb/api/throttling.py           | 33 +++++++++++++++++++++++++++--
 gracedb/api/v1/events/throttling.py |  1 +
 gracedb/api/v1/events/views.py      | 15 +++++++------
 4 files changed, 49 insertions(+), 10 deletions(-)

diff --git a/config/settings/base.py b/config/settings/base.py
index c37a05616..c006b8fae 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -221,7 +221,11 @@ CACHES = {
     'default': {
         'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
         'LOCATION': '127.0.0.1:11211',
-    }
+    },
+    'throttles': {
+        'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
+        'LOCATION': 'api_throttle_cache', # Table name
+    },
 }
 
 # List of settings for all template engines. Each item is a dict
@@ -347,7 +351,11 @@ REST_FRAMEWORK = {
     'DEFAULT_PAGINATION_CLASS':
         'rest_framework.pagination.LimitOffsetPagination',
     'PAGE_SIZE': 1e7,
+    'DEFAULT_THROTTLE_CLASSES': (
+        'api.throttling.BurstAnonRateThrottle',
+    ),
     'DEFAULT_THROTTLE_RATES': {
+        'anon_burst': '3/second',
         'event_creation': '1/second',
         'annotation'    : '10/second',
     },
diff --git a/gracedb/api/throttling.py b/gracedb/api/throttling.py
index 1c7a5e159..58f270dfb 100644
--- a/gracedb/api/throttling.py
+++ b/gracedb/api/throttling.py
@@ -1,7 +1,36 @@
-from rest_framework.throttling import UserRateThrottle
+from django.core.cache import caches
 
+from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
 
-class PostOrPutUserRateThrottle(UserRateThrottle):
+
+# NOTE: we have to use database-backed throttles to have a centralized location
+# where multiple workers (like in the production instance) can access and
+# update the same throttling information.
+
+
+###############################################################################
+# Base throttle classes #######################################################
+###############################################################################
+class DbCachedThrottleMixin(object):
+    """Uses a non-default (database-backed) cache"""
+    cache = caches['throttles']
+
+
+###############################################################################
+# Throttles for unauthenticated users #########################################
+###############################################################################
+class BurstAnonRateThrottle(DbCachedThrottleMixin, AnonRateThrottle):
+    scope = 'anon_burst'
+
+
+class SustainedAnonRateThrottle(DbCachedThrottleMixin, AnonRateThrottle):
+    scope = 'anon_sustained'
+
+
+###############################################################################
+# Throttles for authenticated users #########################################
+###############################################################################
+class PostOrPutUserRateThrottle(DbCachedThrottleMixin, UserRateThrottle):
 
     def allow_request(self, request, view):
         """
diff --git a/gracedb/api/v1/events/throttling.py b/gracedb/api/v1/events/throttling.py
index 66332e5b4..006ea41ff 100644
--- a/gracedb/api/v1/events/throttling.py
+++ b/gracedb/api/v1/events/throttling.py
@@ -4,6 +4,7 @@ from api.throttling import PostOrPutUserRateThrottle
 class EventCreationThrottle(PostOrPutUserRateThrottle):
     scope = 'event_creation'
 
+
 class AnnotationThrottle(PostOrPutUserRateThrottle):
     scope = 'annotation'
 
diff --git a/gracedb/api/v1/events/views.py b/gracedb/api/v1/events/views.py
index 660f83a94..e1c62e031 100644
--- a/gracedb/api/v1/events/views.py
+++ b/gracedb/api/v1/events/views.py
@@ -34,6 +34,7 @@ from rest_framework.views import APIView
 
 from alerts.events.utils import EventAlertIssuer, EventLogAlertIssuer, \
     EventVOEventAlertIssuer, EventPermissionsAlertIssuer
+from api.throttling import BurstAnonRateThrottle
 from core.http import check_and_serve_file
 from core.vfile import VersionedFile
 from events.buildVOEvent import buildVOEvent, VOEventBuilderException
@@ -52,7 +53,7 @@ from events.view_utils import eventToDict, eventLogToDict, labelToDict, \
 from search.forms import SimpleSearchForm
 from search.query.events import parseQuery, ParseException
 from superevents.models import Superevent
-from .throttles import EventCreationThrottle, AnnotationThrottle
+from .throttling import EventCreationThrottle, AnnotationThrottle
 from ...utils import api_reverse
 
 # Set up logger
@@ -348,7 +349,7 @@ class EventList(APIView):
     permission_classes = (IsAuthenticated,IsAuthorizedForPipeline)
     parser_classes = (parsers.MultiPartParser,)
     renderer_classes = (JSONRenderer, BrowsableAPIRenderer, LigoLwRenderer, TSVRenderer,)
-    throttle_classes = (EventCreationThrottle,)
+    throttle_classes = (BurstAnonRateThrottle, EventCreationThrottle,)
 
     def get(self, request, *args, **kwargs):
 
@@ -728,7 +729,7 @@ class EventLogList(APIView):
     POST param 'message'
     """
     permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
-    throttle_classes = (AnnotationThrottle,)
+    throttle_classes = (BurstAnonRateThrottle, AnnotationThrottle,)
 
     @event_and_auth_required
     def get(self, request, event):
@@ -874,7 +875,7 @@ class EMBBEventLogList(APIView):
     POST param 'message'
     """
     permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
-    throttle_classes = (AnnotationThrottle,)
+    throttle_classes = (BurstAnonRateThrottle, AnnotationThrottle,)
 
     @event_and_auth_required
     def get(self, request, event):
@@ -939,7 +940,7 @@ class EMObservationList(APIView):
     POST param 'message'
     """
     permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
-    throttle_classes = (AnnotationThrottle,)
+    throttle_classes = (BurstAnonRateThrottle, AnnotationThrottle,)
 
     @event_and_auth_required
     def get(self, request, event):
@@ -1559,7 +1560,7 @@ class VOEventList(APIView):
     """VOEvent List Resource
     """
     permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
-    throttle_classes = (AnnotationThrottle,)
+    throttle_classes = (BurstAnonRateThrottle, AnnotationThrottle,)
 
     @event_and_auth_required
     def get(self, request, event):
@@ -1693,7 +1694,7 @@ class OperatorSignoffList(APIView):
     At present, this only supports GET
     """
     permission_classes = (IsAuthenticated,IsAuthorizedForEvent,)
-    throttle_classes = (AnnotationThrottle,)
+    throttle_classes = (BurstAnonRateThrottle, AnnotationThrottle,)
 
     @event_and_auth_required
     def get(self, request, event):
-- 
GitLab