Update ACL view to use new AddRemoveView

Add ACL created and edited events.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-04-27 19:15:41 -04:00
parent 1a91640047
commit 4963a9689f
8 changed files with 204 additions and 83 deletions

View File

@@ -185,6 +185,8 @@
* Increase default title truncation length to 120 characters.
* Improve inherited permission computation.
* Add test case mixin that produces ephimeral models.
* Update ACL permissions view to use the new AddRemoveView class.
* Add ACL created and edited events.
3.1.11 (2019-04-XX)
===================

View File

@@ -217,6 +217,8 @@ Other changes
* Increase default title truncation length to 120 characters.
* Improve inherited permission computation.
* Add test case mixin that produces ephimeral models.
* Update ACL permissions view to use the new AddRemoveView class.
* Add ACL created and edited events.
Removals
--------

View File

@@ -4,8 +4,13 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.apps import MayanAppConfig
from mayan.apps.common.menus import menu_object, menu_secondary
from mayan.apps.events.classes import ModelEventType
from mayan.apps.events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list
)
from mayan.apps.navigation.classes import SourceColumn
from .events import event_acl_created, event_acl_edited
from .links import link_acl_create, link_acl_delete, link_acl_permissions
@@ -19,17 +24,29 @@ 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
)
SourceColumn(
attribute='role', is_sortable=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_secondary.bind_links(
links=(link_acl_create,), sources=('acls:acl_list',)
)
registry.register(AccessControlList)

16
mayan/apps/acls/events.py Normal file
View File

@@ -0,0 +1,16 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.events.classes 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'
)

View File

@@ -4,13 +4,14 @@ import logging
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 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__)
@@ -73,3 +74,32 @@ class AccessControlList(models.Model):
return AccessControlList.objects.get_inherited_permissions(
obj=self.content_object, role=self.role
)
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
)

View File

@@ -1,13 +1,18 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from mayan.apps.acls.models import AccessControlList
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 ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin):
def setUp(self):
@@ -27,7 +32,7 @@ class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin):
)
class ACLTestMixin(PermissionTestMixin, RoleTestMixin):
class ACLTestMixin(PermissionTestMixin, RoleTestMixin, TestModelTestMixin):
auto_create_test_role = True
def _create_test_acl(self):
@@ -39,3 +44,32 @@ class ACLTestMixin(PermissionTestMixin, RoleTestMixin):
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(
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()

View File

@@ -10,6 +10,7 @@ from mayan.apps.documents.tests import (
TEST_DOCUMENT_TYPE_2_LABEL
)
from ..classes import ModelPermission
from ..models import AccessControlList
from .mixins import ACLTestMixin
@@ -148,3 +149,36 @@ class PermissionTestCase(ACLTestMixin, BaseTestCase):
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.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)

View File

@@ -1,6 +1,5 @@
from __future__ import absolute_import, unicode_literals
import itertools
import logging
from django.contrib.contenttypes.models import ContentType
@@ -12,11 +11,10 @@ from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.generics import (
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
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
@@ -96,6 +94,8 @@ class ACLDeleteView(SingleObjectDeleteView):
def get_extra_context(self):
return {
'acl': self.get_object(),
'navigation_object_list': ('object', 'acl'),
'object': self.get_object().content_object,
'title': _('Delete ACL: %s') % self.get_object(),
}
@@ -161,102 +161,88 @@ class ACLListView(SingleObjectListView):
)
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 = 'pk'
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 = {}
# Sort permissions by their translatable label
entries = sorted(
entries, key=lambda permission: permission.volatile_permission.label
object_list = sorted(
queryset, key=lambda permission: permission.volatile_permission.label
)
# Group permissions by namespace
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)
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 dispatch(self, request, *args, **kwargs):
acl = get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user,
obj=acl.content_object
)
return super(
ACLPermissionsView, self
).dispatch(request, *args, **kwargs)
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):
"""
Merge or permissions we hold for this object and the permissions we
hold for this object's parent via another ACL
"""
merged_pks = self.get_object().permissions.values_list('pk', flat=True) | self.get_object().get_inherited_permissions().values_list('pk', flat=True)
return StoredPermission.objects.filter(pk__in=merged_pks)
def get_object(self):
return get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk'])
def get_right_list_help_text(self):
if self.get_object().get_inherited_permissions():
def get_list_added_help_text(self):
if self.main_object.get_inherited_permissions():
return _(
'Disabled permissions are inherited from a parent object.'
'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.'
)
else:
return super(ACLPermissionsView, self).get_list_added_help_text()
return None
def get_list_added_queryset(self):
"""
Merge of permissions we hold for this object and the permissions we
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.
"""
queryset_acl = super(ACLPermissionsView, self).get_list_added_queryset()
def left_list(self):
Permission.refresh()
return ACLPermissionsView.generate_choices(self.get_available_list())
return (
queryset_acl | self.main_object.get_inherited_permissions()
).distinct()
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())
def get_secondary_object_source_queryset(self):
return ModelPermission.get_for_instance(
instance=self.main_object.content_object
)