diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index f24775278..a086af568 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -4,6 +4,7 @@ Provides a set of pluggable permission policies. from __future__ import unicode_literals from django.http import Http404 +from django.utils import six from rest_framework import exceptions from rest_framework.compat import is_authenticated @@ -11,6 +12,49 @@ from rest_framework.compat import is_authenticated SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') +class BasePermissionMetaClass(type): + """ + Metaclass for the base permission class. + """ + + def __init__(cls, name, bases, dct): + super(BasePermissionMetaClass, cls).__init__(name, bases, dct) + + def __or__(klass, perm_class): + """ + Returns a new permission class performing a logical OR between two + different permission classes. + """ + assert issubclass(perm_class, BasePermission), ( + "%s is not a subclass of BasePermission" % perm_class + ) + + def has_permission(self, request, view): + perm = klass.has_permission(self, request, view) + otherwise = perm_class().has_permission(request, view) + + return perm or otherwise + + def has_object_permission(self, request, view, obj): + perm = klass.has_object_permission(self, request, view, obj) + otherwise = perm_class().has_object_permission(request, view, obj) + + return perm or otherwise + + t = type( + str('{0}Or{1}').format(klass.__name__, perm_class.__name__), + (klass,), + {} + ) + + t.has_permission = has_permission + t.has_object_permission = has_object_permission + + return t + + +# Needed for performing logical operations between permission classes. +@six.add_metaclass(BasePermissionMetaClass) class BasePermission(object): """ A base class from which all permission classes should inherit. diff --git a/tests/test_permissions.py b/tests/test_permissions.py index cabf66883..c3de9fcca 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -483,3 +483,36 @@ class CustomPermissionsTests(TestCase): detail = response.data.get('detail') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(detail, self.custom_message) + + +class AllowPermission(permissions.BasePermission): + def has_permission(self, request, view): + return True + + def has_object_permission(self, request, view, obj): + return True + + +class DenyPermission(permissions.BasePermission): + def has_permission(self, request, view): + return False + + def has_object_permission(self, request, view, obj): + return False + + +class PermissionOperatorTests(TestCase): + def test_permission_logical_or(self): + # We evaluate all possible combinations. + + # We pass None to the has_permission function, request and view aren't + # relevant to these classes. + self.assertFalse((DenyPermission | DenyPermission)().has_permission(None, None)) + self.assertTrue((AllowPermission | DenyPermission)().has_permission(None, None)) + self.assertTrue((DenyPermission | AllowPermission)().has_permission(None, None)) + self.assertTrue((AllowPermission | AllowPermission)().has_permission(None, None)) + + self.assertFalse((DenyPermission | DenyPermission)().has_object_permission(None, None, None)) + self.assertTrue((AllowPermission | DenyPermission)().has_object_permission(None, None, None)) + self.assertTrue((DenyPermission | AllowPermission)().has_object_permission(None, None, None)) + self.assertTrue((AllowPermission | AllowPermission)().has_object_permission(None, None, None))