From f66328139e91338ae5b4cead6c50737e0874678a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 10 Jun 2019 03:40:09 -0400 Subject: [PATCH] Add document ACLs workflow actions Signed-off-by: Roberto Rosario --- HISTORY.rst | 2 + docs/releases/3.2.rst | 2 + mayan/apps/acls/classes.py | 19 ++++++- mayan/apps/acls/tests/test_actions.py | 36 ++++++++++++- mayan/apps/acls/workflow_actions.py | 72 ++++++++++++++++++++++++++ mayan/apps/document_states/handlers.py | 16 ++++-- 6 files changed, 140 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index bd3769b4ab..78309f0dd1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -296,6 +296,8 @@ * Add Latvian translation. * Support search model selection. * Support passing a queryset factory to the search model. +* Add workflow actions to grant or remove permissions to + a document. 3.1.11 (2019-04-XX) =================== diff --git a/docs/releases/3.2.rst b/docs/releases/3.2.rst index 6d2fea3ff9..1990572988 100644 --- a/docs/releases/3.2.rst +++ b/docs/releases/3.2.rst @@ -725,6 +725,8 @@ Other changes - Add Latvian translation. - Support search model selection. - Support passing a queryset factory to the search model. +- Add workflow actions to grant or remove permissions to + a document. Removals diff --git a/mayan/apps/acls/classes.py b/mayan/apps/acls/classes.py index e736faa4db..1c06e0d05e 100644 --- a/mayan/apps/acls/classes.py +++ b/mayan/apps/acls/classes.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals, absolute_import +import itertools import logging from django.apps import apps +from django.utils.encoding import force_text logger = logging.getLogger(__name__) @@ -53,8 +55,21 @@ class ModelPermission(object): return cls._registry.keys() @classmethod - def get_for_class(cls, klass): - return cls._registry.get(klass, ()) + def get_for_class(cls, klass, as_choices=False): + if as_choices: + results = [] + + for namespace, permissions in itertools.groupby(cls.get_for_class(klass=klass, as_choices=False), lambda entry: entry.namespace): + permission_options = [ + (force_text(permission.pk), permission) for permission in permissions + ] + results.append( + (namespace, permission_options) + ) + + return results + else: + return cls._registry.get(klass, ()) @classmethod def get_for_instance(cls, instance): diff --git a/mayan/apps/acls/tests/test_actions.py b/mayan/apps/acls/tests/test_actions.py index eee6438b0c..4553473ca0 100644 --- a/mayan/apps/acls/tests/test_actions.py +++ b/mayan/apps/acls/tests/test_actions.py @@ -5,7 +5,10 @@ from django.contrib.contenttypes.models import ContentType from mayan.apps.document_states.tests.test_actions import ActionTestCase from mayan.apps.documents.permissions import permission_document_view -from ..workflow_actions import GrantAccessAction, RevokeAccessAction +from ..workflow_actions import ( + GrantAccessAction, GrantDocumentAccessAction, RevokeAccessAction, + RevokeDocumentAccessAction +) class ACLActionTestCase(ActionTestCase): @@ -29,6 +32,22 @@ class ACLActionTestCase(ActionTestCase): ) self.assertEqual(self.test_document.acls.first().role, self._test_case_role) + def test_grant_document_access_action(self): + action = GrantDocumentAccessAction( + form_data={ + 'roles': [self._test_case_role.pk], + 'permissions': [permission_document_view.pk], + } + ) + action.execute(context={'document': self.test_document}) + + self.assertEqual(self.test_document.acls.count(), 1) + self.assertEqual( + list(self.test_document.acls.first().permissions.all()), + [permission_document_view.stored_permission] + ) + self.assertEqual(self.test_document.acls.first().role, self._test_case_role) + def test_revoke_access_action(self): self.grant_access( obj=self.test_document, permission=permission_document_view @@ -47,3 +66,18 @@ class ACLActionTestCase(ActionTestCase): action.execute(context={'entry_log': self.entry_log}) self.assertEqual(self.test_document.acls.count(), 0) + + def test_revoke_document_access_action(self): + self.grant_access( + obj=self.test_document, permission=permission_document_view + ) + + action = RevokeDocumentAccessAction( + form_data={ + 'roles': [self._test_case_role.pk], + 'permissions': [permission_document_view.pk], + } + ) + action.execute(context={'document': self.test_document}) + + self.assertEqual(self.test_document.acls.count(), 0) diff --git a/mayan/apps/acls/workflow_actions.py b/mayan/apps/acls/workflow_actions.py index aee7761f90..da916aa780 100644 --- a/mayan/apps/acls/workflow_actions.py +++ b/mayan/apps/acls/workflow_actions.py @@ -8,6 +8,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from mayan.apps.acls.models import AccessControlList +from mayan.apps.documents.models import Document from mayan.apps.document_states.classes import WorkflowAction from mayan.apps.permissions.classes import Permission from mayan.apps.permissions.models import Role @@ -149,3 +150,74 @@ class RevokeAccessAction(GrantAccessAction): AccessControlList.objects.revoke( obj=self.obj, permission=permission, role=role ) + + +class GrantDocumentAccessAction(WorkflowAction): + fields = { + 'roles': { + 'label': _('Roles'), + 'class': 'django.forms.ModelMultipleChoiceField', 'kwargs': { + 'help_text': _('Roles whose access will be modified.'), + 'queryset': Role.objects.all(), 'required': True + } + }, 'permissions': { + 'label': _('Permissions'), + 'class': 'django.forms.MultipleChoiceField', 'kwargs': { + 'help_text': _( + 'Permissions to grant/revoke to/from the role for the ' + 'object selected above.' + ), 'choices': (), + 'required': True + } + } + } + field_order = ('roles', 'permissions') + label = _('Grant document access') + widgets = { + 'roles': { + 'class': 'django.forms.widgets.SelectMultiple', 'kwargs': { + 'attrs': {'class': 'select2'}, + } + }, + 'permissions': { + 'class': 'django.forms.widgets.SelectMultiple', 'kwargs': { + 'attrs': {'class': 'select2'}, + } + } + } + + def get_form_schema(self, *args, **kwargs): + self.fields['permissions']['kwargs']['choices'] = ModelPermission.get_for_class( + klass=Document, as_choices=True + ) + return super(GrantDocumentAccessAction, self).get_form_schema(*args, **kwargs) + + def get_execute_data(self): + self.roles = Role.objects.filter(pk__in=self.form_data['roles']) + self.permissions = [ + Permission.get( + pk=permission, proxy_only=True + ) for permission in self.form_data['permissions'] + ] + + def execute(self, context): + self.get_execute_data() + + for role in self.roles: + for permission in self.permissions: + AccessControlList.objects.grant( + obj=context['document'], permission=permission, role=role + ) + + +class RevokeDocumentAccessAction(GrantDocumentAccessAction): + label = _('Revoke document access') + + def execute(self, context): + self.get_execute_data() + + for role in self.roles: + for permission in self.permissions: + AccessControlList.objects.revoke( + obj=context['document'], permission=permission, role=role + ) diff --git a/mayan/apps/document_states/handlers.py b/mayan/apps/document_states/handlers.py index 7a1ddc4482..a0e3ee6899 100644 --- a/mayan/apps/document_states/handlers.py +++ b/mayan/apps/document_states/handlers.py @@ -37,18 +37,26 @@ def handler_trigger_transition(sender, **kwargs): app_label='document_states', model_name='WorkflowTransition' ) - trigger_transitions = WorkflowTransition.objects.filter(trigger_events__event_type__name=kwargs['instance'].verb) + trigger_transitions = WorkflowTransition.objects.filter( + trigger_events__event_type__name=kwargs['instance'].verb + ) if isinstance(action.target, Document): - workflow_instances = WorkflowInstance.objects.filter(workflow__transitions__in=trigger_transitions, document=action.target).distinct() + workflow_instances = WorkflowInstance.objects.filter( + workflow__transitions__in=trigger_transitions, document=action.target + ).distinct() elif isinstance(action.action_object, Document): - workflow_instances = WorkflowInstance.objects.filter(workflow__transitions__in=trigger_transitions, document=action.action_object).distinct() + workflow_instances = WorkflowInstance.objects.filter( + workflow__transitions__in=trigger_transitions, document=action.action_object + ).distinct() else: workflow_instances = WorkflowInstance.objects.none() for workflow_instance in workflow_instances: # Select the first transition that is valid for this workflow state - valid_transitions = list(set(trigger_transitions) & set(workflow_instance.get_transition_choices())) + valid_transitions = list( + set(trigger_transitions) & set(workflow_instance.get_transition_choices()) + ) if valid_transitions: workflow_instance.do_transition( comment=_('Event trigger: %s') % EventType.get(name=action.verb).label,