From daea0064331e52a9689edacbd6d06d291a6b025f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 19 Jan 2018 17:47:48 +0100 Subject: [PATCH] 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. --- rest_framework/permissions.py | 64 +++++++++++++++++++++++++++++++++++ tests/test_permissions.py | 42 +++++++++++++++++++++++ 2 files changed, 106 insertions(+) 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 37540eb8e..b28e09cb4 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -522,3 +522,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