mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-12-01 22:14:06 +03:00
Improve throttles and docs
This commit is contained in:
parent
b16c45aa6d
commit
6c109ac60f
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue
Block a user