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 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`. 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): class IsAuthenticated(BasePermission):
@ -40,7 +40,7 @@ class IsAuthenticated(BasePermission):
Allows access only to authenticated users. 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(): if request.user and request.user.is_authenticated():
return True return True
return False return False
@ -51,7 +51,7 @@ class IsAdminUser(BasePermission):
Allows access only to admin users. 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: if request.user and request.user.is_staff:
return True return True
return False return False
@ -62,7 +62,7 @@ class IsAuthenticatedOrReadOnly(BasePermission):
The request is authenticated as a user, or is a read-only request. 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 if (request.method in SAFE_METHODS or
request.user and request.user and
request.user.is_authenticated()): request.user.is_authenticated()):
@ -105,7 +105,7 @@ class DjangoModelPermissions(BasePermission):
} }
return [perm % kwargs for perm in self.perms_map[method]] 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 model_cls = self.view.model
perms = self.get_required_permissions(request.method, model_cls) 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.compat import RequestFactory
from djangorestframework.views import APIView from djangorestframework.views import APIView
from djangorestframework.throttling import PerUserThrottling, PerViewThrottling from djangorestframework.throttling import UserRateThrottle
from djangorestframework.response import Response from djangorestframework.response import Response
class MockView(APIView): class User3SecRateThrottle(UserRateThrottle):
throttle_classes = (PerUserThrottling,)
rate = '3/sec' rate = '3/sec'
class User3MinRateThrottle(UserRateThrottle):
rate = '3/min'
class MockView(APIView):
throttle_classes = (User3SecRateThrottle,)
def get(self, request): def get(self, request):
return Response('foo') return Response('foo')
class MockView_PerViewThrottling(MockView): class MockView_MinuteThrottling(APIView):
throttle_classes = (PerViewThrottling,) throttle_classes = (User3MinRateThrottle,)
def get(self, request):
class MockView_MinuteThrottling(MockView): return Response('foo')
rate = '3/min'
class ThrottlingTests(TestCase): class ThrottlingTests(TestCase):
@ -86,12 +92,6 @@ class ThrottlingTests(TestCase):
""" """
self.ensure_is_throttled(MockView, 200) 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): 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 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 django.core.cache import cache
from djangorestframework.settings import api_settings
import time import time
@ -13,11 +14,11 @@ class BaseThrottle(object):
""" """
self.view = view self.view = view
def check_throttle(self, request): def allow_request(self, request):
""" """
Return `True` if the request should be allowed, `False` otherwise. 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): def wait(self):
""" """
@ -27,7 +28,7 @@ class BaseThrottle(object):
return None return None
class SimpleCachingThrottle(BaseThrottle): class SimpleRateThottle(BaseThrottle):
""" """
A simple cache implementation, that only requires `.get_cache_key()` A simple cache implementation, that only requires `.get_cache_key()`
to be overridden. to be overridden.
@ -41,33 +42,51 @@ class SimpleCachingThrottle(BaseThrottle):
Previous request information used for throttling is stored in the cache. Previous request information used for throttling is stored in the cache.
""" """
attr_name = 'rate'
rate = '1000/day'
timer = time.time timer = time.time
settings = api_settings
cache_format = '%(class)s_%(scope)s_%(ident)s'
scope = None
def __init__(self, view): def __init__(self, view):
""" super(SimpleRateThottle, self).__init__(view)
Check the throttling. rate = self.get_rate_description()
Return `None` or raise an :exc:`.ImmediateResponse`. self.num_requests, self.duration = self.parse_rate_description(rate)
"""
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]]
def get_cache_key(self, request): def get_cache_key(self, request):
""" """
Should return a unique cache-key which can be used for throttling. Should return a unique cache-key which can be used for throttling.
Must be overridden. Must be overridden.
May return `None` if the request should not be throttled.
""" """
raise NotImplementedError('.get_cache_key() must be overridden') 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. Implement the check to see if the request should be throttled.
On success calls :meth:`throttle_success`. On success calls `throttle_success`.
On failure calls :meth:`throttle_failure`. On failure calls `throttle_failure`.
""" """
self.key = self.get_cache_key(request) self.key = self.get_cache_key(request)
self.history = cache.get(self.key, []) self.history = cache.get(self.key, [])
@ -110,30 +129,90 @@ class SimpleCachingThrottle(BaseThrottle):
return remaining_duration / float(available_requests) 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. 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 The user id will be used as a unique cache key if the user is
authenticated. For anonymous requests, the IP address of the client will authenticated. For anonymous requests, the IP address of the request will
be used. be used.
""" """
scope = 'user'
def get_cache_key(self, request): def get_cache_key(self, request):
if request.user.is_authenticated(): if request.user.is_authenticated():
ident = request.user.id ident = request.user.id
else: else:
ident = request.META.get('REMOTE_ADDR', None) 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. 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
The class name of the view is used as a unique identifier to throttled. The unique cache key will be generated by concatenating the
throttle against. 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): 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. Check if request should be permitted.
""" """
for permission in self.get_permissions(): for permission in self.get_permissions():
if not permission.check_permission(request, obj): if not permission.has_permission(request, obj):
self.permission_denied(request) self.permission_denied(request)
def check_throttles(self, request): def check_throttles(self, request):
@ -194,7 +194,7 @@ class APIView(_View):
Check if request should be throttled. Check if request should be throttled.
""" """
for throttle in self.get_throttles(): for throttle in self.get_throttles():
if not throttle.check_throttle(request): if not throttle.allow_request(request):
self.throttled(request, throttle.wait()) self.throttled(request, throttle.wait())
def initialize_request(self, request, *args, **kargs): def initialize_request(self, request, *args, **kargs):

View File

@ -88,7 +88,7 @@ The `DjangoModelPermissions` class also supports object-level permissions. Thir
## Custom permissions ## 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. 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. 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 ## How throttling is determined
@ -25,7 +27,7 @@ If any throttle check fails an `exceptions.Throttled` exception will be raised,
## Setting the throttling policy ## 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 = { API_SETTINGS = {
'DEFAULT_THROTTLES': ( '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. You can also set the throttling policy on a per-view basis, using the `APIView` class based views.
class ExampleView(APIView): class ExampleView(APIView):
@ -59,18 +63,67 @@ Or, if you're using the `@api_view` decorator with function based views.
} }
return Response(content) 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. `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 ## 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 [permissions]: permissions.md