Skip to content

Commit

Permalink
permissions: Allow permissions to be composed (#5753)
Browse files Browse the repository at this point in the history
* permissions: Allow permissions to be composed

Implement a system to compose permissions with and / or.
This is performed by returning an `OperationHolder` instance that keeps the
permission classes and type of composition (and / or).
When called it will return a AND/OR instance that will then delegate the
permission check to the operands.

* permissions: Add documentation about composed permissions

* Fix documentation typo in permissions
  • Loading branch information
xordoquy authored and tomchristie committed Oct 3, 2018
1 parent 8908934 commit b41a6cf
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 0 deletions.
21 changes: 21 additions & 0 deletions docs/api-guide/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions rest_framework/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,76 @@
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.
Expand Down
42 changes: 42 additions & 0 deletions tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,3 +540,45 @@ def test_permission_denied_for_object_with_custom_detail(self):
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

0 comments on commit b41a6cf

Please sign in to comment.