diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 0fef12a59..102c6ea98 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -102,6 +102,27 @@ Or, if you're using the `@api_view` decorator with function based views. __Note:__ when you set new permission classes through class attribute or decorators you're telling the view to ignore the default list set over the __settings.py__ file. +Provided they inherit from `rest_framework.permissions.BasePermission`, permissions can be composed using standard Python bitwise operators. For example, `IsAuthenticatedOrReadOnly` could be written: + + from rest_framework.permissions import BasePermission, IsAuthenticated + from rest_framework.response import Response + from rest_framework.views import APIView + + class ReadOnly(BasePermission): + def has_permission(self, request, view): + return request.method in SAFE_METHODS + + class ExampleView(APIView): + permission_classes = (IsAuthenticated|ReadOnly) + + def get(self, request, format=None): + content = { + 'status': 'request was permitted' + } + return Response(content) + +__Note:__ it only supports & -and- and | -or-. + --- # API Reference diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index a48058e66..dfe89ed2a 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -4,12 +4,76 @@ 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 SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') +class OperandHolder: + 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) & + self.op2.has_permission(request, view) + ) + + def has_object_permission(self, request, view, obj): + return ( + self.op1.has_object_permission(request, view, obj) & + 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) | + self.op2.has_permission(request, view) + ) + + def has_object_permission(self, request, view, obj): + return ( + self.op1.has_object_permission(request, view, obj) | + self.op2.has_object_permission(request, view, obj) + ) + + +class BasePermissionMetaclass(type): + def __and__(cls, other): + return OperandHolder(AND, cls, other) + + def __or__(cls, other): + return OperandHolder(OR, cls, other) + + def __rand__(cls, other): + return OperandHolder(AND, other, cls) + + def __ror__(cls, other): + return OperandHolder(OR, other, cls) + + +@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 31ee75b34..15fd9303c 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -540,3 +540,45 @@ class CustomPermissionsTests(TestCase): detail = response.data.get('detail') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(detail, self.custom_message) + + +class FakeUser: + def __init__(self, auth=False): + self.is_authenticated = auth + + +class PermissionsCompositionTests(TestCase): + def test_and_false(self): + request = factory.get('/1', format='json') + request.user = FakeUser(auth=False) + composed_perm = permissions.IsAuthenticated & permissions.AllowAny + assert composed_perm().has_permission(request, None) is False + + def test_and_true(self): + request = factory.get('/1', format='json') + request.user = FakeUser(auth=True) + composed_perm = permissions.IsAuthenticated & permissions.AllowAny + assert composed_perm().has_permission(request, None) is True + + def test_or_false(self): + request = factory.get('/1', format='json') + request.user = FakeUser(auth=False) + composed_perm = permissions.IsAuthenticated | permissions.AllowAny + assert composed_perm().has_permission(request, None) is True + + def test_or_true(self): + request = factory.get('/1', format='json') + request.user = FakeUser(auth=True) + composed_perm = permissions.IsAuthenticated | permissions.AllowAny + assert composed_perm().has_permission(request, None) is True + + def test_several_levels(self): + request = factory.get('/1', format='json') + request.user = FakeUser(auth=True) + composed_perm = ( + permissions.IsAuthenticated & + permissions.IsAuthenticated & + permissions.IsAuthenticated & + permissions.IsAuthenticated + ) + assert composed_perm().has_permission(request, None) is True