diff --git a/mayan/apps/acls/classes.py b/mayan/apps/acls/classes.py index c95ab4e562..abb8085fe1 100644 --- a/mayan/apps/acls/classes.py +++ b/mayan/apps/acls/classes.py @@ -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): diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index 64bb239dd3..7c04b2a447 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -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: diff --git a/mayan/apps/acls/models.py b/mayan/apps/acls/models.py index 997919364e..dba6f71107 100644 --- a/mayan/apps/acls/models.py +++ b/mayan/apps/acls/models.py @@ -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 diff --git a/mayan/apps/acls/tests/test_models.py b/mayan/apps/acls/tests/test_models.py index 673193edd8..92fecdbb9f 100644 --- a/mayan/apps/acls/tests/test_models.py +++ b/mayan/apps/acls/tests/test_models.py @@ -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)