Skip to content
Merged
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
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Direct Contributors
* Ben Wilber
* Mfon Eti-mfon
* Irtaza Akram
* Matthew Ethan Tam


Other Contributors
Expand Down
21 changes: 14 additions & 7 deletions braces/views/_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,14 @@ def check_permissions(self, request):
if self.object_level_permissions:
if hasattr(self, "object") and self.object is not None:
has_permission = request.user.has_perm(
self.get_permission_required(request), self.object
perms, self.object
)
elif hasattr(self, "get_object") and callable(self.get_object):
has_permission = request.user.has_perm(
self.get_permission_required(request), self.get_object()
perms, self.get_object()
)
else:
has_permission = request.user.has_perm(
self.get_permission_required(request)
)
has_permission = request.user.has_perm(perms)
return has_permission

def dispatch(self, request, *args, **kwargs):
Expand Down Expand Up @@ -275,21 +273,30 @@ def check_permissions(self, request):
permissions = self.get_permission_required(request)
perms_all = permissions.get("all")
perms_any = permissions.get("any")
instance_object = None

self._check_permissions_keys_set(perms_all, perms_any)
self._check_perms_keys("all", perms_all)
self._check_perms_keys("any", perms_any)

if self.object_level_permissions:
if hasattr(self, "object") and self.object is not None:
instance_object = self.object
elif hasattr(self, "get_object") and callable(self.get_object):
instance_object = self.get_object()
# Check that user has all permissions in the list/tuple
if perms_all:
# Why not `return request.user.has_perms(perms_all)`?
# There may be optional permissions below.
if not request.user.has_perms(perms_all):
if not request.user.has_perms(perms_all, instance_object):
return False

# If perms_any, check that user has at least one in the list/tuple
if perms_any:
any_perms = [request.user.has_perm(perm) for perm in perms_any]
any_perms = [
request.user.has_perm(perm, instance_object)
for perm in perms_any
]
if not any_perms or not any(any_perms):
return False
return True
Expand Down
1 change: 1 addition & 0 deletions docs/access.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ The ``MultiplePermissionsRequiredMixin`` is a more powerful version of the :ref:
}

The ``MultiplePermissionsRequiredMixin`` also offers a ``check_permissions`` method that should be overridden if you need custom permissions checking.
Additionally similar to ``PermissionRequiredMixin``, ``MultiplePermissionsRequiredMixin`` offers object level permission checking.


.. _GroupRequiredMixin:
Expand Down
28 changes: 28 additions & 0 deletions tests/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Testing backend for object level permissions"""
from tests.helpers import PermissionChecker


class PermissionsCheckerBackend:
"""
Custom Permission Backend for testing Object Level Permissions.
"""
supports_object_permissions = True
supports_anonymous_user = True
supports_inactive_user = True

@staticmethod
def authenticate():
"""Required for a backend"""
return None

@staticmethod
def has_perm(user_obj, perm, obj=None):
"""Used for checking permissions using the `PermissionChecker`"""
check = PermissionChecker(user_obj)
return check.has_perm(perm, obj)

@staticmethod
def has_perms(user_obj, perms: list[str], obj=None):
"""Used for checking multiple permissions using the `PermissionChecker`"""
check = PermissionChecker(user_obj)
return check.has_perms(perms, obj)
35 changes: 34 additions & 1 deletion tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import factory

from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType

from .models import Article
from .models import Article, UserObjectPermissions


def _get_perm(perm_name):
Expand Down Expand Up @@ -54,3 +55,35 @@ def permissions(self, create, extracted, **kwargs):
if create and extracted:
# We have a saved object and a list of permission names
self.user_permissions.add(*[_get_perm(pn) for pn in extracted])


class ContentTypeFactory(factory.django.DjangoModelFactory):
"""Factory for creating `ContentType` model objects"""
app_label = factory.Sequence(lambda n: f"app_label_{n}")
model = factory.Sequence(lambda n: f"model_{n}")

class Meta:
model = ContentType
abstract = False


class PermissionFactory(factory.django.DjangoModelFactory):
"""Factory for creating `Permission` model objects"""
name = factory.Sequence(lambda n: f"name_{n}")
codename = factory.Sequence(lambda n: f"codename_{n}")
content_type = factory.SubFactory(ContentTypeFactory)

class Meta:
model = Permission
abstract = False


class UserObjectPermissionsFactory(factory.django.DjangoModelFactory):
"""Factory for creating `UserObjectPermissions` model objects"""
user = factory.SubFactory(UserFactory)
permission = factory.SubFactory(PermissionFactory)
article_object = factory.SubFactory(ArticleFactory)

class Meta:
model = UserObjectPermissions
abstract = False
40 changes: 39 additions & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django import test
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import AnonymousUser, User, Permission
from django.core.serializers.json import DjangoJSONEncoder

from tests.models import UserObjectPermissions


class TestViewHelper:
"""
Expand Down Expand Up @@ -65,3 +67,39 @@ def default(self, obj):
if isinstance(obj, set):
return list(obj)
return super(DjangoJSONEncoder, self).default(obj)


class PermissionChecker:
"""
Custom Permission checker for testing of Object Level Permissions
"""
def __init__(self, user: User):
self.user = user

def has_perm(self, perm: str, obj=None) -> bool:
"""This function is used to check for object level permissions"""
if self.user and not self.user.is_active:
return False
elif self.user and self.user.is_superuser:
return True
if "." in perm:
perm = perm.split(".", maxsplit=1)[1]
permission_obj = Permission.objects.get(codename=perm)
if obj is None:
return perm in self.user.get_all_permissions(perm)
return UserObjectPermissions.objects.filter(
permission=permission_obj,
user=self.user,
article_object=obj
).exists()

def has_perms(self, perms: list[str], obj=None) -> bool:
"""This function is used to check for object level permissions"""
if self.user and not self.user.is_active:
return False
elif self.user and self.user.is_superuser:
return True
if not perms:
return False
return all(self.has_perm(perm) for perm in perms)

9 changes: 9 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.contrib.auth.models import Permission, User
from django.db import models


Expand Down Expand Up @@ -29,3 +30,11 @@ def get_canonical_slug(self):
if self.author:
return f"{self.author.username}-{self.slug}"
return f"unauthored-{self.slug}"


class UserObjectPermissions(models.Model):
"""Django model used to test and assign object level permissions"""
user = models.ForeignKey(User, on_delete=models.CASCADE)
permission = models.ForeignKey(Permission, on_delete=models.CASCADE)
article_object = models.ForeignKey(Article, on_delete=models.CASCADE)

5 changes: 5 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}
}

AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'tests.backends.PermissionsCheckerBackend',
)

MIDDLEWARE_CLASSES = [
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
Expand Down
110 changes: 109 additions & 1 deletion tests/test_access_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from django import test
from django.contrib.auth.models import Permission
from django.test.utils import override_settings
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.http import Http404, HttpResponse
Expand All @@ -11,7 +12,7 @@

from django.urls import reverse_lazy

from .factories import GroupFactory, UserFactory
from .factories import GroupFactory, UserFactory, UserObjectPermissionsFactory, ArticleFactory
from .helpers import TestViewHelper
from .views import (
PermissionRequiredView,
Expand Down Expand Up @@ -413,6 +414,50 @@ def test_invalid_permission(self):
with self.assertRaises(ImproperlyConfigured):
self.dispatch_view(self.build_request(), permission_required=None)

def test_object_level_permissions(self):
"""
Tests that object level permissions perform as expected, where object level permissions and
global level permissions
"""
# Arrange
article = ArticleFactory()
self.view_class = PermissionRequiredView
self.view_url = f"/object_level_permission_required/?pk={article.pk}"
tests_add_article = Permission.objects.get(codename="add_article")
permissions = "tests.add_article"
valid_user = UserFactory(permissions=[permissions])
invalid_user_1 = UserFactory(permissions=["auth.add_user"])
invalid_user_2 = UserFactory(permissions=[permissions])
UserObjectPermissionsFactory(
user=valid_user, permission=tests_add_article, article_object=article
)
# Act
valid_req = self.build_request(path=self.view_url, user=valid_user)
valid_resp = self.dispatch_view(
valid_req,
permission_required=permissions,
object_level_permissions=True,
raise_exception=True
)
invalid_req_1 = self.build_request(path=self.view_url, user=invalid_user_1)
invalid_req_2 = self.build_request(path=self.view_url, user=invalid_user_2)
# Assert
self.assertEqual(valid_resp.status_code, 200)
with self.assertRaises(PermissionDenied):
self.dispatch_view(
invalid_req_1,
permission_required=permissions,
object_level_permissions=True,
raise_exception=True
)
with self.assertRaises(PermissionDenied):
self.dispatch_view(
invalid_req_2,
permission_required=permissions,
object_level_permissions=True,
raise_exception=True
)


@pytest.mark.django_db
class TestMultiplePermissionsRequiredMixin(
Expand Down Expand Up @@ -534,6 +579,69 @@ def test_any_permissions_key(self):
permissions=permissions,
)

def test_all_object_level_permissions_key(self):
"""
Tests that when a user has all the correct object level permissions, response is OK,
else forbidden.
"""
# Arrange
article = ArticleFactory()
self.view_class = MultiplePermissionsRequiredView
self.view_url = f"/multiple_object_level_permissions_required/?pk={article.pk}"
auth_add_user = Permission.objects.get(codename="add_user")
tests_add_article = Permission.objects.get(codename="add_article")
permissions = {"all": ["auth.add_user", "tests.add_article"]}
valid_user = UserFactory(permissions=permissions["all"])
invalid_user = UserFactory(permissions=["auth.add_user"])
UserObjectPermissionsFactory(user=valid_user, permission=auth_add_user, article_object=article)
UserObjectPermissionsFactory(user=valid_user, permission=tests_add_article, article_object=article)
# Act
valid_req = self.build_request(path=self.view_url, user=valid_user)
valid_resp = self.dispatch_view(
valid_req, permissions=permissions, object_level_permissions=True
)
invalid_req = self.build_request(path=self.view_url, user=invalid_user)
# Arrange
self.assertEqual(valid_resp.status_code, 200)
with self.assertRaises(PermissionDenied):
self.dispatch_view(
invalid_req, permissions=permissions, object_level_permissions=True, raise_exception=True
)

def test_any_object_level_permissions_key(self):
"""
Tests that when a user has any the correct object level permissions, response is OK,
else forbidden.
"""
# Arrange
article = ArticleFactory()
self.view_url = f"/multiple_object_level_permissions_required/?pk={article.pk}"
self.view_class = MultiplePermissionsRequiredView
auth_add_user = Permission.objects.get(codename="add_user")
tests_add_article = Permission.objects.get(codename="add_article")
permissions = {"any": ["auth.add_user", "tests.add_article"]}
user = UserFactory(permissions=[permissions["any"][0]])
user_1 = UserFactory()
user_2 = UserFactory(permissions=permissions["any"])
UserObjectPermissionsFactory(user=user, permission=auth_add_user, article_object=article)
UserObjectPermissionsFactory(user=user, permission=tests_add_article, article_object=article)
# Act
valid_req = self.build_request(path=self.view_url, user=user)
valid_resp = self.dispatch_view(
valid_req, permissions=permissions, object_level_permissions=True, raise_exception=True
)
invalid_req_1 = self.build_request(path=self.view_url, user=user_1)
invalid_req_2 = self.build_request(path=self.view_url, user=user_2)
# Assert
self.assertEqual(valid_resp.status_code, 200)
with self.assertRaises(PermissionDenied):
self.dispatch_view(
invalid_req_1, permissions=permissions, object_level_permissions=True, raise_exception=True
)
with self.assertRaises(PermissionDenied):
self.dispatch_view(invalid_req_2, permissions=permissions, object_level_permissions=True, raise_exception=True)



@pytest.mark.django_db
class TestSuperuserRequiredMixin(_TestAccessBasicsMixin, test.TestCase):
Expand Down
5 changes: 5 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,16 @@
path("context/", views.ContextView.as_view(), name="context"),
# PermissionRequiredMixin tests
path("permission_required/", views.PermissionRequiredView.as_view()),
path("object_level_permission_required/", views.PermissionRequiredView.as_view(object_level_permissions=True)),
# MultiplePermissionsRequiredMixin tests
path(
"multiple_permissions_required/",
views.MultiplePermissionsRequiredView.as_view(),
),
path(
"multiple_object_level_permissions_required/",
views.MultiplePermissionsRequiredView.as_view(object_level_permissions=True),
),
# SuperuserRequiredMixin tests
path("superuser_required/", views.SuperuserRequiredView.as_view()),
# StaffuserRequiredMixin tests
Expand Down
Loading
Loading