mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-28 04:24:00 +03:00
35025adef8
Django permissions allows for 'view_modelname'. When users have view_modelname permission, these permission classes don't recognize it and reject access to the user. My specific case was assigning customers to a group with the group having specific permissions allowed from the model permissions. Made this edit in an extension of DjangoModelPermissions to make it work. Thought it would be useful to have inherently.
301 lines
9.0 KiB
Python
301 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_object_permission(request, view, obj) or
|
|
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': ['%(app_label)s.view_%(model_name)s'],
|
|
'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': ['%(app_label)s.view_%(model_name)s'],
|
|
'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
|