diff --git a/HISTORY.rst b/HISTORY.rst index 0ff65ca33d..832717249a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -217,6 +217,42 @@ - The tags app permission workflow is now reciprocal. In order to attach a tag, the user's role will need the tag attach permissions for both, the document and the tag. +- Refactor and optimize the access control computation. Most of + the computation has been moved to the database instead of doing + filtering in Python. The refactor added cascading access checking + in preparation for nested cabinet access control and the removal + of the permission proxy support which is now redundant. +- Remove the permissions to grant or revoke a permission to a role. + The instead the role edit permission is used. +- Add a test mixin to generate random model primary keys. +- Add support for checkout and check in multiple documents at + the same time. +- Move file and storage code to the storage app. The setting + COMMON_TEMPORARY_DIRECTORY is now STORAGE_TEMPORARY_DIRECTORY. +- To lower memory usage and reduce memory leaks, the entire + entire converter class is no longer cached and instead loaded + on demand. This allows the garbage collector to clear the memory + used. +- Update the permission requirements for the index template + document type selection screen. The document type view + permission is now required in addition to the index + template edit permission. +- Update the links display templates to show which object the + links belong to when there is more than one object. +- Update the links display templates to show which menu + the links belong to when there is more than one menu. +- Remove the sidebar menu and unify its links with the + secondary menu. +- Increate the default maximum title lenght to 120 characters. +- In the search API, the search function is now a service + of the search model resource. +- The simple and advance search are now the same service. The + difference is determined by the URL query. A ?q= means a + simple search. For advanced search pass the search model + fields in the URL query, example: ?q=document_type__label= +- Remove django-mathfilters from requirements. These tags + are provided by default by Jinja2 template engine + (http://jinja.pocoo.org/docs/2.10/templates/#math). 3.1.9 (2018-11-01) ================== diff --git a/Makefile b/Makefile index 6735854b4e..7541282865 100644 --- a/Makefile +++ b/Makefile @@ -62,15 +62,16 @@ clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -R -f {} + # Testing test: - ./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations + ./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS) test-all: - ./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations + ./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS) test-launch-postgres: @docker rm -f test-postgres || true diff --git a/docs/mercs/merging-roles-and-groups.rst b/docs/mercs/merging-roles-and-groups.rst index 34bb4aea72..edc5e2e9e5 100644 --- a/docs/mercs/merging-roles-and-groups.rst +++ b/docs/mercs/merging-roles-and-groups.rst @@ -63,5 +63,5 @@ Changes needed: the Role model's permissions many to many field. 4. Update the ``AccessControlList`` models roles field to point to the group models. -5. Update the role checks in the ``check_access`` and ``filter_by_access`` +5. Update the role checks in the ``check_access`` and ``restrict_queryset`` ``AccessControlList`` model manager methods. diff --git a/mayan/apps/acls/api_views.py b/mayan/apps/acls/api_views.py index 989d147717..fec0fbdbb1 100644 --- a/mayan/apps/acls/api_views.py +++ b/mayan/apps/acls/api_views.py @@ -1,203 +1,121 @@ from __future__ import absolute_import, unicode_literals -from django.contrib.contenttypes.models import ContentType -from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response -from rest_framework import generics - -from .models import AccessControlList -from .permissions import permission_acl_edit, permission_acl_view -from .serializers import ( - AccessControlListPermissionSerializer, AccessControlListSerializer, - WritableAccessControlListPermissionSerializer, - WritableAccessControlListSerializer +from mayan.apps.common.mixins import ContentTypeViewMixin +from mayan.apps.permissions.serializers import ( + PermissionSerializer, RolePermissionAddRemoveSerializer ) +from mayan.apps.rest_api.mixins import ExternalObjectAPIViewSetMixin +from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet + +from .permissions import permission_acl_edit, permission_acl_view +from .serializers import AccessControlListSerializer -class APIObjectACLListView(generics.ListCreateAPIView): - """ - get: Returns a list of all the object's access control lists - post: Create a new access control list for the selected object. - """ - def get_content_object(self): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - content_object = get_object_or_404( - klass=content_type.model_class(), pk=self.kwargs['object_id'] - ) - - if self.request.method == 'GET': - permission_required = permission_acl_view - else: - permission_required = permission_acl_edit - - AccessControlList.objects.check_access( - permissions=permission_required, user=self.request.user, - obj=content_object - ) - - return content_object - - def get_queryset(self): - return self.get_content_object().acls.all() - - def get_serializer_context(self): - """ - Extra context provided to the serializer class. - """ - context = super(APIObjectACLListView, self).get_serializer_context() - if self.kwargs: - context.update( - { - 'content_object': self.get_content_object(), - } - ) - - return context - - def get_serializer(self, *args, **kwargs): - if not self.request: - return None - - return super(APIObjectACLListView, self).get_serializer(*args, **kwargs) - - def get_serializer_class(self): - if self.request.method == 'GET': - return AccessControlListSerializer - else: - return WritableAccessControlListSerializer - - -class APIObjectACLView(generics.RetrieveDestroyAPIView): - """ - delete: Delete the selected access control list. - get: Returns the details of the selected access control list. - """ +class ObjectACLAPIViewSet(ContentTypeViewMixin, ExternalObjectAPIViewSetMixin, MayanAPIModelViewSet): + content_type_url_kw_args = { + 'app_label': 'app_label', + 'model': 'model_name' + } + external_object_pk_url_kwarg = 'object_id' + lookup_url_kwarg = 'acl_id' serializer_class = AccessControlListSerializer - def get_content_object(self): - if self.request.method == 'GET': - permission_required = permission_acl_view - else: - permission_required = permission_acl_edit - - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.validated_data.update( + { + 'object_id': self.external_object.pk, + 'content_type': self.get_content_type(), + } ) + self.perform_create(serializer) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - content_object = get_object_or_404( - klass=content_type.model_class(), pk=self.kwargs['object_id'] - ) - - AccessControlList.objects.check_access( - permissions=permission_required, user=self.request.user, - obj=content_object - ) - - return content_object - - def get_queryset(self): - return self.get_content_object().acls.all() - - -class APIObjectACLPermissionListView(generics.ListCreateAPIView): - """ - get: Returns the access control list permission list. - post: Add a new permission to the selected access control list. - """ - def get_acl(self): - return get_object_or_404( - klass=self.get_content_object().acls, pk=self.kwargs['acl_pk'] - ) - - def get_content_object(self): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - content_object = get_object_or_404( - klass=content_type.model_class(), pk=self.kwargs['object_id'] - ) - - AccessControlList.objects.check_access( - permissions=permission_acl_view, user=self.request.user, - obj=content_object - ) - - return content_object - - def get_queryset(self): - return self.get_acl().permissions.all() - - def get_serializer(self, *args, **kwargs): - if not self.request: + def get_external_object_permission(self): + action = getattr(self, 'action', None) + if action is None: return None - - return super(APIObjectACLPermissionListView, self).get_serializer(*args, **kwargs) - - def get_serializer_class(self): - if self.request.method == 'GET': - return AccessControlListPermissionSerializer + elif action in ['list', 'retrieve', 'permission_list', 'permission_inherited_list']: + return permission_acl_view else: - return WritableAccessControlListPermissionSerializer + return permission_acl_edit - def get_serializer_context(self): - context = super(APIObjectACLPermissionListView, self).get_serializer_context() - if self.kwargs: - context.update( - { - 'acl': self.get_acl(), - } - ) - - return context - - -class APIObjectACLPermissionView(generics.RetrieveDestroyAPIView): - """ - delete: Remove the permission from the selected access control list. - get: Returns the details of the selected access control list permission. - """ - lookup_url_kwarg = 'permission_pk' - serializer_class = AccessControlListPermissionSerializer - - def get_acl(self): - return get_object_or_404( - klass=self.get_content_object().acls, pk=self.kwargs['acl_pk'] - ) - - def get_content_object(self): - content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - content_object = get_object_or_404( - klass=content_type.model_class(), pk=self.kwargs['object_id'] - ) - - AccessControlList.objects.check_access( - permissions=permission_acl_view, user=self.request.user, - obj=content_object - ) - - return content_object + def get_external_object_queryset(self): + # Here we get a queryset the object model for which the event + # will be accessed. + return self.get_content_type().get_all_objects_for_this_type() def get_queryset(self): - return self.get_acl().permissions.all() + return self.get_external_object().acls.all() - def get_serializer_context(self): - context = super(APIObjectACLPermissionView, self).get_serializer_context() - if self.kwargs: - context.update( - { - 'acl': self.get_acl(), - } - ) + @action( + detail=True, lookup_url_kwarg='acl_id', methods=('post',), + serializer_class=RolePermissionAddRemoveSerializer, + url_name='permission-add', url_path='permissions/add' + ) + def permission_add(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.permissions_add(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, headers=headers, status=status.HTTP_200_OK + ) - return context + @action( + detail=True, lookup_url_kwarg='acl_id', + serializer_class=PermissionSerializer, url_name='permission-list', + url_path='permissions' + ) + def permission_list(self, request, *args, **kwargs): + queryset = self.get_object().permissions.all() + page = self.paginate_queryset(queryset) + + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) + + if page is not None: + return self.get_paginated_response(serializer.data) + + return Response(serializer.data) + + @action( + detail=True, lookup_url_kwarg='acl_id', + serializer_class=PermissionSerializer, + url_name='permission-inherited-list', url_path='permissions/inherited' + ) + def permission_inherited_list(self, request, *args, **kwargs): + queryset = self.get_object().get_inherited_permissions() + page = self.paginate_queryset(queryset) + + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) + + if page is not None: + return self.get_paginated_response(serializer.data) + + return Response(serializer.data) + + @action( + detail=True, lookup_url_kwarg='acl_id', + methods=('post',), serializer_class=RolePermissionAddRemoveSerializer, + url_name='permission-remove', url_path='permissions/remove' + ) + def permission_remove(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.permissions_remove(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, headers=headers, status=status.HTTP_200_OK + ) diff --git a/mayan/apps/acls/apps.py b/mayan/apps/acls/apps.py index 250fc45e33..600dfb4f0f 100644 --- a/mayan/apps/acls/apps.py +++ b/mayan/apps/acls/apps.py @@ -2,9 +2,15 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ -from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar +from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary +from mayan.apps.events import ModelEventType +from mayan.apps.events.links import ( + link_events_for_object, link_object_event_types_user_subcriptions_list +) from mayan.apps.navigation import SourceColumn +from .classes import ModelPermission +from .events import event_acl_created, event_acl_edited from .links import link_acl_create, link_acl_delete, link_acl_permissions @@ -18,22 +24,33 @@ class ACLsApp(MayanAppConfig): def ready(self): super(ACLsApp, self).ready() + from actstream import registry AccessControlList = self.get_model(model_name='AccessControlList') + ModelEventType.register( + event_types=(event_acl_created, event_acl_edited), + model=AccessControlList + ) + ModelPermission.register_inheritance( + model=AccessControlList, related='content_object', + ) + SourceColumn( attribute='role', is_identifier=True, is_sortable=True, source=AccessControlList ) - SourceColumn( - attribute='get_permission_titles', include_label=True, - source=AccessControlList - ) menu_object.bind_links( - links=(link_acl_permissions, link_acl_delete), + links=( + link_acl_permissions, link_acl_delete, + link_events_for_object, + link_object_event_types_user_subcriptions_list + ), sources=(AccessControlList,) ) - menu_sidebar.bind_links( + menu_secondary.bind_links( links=(link_acl_create,), sources=('acls:acl_list',) ) + + registry.register(AccessControlList) diff --git a/mayan/apps/acls/classes.py b/mayan/apps/acls/classes.py index 94ee4c6f69..abb8085fe1 100644 --- a/mayan/apps/acls/classes.py +++ b/mayan/apps/acls/classes.py @@ -40,25 +40,15 @@ class ModelPermission(object): app_label='permissions', model_name='StoredPermission' ) - permissions = [] - - class_permissions = cls.get_for_class(klass=type(instance)) - - if class_permissions: - permissions.extend(class_permissions) - - proxy = cls._proxies.get(type(instance)) - - if proxy: - permissions.extend(cls._registry.get(proxy)) + permissions = cls.get_for_class(klass=type(instance)) pks = [ - permission.stored_permission.pk for permission in set(permissions) + permission.stored_permission.pk for permission in permissions ] return StoredPermission.objects.filter(pk__in=pks) @classmethod - def get_inheritance(cls, model): + def get_inheritances(cls, model): return cls._inheritances[model] @classmethod @@ -79,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/events.py b/mayan/apps/acls/events.py new file mode 100644 index 0000000000..8f75bc26e1 --- /dev/null +++ b/mayan/apps/acls/events.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from mayan.apps.events import EventTypeNamespace + +namespace = EventTypeNamespace( + label=_('Access control lists'), name='acls' +) + +event_acl_created = namespace.add_event_type( + label=_('ACL created'), name='acl_created' +) +event_acl_edited = namespace.add_event_type( + label=_('ACL edited'), name='acl_edited' +) diff --git a/mayan/apps/acls/icons.py b/mayan/apps/acls/icons.py index 00cda1f009..1ecd7f5bdb 100644 --- a/mayan/apps/acls/icons.py +++ b/mayan/apps/acls/icons.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals from mayan.apps.appearance.classes import Icon -icon_acl_delete = Icon(driver_name='fontawesome', symbol='minus') +icon_acl_delete = Icon(driver_name='fontawesome', symbol='times') icon_acl_list = Icon(driver_name='fontawesome', symbol='lock') icon_acl_new = Icon( driver_name='fontawesome-dual', primary_symbol='lock', diff --git a/mayan/apps/acls/links.py b/mayan/apps/acls/links.py index da2f289e10..8652c4bde8 100644 --- a/mayan/apps/acls/links.py +++ b/mayan/apps/acls/links.py @@ -21,7 +21,7 @@ def get_kwargs_factory(variable_name): ) return { 'app_label': '"{}"'.format(content_type.app_label), - 'model': '"{}"'.format(content_type.model), + 'model_name': '"{}"'.format(content_type.model), 'object_id': '{}.pk'.format(variable_name) } @@ -29,21 +29,21 @@ def get_kwargs_factory(variable_name): link_acl_delete = Link( - args='resolved_object.pk', icon_class=icon_acl_delete, - permissions=(permission_acl_edit,), permissions_related='content_object', - tags='dangerous', text=_('Delete'), view='acls:acl_delete', + icon_class=icon_acl_delete, kwargs={'acl_id': 'resolved_object.pk'}, + permission=permission_acl_edit, tags='dangerous', text=_('Delete'), + view='acls:acl_delete', ) link_acl_list = Link( - icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list' + icon_class=icon_acl_list, kwargs=get_kwargs_factory( + variable_name='resolved_object' + ), permission=permission_acl_view, text=_('ACLs'), view='acls:acl_list' ) link_acl_create = Link( icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'), - permissions=(permission_acl_edit,), text=_('New ACL'), - view='acls:acl_create' + permission=permission_acl_edit, text=_('New ACL'), view='acls:acl_create' ) link_acl_permissions = Link( args='resolved_object.pk', icon_class=icon_permission, - permissions=(permission_acl_edit,), permissions_related='content_object', - text=_('Permissions'), view='acls:acl_permissions', + permission=permission_acl_edit, text=_('Permissions'), + view='acls:acl_permissions', ) diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index 7b2aae92bf..7c04b2a447 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -1,15 +1,21 @@ from __future__ import absolute_import, unicode_literals import logging +import operator +import warnings +from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.db import models -from django.db.models import Q -from django.utils.translation import ugettext -from django.utils.translation import ugettext_lazy as _ +from django.db.models import CharField, Value as V, Q +from django.db.models.functions import Concat +from django.http import Http404 -from mayan.apps.common.utils import resolve_attribute, return_related +from mayan.apps.common.utils import ( + get_related_field, resolve_attribute, return_related +) +from mayan.apps.common.warnings import InterfaceWarning from mayan.apps.permissions import Permission from mayan.apps.permissions.models import StoredPermission @@ -24,210 +30,189 @@ class AccessControlListManager(models.Manager): Implement a 3 tier permission system, involving a permissions, an actor and an object """ - def check_access(self, permissions, user, obj, related=None): - if user.is_superuser or user.is_staff: - logger.debug( - 'Permissions "%s" on "%s" granted to user "%s" as superuser ' - 'or staff', permissions, obj, user + def _get_acl_filters(self, queryset, stored_permission, user, related_field_name=None): + """ + This method does the bulk of the work. It generates filters for the + AccessControlList model to determine if there are ACL entries for the + members of the queryset's model provided. + """ + # Determine which of the cases we need to address + # 1: No related field + # 2: Related field + # 3: Related field that is Generic Foreign Key + # 4: No related field, but has an inherited related field, solved by + # recursion, branches to #2 or #3. + # 5: Inherited field of a related field + # -- Not addressed yet -- + # 6: Inherited field of a related field that is Generic Foreign Key + result = [] + + if related_field_name: + related_field = get_related_field( + model=queryset.model, related_field_name=related_field_name ) - return True - try: - return Permission.check_permissions( - requester=user, permissions=permissions - ) - except PermissionDenied: - try: - stored_permissions = [ - permission.stored_permission for permission in permissions - ] - except TypeError: - # Not a list of permissions, just one - stored_permissions = (permissions.stored_permission,) + if isinstance(related_field, GenericForeignKey): + # Case 3: Generic Foreign Key, multiple ContentTypes + object + # id combinations + content_type_object_id_queryset = queryset.annotate( + ct_fk_combination=Concat( + related_field.ct_field, V('-'), related_field.fk_field, + output_field=CharField() + ) + ).values('ct_fk_combination') - if related: - obj = resolve_attribute(obj=obj, attribute=related) + acl_filter = self.annotate( + ct_fk_combination=Concat( + 'content_type', V('-'), 'object_id', output_field=CharField() + ) + ).filter( + permissions=stored_permission, role__groups__user=user, + ct_fk_combination__in=content_type_object_id_queryset + ).values('object_id') - try: - parent_accessor = ModelPermission.get_inheritance( - model=obj._meta.model + field_lookup = 'object_id__in' + + result.append(Q(**{field_lookup: acl_filter})) + else: + # Case 2: Related field of a single type, single ContentType, + # multiple object id + content_type = ContentType.objects.get_for_model( + model=related_field.related_model ) - except AttributeError: - # AttributeError means non model objects: ie Statistics - # These can't have ACLs so we raise PermissionDenied - raise PermissionDenied( - _('Insufficient access for: %(object)s') % {'object': obj} + field_lookup = '{}_id__in'.format(related_field_name) + acl_filter = self.filter( + content_type=content_type, permissions=stored_permission, + role__groups__user=user + ).values('object_id') + result.append(Q(**{field_lookup: acl_filter})) + # Case 5: Related field, has an inherited related field itself + # Bubble up permssion check + # TODO: Add relationship support: OR or AND + # TODO: OR for document pages, version, doc, and types + # TODO: AND for new cabinet levels ACLs + try: + related_field_model_related_fields = ModelPermission.get_inheritances( + model=related_field.related_model + ) + except KeyError: + pass + else: + 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) + field_lookup = 'id__in' + acl_filter = self.filter( + content_type=content_type, permissions=stored_permission, + role__groups__user=user + ).values('object_id') + result.append(Q(**{field_lookup: acl_filter})) + + # Case 4: Original model, has an inherited related field + try: + related_fields = ModelPermission.get_inheritances( + model=queryset.model ) except KeyError: pass else: - try: - return self.check_access( - obj=getattr(obj, parent_accessor), - permissions=permissions, user=user + 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 ) - except AttributeError: - # Has no such attribute, try it as a related field - try: - return self.check_access( - obj=return_related( - instance=obj, related_field=parent_accessor - ), permissions=permissions, user=user - ) - except PermissionDenied: - pass - except PermissionDenied: - pass + relation_result.append(reduce(operator.and_, inherited_acl_queries)) - user_roles = [] - for group in user.groups.all(): - for role in group.roles.all(): - if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))): - logger.debug( - 'Permissions "%s" on "%s" granted to user "%s" through role "%s" via inherited ACL', - permissions, obj, user, role - ) - return True + result.append(reduce(operator.or_, relation_result)) - user_roles.append(role) + return result - if not self.filter(content_type=ContentType.objects.get_for_model(model=obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists(): - logger.debug( - 'Permissions "%s" on "%s" denied for user "%s"', - permissions, obj, user - ) - raise PermissionDenied(ugettext('Insufficient access for: %s') % obj) + def check_access(self, obj, permission, user, raise_404=False): + warnings.warn( + 'check_access() is deprecated, use restrict_queryset() to ' + 'produce a queryset from which to .get() the corresponding ' + 'object in the local code.', InterfaceWarning + ) + queryset = self.restrict_queryset( + permission=permission, queryset=obj._meta.default_manager.all(), + user=user + ) - logger.debug( - 'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL', - permissions, obj, user, user_roles - ) - - def filter_by_access(self, permission, user, queryset): - if user.is_superuser or user.is_staff: - logger.debug( - 'Unfiltered queryset returned to user "%s" as superuser ' - 'or staff', user - ) - return queryset - - try: - Permission.check_permissions( - requester=user, permissions=(permission,) - ) - except PermissionDenied: - user_roles = [] - for group in user.groups.all(): - for role in group.roles.all(): - user_roles.append(role) - - try: - parent_accessor = ModelPermission.get_inheritance( - model=queryset.model - ) - except KeyError: - parent_acl_query = Q() + if queryset.filter(pk=obj.pk).exists(): + return True + else: + if raise_404: + raise Http404 else: - instance = queryset.first() - if instance: - parent_object = return_related( - instance=instance, related_field=parent_accessor - ) + raise PermissionDenied - try: - # Try to see if parent_object is a function - parent_object() - except TypeError: - # Is not a function, try it as a field - parent_content_type = ContentType.objects.get_for_model( - model=parent_object - ) - parent_queryset = self.filter( - content_type=parent_content_type, - role__in=user_roles, - permissions=permission.stored_permission - ) - parent_acl_query = Q( - **{ - '{}__pk__in'.format( - parent_accessor - ): parent_queryset.values_list( - 'object_id', flat=True - ) - } - ) - else: - # Is a function. Can't perform Q object filtering. - # Perform iterative filtering. - result = [] - for entry in queryset: - try: - self.check_access( - obj=entry, permissions=permission, - user=user - ) - except PermissionDenied: - pass - else: - result.append(entry.pk) + def get_inherited_permissions(self, obj, role): + queryset = self._get_inherited_object_permissions(obj=obj, role=role) - return queryset.filter(pk__in=result) - else: - parent_acl_query = Q() + queryset = queryset | role.permissions.all() - # Directly granted access - content_type = ContentType.objects.get_for_model( - model=queryset.model - ) - acl_query = Q(pk__in=self.filter( - content_type=content_type, role__in=user_roles, - permissions=permission.stored_permission - ).values_list('object_id', flat=True)) - logger.debug( - 'Filtered queryset returned to user "%s" based on roles "%s"', - user, user_roles - ) + # Filter the permissions to the ones that apply to the model + queryset = ModelPermission.get_for_instance( + instance=obj + ).filter( + pk__in=queryset + ) - return queryset.filter(parent_acl_query | acl_query) - else: + return queryset + + def _get_inherited_object_permissions(self, obj, role): + queryset = StoredPermission.objects.none() + + if not obj: return queryset - def get_inherited_permissions(self, role, obj): try: - instance = obj.first() - except AttributeError: - instance = obj - else: - if not instance: - return StoredPermission.objects.none() - - try: - parent_accessor = ModelPermission.get_inheritance( - model=type(instance) + related_fields = ModelPermission.get_inheritances( + model=type(obj) ) except KeyError: - return StoredPermission.objects.none() + pass else: - try: - parent_object = resolve_attribute( - obj=instance, attribute=parent_accessor - ) - except AttributeError: - # Parent accessor is not an attribute, try it as a related - # field. - parent_object = return_related( - instance=instance, related_field=parent_accessor - ) - content_type = ContentType.objects.get_for_model(parent_object) - try: - return self.get( - role=role, content_type=content_type, - object_id=parent_object.pk - ).permissions.all() - except self.model.DoesNotExist: - return StoredPermission.objects.none() + 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 - def grant(self, permission, role, obj): + queryset = queryset | self._get_inherited_object_permissions( + obj=parent_object, role=role + ) + + return queryset + + def grant(self, obj, permission, role): class_permissions = ModelPermission.get_for_class(klass=obj.__class__) if permission not in class_permissions: raise PermissionNotValidForClass @@ -242,7 +227,42 @@ class AccessControlListManager(models.Manager): return acl - def revoke(self, permission, role, obj): + 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: + Permission.check_user_permission(permission=permission, user=user) + except PermissionDenied: + acl_filters = self._get_acl_filters( + queryset=queryset, + stored_permission=permission.stored_permission, user=user + ) + + final_query = None + for acl_filter in acl_filters: + if final_query is None: + final_query = acl_filter + else: + final_query = final_query | acl_filter + + return queryset.filter(final_query) + else: + # User has direct permission assignment via a role, is superuser or + # is staff. Return the entire queryset. + return queryset + + def revoke(self, obj, permission, role): content_type = ContentType.objects.get_for_model(model=obj) acl, created = self.get_or_create( content_type=content_type, object_id=obj.pk, diff --git a/mayan/apps/acls/models.py b/mayan/apps/acls/models.py index e93f00f486..dba6f71107 100644 --- a/mayan/apps/acls/models.py +++ b/mayan/apps/acls/models.py @@ -1,16 +1,18 @@ 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 -from django.db import models +from django.db import models, transaction from django.urls import reverse from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from mayan.apps.permissions.models import Role, StoredPermission +from .events import event_acl_created, event_acl_edited from .managers import AccessControlListManager logger = logging.getLogger(__name__) @@ -30,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 @@ -58,16 +65,15 @@ class AccessControlList(models.Model): def __str__(self): return _( - 'Permissions "%(permissions)s" to role "%(role)s" for "%(object)s"' + 'Role "%(role)s" permission\'s for "%(object)s"' ) % { - 'permissions': self.get_permission_titles(), 'object': self.content_object, - 'role': self.role + 'role': self.role, } def get_absolute_url(self): return reverse( - viewname='acls:acl_permissions', kwargs={'acl_pk': self.pk} + viewname='acls:acl_permissions', kwargs={'acl_id': self.pk} ) def get_inherited_permissions(self): @@ -85,3 +91,32 @@ class AccessControlList(models.Model): return result or _('None') get_permission_titles.short_description = _('Permissions') + + def permissions_add(self, queryset, _user=None): + with transaction.atomic(): + event_acl_edited.commit( + actor=_user, target=self + ) + self.permissions.add(*queryset) + + def permissions_remove(self, queryset, _user=None): + with transaction.atomic(): + event_acl_edited.commit( + actor=_user, target=self + ) + self.permissions.remove(*queryset) + + def save(self, *args, **kwargs): + _user = kwargs.pop('_user', None) + + with transaction.atomic(): + is_new = not self.pk + super(AccessControlList, self).save(*args, **kwargs) + if is_new: + event_acl_created.commit( + actor=_user, target=self + ) + else: + event_acl_edited.commit( + actor=_user, target=self + ) diff --git a/mayan/apps/acls/serializers.py b/mayan/apps/acls/serializers.py index 5a8a02157b..9cec79ad7d 100644 --- a/mayan/apps/acls/serializers.py +++ b/mayan/apps/acls/serializers.py @@ -1,216 +1,143 @@ from __future__ import absolute_import, unicode_literals -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError as DjangoValidationError -from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from rest_framework.exceptions import ValidationError -from rest_framework.reverse import reverse from mayan.apps.common.serializers import ContentTypeSerializer -from mayan.apps.permissions import Permission -from mayan.apps.permissions.models import Role, StoredPermission -from mayan.apps.permissions.serializers import ( - PermissionSerializer, RoleSerializer -) +from mayan.apps.permissions.models import Role +from mayan.apps.permissions.permissions import permission_role_edit +from mayan.apps.permissions.serializers import RoleSerializer +from mayan.apps.rest_api.mixins import ExternalObjectSerializerMixin +from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField from .models import AccessControlList -class AccessControlListSerializer(serializers.ModelSerializer): +class AccessControlListSerializer(ExternalObjectSerializerMixin, serializers.ModelSerializer): content_type = ContentTypeSerializer(read_only=True) - permissions_url = serializers.SerializerMethodField( - help_text=_( - 'API URL pointing to the list of permissions for this access ' - 'control list.' - ) - ) role = RoleSerializer(read_only=True) - url = serializers.SerializerMethodField() + permission_add_url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label', + }, + { + 'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name', + }, + { + 'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id', + }, + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id', + } + ), + view_name='rest_api:object-acl-permission-add' + ) + permission_list_url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label', + }, + { + 'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name', + }, + { + 'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id', + }, + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id', + } + ), + view_name='rest_api:object-acl-permission-list' + ) + permission_list_inherited_url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label', + }, + { + 'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name', + }, + { + 'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id', + }, + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id', + } + ), + view_name='rest_api:object-acl-permission-inherited-list' + ) + permission_remove_url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label', + }, + { + 'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name', + }, + { + 'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id', + }, + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id', + } + ), + view_name='rest_api:object-acl-permission-remove' + ) + role_id = serializers.CharField( + label=_('Role ID'), + help_text=_( + 'Primary key of the role of the ACL that will be created or edited.' + ), required=False, write_only=True + ) + url = MultiKwargHyperlinkedIdentityField( + view_kwargs=( + { + 'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label', + }, + { + 'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name', + }, + { + 'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id', + }, + { + 'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id', + } + ), + view_name='rest_api:object-acl-detail' + ) class Meta: + external_object_model = Role + external_object_pk_field = 'role_id' + external_object_permission = permission_role_edit fields = ( - 'content_type', 'id', 'object_id', 'permissions_url', 'role', 'url' + 'content_type', 'id', 'object_id', 'permission_add_url', + 'permission_list_url', 'permission_list_inherited_url', + 'permission_remove_url', 'role', 'role_id', + 'url' ) model = AccessControlList - - def get_permissions_url(self, instance): - return reverse( - viewname='rest_api:accesscontrollist-permission-list', kwargs={ - 'app_label': instance.content_type.app_label, - 'model': instance.content_type.model, - 'object_id': instance.object_id, - 'acl_pk': instance.pk - }, request=self.context['request'], format=self.context['format'] - ) - - def get_url(self, instance): - return reverse( - 'rest_api:accesscontrollist-detail', kwargs={ - 'app_label': instance.content_type.app_label, - 'model': instance.content_type.model, - 'object_id': instance.object_id, - 'acl_pk': instance.pk - }, request=self.context['request'], format=self.context['format'] - ) - - -class AccessControlListPermissionSerializer(PermissionSerializer): - acl_permission_url = serializers.SerializerMethodField( - help_text=_( - 'API URL pointing to a permission in relation to the ' - 'access control list to which it is attached. This URL is ' - 'different than the canonical workflow URL.' - ) - ) - acl_url = serializers.SerializerMethodField() - - def get_acl_permission_url(self, instance): - return reverse( - 'rest_api:accesscontrollist-permission-detail', kwargs={ - 'app_label': self.context['acl'].content_type.app_label, - 'model': self.context['acl'].content_type.model, - 'object_id': self.context['acl'].object_id, - 'acl_pk': self.context['acl'].pk, - 'permission_pk': instance.stored_permission.pk - }, request=self.context['request'], format=self.context['format'] - ) - - def get_acl_url(self, instance): - return reverse( - 'rest_api:accesscontrollist-detail', kwargs={ - 'app_label': self.context['acl'].content_type.app_label, - 'model': self.context['acl'].content_type.model, - 'object_id': self.context['acl'].object_id, - 'acl_pk': self.context['acl'].pk - }, request=self.context['request'], format=self.context['format'] - ) - - -class WritableAccessControlListPermissionSerializer(AccessControlListPermissionSerializer): - permission_pk = serializers.CharField( - help_text=_( - 'Primary key of the new permission to grant to the access control ' - 'list.' - ), write_only=True - ) - - class Meta: - fields = ('namespace',) - read_only_fields = ('namespace',) + read_only_fields = ('object_id',) def create(self, validated_data): - for permission in validated_data['permissions']: - self.context['acl'].permissions.add(permission) + role = self.get_external_object() - return validated_data['permissions'][0] + if role: + validated_data['role'] = role - def validate(self, attrs): - permissions_pk_list = attrs.pop('permission_pk', None) - permissions_result = [] - - if permissions_pk_list: - for pk in permissions_pk_list.split(','): - try: - permission = Permission.get(pk=pk) - except KeyError: - raise ValidationError(_('No such permission: %s') % pk) - else: - # Accumulate valid stored permission pks - permissions_result.append(permission.pk) - - attrs['permissions'] = StoredPermission.objects.filter( - pk__in=permissions_result - ) - return attrs - - -class WritableAccessControlListSerializer(serializers.ModelSerializer): - content_type = ContentTypeSerializer(read_only=True) - permissions_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of permission primary keys to grant to this ' - 'access control list.' - ), required=False - ) - permissions_url = serializers.SerializerMethodField( - help_text=_( - 'API URL pointing to the list of permissions for this access ' - 'control list.' - ), read_only=True - ) - role_pk = serializers.IntegerField( - help_text=_( - 'Primary keys of the role to which this access control list ' - 'binds to.' - ), write_only=True - ) - url = serializers.SerializerMethodField() - - class Meta: - fields = ( - 'content_type', 'id', 'object_id', 'permissions_pk_list', - 'permissions_url', 'role_pk', 'url' - ) - model = AccessControlList - read_only_fields = ('content_type', 'object_id') - - def get_permissions_url(self, instance): - return reverse( - 'rest_api:accesscontrollist-permission-list', kwargs={ - 'app_label': instance.content_type.app_label, - 'model': instance.content_type.model, - 'object_id': instance.object_id, - 'acl_pk': instance.pk - }, request=self.context['request'], format=self.context['format'] + return super(AccessControlListSerializer, self).create( + validated_data=validated_data ) - def get_url(self, instance): - return reverse( - 'rest_api:accesscontrollist-detail', kwargs={ - 'app_label': instance.content_type.app_label, - 'model': instance.content_type.model, - 'object_id': instance.object_id, - 'acl_pk': instance.pk - }, request=self.context['request'], format=self.context['format'] + def update(self, instance, validated_data): + role = self.get_external_object() + + if role: + validated_data['role'] = role + + return super(AccessControlListSerializer, self).update( + instance=instance, validated_data=validated_data ) - - def validate(self, attrs): - attrs['content_type'] = ContentType.objects.get_for_model( - self.context['content_object'] - ) - attrs['object_id'] = self.context['content_object'].pk - - try: - attrs['role'] = Role.objects.get(pk=attrs.pop('role_pk')) - except Role.DoesNotExist as exception: - raise ValidationError(force_text(exception)) - - permissions_pk_list = attrs.pop('permissions_pk_list', None) - permissions_result = [] - - if permissions_pk_list: - for pk in permissions_pk_list.split(','): - try: - permission = Permission.get(pk=pk) - except KeyError: - raise ValidationError(_('No such permission: %s') % pk) - else: - # Accumulate valid stored permission pks - permissions_result.append(permission.pk) - - instance = AccessControlList(**attrs) - - try: - instance.full_clean() - except DjangoValidationError as exception: - raise ValidationError(exception) - - # Add a queryset of valid stored permissions so that they get added - # after the ACL gets created. - attrs['permissions'] = StoredPermission.objects.filter( - pk__in=permissions_result - ) - return attrs diff --git a/mayan/apps/acls/tests/mixins.py b/mayan/apps/acls/tests/mixins.py index 353d734b03..7886215ee9 100644 --- a/mayan/apps/acls/tests/mixins.py +++ b/mayan/apps/acls/tests/mixins.py @@ -1,47 +1,73 @@ from __future__ import unicode_literals -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured -from mayan.apps.permissions.models import Role -from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL -from mayan.apps.user_management.tests import ( - TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, - TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD, TEST_USER_USERNAME +from mayan.apps.common.tests.mixins import TestModelTestMixin +from mayan.apps.permissions.tests.mixins import ( + PermissionTestMixin, RoleTestCaseMixin, RoleTestMixin ) +from mayan.apps.user_management.tests.mixins import UserTestCaseMixin +from ..classes import ModelPermission from ..models import AccessControlList +from ..permissions import permission_acl_edit, permission_acl_view -class ACLBaseTestMixin(object): - auto_create_group = True - auto_create_users = True - +class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin): def setUp(self): - super(ACLBaseTestMixin, self).setUp() - if self.auto_create_users: - self.admin_user = get_user_model().objects.create_superuser( - username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, - password=TEST_ADMIN_PASSWORD - ) - - self.user = get_user_model().objects.create_user( - username=TEST_USER_USERNAME, email=TEST_USER_EMAIL, - password=TEST_USER_PASSWORD - ) - - if self.auto_create_group: - self.group = Group.objects.create(name=TEST_GROUP_NAME) - self.role = Role.objects.create(label=TEST_ROLE_LABEL) - self.group.user_set.add(self.user) - self.role.groups.add(self.group) + super(ACLTestCaseMixin, self).setUp() + if hasattr(self, '_test_case_user'): + self._test_case_role.groups.add(self._test_case_group) def grant_access(self, obj, permission): - return AccessControlList.objects.grant( - obj=obj, permission=permission, role=self.role + if not hasattr(self, '_test_case_role'): + raise ImproperlyConfigured( + 'Enable the creation of the test case user, group, and role ' + 'in order to enable the usage of ACLs in tests.' + ) + + self._test_case_acl = AccessControlList.objects.grant( + obj=obj, permission=permission, role=self._test_case_role ) - def grant_permission(self, permission): - self.role.permissions.add( - permission.stored_permission + +class ACLTestMixin(PermissionTestMixin, RoleTestMixin, TestModelTestMixin): + auto_create_test_role = True + + def _create_test_acl(self): + self.test_acl = AccessControlList.objects.create( + content_object=self.test_object, role=self.test_role ) + + def setUp(self): + super(ACLTestMixin, self).setUp() + if self.auto_create_test_role: + self._create_test_role() + + def _inject_test_object_content_type(self): + self.test_object_content_type = ContentType.objects.get_for_model(self.test_object) + + self.test_content_object_view_kwargs = { + 'app_label': self.test_object_content_type.app_label, + 'model_name': self.test_object_content_type.model, + 'object_id': self.test_object.pk + } + + def _setup_test_object(self): + self._create_test_model() + self._create_test_object() + ModelPermission.register( + model=self.test_object._meta.model, permissions=( + permission_acl_edit, permission_acl_view, + ) + ) + + self._create_test_permission() + ModelPermission.register( + model=self.test_object._meta.model, permissions=( + self.test_permission, + ) + ) + + self._inject_test_object_content_type() diff --git a/mayan/apps/acls/tests/test_actions.py b/mayan/apps/acls/tests/test_actions.py index f638fef451..d87eceb24b 100644 --- a/mayan/apps/acls/tests/test_actions.py +++ b/mayan/apps/acls/tests/test_actions.py @@ -9,16 +9,13 @@ from ..workflow_actions import GrantAccessAction, RevokeAccessAction class ACLActionTestCase(ActionTestCase): - def setUp(self): - super(ACLActionTestCase, self).setUp() - def test_grant_access_action(self): action = GrantAccessAction( form_data={ 'content_type': ContentType.objects.get_for_model(model=self.document).pk, 'object_id': self.document.pk, - 'roles': [self.role.pk], - 'permissions': [permission_document_view.uuid], + 'roles': [self._test_case_role.pk], + 'permissions': [permission_document_view.pk], } ) action.execute(context={'entry_log': self.entry_log}) @@ -28,7 +25,7 @@ class ACLActionTestCase(ActionTestCase): list(self.document.acls.first().permissions.all()), [permission_document_view.stored_permission] ) - self.assertEqual(self.document.acls.first().role, self.role) + self.assertEqual(self.document.acls.first().role, self._test_case_role) def test_revoke_access_action(self): self.grant_access( @@ -39,8 +36,8 @@ class ACLActionTestCase(ActionTestCase): form_data={ 'content_type': ContentType.objects.get_for_model(model=self.document).pk, 'object_id': self.document.pk, - 'roles': [self.role.pk], - 'permissions': [permission_document_view.uuid], + 'roles': [self._test_case_role.pk], + 'permissions': [permission_document_view.pk], } ) action.execute(context={'entry_log': self.entry_log}) diff --git a/mayan/apps/acls/tests/test_api.py b/mayan/apps/acls/tests/test_api.py index 55433b2fb3..4594dbe9c8 100644 --- a/mayan/apps/acls/tests/test_api.py +++ b/mayan/apps/acls/tests/test_api.py @@ -1,205 +1,189 @@ from __future__ import absolute_import, unicode_literals -from django.contrib.contenttypes.models import ContentType - from rest_framework import status -from mayan.apps.documents.permissions import permission_document_view -from mayan.apps.documents.tests import DocumentTestMixin -from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL from mayan.apps.rest_api.tests import BaseAPITestCase from ..models import AccessControlList -from ..permissions import permission_acl_view +from ..permissions import permission_acl_edit, permission_acl_view + +from .mixins import ACLTestMixin -class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase): +class ACLAPITestCase(ACLTestMixin, BaseAPITestCase): def setUp(self): super(ACLAPITestCase, self).setUp() - self.login_admin_user() + self._setup_test_object() + self._create_test_acl() + self.test_acl.permissions.add(self.test_permission.stored_permission) - self.document_content_type = ContentType.objects.get_for_model( - self.document + def _request_object_acl_list_api_view(self): + return self.get( + viewname='rest_api:object-acl-list', + kwargs=self.test_content_object_view_kwargs ) - def _create_acl(self): - self.acl = AccessControlList.objects.create( - content_object=self.document, - role=self.role - ) + def test_object_acl_list_api_view_no_permission(self): + response = self._request_object_acl_list_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.acl.permissions.add(permission_document_view.stored_permission) + def test_object_acl_list_api_view_with_access(self): + self.grant_access(obj=self.test_object, permission=permission_acl_view) - def test_object_acl_list_view(self): - self._create_acl() - - response = self.get( - viewname='rest_api:accesscontrollist-list', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk - } - ) + response = self._request_object_acl_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.data['results'][0]['content_type']['app_label'], - self.document_content_type.app_label + self.test_object_content_type.app_label ) self.assertEqual( - response.data['results'][0]['role']['label'], TEST_ROLE_LABEL + response.data['results'][0]['role']['label'], + self.test_acl.role.label ) - def test_object_acl_delete_view(self): - self._create_acl() + def _request_acl_delete_api_view(self): + kwargs = self.test_content_object_view_kwargs.copy() + kwargs['acl_id'] = self.test_acl.pk - response = self.delete( - viewname='rest_api:accesscontrollist-detail', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk, - 'acl_pk': self.acl.pk - } + return self.delete( + viewname='rest_api:object-acl-detail', + kwargs=kwargs ) + def test_object_acl_delete_api_view_with_access(self): + self.expected_content_type = None + + self.grant_access(obj=self.test_object, permission=permission_acl_edit) + response = self._request_acl_delete_api_view() + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(AccessControlList.objects.count(), 0) + self.assertTrue(self.test_acl not in AccessControlList.objects.all()) - def test_object_acl_detail_view(self): - self._create_acl() + def test_object_acl_delete_api_view_no_permission(self): + response = self._request_acl_delete_api_view() - response = self.get( - viewname='rest_api:accesscontrollist-detail', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk, - 'acl_pk': self.acl.pk - } + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue(self.test_acl in AccessControlList.objects.all()) + + def _request_object_acl_detail_api_view(self): + kwargs = self.test_content_object_view_kwargs.copy() + kwargs['acl_id'] = self.test_acl.pk + + return self.get( + viewname='rest_api:object-acl-detail', + kwargs=kwargs ) + + def test_object_acl_detail_api_view_with_access(self): + self.grant_access(obj=self.test_object, permission=permission_acl_view) + + response = self._request_object_acl_detail_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.data['content_type']['app_label'], - self.document_content_type.app_label + self.test_object_content_type.app_label ) self.assertEqual( - response.data['role']['label'], TEST_ROLE_LABEL + response.data['role']['label'], self.test_acl.role.label ) - def test_object_acl_permission_delete_view(self): - self._create_acl() - permission = self.acl.permissions.first() + def test_object_acl_detail_api_view_no_permission(self): + response = self._request_object_acl_detail_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response = self.delete( - viewname='rest_api:accesscontrollist-permission-detail', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk, - 'acl_pk': self.acl.pk, 'permission_pk': permission.pk - } - ) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(self.acl.permissions.count(), 0) + def _request_object_acl_permission_list_api_view(self): + kwargs = self.test_content_object_view_kwargs.copy() + kwargs['acl_id'] = self.test_acl.pk - def test_object_acl_permission_detail_view(self): - self._create_acl() - permission = self.acl.permissions.first() - - response = self.get( - viewname='rest_api:accesscontrollist-permission-detail', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk, 'acl_pk': self.acl.pk, - 'permission_pk': permission.pk - } + return self.get( + viewname='rest_api:object-acl-permission-list', + kwargs=kwargs ) + def test_object_acl_permission_list_api_view_with_access(self): + self.grant_access(obj=self.test_object, permission=permission_acl_view) + + response = self._request_object_acl_permission_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( - response.data['permission_pk'], permission_document_view.pk + response.data['results'][0]['pk'], + self.test_permission.pk ) - def test_object_acl_permission_list_view(self): - self._create_acl() + def test_object_acl_permission_list_api_view_no_permission(self): + response = self._request_object_acl_permission_list_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response = self.get( - viewname='rest_api:accesscontrollist-permission-list', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk, - 'acl_pk': self.acl.pk - } + def _request_object_acl_permission_remove_api_view(self): + kwargs = self.test_content_object_view_kwargs.copy() + kwargs['acl_id'] = self.test_acl.pk + + return self.post( + viewname='rest_api:object-acl-permission-remove', + kwargs=kwargs, data={'permission_id_list': self.test_permission.pk} ) + def test_object_acl_permission_remove_api_view_with_access(self): + self.grant_access(obj=self.test_object, permission=permission_acl_edit) + + response = self._request_object_acl_permission_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all()) + + def test_object_acl_permission_remove_api_view_no_permission(self): + response = self._request_object_acl_permission_remove_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all()) + + def _request_object_acl_permission_add_api_view(self): + kwargs = self.test_content_object_view_kwargs.copy() + kwargs['acl_id'] = self.test_acl.pk + + return self.post( + viewname='rest_api:object-acl-permission-add', + kwargs=kwargs, data={'permission_id_list': self.test_permission.pk} + ) + + def test_object_acl_permission_add_api_view_with_access(self): + self.test_acl.permissions.clear() + self.grant_access(obj=self.test_object, permission=permission_acl_edit) + + response = self._request_object_acl_permission_add_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all()) + + def test_object_acl_permission_add_api_view_no_permission(self): + self.test_acl.permissions.clear() + + response = self._request_object_acl_permission_add_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all()) + + def _request_object_acl_inherited_permission_list_api_view(self): + kwargs = self.test_content_object_view_kwargs.copy() + kwargs['acl_id'] = self.test_acl.pk + + return self.get( + viewname='rest_api:object-acl-permission-inherited-list', + kwargs=kwargs + ) + + def test_object_acl_inherited_permission_list_api_view_with_access(self): + self.test_acl.permissions.clear() + self.test_role.grant(permission=self.test_permission) + + self.grant_access(obj=self.test_object, permission=permission_acl_view) + + response = self._request_object_acl_inherited_permission_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( - response.data['results'][0]['permission_pk'], - permission_document_view.pk + response.data['results'][0]['pk'], + self.test_permission.pk ) - def test_object_acl_permission_list_post_view(self): - self._create_acl() + def test_object_acl_inherited_permission_list_api_view_no_permission(self): + self.test_acl.permissions.clear() + self.test_role.grant(permission=self.test_permission) - response = self.post( - viewname='rest_api:accesscontrollist-permission-list', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk, 'acl_pk': self.acl.pk - }, data={'permission_pk': permission_acl_view.pk} - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertQuerysetEqual( - ordered=False, qs=self.acl.permissions.all(), values=( - repr(permission_document_view.stored_permission), - repr(permission_acl_view.stored_permission) - ) - ) - - def test_object_acl_post_no_permissions_added_view(self): - response = self.post( - viewname='rest_api:accesscontrollist-list', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk - }, data={'role_pk': self.role.pk} - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual( - self.document.acls.first().role, self.role - ) - self.assertEqual( - self.document.acls.first().content_object, self.document - ) - self.assertEqual( - self.document.acls.first().permissions.count(), 0 - ) - - def test_object_acl_post_with_permissions_added_view(self): - response = self.post( - viewname='rest_api:accesscontrollist-list', - kwargs={ - 'app_label': self.document_content_type.app_label, - 'model': self.document_content_type.model, - 'object_id': self.document.pk - }, data={ - 'role_pk': self.role.pk, - 'permissions_pk_list': permission_acl_view.pk - - } - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual( - self.document.acls.first().content_object, self.document - ) - self.assertEqual( - self.document.acls.first().role, self.role - ) - self.assertEqual( - self.document.acls.first().permissions.first(), - permission_acl_view.stored_permission - ) + response = self._request_object_acl_inherited_permission_list_api_view() + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/mayan/apps/acls/tests/test_links.py b/mayan/apps/acls/tests/test_links.py index 231ad6e78d..e9407b84c4 100644 --- a/mayan/apps/acls/tests/test_links.py +++ b/mayan/apps/acls/tests/test_links.py @@ -1,100 +1,84 @@ from __future__ import unicode_literals -from django.contrib.contenttypes.models import ContentType from django.urls import reverse -from mayan.apps.documents.tests import GenericDocumentViewTestCase +from mayan.apps.common.tests import GenericViewTestCase from ..links import ( link_acl_create, link_acl_delete, link_acl_list, link_acl_permissions ) -from ..models import AccessControlList from ..permissions import permission_acl_edit, permission_acl_view +from .mixins import ACLTestMixin -class ACLsLinksTestCase(GenericDocumentViewTestCase): - def test_document_acl_create_link(self): - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) - acl.permissions.add(permission_acl_edit.stored_permission) - self.login_user() +class AccessControlListLinksTestCase(ACLTestMixin, GenericViewTestCase): + auto_create_test_role = False - self.add_test_view(test_object=self.document) + def setUp(self): + super(AccessControlListLinksTestCase, self).setUp() + self._setup_test_object() + + def test_object_acl_create_link(self): + self.grant_access(obj=self.test_object, permission=permission_acl_edit) + + self.add_test_view(test_object=self.test_object) context = self.get_test_view() resolved_link = link_acl_create.resolve(context=context) self.assertNotEqual(resolved_link, None) - content_type = ContentType.objects.get_for_model(self.document) - kwargs = { - 'app_label': content_type.app_label, - 'model': content_type.model, - 'object_id': self.document.pk - } - self.assertEqual( - resolved_link.url, reverse(viewname='acls:acl_create', kwargs=kwargs) + resolved_link.url, reverse( + viewname='acls:acl_create', + kwargs=self.test_content_object_view_kwargs + ) ) - def test_document_acl_delete_link(self): - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) + def test_object_acl_delete_link(self): + self.grant_access(obj=self.test_object, permission=permission_acl_edit) - acl.permissions.add(permission_acl_edit.stored_permission) - self.login_user() - - self.add_test_view(test_object=acl) + self.add_test_view(test_object=self._test_case_acl) context = self.get_test_view() resolved_link = link_acl_delete.resolve(context=context) self.assertNotEqual(resolved_link, None) self.assertEqual( - resolved_link.url, reverse(viewname='acls:acl_delete', kwargs={'acl_pk': acl.pk}) + resolved_link.url, reverse( + viewname='acls:acl_delete', + kwargs={'acl_id': self._test_case_acl.pk} + ) ) - def test_document_acl_edit_link(self): - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) + def test_object_acl_edit_link(self): + self.grant_access(obj=self.test_object, permission=permission_acl_edit) - acl.permissions.add(permission_acl_edit.stored_permission) - self.login_user() - - self.add_test_view(test_object=acl) + self.add_test_view(test_object=self._test_case_acl) context = self.get_test_view() resolved_link = link_acl_permissions.resolve(context=context) self.assertNotEqual(resolved_link, None) self.assertEqual( - resolved_link.url, reverse(viewname='acls:acl_permissions', kwargs={'acl_pk': acl.pk}) + resolved_link.url, reverse( + viewname='acls:acl_permissions', + kwargs={'acl_id': self._test_case_acl.pk} + ) ) - def test_document_acl_list_link(self): - acl = AccessControlList.objects.create( - content_object=self.document, role=self.role - ) + def test_object_acl_list_link(self): + self.grant_access(obj=self.test_object, permission=permission_acl_view) - acl.permissions.add(permission_acl_view.stored_permission) - self.login_user() - - self.add_test_view(test_object=self.document) + self.add_test_view(test_object=self.test_object) context = self.get_test_view() resolved_link = link_acl_list.resolve(context=context) self.assertNotEqual(resolved_link, None) - content_type = ContentType.objects.get_for_model(self.document) - kwargs = { - 'app_label': content_type.app_label, - 'model': content_type.model, - 'object_id': self.document.pk - } - self.assertEqual( - resolved_link.url, reverse(viewname='acls:acl_list', kwargs=kwargs) + resolved_link.url, reverse( + viewname='acls:acl_list', + kwargs=self.test_content_object_view_kwargs + ) ) diff --git a/mayan/apps/acls/tests/test_models.py b/mayan/apps/acls/tests/test_models.py index 1eaeb57e24..92fecdbb9f 100644 --- a/mayan/apps/acls/tests/test_models.py +++ b/mayan/apps/acls/tests/test_models.py @@ -1,159 +1,401 @@ from __future__ import absolute_import, unicode_literals from django.core.exceptions import PermissionDenied +from django.db import models from mayan.apps.common.tests import BaseTestCase from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.tests import ( - TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL, - TEST_SMALL_DOCUMENT_PATH + DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL ) +from ..classes import ModelPermission from ..models import AccessControlList +from .mixins import ACLTestMixin + + +class PermissionTestCase(DocumentTestMixin, BaseTestCase): + auto_create_document_type = False -class PermissionTestCase(BaseTestCase): def setUp(self): super(PermissionTestCase, self).setUp() - self.document_type_1 = DocumentType.objects.create( + self.test_document_type_1 = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE_LABEL ) - self.document_type_2 = DocumentType.objects.create( + self.test_document_type_2 = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE_2_LABEL ) - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document_1 = self.document_type_1.new_document( - file_object=file_object - ) - - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document_2 = self.document_type_1.new_document( - file_object=file_object - ) - - with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: - self.document_3 = self.document_type_2.new_document( - file_object=file_object - ) - - def tearDown(self): - for document_type in DocumentType.objects.all(): - document_type.delete() - super(PermissionTestCase, self).tearDown() + self.test_document_1 = self.upload_document( + document_type=self.test_document_type_1 + ) + self.test_document_2 = self.upload_document( + document_type=self.test_document_type_1 + ) + self.test_document_3 = self.upload_document( + document_type=self.test_document_type_2 + ) def test_check_access_without_permissions(self): with self.assertRaises(PermissionDenied): AccessControlList.objects.check_access( - permissions=(permission_document_view,), - user=self.user, obj=self.document_1 + obj=self.test_document_1, permission=permission_document_view, + user=self._test_case_user ) def test_filtering_without_permissions(self): - self.assertQuerysetEqual( - AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=self.user, - queryset=Document.objects.all() - ), [] + self.assertEqual( + AccessControlList.objects.restrict_queryset( + permission=permission_document_view, + queryset=Document.objects.all(), user=self._test_case_user, + ).count(), 0 ) def test_check_access_with_acl(self): acl = AccessControlList.objects.create( - content_object=self.document_1, role=self.role + content_object=self.test_document_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) try: AccessControlList.objects.check_access( - permissions=(permission_document_view,), user=self.user, - obj=self.document_1 + obj=self.test_document_1, permission=permission_document_view, + user=self._test_case_user ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') def test_filtering_with_permissions(self): acl = AccessControlList.objects.create( - content_object=self.document_1, role=self.role + content_object=self.test_document_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) self.assertQuerysetEqual( - AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=self.user, - queryset=Document.objects.all() - ), (repr(self.document_1),) + AccessControlList.objects.restrict_queryset( + permission=permission_document_view, + queryset=Document.objects.all(), user=self._test_case_user + ), (repr(self.test_document_1),) ) def test_check_access_with_inherited_acl(self): acl = AccessControlList.objects.create( - content_object=self.document_type_1, role=self.role + content_object=self.test_document_type_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) try: AccessControlList.objects.check_access( - permissions=(permission_document_view,), user=self.user, - obj=self.document_1 + obj=self.test_document_1, permission=permission_document_view, + user=self._test_case_user ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') - def test_check_access_with_inherited_acl_and_local_acl(self): - acl = AccessControlList.objects.create( - content_object=self.document_type_1, role=self.role + def test_check_access_with_inherited_acl_and_direct_acl(self): + test_acl_1 = AccessControlList.objects.create( + content_object=self.test_document_type_1, role=self._test_case_role ) - acl.permissions.add(permission_document_view.stored_permission) + test_acl_1.permissions.add(permission_document_view.stored_permission) - acl = AccessControlList.objects.create( - content_object=self.document_3, role=self.role + test_acl_2 = AccessControlList.objects.create( + content_object=self.test_document_3, role=self._test_case_role ) - acl.permissions.add(permission_document_view.stored_permission) + test_acl_2.permissions.add(permission_document_view.stored_permission) try: AccessControlList.objects.check_access( - permissions=(permission_document_view,), user=self.user, - obj=self.document_3 + obj=self.test_document_3, permission=permission_document_view, + user=self._test_case_user ) except PermissionDenied: self.fail('PermissionDenied exception was not expected.') def test_filtering_with_inherited_permissions(self): acl = AccessControlList.objects.create( - content_object=self.document_type_1, role=self.role + content_object=self.test_document_type_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) - result = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=self.user, - queryset=Document.objects.all() + result = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, queryset=Document.objects.all(), + user=self._test_case_user ) # Since document_1 and document_2 are of document_type_1 # they are the only ones that should be returned - - self.assertTrue(self.document_1 in result) - self.assertTrue(self.document_2 in result) - self.assertTrue(self.document_3 not in result) + self.assertTrue(self.test_document_1 in result) + self.assertTrue(self.test_document_2 in result) + self.assertTrue(self.test_document_3 not in result) def test_filtering_with_inherited_permissions_and_local_acl(self): - self.role.permissions.add(permission_document_view.stored_permission) + self._test_case_role.permissions.add( + permission_document_view.stored_permission + ) acl = AccessControlList.objects.create( - content_object=self.document_type_1, role=self.role + content_object=self.test_document_type_1, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) acl = AccessControlList.objects.create( - content_object=self.document_3, role=self.role + content_object=self.test_document_3, role=self._test_case_role ) acl.permissions.add(permission_document_view.stored_permission) - result = AccessControlList.objects.filter_by_access( - permission=permission_document_view, user=self.user, - queryset=Document.objects.all() + result = AccessControlList.objects.restrict_queryset( + permission=permission_document_view, queryset=Document.objects.all(), + user=self._test_case_user, ) - self.assertTrue(self.document_1 in result) - self.assertTrue(self.document_2 in result) - self.assertTrue(self.document_3 in result) + self.assertTrue(self.test_document_1 in result) + self.assertTrue(self.test_document_2 in result) + self.assertTrue(self.test_document_3 in result) + + +class InheritedPermissionTestCase(ACLTestMixin, BaseTestCase): + def test_retrieve_inherited_role_permission_not_model_applicable(self): + self._create_test_model() + self.test_object = self.TestModel.objects.create() + self._create_test_acl() + self._create_test_permission() + + self.test_role.grant(permission=self.test_permission) + + queryset = AccessControlList.objects.get_inherited_permissions( + obj=self.test_object, role=self.test_role + ) + self.assertTrue(self.test_permission.stored_permission not in queryset) + + def test_retrieve_inherited_role_permission_model_applicable(self): + self._create_test_model() + self.test_object = self.TestModel.objects.create() + self._create_test_acl() + self._create_test_permission() + + ModelPermission.register( + model=self.test_object._meta.model, permissions=( + self.test_permission, + ) + ) + self.test_role.grant(permission=self.test_permission) + + queryset = AccessControlList.objects.get_inherited_permissions( + obj=self.test_object, role=self.test_role + ) + self.assertTrue(self.test_permission.stored_permission in queryset) + + def test_retrieve_inherited_related_parent_child_permission(self): + self._create_test_permission() + + self._create_test_model(model_name='TestModelParent') + self._create_test_model( + fields={ + 'parent': models.ForeignKey( + on_delete=models.CASCADE, related_name='children', + to='TestModelParent', + ) + }, model_name='TestModelChild' + ) + + ModelPermission.register( + model=self.TestModelParent, permissions=( + self.test_permission, + ) + ) + ModelPermission.register( + model=self.TestModelChild, permissions=( + self.test_permission, + ) + ) + ModelPermission.register_inheritance( + model=self.TestModelChild, related='parent', + ) + + parent = self.TestModelParent.objects.create() + child = self.TestModelChild.objects.create(parent=parent) + + AccessControlList.objects.grant( + obj=parent, permission=self.test_permission, role=self.test_role + ) + + queryset = AccessControlList.objects.get_inherited_permissions( + obj=child, role=self.test_role + ) + + self.assertTrue(self.test_permission.stored_permission in queryset) + + def test_retrieve_inherited_related_grandparent_parent_child_permission(self): + self._create_test_permission() + + self._create_test_model(model_name='TestModelGrandParent') + self._create_test_model( + fields={ + 'parent': models.ForeignKey( + on_delete=models.CASCADE, related_name='children', + to='TestModelGrandParent', + ) + }, model_name='TestModelParent' + ) + self._create_test_model( + fields={ + 'parent': models.ForeignKey( + on_delete=models.CASCADE, related_name='children', + to='TestModelParent', + ) + }, model_name='TestModelChild' + ) + + ModelPermission.register( + model=self.TestModelGrandParent, permissions=( + self.test_permission, + ) + ) + ModelPermission.register( + model=self.TestModelParent, permissions=( + self.test_permission, + ) + ) + ModelPermission.register( + model=self.TestModelChild, permissions=( + self.test_permission, + ) + ) + + ModelPermission.register_inheritance( + model=self.TestModelChild, related='parent', + ) + ModelPermission.register_inheritance( + model=self.TestModelParent, related='parent', + ) + + grandparent = self.TestModelGrandParent.objects.create() + parent = self.TestModelParent.objects.create(parent=grandparent) + child = self.TestModelChild.objects.create(parent=parent) + + AccessControlList.objects.grant( + obj=grandparent, permission=self.test_permission, + role=self.test_role + ) + + queryset = AccessControlList.objects.get_inherited_permissions( + obj=child, role=self.test_role + ) + + 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) diff --git a/mayan/apps/acls/tests/test_views.py b/mayan/apps/acls/tests/test_views.py index 05a839f298..696db3c44a 100644 --- a/mayan/apps/acls/tests/test_views.py +++ b/mayan/apps/acls/tests/test_views.py @@ -1,136 +1,159 @@ from __future__ import absolute_import, unicode_literals -from django.contrib.contenttypes.models import ContentType from django.utils.encoding import force_text -from mayan.apps.documents.tests import GenericDocumentViewTestCase -from mayan.apps.permissions.tests.mixins import RoleTestMixin +from mayan.apps.common.tests import GenericViewTestCase +from ..classes import ModelPermission from ..models import AccessControlList from ..permissions import permission_acl_edit, permission_acl_view +from .mixins import ACLTestMixin -class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase): + +class AccessControlListViewTestCase(ACLTestMixin, GenericViewTestCase): def setUp(self): super(AccessControlListViewTestCase, self).setUp() - self.login_user() - self._create_test_role() - self.test_object = self.document + self._create_test_model() + self._create_test_object() + ModelPermission.register( + model=self.test_object._meta.model, permissions=( + permission_acl_edit, permission_acl_view, + ) + ) - content_type = ContentType.objects.get_for_model(self.test_object) + self._create_test_permission() + ModelPermission.register( + model=self.test_object._meta.model, permissions=( + self.test_permission, + ) + ) - self.view_content_object_arguments = { - 'app_label': content_type.app_label, - 'model': content_type.model, - 'object_id': self.test_object.pk - } + self._inject_test_object_content_type() - def _request_get_acl_create_view(self): + self._create_test_acl() + self.test_acl.permissions.add(self.test_permission.stored_permission) + + def _request_acl_create_get_view(self): return self.get( viewname='acls:acl_create', - kwargs=self.view_content_object_arguments, data={ + kwargs=self.test_content_object_view_kwargs, data={ 'role': self.test_role.pk } ) - def test_acl_create_view_get_no_permission(self): - response = self._request_get_acl_create_view() + def test_acl_create_get_view_no_permission(self): + self.test_acl.delete() + response = self._request_acl_create_get_view() self.assertEqual(response.status_code, 404) - self.assertEqual(AccessControlList.objects.count(), 0) - def test_acl_create_view_get_with_document_access(self): + self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists()) + + def test_acl_create_get_view_with_object_access(self): + self.test_acl.delete() self.grant_access(obj=self.test_object, permission=permission_acl_edit) - response = self._request_get_acl_create_view() - + response = self._request_acl_create_get_view() self.assertContains( response=response, text=force_text(self.test_object), status_code=200 ) - def _request_post_acl_create_view(self): + self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists()) + + def _request_acl_create_post_view(self): return self.post( viewname='acls:acl_create', - kwargs=self.view_content_object_arguments, data={ + kwargs=self.test_content_object_view_kwargs, data={ 'role': self.test_role.pk } ) def test_acl_create_view_post_no_permission(self): - response = self._request_post_acl_create_view() + self.test_acl.delete() + response = self._request_acl_create_post_view() self.assertEqual(response.status_code, 404) - self.assertEqual(AccessControlList.objects.count(), 0) - def test_acl_create_view_post_with_document_access(self): + self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists()) + + def test_acl_create_view_post_with_access(self): + self.test_acl.delete() self.grant_access(obj=self.test_object, permission=permission_acl_edit) - response = self._request_post_acl_create_view() + response = self._request_acl_create_post_view() self.assertEqual(response.status_code, 302) - # 2 ACLs: 1 created by the test and the other by the self.grant_access - self.assertEqual(AccessControlList.objects.count(), 2) - def test_acl_create_duplicate_view_with_permission(self): + self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists()) + + def test_acl_create_duplicate_view_with_access(self): """ Test creating a duplicate ACL entry: same object & role Result: Should redirect to existing ACL for object + role combination """ - self._create_test_acl() - self.grant_access(obj=self.test_object, permission=permission_acl_edit) - response = self._request_post_acl_create_view() + response = self._request_acl_create_post_view() self.assertNotContains( response=response, text=force_text(self.test_acl.role), status_code=200 ) + # 2 ACLs: 1 created by the test and the other by the self.grant_access self.assertEqual(AccessControlList.objects.count(), 2) - self.assertEqual( - AccessControlList.objects.first().pk, self.test_acl.pk + + # Sorted by role PK + expected_results = sorted( + [ + { + # Test role, created and then requested, + # but created only once + 'object_id': self.test_object.pk, + 'role': self.test_role.pk + }, + { + # Test case ACL for the test case role, ignored + 'object_id': self.test_object.pk, + 'role': self._test_case_role.pk + }, + ], key=lambda item: item['role'] ) - def _create_test_acl(self): - self.test_acl = AccessControlList.objects.create( - content_object=self.test_object, role=self.test_role + self.assertQuerysetEqual( + qs=AccessControlList.objects.order_by('role__id').values( + 'object_id', 'role', + ), transform=dict, values=expected_results ) def _request_acl_delete_view(self): return self.post( - viewname='acls:acl_delete', kwargs={'acl_pk': self.test_acl.pk} + viewname='acls:acl_delete', kwargs={'acl_id': self.test_acl.pk} ) def test_acl_delete_view_no_permission(self): - self._create_test_acl() - response = self._request_acl_delete_view() self.assertNotContains( response=response, text=force_text(self.test_object), status_code=404 ) - # 1 ACL: the test one - self.assertQuerysetEqual( - qs=AccessControlList.objects.all(), values=(repr(self.test_acl),) - ) + + self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists()) def test_acl_delete_view_with_access(self): - self._create_test_acl() - - acl = self.grant_access( + self.grant_access( obj=self.test_object, permission=permission_acl_edit ) + response = self._request_acl_delete_view() self.assertEqual(response.status_code, 302) - # 1 ACL: the one created by the self.grant_access - self.assertQuerysetEqual( - qs=AccessControlList.objects.all(), values=(repr(acl),) - ) + + self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists()) def _request_acl_list_view(self): return self.get( - viewname='acls:acl_list', kwargs=self.view_content_object_arguments + viewname='acls:acl_list', kwargs=self.test_content_object_view_kwargs ) def test_acl_list_view_no_permission(self): @@ -151,28 +174,66 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase): status_code=200 ) - def _request_get_acl_permissions_view(self): + def _request_get_acl_permissions_get_view(self): return self.get( viewname='acls:acl_permissions', - kwargs={'acl_pk': self.test_acl.pk} + kwargs={'acl_id': self.test_acl.pk} ) - def test_acl_permissions_view_get_no_permission(self): - self._create_test_acl() + def test_acl_permissions_get_view_no_permission(self): + self.test_acl.permissions.clear() - response = self._request_get_acl_permissions_view() + response = self._request_get_acl_permissions_get_view() self.assertNotContains( response=response, text=force_text(self.test_object), status_code=404 ) - def test_acl_permissions_view_get_with_access(self): - self._create_test_acl() + self.assertFalse( + self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists() + ) + def test_acl_permissions_get_view_with_access(self): + self.test_acl.permissions.clear() self.grant_access(obj=self.test_object, permission=permission_acl_edit) - response = self._request_get_acl_permissions_view() + response = self._request_get_acl_permissions_get_view() self.assertContains( response=response, text=force_text(self.test_object), status_code=200 ) + + self.assertFalse( + self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists() + ) + + def _request_post_acl_permissions_post_view(self): + return self.post( + viewname='acls:acl_permissions', + kwargs={'acl_id': self.test_acl.pk}, + data={'available-selection': self.test_permission.stored_permission.pk} + ) + + def test_acl_permissions_post_view_no_permission(self): + self.test_acl.permissions.clear() + + response = self._request_post_acl_permissions_post_view() + self.assertNotContains( + response=response, text=force_text(self.test_object), + status_code=404 + ) + + self.assertFalse( + self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists() + ) + + def test_acl_permissions_post_view_with_access(self): + self.test_acl.permissions.clear() + self.grant_access(obj=self.test_object, permission=permission_acl_edit) + + response = self._request_post_acl_permissions_post_view() + self.assertEqual(response.status_code, 302) + + self.assertTrue( + self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists() + ) diff --git a/mayan/apps/acls/urls.py b/mayan/apps/acls/urls.py index dc73f8a5b7..d0d370dd26 100644 --- a/mayan/apps/acls/urls.py +++ b/mayan/apps/acls/urls.py @@ -2,50 +2,33 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import ( - APIObjectACLListView, APIObjectACLPermissionListView, - APIObjectACLPermissionView, APIObjectACLView -) +from .api_views import ObjectACLAPIViewSet from .views import ( ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView ) urlpatterns = [ url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/create/$', + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/create/$', name='acl_create', view=ACLCreateView.as_view() ), url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/list/$', + regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/list/$', name='acl_list', view=ACLListView.as_view() ), url( - regex=r'^acls/(?P\d+)/delete/$', name='acl_delete', + regex=r'^acls/(?P\d+)/delete/$', name='acl_delete', view=ACLDeleteView.as_view() ), url( - regex=r'^acls/(?P\d+)/permissions/$', name='acl_permissions', + regex=r'^acls/(?P\d+)/permissions/$', name='acl_permissions', view=ACLPermissionsView.as_view() ), ] -api_urls = [ - url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/$', - name='accesscontrollist-list', view=APIObjectACLListView.as_view() - ), - url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/$', - name='accesscontrollist-detail', view=APIObjectACLView.as_view() - ), - url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/$', - name='accesscontrollist-permission-list', - view=APIObjectACLPermissionListView.as_view() - ), - url( - regex=r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/(?P\d+)/$', - name='accesscontrollist-permission-detail', - view=APIObjectACLPermissionView.as_view() - ), -] +api_router_entries = ( + { + 'prefix': r'apps/(?P[^/.]+)/models/(?P[^/.]+)/objects/(?P[^/.]+)/acls', + 'viewset': ObjectACLAPIViewSet, 'basename': 'object-acl' + }, +) diff --git a/mayan/apps/acls/views.py b/mayan/apps/acls/views.py index 410f75b006..dc11e41648 100644 --- a/mayan/apps/acls/views.py +++ b/mayan/apps/acls/views.py @@ -1,24 +1,20 @@ from __future__ import absolute_import, unicode_literals -import itertools import logging -from django.core.exceptions import PermissionDenied -from django.shortcuts import get_object_or_404 from django.template import RequestContext from django.urls import reverse from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.mixins import ( - ContentTypeViewMixin, ExternalObjectViewMixin + ContentTypeViewMixin, ExternalObjectMixin ) -from mayan.apps.common.views import ( - AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, +from mayan.apps.common.generics import ( + AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView, SingleObjectListView ) -from mayan.apps.permissions import Permission, PermissionNamespace -from mayan.apps.permissions.models import Role, StoredPermission +from mayan.apps.permissions.models import Role from .classes import ModelPermission from .forms import ACLCreateForm @@ -30,7 +26,11 @@ from .permissions import permission_acl_edit, permission_acl_view logger = logging.getLogger(__name__) -class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectCreateView): +class ACLCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreateView): + content_type_url_kw_args = { + 'app_label': 'app_label', + 'model': 'model_name' + } external_object_permission = permission_acl_edit external_object_pk_url_kwarg = 'object_id' form_class = ACLCreateForm @@ -44,7 +44,7 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC def get_external_object_queryset(self): # Here we get a queryset the object model for which an ACL will be # created. - return self.get_content_type().model_class().objects.all() + return self.get_content_type().get_all_objects_for_this_type() def get_extra_context(self): return { @@ -61,7 +61,8 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC 'queryset': Role.objects.exclude( pk__in=self.get_external_object().acls.values('role') ), - 'widget_attributes': {'class': 'select2'} + 'widget_attributes': {'class': 'select2'}, + 'user': self.request.user } def get_instance_extra_data(self): @@ -77,15 +78,17 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC class ACLDeleteView(SingleObjectDeleteView): - object_permission = permission_acl_edit - object_permission_related = 'content_object' - object_permission_raise_404 = True model = AccessControlList - pk_url_kwarg = 'acl_pk' + object_permission = permission_acl_edit + pk_url_kwarg = 'acl_id' def get_extra_context(self): + acl = self.get_object() + return { - 'object': self.get_object().content_object, + 'acl': acl, + 'object': acl.content_object, + 'navigation_object_list': ('object', 'acl'), 'title': _('Delete ACL: %s') % self.get_object(), } @@ -94,20 +97,24 @@ class ACLDeleteView(SingleObjectDeleteView): return reverse( 'acls:acl_list', kwargs={ 'app_label': instance.content_type.app_label, - 'model': instance.content_type.model, + 'model_name': instance.content_type.model, 'object_id': instance.object_id } ) -class ACLListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectListView): +class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView): + content_type_url_kw_args = { + 'app_label': 'app_label', + 'model': 'model_name' + } external_object_permission = permission_acl_view external_object_pk_url_kwarg = 'object_id' def get_external_object_queryset(self): # Here we get a queryset the object model for which an ACL will be # created. - return self.get_content_type().model_class().objects.all() + return self.get_content_type().get_all_objects_for_this_type() def get_extra_context(self): return { @@ -135,118 +142,88 @@ class ACLListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectLis ), } - def get_object_list(self): + def get_source_queryset(self): return self.get_external_object().acls.all() -class ACLPermissionsView(AssignRemoveView): - grouped = True - left_list_title = _('Available permissions') - right_list_title = _('Granted permissions') +class ACLPermissionsView(AddRemoveView): + action_add_method = 'permissions_add' + action_remove_method = 'permissions_remove' + main_object_model = AccessControlList + main_object_permission = permission_acl_edit + main_object_pk_url_kwarg = 'acl_id' + list_added_title = _('Granted permissions') + list_available_title = _('Available permissions') + related_field = 'permissions' - @staticmethod - def generate_choices(entries): - results = [] + def generate_choices(self, queryset): + namespaces_dictionary = {} - entries = sorted( - entries, key=lambda x: ( - x.volatile_permission.namespace.label, - x.volatile_permission.label - ) + # Sort permissions by their translatable label + object_list = sorted( + queryset, key=lambda permission: permission.volatile_permission.label ) - for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace): - permission_options = [ - (force_text(permission.pk), permission) for permission in permissions - ] - results.append( - (PermissionNamespace.get(name=namespace), permission_options) + # Group permissions by namespace + for permission in object_list: + namespaces_dictionary.setdefault( + permission.volatile_permission.namespace.label, + [] + ) + namespaces_dictionary[permission.volatile_permission.namespace.label].append( + (permission.pk, force_text(permission)) ) - return results + # Sort permissions by their translatable namespace label + return sorted(namespaces_dictionary.items()) - def add(self, item): - permission = get_object_or_404(klass=StoredPermission, pk=item) - self.get_object().permissions.add(permission) - - def get_available_list(self): - return ModelPermission.get_for_instance( - instance=self.get_object().content_object - ).exclude(id__in=self.get_granted_list().values_list('pk', flat=True)) + def get_actions_extra_kwargs(self): + return {'_user': self.request.user} def get_disabled_choices(self): """ - Get permissions from a parent's acls but remove the permissions we - already hold for this object + Get permissions from a parent's ACLs or directly granted to the role. + We return a list since that is what the form widget's can process. """ - return map( - str, set( - self.get_object().get_inherited_permissions().values_list( - 'pk', flat=True - ) - ).difference( - self.get_object().permissions.values_list('pk', flat=True) - ) - ) + return self.main_object.get_inherited_permissions().values_list('pk', flat=True) def get_extra_context(self): return { - 'object': self.get_object().content_object, - 'title': _('Role "%(role)s" permission\'s for "%(object)s"') % { - 'role': self.get_object().role, - 'object': self.get_object().content_object, - }, + 'acl': self.main_object, + 'object': self.main_object.content_object, + 'navigation_object_list': ('object', 'acl'), + 'title': _('Role "%(role)s" permission\'s for "%(object)s".') % { + 'role': self.main_object.role, + 'object': self.main_object.content_object, + } } - def get_granted_list(self): + def get_list_added_help_text(self): + if self.main_object.get_inherited_permissions(): + return _( + 'Disabled permissions are inherited from a parent object or ' + 'directly granted to the role and can\'t be removed from this ' + 'view. Inherited permissions need to be removed from the ' + 'parent object\'s ACL or from them role via the Setup menu.' + ) + + def get_list_added_queryset(self): """ Merge of permissions we hold for this object and the permissions we - hold for this object's parent via another ACL. + hold for this object's parents via another ACL. .distinct() is added + in case the permission was added to the ACL and then added to a + parent ACL's and thus inherited and would appear twice. If + order to remove the double permission from the ACL it would need to be + remove from the parent first to enable the choice in the form, + remove it from the ACL and then re-add it to the parent ACL. """ - merged_pks = self.get_object().permissions.values_list( - 'pk', flat=True - ) | self.get_object().get_inherited_permissions().values_list( - 'pk', flat=True + queryset_acl = super(ACLPermissionsView, self).get_list_added_queryset() + + return ( + queryset_acl | self.main_object.get_inherited_permissions() + ).distinct() + + def get_secondary_object_source_queryset(self): + return ModelPermission.get_for_instance( + instance=self.main_object.content_object ) - return StoredPermission.objects.filter(pk__in=merged_pks) - - def get_object(self): - acl = get_object_or_404( - klass=AccessControlList, pk=self.kwargs['acl_pk'] - ) - - # Get the ACL, from this get the object of the ACL, from the object - # get all ACLs it holds as a filtered queryset by access. - - try: - AccessControlList.objects.check_access( - permissions=(permission_acl_edit,), obj=acl.content_object, - user=self.request.user - ) - except PermissionDenied: - queryset = AccessControlList.objects.none() - else: - queryset = acl.content_object.acls.all() - - return get_object_or_404(klass=queryset, pk=self.kwargs['acl_pk']) - - def get_right_list_help_text(self): - if self.get_object().get_inherited_permissions(): - return _( - 'Disabled permissions are inherited from a parent object and ' - 'can\'t be removed from this view, they need to be removed ' - 'from the parent object\'s ACL view.' - ) - - return self.right_list_help_text - - def left_list(self): - Permission.refresh() - return ACLPermissionsView.generate_choices(self.get_available_list()) - - def remove(self, item): - permission = get_object_or_404(klass=StoredPermission, pk=item) - self.get_object().permissions.remove(permission) - - def right_list(self): - return ACLPermissionsView.generate_choices(self.get_granted_list()) diff --git a/mayan/apps/acls/workflow_actions.py b/mayan/apps/acls/workflow_actions.py index 0d333914dd..8da80d1c18 100644 --- a/mayan/apps/acls/workflow_actions.py +++ b/mayan/apps/acls/workflow_actions.py @@ -89,7 +89,8 @@ class GrantAccessAction(WorkflowAction): try: AccessControlList.objects.check_access( - permissions=permission_acl_edit, user=request.user, obj=obj + obj=obj, permissions=permission_acl_edit, + user=request.user ) except Exception as exception: raise ValidationError(exception) diff --git a/mayan/apps/appearance/literals.py b/mayan/apps/appearance/literals.py index 6266e3e975..87ce87d1f8 100644 --- a/mayan/apps/appearance/literals.py +++ b/mayan/apps/appearance/literals.py @@ -1 +1 @@ -DEFAULT_MAXIMUM_TITLE_LENGTH = 80 +DEFAULT_MAXIMUM_TITLE_LENGTH = 120 diff --git a/mayan/apps/appearance/static/appearance/js/mayan_app.js b/mayan/apps/appearance/static/appearance/js/mayan_app.js index b77e94859e..967a3a62f9 100644 --- a/mayan/apps/appearance/static/appearance/js/mayan_app.js +++ b/mayan/apps/appearance/static/appearance/js/mayan_app.js @@ -8,10 +8,10 @@ class MayanApp { ajaxMenusOptions: [] } - this.ajaxSpinnerSeletor = '#ajax-spinner'; this.ajaxExecuting = false; this.ajaxMenusOptions = options.ajaxMenusOptions; this.ajaxMenuHashes = {}; + this.ajaxSpinnerSeletor = '#ajax-spinner'; this.window = $(window); } @@ -29,29 +29,6 @@ class MayanApp { } } - static mayanNotificationBadge (options, data) { - // Callback to add the notifications count inside a badge markup - var notifications = data[options.attributeName]; - - if (notifications > 0) { - // Save the original link text before adding the initial badge markup - if (!options.element.data('mn-saved-text')) { - options.element.data('mn-saved-text', options.element.html()); - } - - options.element.html( - options.element.data('mn-saved-text') + ' ' + notifications + '' - ); - } else { - if (options.element.data('mn-saved-text')) { - // If there is a saved original link text, restore it - options.element.html( - options.element.data('mn-saved-text') - ); - } - } - } - static setupMultiItemActions () { $('body').on('change', '.check-all-slave', function () { MayanApp.countChecked(); @@ -81,22 +58,6 @@ class MayanApp { }); } - static tagSelectionTemplate (tag, container) { - var $tag = $( - ' ' + tag.text + '' - ); - container[0].style.background = tag.element.dataset.color; - return $tag; - } - - static tagResultTemplate (tag) { - if (!tag.element) { return ''; } - var $tag = $( - ' ' + tag.text + '' - ); - return $tag; - } - static updateNavbarState () { var uri = new URI(window.location.hash); var uriFragment = uri.fragment(); @@ -110,35 +71,6 @@ class MayanApp { // Instance methods - AJAXperiodicWorker (options) { - var app = this; - - $.ajax({ - complete: function() { - if (!options.app) { - // Preserve the app reference between consecutive calls - options.app = app; - } - setTimeout(options.app.AJAXperiodicWorker, options.interval, options); - }, - success: function(data) { - if (options.callback) { - // Convert the callback string to an actual function - var callbackFunction = window; - - $.each(options.callback.split('.'), function (index, value) { - callbackFunction = callbackFunction[value] - }); - - callbackFunction(options, data); - } else { - options.element.text(data[options.attributeName]); - } - }, - url: options.APIURL - }); - } - callbackAJAXSpinnerUpdate () { if (this.ajaxExecuting) { $(this.ajaxSpinnerSeletor).fadeIn(50); @@ -239,7 +171,6 @@ class MayanApp { initialize () { var self = this; - this.setupAJAXPeriodicWorkers(); this.setupAJAXSpinner(); this.setupFormHotkeys(); this.setupFullHeightResizing(); @@ -256,22 +187,6 @@ class MayanApp { partialNavigation.initialize(); } - setupAJAXPeriodicWorkers () { - var app = this; - - $('a[data-apw-url]').each(function() { - var $this = $(this); - - app.AJAXperiodicWorker({ - attributeName: $this.data('apw-attribute'), - APIURL: $this.data('apw-url'), - callback: $this.data('apw-callback'), - element: $this, - interval: $this.data('apw-interval'), - }); - }); - } - setupAJAXSpinner () { var self = this; @@ -445,12 +360,6 @@ class MayanApp { dropdownAutoWidth: true, width: '100%' }); - - $('.select2-tags').select2({ - templateSelection: MayanApp.tagSelectionTemplate, - templateResult: MayanApp.tagResultTemplate, - width: '100%' - }); } resizeFullHeight () { diff --git a/mayan/apps/appearance/templates/appearance/base.html b/mayan/apps/appearance/templates/appearance/base.html index c1db3d4374..48cfcc0143 100644 --- a/mayan/apps/appearance/templates/appearance/base.html +++ b/mayan/apps/appearance/templates/appearance/base.html @@ -37,7 +37,7 @@ - {% get_menus_links names='facet,list facet' sort_results=True as links_facet %} + {% navigation_resolve_menus names='facet,list facet' sort_results=True as facet_menus_link_results %}