Skip to content

Feat/add cache to permissions #9003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions rest_framework/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@
SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')


class PermissionCacheMixin:
def __init__(self):
self._cache = {}

def has_permission_value(self, request, view):
key = (request, view)
if key not in self._cache:
self._cache[key] = self.has_permission(request, view)
return self._cache[key]

def has_object_permission_value(self, request, view, obj):
key = (request, view, obj)
if key not in self._cache:
self._cache[key] = self.has_object_permission(request, view, obj)
return self._cache[key]


class OperationHolderMixin:
def __and__(self, other):
return OperandHolder(AND, self, other)
Expand Down Expand Up @@ -55,61 +72,64 @@ def __eq__(self, other):
)


class AND:
class AND(PermissionCacheMixin):
def __init__(self, op1, op2):
super().__init__()
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)
self.op1.has_permission_value(request, view) and
self.op2.has_permission_value(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)
self.op1.has_object_permission_value(request, view, obj) and
self.op2.has_object_permission_value(request, view, obj)
)


class OR:
class OR(PermissionCacheMixin):
def __init__(self, op1, op2):
super().__init__()
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)
self.op1.has_permission_value(request, view) or
self.op2.has_permission_value(request, view)
)

def has_object_permission(self, request, view, obj):
return (
self.op1.has_permission(request, view)
and self.op1.has_object_permission(request, view, obj)
self.op1.has_permission_value(request, view)
and self.op1.has_object_permission_value(request, view, obj)
) or (
self.op2.has_permission(request, view)
and self.op2.has_object_permission(request, view, obj)
self.op2.has_permission_value(request, view)
and self.op2.has_object_permission_value(request, view, obj)
)


class NOT:
class NOT(PermissionCacheMixin):
def __init__(self, op1):
super().__init__()
self.op1 = op1

def has_permission(self, request, view):
return not self.op1.has_permission(request, view)
return not self.op1.has_permission_value(request, view)

def has_object_permission(self, request, view, obj):
return not self.op1.has_object_permission(request, view, obj)
return not self.op1.has_object_permission_value(request, view, obj)


class BasePermissionMetaclass(OperationHolderMixin, type):
pass


class BasePermission(metaclass=BasePermissionMetaclass):
class BasePermission(PermissionCacheMixin, metaclass=BasePermissionMetaclass):
"""
A base class from which all permission classes should inherit.
"""
Expand Down
14 changes: 10 additions & 4 deletions rest_framework/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""
Provides an APIView class that is the base of all views in REST framework.
"""

from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db import connections, models
from django.http import Http404
from django.http.response import HttpResponseBase
from django.utils.cache import cc_delim_re, patch_vary_headers
from django.utils.encoding import smart_str
from django.utils.functional import cached_property
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View

Expand Down Expand Up @@ -277,6 +279,10 @@ def get_permissions(self):
"""
return [permission() for permission in self.permission_classes]

@cached_property
def cached_permissions(self):
return self.get_permissions()

def get_throttles(self):
"""
Instantiates and returns the list of throttles that this view uses.
Expand Down Expand Up @@ -328,8 +334,8 @@ def check_permissions(self, request):
Check if the request should be permitted.
Raises an appropriate exception if the request is not permitted.
"""
for permission in self.get_permissions():
if not permission.has_permission(request, self):
for permission in self.cached_permissions:
if not permission.has_permission_value(request, self):
self.permission_denied(
request,
message=getattr(permission, 'message', None),
Expand All @@ -341,8 +347,8 @@ def check_object_permissions(self, request, obj):
Check if the request should be permitted for a given object.
Raises an appropriate exception if the request is not permitted.
"""
for permission in self.get_permissions():
if not permission.has_object_permission(request, self, obj):
for permission in self.cached_permissions:
if not permission.has_object_permission_value(request, self, obj):
self.permission_denied(
request,
message=getattr(permission, 'message', None),
Expand Down
29 changes: 29 additions & 0 deletions tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,3 +735,32 @@ def has_object_permission(self, request, view, obj):
composed_perm = (IsAuthenticatedUserOwner | permissions.IsAdminUser)
hasperm = composed_perm().has_object_permission(request, None, None)
assert hasperm is False


class PermissionsCacheTests(TestCase):

class IsAuthenticatedUserOwnerWithCounter(permissions.IsAuthenticated):

def __init__(self):
super().__init__()
self.call_counter = 0

def has_permission(self, request, view):
self.call_counter += 1
return True

def test_composed_perm_permissions(self):
request = factory.get('/1', format='json')
request.user = AnonymousUser()

composed_perm = (self.IsAuthenticatedUserOwnerWithCounter | permissions.IsAdminUser)
composed_perm_instance = composed_perm()
# in OR composed permissions has_object_permission will call has_permission too.
# we must ensure that this method (has_permission) is called once
has_permission_value = composed_perm_instance.has_permission(request, None)
has_object_permission_value = composed_perm_instance.has_object_permission(request, None, None)

self.assertTrue(has_permission_value)
self.assertTrue(has_object_permission_value)

self.assertEqual(composed_perm_instance.op1.call_counter, 1)