Add support for multi access filtering
This change allows filtering a queryset by multiple permission following a logic operator to define the relationship. Example: In order to access an instance of MetadataTypeDocumentType the document type view and metadata type view permissions are required. The computation for this access control can now be coded using .restrict_queryset_by_accesses. Custom permission checking in the view is no longer required. Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
This commit is contained in:
@@ -48,7 +48,7 @@ class ModelPermission(object):
|
||||
return StoredPermission.objects.filter(pk__in=pks)
|
||||
|
||||
@classmethod
|
||||
def get_inheritance(cls, model):
|
||||
def get_inheritances(cls, model):
|
||||
return cls._inheritances[model]
|
||||
|
||||
@classmethod
|
||||
@@ -69,7 +69,8 @@ class ModelPermission(object):
|
||||
|
||||
@classmethod
|
||||
def register_inheritance(cls, model, related):
|
||||
cls._inheritances[model] = related
|
||||
cls._inheritances.setdefault(model, [])
|
||||
cls._inheritances[model].append(related)
|
||||
|
||||
@classmethod
|
||||
def register_proxy(cls, source, model):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import warnings
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
@@ -76,9 +77,6 @@ class AccessControlListManager(models.Manager):
|
||||
else:
|
||||
# Case 2: Related field of a single type, single ContentType,
|
||||
# multiple object id
|
||||
related_field = get_related_field(
|
||||
model=queryset.model, related_field_name=related_field_name
|
||||
)
|
||||
content_type = ContentType.objects.get_for_model(
|
||||
model=related_field.related_model
|
||||
)
|
||||
@@ -94,18 +92,23 @@ class AccessControlListManager(models.Manager):
|
||||
# TODO: OR for document pages, version, doc, and types
|
||||
# TODO: AND for new cabinet levels ACLs
|
||||
try:
|
||||
related_field_model_related_field_name = ModelPermission.get_inheritance(
|
||||
related_field_model_related_fields = ModelPermission.get_inheritances(
|
||||
model=related_field.related_model
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
related_field_name = '{}__{}'.format(related_field_name, related_field_model_related_field_name)
|
||||
related_field_inherited_acl_queries = self._get_acl_filters(
|
||||
queryset=queryset, stored_permission=stored_permission,
|
||||
user=user, related_field_name=related_field_name
|
||||
)
|
||||
result.extend(related_field_inherited_acl_queries)
|
||||
relation_result = []
|
||||
for related_field_model_related_field_name in related_field_model_related_fields:
|
||||
related_field_name = '{}__{}'.format(related_field_name, related_field_model_related_field_name)
|
||||
related_field_inherited_acl_queries = self._get_acl_filters(
|
||||
queryset=queryset, stored_permission=stored_permission,
|
||||
user=user, related_field_name=related_field_name
|
||||
)
|
||||
|
||||
relation_result.append(reduce(operator.and_, related_field_inherited_acl_queries))
|
||||
|
||||
result.append(reduce(operator.or_, relation_result))
|
||||
else:
|
||||
# Case 1: Original model, single ContentType, multiple object id
|
||||
content_type = ContentType.objects.get_for_model(model=queryset.model)
|
||||
@@ -118,17 +121,22 @@ class AccessControlListManager(models.Manager):
|
||||
|
||||
# Case 4: Original model, has an inherited related field
|
||||
try:
|
||||
related_field_name = ModelPermission.get_inheritance(
|
||||
related_fields = ModelPermission.get_inheritances(
|
||||
model=queryset.model
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
inherited_acl_queries = self._get_acl_filters(
|
||||
queryset=queryset, stored_permission=stored_permission,
|
||||
related_field_name=related_field_name, user=user
|
||||
)
|
||||
result.extend(inherited_acl_queries)
|
||||
relation_result = []
|
||||
|
||||
for related_field_name in related_fields:
|
||||
inherited_acl_queries = self._get_acl_filters(
|
||||
queryset=queryset, stored_permission=stored_permission,
|
||||
related_field_name=related_field_name, user=user
|
||||
)
|
||||
relation_result.append(reduce(operator.and_, inherited_acl_queries))
|
||||
|
||||
result.append(reduce(operator.or_, relation_result))
|
||||
|
||||
return result
|
||||
|
||||
@@ -172,32 +180,35 @@ class AccessControlListManager(models.Manager):
|
||||
return queryset
|
||||
|
||||
try:
|
||||
parent_accessor = ModelPermission.get_inheritance(
|
||||
related_fields = ModelPermission.get_inheritances(
|
||||
model=type(obj)
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
parent_object = resolve_attribute(
|
||||
obj=obj, attribute=parent_accessor
|
||||
)
|
||||
except AttributeError:
|
||||
# Parent accessor is not an attribute, try it as a related
|
||||
# field.
|
||||
parent_object = return_related(
|
||||
instance=obj, related_field=parent_accessor
|
||||
)
|
||||
content_type = ContentType.objects.get_for_model(model=parent_object)
|
||||
try:
|
||||
queryset = queryset | self.get(
|
||||
content_type=content_type, object_id=parent_object.pk,
|
||||
role=role
|
||||
).permissions.all()
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
for related_field_name in related_fields:
|
||||
try:
|
||||
parent_object = resolve_attribute(
|
||||
obj=obj, attribute=related_field_name
|
||||
)
|
||||
except AttributeError:
|
||||
# Parent accessor is not an attribute, try it as a related
|
||||
# field.
|
||||
parent_object = return_related(
|
||||
instance=obj, related_field=related_field_name
|
||||
)
|
||||
content_type = ContentType.objects.get_for_model(model=parent_object)
|
||||
try:
|
||||
queryset = queryset | self.get(
|
||||
content_type=content_type, object_id=parent_object.pk,
|
||||
role=role
|
||||
).permissions.all()
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
queryset = queryset | self._get_inherited_object_permissions(obj=parent_object, role=role)
|
||||
queryset = queryset | self._get_inherited_object_permissions(
|
||||
obj=parent_object, role=role
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
@@ -216,6 +227,18 @@ class AccessControlListManager(models.Manager):
|
||||
|
||||
return acl
|
||||
|
||||
def restrict_queryset_by_accesses(self, operator, permissions, queryset, user):
|
||||
result = []
|
||||
|
||||
for permission in permissions:
|
||||
result.append(
|
||||
self.restrict_queryset(
|
||||
permission=permission, queryset=queryset, user=user
|
||||
)
|
||||
)
|
||||
|
||||
return reduce(operator, result)
|
||||
|
||||
def restrict_queryset(self, permission, queryset, user):
|
||||
# Check directly granted permission via a role
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import operator
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@@ -31,6 +32,11 @@ class AccessControlList(models.Model):
|
||||
* Role - Custom role that is being granted a permission. Roles are created
|
||||
in the Setup menu.
|
||||
"""
|
||||
# Multiple inheritance operator types
|
||||
OPERATOR_AND = operator.and_
|
||||
OPERATOR_OR = operator.or_
|
||||
operator_default = OPERATOR_AND
|
||||
|
||||
content_type = models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='object_content_type',
|
||||
to=ContentType
|
||||
|
||||
@@ -285,3 +285,117 @@ class InheritedPermissionTestCase(ACLTestMixin, BaseTestCase):
|
||||
)
|
||||
|
||||
self.assertTrue(self.test_permission.stored_permission in queryset)
|
||||
|
||||
|
||||
class MultipleAccessTestCase(ACLTestMixin, BaseTestCase):
|
||||
def setUp(self):
|
||||
super(MultipleAccessTestCase, self).setUp()
|
||||
self._create_test_permission()
|
||||
self._create_test_permission_2()
|
||||
|
||||
self._create_test_model(model_name='TestModelParent1')
|
||||
self._create_test_model(model_name='TestModelParent2')
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent_1': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children1',
|
||||
to='TestModelParent1',
|
||||
),
|
||||
'parent_2': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children2',
|
||||
to='TestModelParent2',
|
||||
)
|
||||
}, model_name='TestModelChild'
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent1, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent2, permissions=(
|
||||
self.test_permission_2,
|
||||
)
|
||||
)
|
||||
|
||||
self.test_object_parent_1 = self.TestModelParent1.objects.create()
|
||||
self.test_object_parent_2 = self.TestModelParent2.objects.create()
|
||||
self.test_object_child = self.TestModelChild.objects.create(
|
||||
parent_1=self.test_object_parent_1, parent_2=self.test_object_parent_2
|
||||
)
|
||||
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent_1'
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent_2'
|
||||
)
|
||||
|
||||
def test_restrict_queryset_and_operator_first_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child not in queryset)
|
||||
|
||||
def test_restrict_queryset_and_operator_second_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child not in queryset)
|
||||
|
||||
def test_restrict_queryset_and_operator_both_permissions(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_first_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_second_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_both_permissions(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
Reference in New Issue
Block a user