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:
Roberto Rosario
2019-03-05 19:36:43 -04:00
parent 378511aea3
commit 4b444a75cc
4 changed files with 182 additions and 38 deletions

View File

@@ -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):

View File

@@ -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:

View File

@@ -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

View File

@@ -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)