mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-10 19:56:59 +03:00
4aea8dd65a
* Change semantic of OR of two permission classes The original semantic of OR is defined as: the request pass either of the two has_permission() check, and pass either of the two has_object_permission() check, which could lead to situations that a request passes has_permission() but fails on has_object_permission() of Permission Class A, fails has_permission() but passes has_object_permission() of Permission Class B, passes the OR permission check. This should not be the desired permission check semantic in applications, because such a request should fail on either Permission Class (on Django object permission) alone, but passes the OR or the two. My code fix this by changing the semantic so that the request has to pass either class's has_permission() and has_object_permission() to get the Django object permission of the OR check. * Update rest_framework/permissions.py * Update setup.cfg Co-authored-by: Mark Yu <markyu98@outlook.com> Co-authored-by: Tom Christie <tom@tomchristie.com>
304 lines
9.0 KiB
Python
304 lines
9.0 KiB
Python
"""
|
|
Provides a set of pluggable permission policies.
|
|
"""
|
|
from django.http import Http404
|
|
|
|
from rest_framework import exceptions
|
|
|
|
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')
|
|
|
|
|
|
class OperationHolderMixin:
|
|
def __and__(self, other):
|
|
return OperandHolder(AND, self, other)
|
|
|
|
def __or__(self, other):
|
|
return OperandHolder(OR, self, other)
|
|
|
|
def __rand__(self, other):
|
|
return OperandHolder(AND, other, self)
|
|
|
|
def __ror__(self, other):
|
|
return OperandHolder(OR, other, self)
|
|
|
|
def __invert__(self):
|
|
return SingleOperandHolder(NOT, self)
|
|
|
|
|
|
class SingleOperandHolder(OperationHolderMixin):
|
|
def __init__(self, operator_class, op1_class):
|
|
self.operator_class = operator_class
|
|
self.op1_class = op1_class
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
op1 = self.op1_class(*args, **kwargs)
|
|
return self.operator_class(op1)
|
|
|
|
|
|
class OperandHolder(OperationHolderMixin):
|
|
def __init__(self, operator_class, op1_class, op2_class):
|
|
self.operator_class = operator_class
|
|
self.op1_class = op1_class
|
|
self.op2_class = op2_class
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
op1 = self.op1_class(*args, **kwargs)
|
|
op2 = self.op2_class(*args, **kwargs)
|
|
return self.operator_class(op1, op2)
|
|
|
|
|
|
class AND:
|
|
def __init__(self, op1, op2):
|
|
self.op1 = op1
|
|
self.op2 = op2
|
|
|
|
def has_permission(self, request, view):
|
|
return (
|
|
self.op1.has_permission(request, view) and
|
|
self.op2.has_permission(request, view)
|
|
)
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
return (
|
|
self.op1.has_object_permission(request, view, obj) and
|
|
self.op2.has_object_permission(request, view, obj)
|
|
)
|
|
|
|
|
|
class OR:
|
|
def __init__(self, op1, op2):
|
|
self.op1 = op1
|
|
self.op2 = op2
|
|
|
|
def has_permission(self, request, view):
|
|
return (
|
|
self.op1.has_permission(request, view) or
|
|
self.op2.has_permission(request, view)
|
|
)
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
return (
|
|
self.op1.has_permission(request, view)
|
|
and self.op1.has_object_permission(request, view, obj)
|
|
) or (
|
|
self.op2.has_permission(request, view)
|
|
and self.op2.has_object_permission(request, view, obj)
|
|
)
|
|
|
|
|
|
class NOT:
|
|
def __init__(self, op1):
|
|
self.op1 = op1
|
|
|
|
def has_permission(self, request, view):
|
|
return not self.op1.has_permission(request, view)
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
return not self.op1.has_object_permission(request, view, obj)
|
|
|
|
|
|
class BasePermissionMetaclass(OperationHolderMixin, type):
|
|
pass
|
|
|
|
|
|
class BasePermission(metaclass=BasePermissionMetaclass):
|
|
"""
|
|
A base class from which all permission classes should inherit.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
"""
|
|
Return `True` if permission is granted, `False` otherwise.
|
|
"""
|
|
return True
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
"""
|
|
Return `True` if permission is granted, `False` otherwise.
|
|
"""
|
|
return True
|
|
|
|
|
|
class AllowAny(BasePermission):
|
|
"""
|
|
Allow any access.
|
|
This isn't strictly required, since you could use an empty
|
|
permission_classes list, but it's useful because it makes the intention
|
|
more explicit.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
return True
|
|
|
|
|
|
class IsAuthenticated(BasePermission):
|
|
"""
|
|
Allows access only to authenticated users.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
return bool(request.user and request.user.is_authenticated)
|
|
|
|
|
|
class IsAdminUser(BasePermission):
|
|
"""
|
|
Allows access only to admin users.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
return bool(request.user and request.user.is_staff)
|
|
|
|
|
|
class IsAuthenticatedOrReadOnly(BasePermission):
|
|
"""
|
|
The request is authenticated as a user, or is a read-only request.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
return bool(
|
|
request.method in SAFE_METHODS or
|
|
request.user and
|
|
request.user.is_authenticated
|
|
)
|
|
|
|
|
|
class DjangoModelPermissions(BasePermission):
|
|
"""
|
|
The request is authenticated using `django.contrib.auth` permissions.
|
|
See: https://docs.djangoproject.com/en/dev/topics/auth/#permissions
|
|
|
|
It ensures that the user is authenticated, and has the appropriate
|
|
`add`/`change`/`delete` permissions on the model.
|
|
|
|
This permission can only be applied against view classes that
|
|
provide a `.queryset` attribute.
|
|
"""
|
|
|
|
# Map methods into required permission codes.
|
|
# Override this if you need to also provide 'view' permissions,
|
|
# or if you want to provide custom permission codes.
|
|
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'],
|
|
}
|
|
|
|
authenticated_users_only = True
|
|
|
|
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.model_name
|
|
}
|
|
|
|
if method not in self.perms_map:
|
|
raise exceptions.MethodNotAllowed(method)
|
|
|
|
return [perm % kwargs for perm in self.perms_map[method]]
|
|
|
|
def _queryset(self, view):
|
|
assert hasattr(view, 'get_queryset') \
|
|
or getattr(view, 'queryset', None) is not None, (
|
|
'Cannot apply {} on a view that does not set '
|
|
'`.queryset` or have a `.get_queryset()` method.'
|
|
).format(self.__class__.__name__)
|
|
|
|
if hasattr(view, 'get_queryset'):
|
|
queryset = view.get_queryset()
|
|
assert queryset is not None, (
|
|
'{}.get_queryset() returned None'.format(view.__class__.__name__)
|
|
)
|
|
return queryset
|
|
return view.queryset
|
|
|
|
def has_permission(self, request, view):
|
|
# Workaround to ensure DjangoModelPermissions are not applied
|
|
# to the root view when using DefaultRouter.
|
|
if getattr(view, '_ignore_model_permissions', False):
|
|
return True
|
|
|
|
if not request.user or (
|
|
not request.user.is_authenticated and self.authenticated_users_only):
|
|
return False
|
|
|
|
queryset = self._queryset(view)
|
|
perms = self.get_required_permissions(request.method, queryset.model)
|
|
|
|
return request.user.has_perms(perms)
|
|
|
|
|
|
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
|
|
"""
|
|
Similar to DjangoModelPermissions, except that anonymous users are
|
|
allowed read-only access.
|
|
"""
|
|
authenticated_users_only = False
|
|
|
|
|
|
class DjangoObjectPermissions(DjangoModelPermissions):
|
|
"""
|
|
The request is authenticated using Django's object-level permissions.
|
|
It requires an object-permissions-enabled backend, such as Django Guardian.
|
|
|
|
It ensures that the user is authenticated, and has the appropriate
|
|
`add`/`change`/`delete` permissions on the object using .has_perms.
|
|
|
|
This permission can only be applied against view classes that
|
|
provide a `.queryset` attribute.
|
|
"""
|
|
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_object_permissions(self, method, model_cls):
|
|
kwargs = {
|
|
'app_label': model_cls._meta.app_label,
|
|
'model_name': model_cls._meta.model_name
|
|
}
|
|
|
|
if method not in self.perms_map:
|
|
raise exceptions.MethodNotAllowed(method)
|
|
|
|
return [perm % kwargs for perm in self.perms_map[method]]
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
# authentication checks have already executed via has_permission
|
|
queryset = self._queryset(view)
|
|
model_cls = queryset.model
|
|
user = request.user
|
|
|
|
perms = self.get_required_object_permissions(request.method, model_cls)
|
|
|
|
if not user.has_perms(perms, obj):
|
|
# If the user does not have permissions we need to determine if
|
|
# they have read permissions to see 403, or not, and simply see
|
|
# a 404 response.
|
|
|
|
if request.method in SAFE_METHODS:
|
|
# Read permissions already checked and failed, no need
|
|
# to make another lookup.
|
|
raise Http404
|
|
|
|
read_perms = self.get_required_object_permissions('GET', model_cls)
|
|
if not user.has_perms(read_perms, obj):
|
|
raise Http404
|
|
|
|
# Has read permissions.
|
|
return False
|
|
|
|
return True
|