django-rest-framework/djangorestframework/permissions.py

255 lines
7.5 KiB
Python
Raw Normal View History

"""
2012-02-24 01:34:20 +04:00
The :mod:`permissions` module bundles a set of permission classes that are used
for checking if a request passes a certain set of constraints.
Permission behavior is provided by mixing the :class:`mixins.PermissionsMixin` class into a :class:`View` class.
"""
from django.core.cache import cache
from djangorestframework import status
from djangorestframework.response import ImmediateResponse
import time
2011-05-10 13:49:28 +04:00
__all__ = (
'BasePermission',
'FullAnonAccess',
'IsAuthenticated',
'IsAdminUser',
'IsUserOrIsAnonReadOnly',
'PerUserThrottling',
'PerViewThrottling',
2011-05-10 13:49:28 +04:00
)
2012-02-11 22:43:58 +04:00
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
2011-05-10 13:49:28 +04:00
_403_FORBIDDEN_RESPONSE = ImmediateResponse(
2012-02-10 12:18:39 +04:00
{'detail': 'You do not have permission to access this resource. ' +
'You may need to login or otherwise authenticate the request.'},
status=status.HTTP_403_FORBIDDEN)
2011-05-10 13:49:28 +04:00
_503_SERVICE_UNAVAILABLE = ImmediateResponse(
2012-02-10 12:18:39 +04:00
{'detail': 'request was throttled'},
status=status.HTTP_503_SERVICE_UNAVAILABLE)
2011-05-10 13:49:28 +04:00
class BasePermission(object):
2011-05-10 13:49:28 +04:00
"""
A base class from which all permission classes should inherit.
"""
def __init__(self, view):
2011-05-10 13:49:28 +04:00
"""
Permission classes are always passed the current view on creation.
"""
self.view = view
2011-05-10 13:49:28 +04:00
def check_permission(self, auth):
"""
Should simply return, or raise an :exc:`response.ImmediateResponse`.
2011-05-10 13:49:28 +04:00
"""
pass
2011-05-04 12:21:17 +04:00
class FullAnonAccess(BasePermission):
2011-05-10 13:49:28 +04:00
"""
Allows full access.
"""
def check_permission(self, user):
pass
2011-05-04 12:21:17 +04:00
class IsAuthenticated(BasePermission):
2011-05-10 13:49:28 +04:00
"""
Allows access only to authenticated users.
"""
2011-05-10 13:49:28 +04:00
def check_permission(self, user):
if not user.is_authenticated():
raise _403_FORBIDDEN_RESPONSE
class IsAdminUser(BasePermission):
2011-05-10 13:49:28 +04:00
"""
Allows access only to admin users.
"""
def check_permission(self, user):
if not user.is_staff:
2011-05-10 13:49:28 +04:00
raise _403_FORBIDDEN_RESPONSE
class IsUserOrIsAnonReadOnly(BasePermission):
"""
The request is authenticated as a user, or is a read-only request.
"""
def check_permission(self, user):
2011-05-10 13:49:28 +04:00
if (not user.is_authenticated() and
2012-02-11 22:43:58 +04:00
self.view.method not in SAFE_METHODS):
2011-05-10 13:49:28 +04:00
raise _403_FORBIDDEN_RESPONSE
2011-06-14 14:08:29 +04:00
2012-02-11 21:48:35 +04:00
class DjangoModelPermissions(BasePermission):
2012-02-11 04:49:28 +04:00
"""
The request is authenticated using `django.contrib.auth` permissions.
See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions
2012-02-11 04:49:28 +04:00
It ensures that the user is authenticated, and has the appropriate
`add`/`change`/`delete` permissions on the model.
This permission should only be used on views with a `ModelResource`.
2012-02-11 17:00:38 +04:00
"""
2012-02-11 17:00:38 +04:00
# Map methods into required permission codes.
# Override this if you need to also provide 'read' permissions,
2012-02-12 01:15:06 +04:00
# or if you want to provide custom permission codes.
2012-02-11 17:00:38 +04:00
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
def get_required_permissions(self, method, model_cls):
"""
Given a model and an HTTP method, return the list of permission
codes that the user is required to have.
"""
kwargs = {
'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.module_name
2012-02-11 04:49:28 +04:00
}
2012-02-11 17:00:38 +04:00
try:
return [perm % kwargs for perm in self.perms_map[method]]
except KeyError:
2012-04-11 20:38:47 +04:00
ImmediateResponse(status.HTTP_405_METHOD_NOT_ALLOWED)
2012-02-11 04:49:28 +04:00
2012-02-11 17:00:38 +04:00
def check_permission(self, user):
method = self.view.method
model_cls = self.view.resource.model
perms = self.get_required_permissions(method, model_cls)
2012-02-11 04:49:28 +04:00
if not user.is_authenticated or not user.has_perms(perms):
2011-05-10 13:49:28 +04:00
raise _403_FORBIDDEN_RESPONSE
2011-06-14 14:08:29 +04:00
class BaseThrottle(BasePermission):
2011-05-10 13:49:28 +04:00
"""
Rate throttling of requests.
2011-05-10 13:49:28 +04:00
2011-06-14 14:08:29 +04:00
The rate (requests / seconds) is set by a :attr:`throttle` attribute
on the :class:`.View` class. The attribute is a string of the form 'number of
requests/period'.
Period should be one of: ('s', 'sec', 'm', 'min', 'h', 'hour', 'd', 'day')
Previous request information used for throttling is stored in the cache.
"""
2011-05-10 13:49:28 +04:00
2011-06-14 14:08:29 +04:00
attr_name = 'throttle'
default = '0/sec'
timer = time.time
def get_cache_key(self):
2011-06-14 14:08:29 +04:00
"""
Should return a unique cache-key which can be used for throttling.
2011-12-09 17:39:56 +04:00
Must be overridden.
"""
pass
def check_permission(self, auth):
2011-06-14 14:08:29 +04:00
"""
Check the throttling.
Return `None` or raise an :exc:`.ImmediateResponse`.
2011-06-14 14:08:29 +04:00
"""
num, period = getattr(self.view, self.attr_name, self.default).split('/')
self.num_requests = int(num)
self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
self.auth = auth
self.check_throttle()
def check_throttle(self):
2011-06-14 14:08:29 +04:00
"""
Implement the check to see if the request should be throttled.
On success calls :meth:`throttle_success`.
On failure calls :meth:`throttle_failure`.
"""
self.key = self.get_cache_key()
self.history = cache.get(self.key, [])
2011-06-14 14:08:29 +04:00
self.now = self.timer()
2011-06-14 14:08:29 +04:00
# Drop any requests from the history which have now passed the
# throttle duration
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
self.throttle_failure()
else:
self.throttle_success()
2011-06-13 22:42:37 +04:00
def throttle_success(self):
2011-06-14 14:08:29 +04:00
"""
Inserts the current request's timestamp along with the key
into the cache.
"""
self.history.insert(0, self.now)
cache.set(self.key, self.history, self.duration)
header = 'status=SUCCESS; next=%s sec' % self.next()
self.view.headers['X-Throttle'] = header
def throttle_failure(self):
2011-06-14 14:08:29 +04:00
"""
Called when a request to the API has failed due to throttling.
Raises a '503 service unavailable' response.
"""
header = 'status=FAILURE; next=%s sec' % self.next()
self.view.headers['X-Throttle'] = header
2011-06-14 14:08:29 +04:00
raise _503_SERVICE_UNAVAILABLE
2011-06-13 22:42:37 +04:00
def next(self):
"""
Returns the recommended next request time in seconds.
"""
if self.history:
remaining_duration = self.duration - (self.now - self.history[-1])
else:
remaining_duration = self.duration
available_requests = self.num_requests - len(self.history) + 1
return '%.2f' % (remaining_duration / float(available_requests))
2011-06-14 14:08:29 +04:00
class PerUserThrottling(BaseThrottle):
"""
2011-06-14 14:08:29 +04:00
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
be used.
"""
def get_cache_key(self):
if self.auth.is_authenticated():
2012-01-30 19:54:38 +04:00
ident = self.auth.id
else:
ident = self.view.request.META.get('REMOTE_ADDR', None)
2011-06-14 14:08:29 +04:00
return 'throttle_user_%s' % ident
class PerViewThrottling(BaseThrottle):
"""
2011-06-14 14:08:29 +04:00
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.
"""
def get_cache_key(self):
2011-06-14 14:08:29 +04:00
return 'throttle_view_%s' % self.view.__class__.__name__