Improve throttles and docs

This commit is contained in:
Tom Christie 2012-09-13 18:32:56 +01:00
parent b16c45aa6d
commit 6c109ac60f
6 changed files with 189 additions and 57 deletions

View File

@ -28,11 +28,11 @@ class BasePermission(object):
"""
self.view = view
def check_permission(self, request, obj=None):
def has_permission(self, request, obj=None):
"""
Should simply return, or raise an :exc:`response.ImmediateResponse`.
"""
raise NotImplementedError(".check_permission() must be overridden.")
raise NotImplementedError(".has_permission() must be overridden.")
class IsAuthenticated(BasePermission):
@ -40,7 +40,7 @@ class IsAuthenticated(BasePermission):
Allows access only to authenticated users.
"""
def check_permission(self, request, obj=None):
def has_permission(self, request, obj=None):
if request.user and request.user.is_authenticated():
return True
return False
@ -51,7 +51,7 @@ class IsAdminUser(BasePermission):
Allows access only to admin users.
"""
def check_permission(self, request, obj=None):
def has_permission(self, request, obj=None):
if request.user and request.user.is_staff:
return True
return False
@ -62,7 +62,7 @@ class IsAuthenticatedOrReadOnly(BasePermission):
The request is authenticated as a user, or is a read-only request.
"""
def check_permission(self, request, obj=None):
def has_permission(self, request, obj=None):
if (request.method in SAFE_METHODS or
request.user and
request.user.is_authenticated()):
@ -105,7 +105,7 @@ class DjangoModelPermissions(BasePermission):
}
return [perm % kwargs for perm in self.perms_map[method]]
def check_permission(self, request, obj=None):
def has_permission(self, request, obj=None):
model_cls = self.view.model
perms = self.get_required_permissions(request.method, model_cls)

View File

@ -8,24 +8,30 @@ from django.core.cache import cache
from djangorestframework.compat import RequestFactory
from djangorestframework.views import APIView
from djangorestframework.throttling import PerUserThrottling, PerViewThrottling
from djangorestframework.throttling import UserRateThrottle
from djangorestframework.response import Response
class MockView(APIView):
throttle_classes = (PerUserThrottling,)
class User3SecRateThrottle(UserRateThrottle):
rate = '3/sec'
class User3MinRateThrottle(UserRateThrottle):
rate = '3/min'
class MockView(APIView):
throttle_classes = (User3SecRateThrottle,)
def get(self, request):
return Response('foo')
class MockView_PerViewThrottling(MockView):
throttle_classes = (PerViewThrottling,)
class MockView_MinuteThrottling(APIView):
throttle_classes = (User3MinRateThrottle,)
class MockView_MinuteThrottling(MockView):
rate = '3/min'
def get(self, request):
return Response('foo')
class ThrottlingTests(TestCase):
@ -86,12 +92,6 @@ class ThrottlingTests(TestCase):
"""
self.ensure_is_throttled(MockView, 200)
def test_request_throttling_is_per_view(self):
"""
Ensure request rate is limited globally per View for PerViewThrottles
"""
self.ensure_is_throttled(MockView_PerViewThrottling, 429)
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
"""
Ensure the response returns an X-Throttle field with status and next attributes

View File

@ -1,4 +1,5 @@
from django.core.cache import cache
from djangorestframework.settings import api_settings
import time
@ -13,11 +14,11 @@ class BaseThrottle(object):
"""
self.view = view
def check_throttle(self, request):
def allow_request(self, request):
"""
Return `True` if the request should be allowed, `False` otherwise.
"""
raise NotImplementedError('.check_throttle() must be overridden')
raise NotImplementedError('.allow_request() must be overridden')
def wait(self):
"""
@ -27,7 +28,7 @@ class BaseThrottle(object):
return None
class SimpleCachingThrottle(BaseThrottle):
class SimpleRateThottle(BaseThrottle):
"""
A simple cache implementation, that only requires `.get_cache_key()`
to be overridden.
@ -41,33 +42,51 @@ class SimpleCachingThrottle(BaseThrottle):
Previous request information used for throttling is stored in the cache.
"""
attr_name = 'rate'
rate = '1000/day'
timer = time.time
settings = api_settings
cache_format = '%(class)s_%(scope)s_%(ident)s'
scope = None
def __init__(self, view):
"""
Check the throttling.
Return `None` or raise an :exc:`.ImmediateResponse`.
"""
super(SimpleCachingThrottle, self).__init__(view)
num, period = getattr(view, self.attr_name, self.rate).split('/')
self.num_requests = int(num)
self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
super(SimpleRateThottle, self).__init__(view)
rate = self.get_rate_description()
self.num_requests, self.duration = self.parse_rate_description(rate)
def get_cache_key(self, request):
"""
Should return a unique cache-key which can be used for throttling.
Must be overridden.
May return `None` if the request should not be throttled.
"""
raise NotImplementedError('.get_cache_key() must be overridden')
def check_throttle(self, request):
def get_rate_description(self):
"""
Determine the string representation of the allowed request rate.
"""
try:
return self.rate
except AttributeError:
return self.settings.DEFAULT_THROTTLE_RATES.get(self.scope)
def parse_rate_description(self, rate):
"""
Given the request rate string, return a two tuple of:
<allowed number of requests>, <period of time in seconds>
"""
assert rate, "No throttle rate set for '%s'" % self.__class__.__name__
num, period = rate.split('/')
num_requests = int(num)
duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
return (num_requests, duration)
def allow_request(self, request):
"""
Implement the check to see if the request should be throttled.
On success calls :meth:`throttle_success`.
On failure calls :meth:`throttle_failure`.
On success calls `throttle_success`.
On failure calls `throttle_failure`.
"""
self.key = self.get_cache_key(request)
self.history = cache.get(self.key, [])
@ -110,30 +129,90 @@ class SimpleCachingThrottle(BaseThrottle):
return remaining_duration / float(available_requests)
class PerUserThrottling(SimpleCachingThrottle):
class AnonRateThrottle(SimpleRateThottle):
"""
Limits the rate of API calls that may be made by a anonymous users.
The IP address of the request will be used as the unqiue cache key.
"""
scope = 'anon'
def get_cache_key(self, request):
if request.user.is_authenticated():
return None # Only throttle unauthenticated requests.
ident = request.META.get('REMOTE_ADDR', None)
return self.cache_format % {
'class': self.__class__.__name__,
'scope': self.scope,
'ident': ident
}
class UserRateThrottle(SimpleRateThottle):
"""
Limits the rate of API calls that may be made by a given user.
The user id will be used as a unique identifier if the user is
authenticated. For anonymous requests, the IP address of the client will
The user id will be used as a unique cache key if the user is
authenticated. For anonymous requests, the IP address of the request will
be used.
"""
scope = 'user'
def get_cache_key(self, request):
if request.user.is_authenticated():
ident = request.user.id
else:
ident = request.META.get('REMOTE_ADDR', None)
return 'throttle_user_%s' % ident
return self.cache_format % {
'class': self.__class__.__name__,
'scope': self.scope,
'ident': ident
}
class PerViewThrottling(SimpleCachingThrottle):
class ScopedRateThrottle(SimpleRateThottle):
"""
Limits the rate of API calls that may be used on a given view.
The class name of the view is used as a unique identifier to
throttle against.
Limits the rate of API calls by different amounts for various parts of
the API. Any view that has the `throttle_scope` property set will be
throttled. The unique cache key will be generated by concatenating the
user id of the request, and the scope of the view being accessed.
"""
def __init__(self, view):
"""
Scope is determined from the view being accessed.
"""
self.scope = getattr(self.view, 'throttle_scope', None)
super(ScopedRateThrottle, self).__init__(view)
def parse_rate_description(self, rate):
"""
Subclassed so that we don't fail if `view.throttle_scope` is not set.
"""
if not rate:
return (None, None)
return super(ScopedRateThrottle, self).parse_rate_description(rate)
def get_cache_key(self, request):
return 'throttle_view_%s' % self.view.__class__.__name__
"""
If `view.throttle_scope` is not set, don't apply this throttle.
Otherwise generate the unique cache key by concatenating the user id
with the '.throttle_scope` property of the view.
"""
if not self.scope:
return None # Only throttle views with `.throttle_scope` set.
if request.user.is_authenticated():
ident = request.user.id
else:
ident = request.META.get('REMOTE_ADDR', None)
return self.cache_format % {
'class': self.__class__.__name__,
'scope': self.scope,
'ident': ident
}

View File

@ -186,7 +186,7 @@ class APIView(_View):
Check if request should be permitted.
"""
for permission in self.get_permissions():
if not permission.check_permission(request, obj):
if not permission.has_permission(request, obj):
self.permission_denied(request)
def check_throttles(self, request):
@ -194,7 +194,7 @@ class APIView(_View):
Check if request should be throttled.
"""
for throttle in self.get_throttles():
if not throttle.check_throttle(request):
if not throttle.allow_request(request):
self.throttled(request, throttle.wait())
def initialize_request(self, request, *args, **kargs):

View File

@ -88,7 +88,7 @@ The `DjangoModelPermissions` class also supports object-level permissions. Thir
## Custom permissions
To implement a custom permission, override `BasePermission` and implement the `.check_permission(self, request, obj=None)` method.
To implement a custom permission, override `BasePermission` and implement the `.has_permission(self, request, obj=None)` method.
The method should return `True` if the request should be granted access, and `False` otherwise.

View File

@ -12,9 +12,11 @@ Throttling is similar to [permissions], in that it determines if a request shoul
As with permissions, multiple throttles may be used. Your API might have a restrictive throttle for unauthenticated requests, and a less restrictive throttle for authenticated requests.
Another scenario where you might want to use multiple throttles would be if you need to impose different constraints on different parts of the API, due ato some services being particularly resource-intensive.
Another scenario where you might want to use multiple throttles would be if you need to impose different constraints on different parts of the API, due to some services being particularly resource-intensive.
Throttles do not necessarily only refer to rate-limiting requests. For example a storage service might also need to throttle against bandwidth.
Multiple throttles can also be used if you want to impose both burst throttling rates, and sustained throttling rates. For example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day.
Throttles do not necessarily only refer to rate-limiting requests. For example a storage service might also need to throttle against bandwidth, and a paid data service might want to throttle against a certain number of a records being accessed.
## How throttling is determined
@ -25,7 +27,7 @@ If any throttle check fails an `exceptions.Throttled` exception will be raised,
## Setting the throttling policy
The default throttling policy may be set globally, using the `DEFAULT_THROTTLES` setting. For example.
The default throttling policy may be set globally, using the `DEFAULT_THROTTLES` and `DEFAULT_THROTTLE_RATES` settings. For example.
API_SETTINGS = {
'DEFAULT_THROTTLES': (
@ -38,6 +40,8 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLES`
}
}
The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period.
You can also set the throttling policy on a per-view basis, using the `APIView` class based views.
class ExampleView(APIView):
@ -59,18 +63,67 @@ Or, if you're using the `@api_view` decorator with function based views.
}
return Response(content)
## AnonThrottle
## AnonRateThrottle
The `AnonThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to identify
The `AnonThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against.
The allowed request rate is determined from one of the following (in order of preference).
* The `rate` property on the class, which may be provided by overriding `AnonThrottle` and setting the property.
* The `DEFAULT_THROTTLE_RATES['anon']` setting.
`AnonThrottle` is suitable if you want to restrict the rate of requests from unknown sources.
## UserThrottle
## UserRateThrottle
`UserThrottle` is suitable if you want a simple restriction
The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticted requests will fall back to using the IP address of the incoming request is used to generate a unique key to throttle against.
## ScopedThrottle
The allowed request rate is determined from one of the following (in order of preference).
* The `rate` property on the class, which may be provided by overriding `UserThrottle` and setting the property.
* The `DEFAULT_THROTTLE_RATES['user']` setting.
`UserThrottle` is suitable if you want a simple global rate restriction per-user.
## ScopedRateThrottle
The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unqiue user id or IP address.
The allowed request rate is determined by the `DEFAULT_THROTTLE_RATES` setting using a key from the request "scope".
For example, given the following views...
class ContactListView(APIView):
throttle_scope = 'contacts'
...
class ContactDetailView(ApiView):
throttle_scope = 'contacts'
...
class UploadView(APIView):
throttle_scope = 'uploads'
...
...and the following settings.
API_SETTINGS = {
'DEFAULT_THROTTLES': (
'djangorestframework.throttles.ScopedRateThrottle',
)
'DEFAULT_THROTTLE_RATES': {
'contacts': '1000/day',
'uploads': '20/day'
}
}
User requests to either `ContactListView` or `ContactDetailView` would be restricted to a total of 1000 requests per-day. User requests to `UploadView` would be restricted to 20 requests per day.
## Custom throttles
To implement a custom throttle, override `BaseThrottle` and implement `.allow_request(request)`. The method should return `True` if the request should be allowed, and `False` otherwise.
Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recomended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.check_throttle()` has previously returned `False`.
[permissions]: permissions.md