فهرست منبع

feat(api): enable domain-specific rate limits, closes #525

Peter Thomassen 4 سال پیش
والد
کامیت
cd086dda09
5فایلهای تغییر یافته به همراه46 افزوده شده و 19 حذف شده
  1. 3 2
      api/api/settings.py
  2. 15 4
      api/desecapi/tests/test_throttling.py
  3. 5 0
      api/desecapi/throttling.py
  4. 11 9
      api/desecapi/views.py
  5. 12 4
      docs/rate-limits.rst

+ 3 - 2
api/api/settings.py

@@ -111,9 +111,10 @@ REST_FRAMEWORK = {
         'account_management_passive': ['10/min'],  # things like GET'ing v/* or auth/* URLs, or creating/deleting tokens
         'dyndns': ['1/min'],  # dynDNS updates; anything above 1/min is a client misconfiguration
         'dns_api_read': ['10/s', '50/min'],  # DNS API requests that do not involve pdns
-        'dns_api_write': ['6/s', '50/min', '200/h'],  # DNS API requests that do involve pdns
+        'dns_api_write_domains': ['10/s', '300/min', '1000/h'],  # domains/ endpoint
+        'dns_api_write_rrsets': ['2/s', '15/min', '30/h', '100/d'],  # rrsets/ endpoint, domain-scoped on the view
         # UserRateThrottle
-        'user': '1000/d',  # hard limit on requests by a) an authenticated user, b) an unauthenticated IP address
+        'user': '2000/d',  # hard limit on requests by a) an authenticated user, b) an unauthenticated IP address
     },
     'NUM_PROXIES': 0,  # Do not use X-Forwarded-For header when determining IP for throttling
 }

+ 15 - 4
api/desecapi/tests/test_throttling.py

@@ -35,10 +35,8 @@ class ThrottlingTestCase(TestCase):
         super().setUp()
         self.factory = APIRequestFactory()
 
-    def _test_requests_are_throttled(self, rates, counts):
-        cache.clear()
-        request = self.factory.get('/')
-        with override_rates(rates):
+    def _test_requests_are_throttled(self, rates, counts, buckets=None):
+        def do_test():
             view = MockView.as_view()
             sum_delay = 0
             for delay, count in counts:
@@ -51,6 +49,15 @@ class ThrottlingTestCase(TestCase):
                     response = view(request)
                     self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
 
+        cache.clear()
+        request = self.factory.get('/')
+        with override_rates(rates):
+            do_test()
+            if buckets is not None:
+                for bucket in buckets:
+                    MockView.throttle_scope_bucket = bucket
+                    do_test()
+
     def test_requests_are_throttled_4sec(self):
         self._test_requests_are_throttled(['4/sec'], [(0, 4), (1, 4)])
 
@@ -64,3 +71,7 @@ class ThrottlingTestCase(TestCase):
     def test_requests_are_throttled_multiple_cascade(self):
         # We test that we can do 4 requests in the first second and only 2 in the second second
         self._test_requests_are_throttled(['4/s', '6/day'], [(0, 4), (1, 2)])
+
+    def test_requests_are_throttled_multiple_cascade_with_buckets(self):
+        # We test that we can do 4 requests in the first second and only 2 in the second second
+        self._test_requests_are_throttled(['4/s', '6/day'], [(0, 4), (1, 2)], buckets=['foo', 'bar'])

+ 5 - 0
api/desecapi/throttling.py

@@ -25,6 +25,11 @@ class ScopedRatesThrottle(throttling.ScopedRateThrottle):
         if self.rate is None:
             return True
 
+        # Amend scope with optional bucket
+        bucket = getattr(view, self.scope_attr + '_bucket', None)
+        if bucket is not None:
+            self.scope += ':' + bucket
+
         self.now = self.timer()
         self.num_requests, self.duration = zip(*self.parse_rate(self.rate))
         self.key = self.get_cache_key(request, view)

+ 11 - 9
api/desecapi/views.py

@@ -2,6 +2,7 @@ import base64
 import binascii
 from datetime import timedelta
 from functools import cached_property
+from hashlib import sha1
 
 from django.conf import settings
 from django.contrib.auth import user_logged_in
@@ -68,6 +69,15 @@ class IdempotentDestroyMixin:
 
 class DomainViewMixin:
 
+    @property
+    def throttle_scope(self):
+        return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write_rrsets'
+
+    @property
+    def throttle_scope_bucket(self):
+        # Note: bucket should remain constant even when domain is recreated
+        return None if self.request.method in SAFE_METHODS else sha1(self.kwargs['name'].encode()).hexdigest()
+
     def get_serializer_context(self):
         # noinspection PyUnresolvedReferences
         return {**super().get_serializer_context(), 'domain': self.domain}
@@ -113,7 +123,7 @@ class DomainViewSet(IdempotentDestroyMixin,
 
     @property
     def throttle_scope(self):
-        return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write'
+        return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write_domains'
 
     def get_queryset(self):
         return self.request.user.domains
@@ -161,10 +171,6 @@ class RRsetDetail(IdempotentDestroyMixin, DomainViewMixin, generics.RetrieveUpda
     serializer_class = serializers.RRsetSerializer
     permission_classes = (IsAuthenticated, IsDomainOwner,)
 
-    @property
-    def throttle_scope(self):
-        return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write'
-
     def get_queryset(self):
         return self.domain.rrset_set
 
@@ -199,10 +205,6 @@ class RRsetList(EmptyPayloadMixin, DomainViewMixin, generics.ListCreateAPIView,
     serializer_class = serializers.RRsetSerializer
     permission_classes = (IsAuthenticated, IsDomainOwner,)
 
-    @property
-    def throttle_scope(self):
-        return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write'
-
     def get_queryset(self):
         rrsets = models.RRset.objects.filter(domain=self.domain)
 

+ 12 - 4
docs/rate-limits.rst

@@ -26,11 +26,19 @@ with ``429 Too Many Requests``.
 |                                |          |                                                                                           |
 |                                | 50/min   |                                                                                           |
 +--------------------------------+----------+-------------------------------------------------------------------------------------------+
-| ``dns_api_write``              | 6/s      | DNS write operations (e.g. create a domain, change an RRset)                              |
+| ``dns_api_write_domains``      | 10/s     | DNS write operations: domain creation/deletion                                            |
 |                                |          |                                                                                           |
-|                                | 50/min   |                                                                                           |
+|                                | 300/min  |                                                                                           |
+|                                |          |                                                                                           |
+|                                | 1000/h   |                                                                                           |
++--------------------------------+----------+-------------------------------------------------------------------------------------------+
+| ``dns_api_write_rrsets``       | 2/s      | DNS write operations: RRset creation/deletion/modification (per domain).  If you require  |
+|                                |          | more requests, consider using bulk requests.                                              |
+|                                | 15/min   |                                                                                           |
+|                                |          |                                                                                           |
+|                                | 30/h     |                                                                                           |
 |                                |          |                                                                                           |
-|                                | 200/h    |                                                                                           |
+|                                | 100/day  |                                                                                           |
 +--------------------------------+----------+-------------------------------------------------------------------------------------------+
-| ``user``                       | 1000/day | Any activity of a) authenticated users, b) unauthenticated users (by IP)                  |
+| ``user``                       | 2000/day | Any activity of a) authenticated users, b) unauthenticated users (by IP)                  |
 +--------------------------------+----------+-------------------------------------------------------------------------------------------+