Update ACL app to compy with MERC 5 and 6

Update the entire with keyword arguments. Update the views
to comply with MERC 6 by returning error 404 on access
failure. API are untouched. Add icon to the ACL delete
button. Add additional view tests. Use the new filtered
choice form to display a select2 enabled role selection
widget. Update the ACL creation view to not redirect to an
existing ACL in case of duplication but to instead stop
and display an error with a suggestion to the user
to instead edit the existing ACL.

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-01-02 02:48:19 -04:00
parent 86b0463a38
commit dfd548bf62
15 changed files with 388 additions and 360 deletions
+7 -6
View File
@@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import generics from rest_framework import generics
from .models import AccessControlList from .models import AccessControlList
@@ -25,7 +26,7 @@ class APIObjectACLListView(generics.ListCreateAPIView):
) )
content_object = get_object_or_404( content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk'] klass=content_type.model_class(), pk=self.kwargs['object_id']
) )
if self.request.method == 'GET': if self.request.method == 'GET':
@@ -89,7 +90,7 @@ class APIObjectACLView(generics.RetrieveDestroyAPIView):
) )
content_object = get_object_or_404( content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk'] klass=content_type.model_class(), pk=self.kwargs['object_id']
) )
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
@@ -110,7 +111,7 @@ class APIObjectACLPermissionListView(generics.ListCreateAPIView):
""" """
def get_acl(self): def get_acl(self):
return get_object_or_404( return get_object_or_404(
klass=self.get_content_object().acls, pk=self.kwargs['pk'] klass=self.get_content_object().acls, pk=self.kwargs['acl_pk']
) )
def get_content_object(self): def get_content_object(self):
@@ -120,7 +121,7 @@ class APIObjectACLPermissionListView(generics.ListCreateAPIView):
) )
content_object = get_object_or_404( content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk'] klass=content_type.model_class(), pk=self.kwargs['object_id']
) )
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
@@ -167,7 +168,7 @@ class APIObjectACLPermissionView(generics.RetrieveDestroyAPIView):
def get_acl(self): def get_acl(self):
return get_object_or_404( return get_object_or_404(
klass=self.get_content_object().acls, pk=self.kwargs['pk'] klass=self.get_content_object().acls, pk=self.kwargs['acl_pk']
) )
def get_content_object(self): def get_content_object(self):
@@ -177,7 +178,7 @@ class APIObjectACLPermissionView(generics.RetrieveDestroyAPIView):
) )
content_object = get_object_or_404( content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk'] klass=content_type.model_class(), pk=self.kwargs['object_id']
) )
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
+5 -4
View File
@@ -19,14 +19,15 @@ class ACLsApp(MayanAppConfig):
def ready(self): def ready(self):
super(ACLsApp, self).ready() super(ACLsApp, self).ready()
AccessControlList = self.get_model('AccessControlList') AccessControlList = self.get_model(model_name='AccessControlList')
SourceColumn( SourceColumn(
source=AccessControlList, label=_('Role'), attribute='role' attribute='role', is_identifier=True, is_sortable=True,
source=AccessControlList
) )
SourceColumn( SourceColumn(
source=AccessControlList, label=_('Permissions'), attribute='get_permission_titles', include_label=True,
attribute='get_permission_titles' source=AccessControlList
) )
menu_object.bind_links( menu_object.bind_links(
+13
View File
@@ -0,0 +1,13 @@
from __future__ import unicode_literals
from django import forms
from mayan.apps.common.forms import FilteredSelectionForm
from .models import AccessControlList
class ACLCreateForm(FilteredSelectionForm, forms.ModelForm):
class Meta:
fields = ('role',)
model = AccessControlList
+1
View File
@@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon from mayan.apps.appearance.classes import Icon
icon_acl_delete = Icon(driver_name='fontawesome', symbol='minus')
icon_acl_list = Icon(driver_name='fontawesome', symbol='lock') icon_acl_list = Icon(driver_name='fontawesome', symbol='lock')
icon_acl_new = Icon( icon_acl_new = Icon(
driver_name='fontawesome-dual', primary_symbol='lock', driver_name='fontawesome-dual', primary_symbol='lock',
+8 -7
View File
@@ -4,8 +4,9 @@ from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation import Link from mayan.apps.navigation import Link
from mayan.apps.permissions.icons import icon_permission
from .icons import icon_acl_list, icon_acl_new from .icons import icon_acl_delete, icon_acl_list, icon_acl_new
from .permissions import permission_acl_edit, permission_acl_view from .permissions import permission_acl_edit, permission_acl_view
@@ -28,9 +29,9 @@ def get_kwargs_factory(variable_name):
link_acl_delete = Link( link_acl_delete = Link(
args='resolved_object.pk', permissions=(permission_acl_edit,), args='resolved_object.pk', icon_class=icon_acl_delete,
permissions_related='content_object', tags='dangerous', text=_('Delete'), permissions=(permission_acl_edit,), permissions_related='content_object',
view='acls:acl_delete', tags='dangerous', text=_('Delete'), view='acls:acl_delete',
) )
link_acl_list = Link( link_acl_list = Link(
icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'), icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'),
@@ -42,7 +43,7 @@ link_acl_create = Link(
view='acls:acl_create' view='acls:acl_create'
) )
link_acl_permissions = Link( link_acl_permissions = Link(
args='resolved_object.pk', permissions=(permission_acl_edit,), args='resolved_object.pk', icon_class=icon_permission,
permissions_related='content_object', text=_('Permissions'), permissions=(permission_acl_edit,), permissions_related='content_object',
view='acls:acl_permissions', text=_('Permissions'), view='acls:acl_permissions',
) )
+21 -9
View File
@@ -55,7 +55,9 @@ class AccessControlListManager(models.Manager):
except AttributeError: except AttributeError:
# AttributeError means non model objects: ie Statistics # AttributeError means non model objects: ie Statistics
# These can't have ACLs so we raise PermissionDenied # These can't have ACLs so we raise PermissionDenied
raise PermissionDenied(_('Insufficient access for: %s') % obj) raise PermissionDenied(
_('Insufficient access for: %(object)s') % {'object': obj}
)
except KeyError: except KeyError:
pass pass
else: else:
@@ -89,7 +91,7 @@ class AccessControlListManager(models.Manager):
user_roles.append(role) user_roles.append(role)
if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists(): 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( logger.debug(
'Permissions "%s" on "%s" denied for user "%s"', 'Permissions "%s" on "%s" denied for user "%s"',
permissions, obj, user permissions, obj, user
@@ -104,8 +106,8 @@ class AccessControlListManager(models.Manager):
def filter_by_access(self, permission, user, queryset): def filter_by_access(self, permission, user, queryset):
if user.is_superuser or user.is_staff: if user.is_superuser or user.is_staff:
logger.debug( logger.debug(
'Unfiltered queryset returned to user "%s" as superuser or staff', 'Unfiltered queryset returned to user "%s" as superuser '
user 'or staff', user
) )
return queryset return queryset
@@ -138,10 +140,11 @@ class AccessControlListManager(models.Manager):
except TypeError: except TypeError:
# Is not a function, try it as a field # Is not a function, try it as a field
parent_content_type = ContentType.objects.get_for_model( parent_content_type = ContentType.objects.get_for_model(
parent_object model=parent_object
) )
parent_queryset = self.filter( parent_queryset = self.filter(
content_type=parent_content_type, role__in=user_roles, content_type=parent_content_type,
role__in=user_roles,
permissions=permission.stored_permission permissions=permission.stored_permission
) )
parent_acl_query = Q( parent_acl_query = Q(
@@ -159,7 +162,10 @@ class AccessControlListManager(models.Manager):
result = [] result = []
for entry in queryset: for entry in queryset:
try: try:
self.check_access(permissions=permission, user=user, obj=entry) self.check_access(
obj=entry, permissions=permission,
user=user
)
except PermissionDenied: except PermissionDenied:
pass pass
else: else:
@@ -170,7 +176,9 @@ class AccessControlListManager(models.Manager):
parent_acl_query = Q() parent_acl_query = Q()
# Directly granted access # Directly granted access
content_type = ContentType.objects.get_for_model(queryset.model) content_type = ContentType.objects.get_for_model(
model=queryset.model
)
acl_query = Q(pk__in=self.filter( acl_query = Q(pk__in=self.filter(
content_type=content_type, role__in=user_roles, content_type=content_type, role__in=user_roles,
permissions=permission.stored_permission permissions=permission.stored_permission
@@ -194,7 +202,9 @@ class AccessControlListManager(models.Manager):
return StoredPermission.objects.none() return StoredPermission.objects.none()
try: try:
parent_accessor = ModelPermission.get_inheritance(type(instance)) parent_accessor = ModelPermission.get_inheritance(
model=type(instance)
)
except KeyError: except KeyError:
return StoredPermission.objects.none() return StoredPermission.objects.none()
else: else:
@@ -230,6 +240,8 @@ class AccessControlListManager(models.Manager):
acl.permissions.add(permission.stored_permission) acl.permissions.add(permission.stored_permission)
return acl
def revoke(self, permission, role, obj): def revoke(self, permission, role, obj):
content_type = ContentType.objects.get_for_model(model=obj) content_type = ContentType.objects.get_for_model(model=obj)
acl, created = self.get_or_create( acl, created = self.get_or_create(
+7
View File
@@ -5,6 +5,7 @@ import logging
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -64,6 +65,11 @@ class AccessControlList(models.Model):
'role': self.role 'role': self.role
} }
def get_absolute_url(self):
return reverse(
viewname='acls:acl_permissions', kwargs={'acl_pk': self.pk}
)
def get_inherited_permissions(self): def get_inherited_permissions(self):
return AccessControlList.objects.get_inherited_permissions( return AccessControlList.objects.get_inherited_permissions(
role=self.role, obj=self.content_object role=self.role, obj=self.content_object
@@ -78,3 +84,4 @@ class AccessControlList(models.Model):
) )
return result or _('None') return result or _('None')
get_permission_titles.short_description = _('Permissions')
+2 -2
View File
@@ -7,8 +7,8 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Access control lists'), name='acls') namespace = PermissionNamespace(label=_('Access control lists'), name='acls')
permission_acl_edit = namespace.add_permission( permission_acl_edit = namespace.add_permission(
name='acl_edit', label=_('Edit ACLs') label=_('Edit ACLs'), name='acl_edit'
) )
permission_acl_view = namespace.add_permission( permission_acl_view = namespace.add_permission(
name='acl_view', label=_('View ACLs') label=_('View ACLs'), name='acl_view'
) )
+37 -27
View File
@@ -38,18 +38,22 @@ class AccessControlListSerializer(serializers.ModelSerializer):
def get_permissions_url(self, instance): def get_permissions_url(self, instance):
return reverse( return reverse(
'rest_api:accesscontrollist-permission-list', args=( viewname='rest_api:accesscontrollist-permission-list', kwargs={
instance.content_type.app_label, instance.content_type.model, 'app_label': instance.content_type.app_label,
instance.object_id, instance.pk 'model': instance.content_type.model,
), request=self.context['request'], format=self.context['format'] 'object_id': instance.object_id,
'acl_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
) )
def get_url(self, instance): def get_url(self, instance):
return reverse( return reverse(
'rest_api:accesscontrollist-detail', args=( 'rest_api:accesscontrollist-detail', kwargs={
instance.content_type.app_label, instance.content_type.model, 'app_label': instance.content_type.app_label,
instance.object_id, instance.pk 'model': instance.content_type.model,
), request=self.context['request'], format=self.context['format'] 'object_id': instance.object_id,
'acl_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
) )
@@ -65,21 +69,23 @@ class AccessControlListPermissionSerializer(PermissionSerializer):
def get_acl_permission_url(self, instance): def get_acl_permission_url(self, instance):
return reverse( return reverse(
'rest_api:accesscontrollist-permission-detail', args=( 'rest_api:accesscontrollist-permission-detail', kwargs={
self.context['acl'].content_type.app_label, 'app_label': self.context['acl'].content_type.app_label,
self.context['acl'].content_type.model, 'model': self.context['acl'].content_type.model,
self.context['acl'].object_id, self.context['acl'].pk, 'object_id': self.context['acl'].object_id,
instance.stored_permission.pk 'acl_pk': self.context['acl'].pk,
), request=self.context['request'], format=self.context['format'] 'permission_pk': instance.stored_permission.pk
}, request=self.context['request'], format=self.context['format']
) )
def get_acl_url(self, instance): def get_acl_url(self, instance):
return reverse( return reverse(
'rest_api:accesscontrollist-detail', args=( 'rest_api:accesscontrollist-detail', kwargs={
self.context['acl'].content_type.app_label, 'app_label': self.context['acl'].content_type.app_label,
self.context['acl'].content_type.model, 'model': self.context['acl'].content_type.model,
self.context['acl'].object_id, self.context['acl'].pk 'object_id': self.context['acl'].object_id,
), request=self.context['request'], format=self.context['format'] 'acl_pk': self.context['acl'].pk
}, request=self.context['request'], format=self.context['format']
) )
@@ -153,18 +159,22 @@ class WritableAccessControlListSerializer(serializers.ModelSerializer):
def get_permissions_url(self, instance): def get_permissions_url(self, instance):
return reverse( return reverse(
'rest_api:accesscontrollist-permission-list', args=( 'rest_api:accesscontrollist-permission-list', kwargs={
instance.content_type.app_label, instance.content_type.model, 'app_label': instance.content_type.app_label,
instance.object_id, instance.pk 'model': instance.content_type.model,
), request=self.context['request'], format=self.context['format'] 'object_id': instance.object_id,
'acl_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
) )
def get_url(self, instance): def get_url(self, instance):
return reverse( return reverse(
'rest_api:accesscontrollist-detail', args=( 'rest_api:accesscontrollist-detail', kwargs={
instance.content_type.app_label, instance.content_type.model, 'app_label': instance.content_type.app_label,
instance.object_id, instance.pk 'model': instance.content_type.model,
), request=self.context['request'], format=self.context['format'] 'object_id': instance.object_id,
'acl_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
) )
def validate(self, attrs): def validate(self, attrs):
+52 -50
View File
@@ -35,13 +35,12 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
response = self.get( response = self.get(
viewname='rest_api:accesscontrollist-list', viewname='rest_api:accesscontrollist-list',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk 'object_id': self.document.pk
) }
) )
self.assertEqual( self.assertEqual(
response.data['results'][0]['content_type']['app_label'], response.data['results'][0]['content_type']['app_label'],
self.document_content_type.app_label self.document_content_type.app_label
@@ -55,11 +54,12 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
response = self.delete( response = self.delete(
viewname='rest_api:accesscontrollist-detail', viewname='rest_api:accesscontrollist-detail',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk, self.acl.pk 'object_id': self.document.pk,
) 'acl_pk': self.acl.pk
}
) )
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -70,11 +70,12 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
response = self.get( response = self.get(
viewname='rest_api:accesscontrollist-detail', viewname='rest_api:accesscontrollist-detail',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk, self.acl.pk 'object_id': self.document.pk,
) 'acl_pk': self.acl.pk
}
) )
self.assertEqual( self.assertEqual(
response.data['content_type']['app_label'], response.data['content_type']['app_label'],
@@ -90,12 +91,12 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
response = self.delete( response = self.delete(
viewname='rest_api:accesscontrollist-permission-detail', viewname='rest_api:accesscontrollist-permission-detail',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk, self.acl.pk, 'object_id': self.document.pk,
permission.pk 'acl_pk': self.acl.pk, 'permission_pk': permission.pk
) }
) )
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(self.acl.permissions.count(), 0) self.assertEqual(self.acl.permissions.count(), 0)
@@ -106,16 +107,16 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
response = self.get( response = self.get(
viewname='rest_api:accesscontrollist-permission-detail', viewname='rest_api:accesscontrollist-permission-detail',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk, self.acl.pk, 'object_id': self.document.pk, 'acl_pk': self.acl.pk,
permission.pk 'permission_pk': permission.pk
) }
) )
self.assertEqual( self.assertEqual(
response.data['pk'], permission_document_view.pk response.data['permission_pk'], permission_document_view.pk
) )
def test_object_acl_permission_list_view(self): def test_object_acl_permission_list_view(self):
@@ -123,15 +124,16 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
response = self.get( response = self.get(
viewname='rest_api:accesscontrollist-permission-list', viewname='rest_api:accesscontrollist-permission-list',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk, self.acl.pk 'object_id': self.document.pk,
) 'acl_pk': self.acl.pk
}
) )
self.assertEqual( self.assertEqual(
response.data['results'][0]['pk'], response.data['results'][0]['permission_pk'],
permission_document_view.pk permission_document_view.pk
) )
@@ -140,11 +142,11 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
response = self.post( response = self.post(
viewname='rest_api:accesscontrollist-permission-list', viewname='rest_api:accesscontrollist-permission-list',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk, self.acl.pk 'object_id': self.document.pk, 'acl_pk': self.acl.pk
), data={'permission_pk': permission_acl_view.pk} }, data={'permission_pk': permission_acl_view.pk}
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@@ -158,11 +160,11 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_object_acl_post_no_permissions_added_view(self): def test_object_acl_post_no_permissions_added_view(self):
response = self.post( response = self.post(
viewname='rest_api:accesscontrollist-list', viewname='rest_api:accesscontrollist-list',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk 'object_id': self.document.pk
), data={'role_pk': self.role.pk} }, data={'role_pk': self.role.pk}
) )
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@@ -179,11 +181,11 @@ class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_object_acl_post_with_permissions_added_view(self): def test_object_acl_post_with_permissions_added_view(self):
response = self.post( response = self.post(
viewname='rest_api:accesscontrollist-list', viewname='rest_api:accesscontrollist-list',
args=( kwargs={
self.document_content_type.app_label, 'app_label': self.document_content_type.app_label,
self.document_content_type.model, 'model': self.document_content_type.model,
self.document.pk 'object_id': self.document.pk
), data={ }, data={
'role_pk': self.role.pk, 'role_pk': self.role.pk,
'permissions_pk_list': permission_acl_view.pk 'permissions_pk_list': permission_acl_view.pk
+4 -4
View File
@@ -35,7 +35,7 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase):
} }
self.assertEqual( self.assertEqual(
resolved_link.url, reverse('acls:acl_create', kwargs=kwargs) resolved_link.url, reverse(viewname='acls:acl_create', kwargs=kwargs)
) )
def test_document_acl_delete_link(self): def test_document_acl_delete_link(self):
@@ -53,7 +53,7 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase):
self.assertNotEqual(resolved_link, None) self.assertNotEqual(resolved_link, None)
self.assertEqual( self.assertEqual(
resolved_link.url, reverse('acls:acl_delete', args=(acl.pk,)) resolved_link.url, reverse(viewname='acls:acl_delete', kwargs={'acl_pk': acl.pk})
) )
def test_document_acl_edit_link(self): def test_document_acl_edit_link(self):
@@ -71,7 +71,7 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase):
self.assertNotEqual(resolved_link, None) self.assertNotEqual(resolved_link, None)
self.assertEqual( self.assertEqual(
resolved_link.url, reverse('acls:acl_permissions', args=(acl.pk,)) resolved_link.url, reverse(viewname='acls:acl_permissions', kwargs={'acl_pk': acl.pk})
) )
def test_document_acl_list_link(self): def test_document_acl_list_link(self):
@@ -96,5 +96,5 @@ class ACLsLinksTestCase(GenericDocumentViewTestCase):
} }
self.assertEqual( self.assertEqual(
resolved_link.url, reverse('acls:acl_list', kwargs=kwargs) resolved_link.url, reverse(viewname='acls:acl_list', kwargs=kwargs)
) )
+114 -127
View File
@@ -1,191 +1,178 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_text
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.permissions.tests.mixins import RoleTestMixin
from ..models import AccessControlList from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view from ..permissions import permission_acl_edit, permission_acl_view
class AccessControlListViewTestCase(GenericDocumentViewTestCase): class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase):
def setUp(self): def setUp(self):
super(AccessControlListViewTestCase, self).setUp() super(AccessControlListViewTestCase, self).setUp()
self.login_user()
self._create_test_role()
content_type = ContentType.objects.get_for_model(self.document) self.test_object = self.document
self.view_arguments = { content_type = ContentType.objects.get_for_model(self.test_object)
self.view_content_object_arguments = {
'app_label': content_type.app_label, 'app_label': content_type.app_label,
'model': content_type.model, 'model': content_type.model,
'object_id': self.document.pk 'object_id': self.test_object.pk
} }
def test_acl_create_view_no_permission(self): def _request_get_acl_create_view(self):
self.login_user() return self.get(
viewname='acls:acl_create',
response = self.get( kwargs=self.view_content_object_arguments, data={
viewname='acls:acl_create', kwargs=self.view_arguments, data={ 'role': self.test_role.pk
'role': self.role.pk
} }
) )
self.assertEquals(response.status_code, 403) def test_acl_create_view_get_no_permission(self):
response = self._request_get_acl_create_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(AccessControlList.objects.count(), 0) self.assertEqual(AccessControlList.objects.count(), 0)
def test_acl_create_view_with_permission(self): def test_acl_create_view_get_with_document_access(self):
self.login_user() self.grant_access(obj=self.test_object, permission=permission_acl_edit)
self.role.permissions.add( response = self._request_get_acl_create_view()
permission_acl_edit.stored_permission
)
response = self.get(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
)
self.assertContains( self.assertContains(
response, text=self.document.label, status_code=200 response=response, text=force_text(self.test_object),
status_code=200
)
def _request_post_acl_create_view(self):
return self.post(
viewname='acls:acl_create',
kwargs=self.view_content_object_arguments, data={
'role': self.test_role.pk
}
) )
def test_acl_create_view_post_no_permission(self): def test_acl_create_view_post_no_permission(self):
self.login_user() response = self._request_post_acl_create_view()
response = self.post( self.assertEqual(response.status_code, 404)
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}
)
self.assertEquals(response.status_code, 403)
self.assertEqual(AccessControlList.objects.count(), 0) self.assertEqual(AccessControlList.objects.count(), 0)
def test_acl_create_view_with_post_permission(self): def test_acl_create_view_post_with_document_access(self):
self.login_user() self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_post_acl_create_view()
self.role.permissions.add( self.assertEqual(response.status_code, 302)
permission_acl_edit.stored_permission # 2 ACLs: 1 created by the test and the other by the self.grant_access
) self.assertEqual(AccessControlList.objects.count(), 2)
response = self.post(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
)
self.assertContains(response, text='created', status_code=200)
self.assertEqual(AccessControlList.objects.count(), 1)
def test_acl_create_duplicate_view_with_permission(self): def test_acl_create_duplicate_view_with_permission(self):
""" """
Test creating a duplicate ACL entry: same object & role Test creating a duplicate ACL entry: same object & role
Result: Should redirect to existing ACL for object + role combination Result: Should redirect to existing ACL for object + role combination
""" """
acl = AccessControlList.objects.create( self._create_test_acl()
content_object=self.document, role=self.role
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_post_acl_create_view()
self.assertNotContains(
response=response, text=force_text(self.test_acl.role),
status_code=200
) )
self.login_user() self.assertEqual(AccessControlList.objects.count(), 2)
self.assertEqual(
self.role.permissions.add( AccessControlList.objects.first().pk, self.test_acl.pk
permission_acl_edit.stored_permission
) )
response = self.post( def _create_test_acl(self):
viewname='acls:acl_create', kwargs=self.view_arguments, data={ self.test_acl = AccessControlList.objects.create(
'role': self.role.pk content_object=self.test_object, role=self.test_role
}, follow=True
) )
self.assertContains( def _request_acl_delete_view(self):
response, text='vailable permissions', status_code=200 return self.post(
) viewname='acls:acl_delete', kwargs={'acl_pk': self.test_acl.pk}
self.assertEqual(AccessControlList.objects.count(), 1)
self.assertEqual(AccessControlList.objects.first().pk, acl.pk)
def test_orphan_acl_create_view_with_permission(self):
"""
Test creating an ACL entry for an object with no model permissions.
Result: Should display a blank permissions list (not optgroup)
"""
self.login_user()
self.role.permissions.add(
permission_acl_edit.stored_permission
) )
recent_entry = self.document.add_as_recent_document_for_user(self.user) def test_acl_delete_view_no_permission(self):
self._create_test_acl()
content_type = ContentType.objects.get_for_model(recent_entry) response = self._request_acl_delete_view()
self.assertNotContains(
view_arguments = { response=response, text=force_text(self.test_object),
'app_label': content_type.app_label, status_code=404
'model': content_type.model, )
'object_id': recent_entry.pk # 1 ACL: the test one
} self.assertQuerysetEqual(
qs=AccessControlList.objects.all(), values=(repr(self.test_acl),)
response = self.post(
viewname='acls:acl_create', kwargs=view_arguments, data={
'role': self.role.pk
}, follow=True
) )
self.assertNotContains(response, text='optgroup', status_code=200) def test_acl_delete_view_with_access(self):
self.assertEqual(AccessControlList.objects.count(), 1) self._create_test_acl()
acl = 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),)
)
def _request_acl_list_view(self):
return self.get(
viewname='acls:acl_list', kwargs=self.view_content_object_arguments
)
def test_acl_list_view_no_permission(self): def test_acl_list_view_no_permission(self):
self.login_user() response = self._request_acl_list_view()
document = self.document.add_as_recent_document_for_user( self.assertNotContains(
self.user response=response, text=force_text(self.test_object),
).document status_code=404
acl = AccessControlList.objects.create(
content_object=document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission)
content_type = ContentType.objects.get_for_model(document)
view_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': document.pk
}
response = self.get(
viewname='acls:acl_list', kwargs=view_arguments
) )
self.assertNotContains(response, text=document.label, status_code=403) def test_acl_list_view_with_access(self):
self.assertNotContains(response, text='otal: 1', status_code=403) self.grant_access(obj=self.test_object, permission=permission_acl_view)
def test_acl_list_view_with_permission(self): response = self._request_acl_list_view()
self.login_user()
self.role.permissions.add( self.assertContains(
permission_acl_view.stored_permission response=response, text=force_text(self.test_object),
status_code=200
) )
document = self.document.add_as_recent_document_for_user( def _request_get_acl_permissions_view(self):
self.user return self.get(
).document viewname='acls:acl_permissions',
kwargs={'acl_pk': self.test_acl.pk}
acl = AccessControlList.objects.create(
content_object=document, role=self.role
) )
acl.permissions.add(permission_acl_view.stored_permission)
content_type = ContentType.objects.get_for_model(document) def test_acl_permissions_view_get_no_permission(self):
self._create_test_acl()
view_arguments = { response = self._request_get_acl_permissions_view()
'app_label': content_type.app_label, self.assertNotContains(
'model': content_type.model, response=response, text=force_text(self.test_object),
'object_id': document.pk status_code=404
} )
response = self.get( def test_acl_permissions_view_get_with_access(self):
viewname='acls:acl_list', kwargs=view_arguments self._create_test_acl()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_get_acl_permissions_view()
self.assertContains(
response=response, text=force_text(self.test_object),
status_code=200
) )
self.assertContains(response, text=document.label, status_code=200)
+20 -15
View File
@@ -12,35 +12,40 @@ from .views import (
urlpatterns = [ urlpatterns = [
url( url(
r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/create/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/create/$',
ACLCreateView.as_view(), name='acl_create' name='acl_create', view=ACLCreateView.as_view()
), ),
url( url(
r'^(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/list/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/list/$',
ACLListView.as_view(), name='acl_list' name='acl_list', view=ACLListView.as_view()
), ),
url(r'^(?P<pk>\d+)/delete/$', ACLDeleteView.as_view(), name='acl_delete'),
url( url(
r'^(?P<pk>\d+)/permissions/$', ACLPermissionsView.as_view(), regex=r'^acls/(?P<acl_pk>\d+)/delete/$', name='acl_delete',
name='acl_permissions' view=ACLDeleteView.as_view()
),
url(
regex=r'^acls/(?P<acl_pk>\d+)/permissions/$', name='acl_permissions',
view=ACLPermissionsView.as_view()
), ),
] ]
api_urls = [ api_urls = [
url( url(
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/$',
APIObjectACLListView.as_view(), name='accesscontrollist-list' name='accesscontrollist-list', view=APIObjectACLListView.as_view()
), ),
url( url(
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/(?P<acl_pk>\d+)/$',
APIObjectACLView.as_view(), name='accesscontrollist-detail' name='accesscontrollist-detail', view=APIObjectACLView.as_view()
), ),
url( url(
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/(?P<acl_pk>\d+)/permissions/$',
APIObjectACLPermissionListView.as_view(), name='accesscontrollist-permission-list' name='accesscontrollist-permission-list',
view=APIObjectACLPermissionListView.as_view()
), ),
url( url(
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/(?P<permission_pk>\d+)/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/(?P<acl_pk>\d+)/permissions/(?P<permission_pk>\d+)/$',
APIObjectACLPermissionView.as_view(), name='accesscontrollist-permission-detail' name='accesscontrollist-permission-detail',
view=APIObjectACLPermissionView.as_view()
), ),
] ]
+94 -108
View File
@@ -3,22 +3,23 @@ from __future__ import absolute_import, unicode_literals
import itertools import itertools
import logging import logging
from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.mixins import ContentTypeViewMixin, ExternalObjectViewMixin
from mayan.apps.common.views import ( from mayan.apps.common.views import (
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectListView SingleObjectListView
) )
from mayan.apps.permissions import Permission, PermissionNamespace from mayan.apps.permissions import Permission, PermissionNamespace
from mayan.apps.permissions.models import StoredPermission from mayan.apps.permissions.models import Role, StoredPermission
from .classes import ModelPermission from .classes import ModelPermission
from .forms import ACLCreateForm
from .icons import icon_acl_list from .icons import icon_acl_list
from .links import link_acl_create from .links import link_acl_create
from .models import AccessControlList from .models import AccessControlList
@@ -27,76 +28,58 @@ from .permissions import permission_acl_edit, permission_acl_view
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ACLCreateView(SingleObjectCreateView): class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectCreateView):
fields = ('role',) external_object_permission = permission_acl_edit
model = AccessControlList external_object_pk_url_kwarg = 'object_id'
form_class = ACLCreateForm
def dispatch(self, request, *args, **kwargs): def get_error_message_duplicate(self):
self.object_content_type = get_object_or_404( return _(
klass=ContentType, app_label=self.kwargs['app_label'], 'An ACL for "%(object)s" using role "%(role)s" already exists. '
model=self.kwargs['model'] 'Edit that ACL entry instead.'
) ) % {'object': self.get_external_object(), 'role': self.object.role}
try: def get_external_object_queryset(self):
self.content_object = self.object_content_type.get_object_for_this_type( # Here we get a queryset the object model for which an ACL will be
pk=self.kwargs['object_id'] # created.
) return self.get_content_type().model_class().objects.all()
except self.object_content_type.model_class().DoesNotExist:
raise Http404
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user,
obj=self.content_object
)
return super(ACLCreateView, self).dispatch(request, *args, **kwargs)
def get_instance_extra_data(self):
return {
'content_object': self.content_object
}
def form_valid(self, form):
try:
acl = AccessControlList.objects.get(
content_type=self.object_content_type,
object_id=self.content_object.pk,
role=form.cleaned_data['role']
)
except AccessControlList.DoesNotExist:
return super(ACLCreateView, self).form_valid(form)
else:
return HttpResponseRedirect(
reverse('acls:acl_permissions', args=(acl.pk,))
)
def get_extra_context(self): def get_extra_context(self):
return { return {
'object': self.content_object, 'object': self.get_external_object(),
'title': _( 'title': _(
'New access control lists for: %s' 'New access control lists for: %s'
) % self.content_object ) % self.get_external_object()
} }
def get_form_extra_kwargs(self):
return {
'field_name': 'role',
'label': _('Role'),
'queryset': Role.objects.exclude(
pk__in=self.get_external_object().acls.values('role')
),
'widget_attributes': {'class': 'select2'}
}
def get_instance_extra_data(self):
return {
'content_object': self.get_external_object()
}
def get_queryset(self):
self.get_external_object().acls.all()
def get_success_url(self): def get_success_url(self):
if self.object.pk: return self.object.get_absolute_url()
return reverse('acls:acl_permissions', args=(self.object.pk,))
else:
return super(ACLCreateView, self).get_success_url()
class ACLDeleteView(SingleObjectDeleteView): class ACLDeleteView(SingleObjectDeleteView):
object_permission = permission_acl_edit
object_permission_related = 'content_object'
object_permission_raise_404 = True
model = AccessControlList model = AccessControlList
pk_url_kwarg = 'acl_pk'
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(ACLDeleteView, self).dispatch(request, *args, **kwargs)
def get_extra_context(self): def get_extra_context(self):
return { return {
@@ -107,33 +90,22 @@ class ACLDeleteView(SingleObjectDeleteView):
def get_post_action_redirect(self): def get_post_action_redirect(self):
instance = self.get_object() instance = self.get_object()
return reverse( return reverse(
'acls:acl_list', args=( 'acls:acl_list', kwargs={
instance.content_type.app_label, 'app_label': instance.content_type.app_label,
instance.content_type.model, instance.object_id 'model': instance.content_type.model,
) 'object_id': instance.object_id
}
) )
class ACLListView(SingleObjectListView): class ACLListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectListView):
def dispatch(self, request, *args, **kwargs): external_object_permission = permission_acl_view
self.object_content_type = get_object_or_404( external_object_pk_url_kwarg = 'object_id'
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
try: def get_external_object_queryset(self):
self.content_object = self.object_content_type.get_object_for_this_type( # Here we get a queryset the object model for which an ACL will be
pk=self.kwargs['object_id'] # created.
) return self.get_content_type().model_class().objects.all()
except self.object_content_type.model_class().DoesNotExist:
raise Http404
AccessControlList.objects.check_access(
permissions=permission_acl_view, user=request.user,
obj=self.content_object
)
return super(ACLListView, self).dispatch(request, *args, **kwargs)
def get_extra_context(self): def get_extra_context(self):
return { return {
@@ -141,7 +113,9 @@ class ACLListView(SingleObjectListView):
'no_results_icon': icon_acl_list, 'no_results_icon': icon_acl_list,
'no_results_main_link': link_acl_create.resolve( 'no_results_main_link': link_acl_create.resolve(
context=RequestContext( context=RequestContext(
self.request, {'resolved_object': self.content_object} self.request, {
'resolved_object': self.get_external_object()
}
) )
), ),
'no_results_title': _( 'no_results_title': _(
@@ -149,17 +123,18 @@ class ACLListView(SingleObjectListView):
), ),
'no_results_text': _( 'no_results_text': _(
'ACL stands for Access Control List and is a precise method ' 'ACL stands for Access Control List and is a precise method '
' to control user access to objects in the system.' ' to control user access to objects in the system. ACLs '
'allow granting a permission to a role but only for a '
'specific object or set of objects.'
),
'object': self.get_external_object(),
'title': _(
'Access control lists for: %s' % self.get_external_object()
), ),
'object': self.content_object,
'title': _('Access control lists for: %s' % self.content_object),
} }
def get_object_list(self): def get_object_list(self):
return AccessControlList.objects.filter( return self.get_external_object().acls.all()
content_type=self.object_content_type,
object_id=self.content_object.pk
)
class ACLPermissionsView(AssignRemoveView): class ACLPermissionsView(AssignRemoveView):
@@ -192,18 +167,6 @@ class ACLPermissionsView(AssignRemoveView):
permission = get_object_or_404(klass=StoredPermission, pk=item) permission = get_object_or_404(klass=StoredPermission, pk=item)
self.get_object().permissions.add(permission) 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): def get_available_list(self):
return ModelPermission.get_for_instance( return ModelPermission.get_for_instance(
instance=self.get_object().content_object instance=self.get_object().content_object
@@ -235,22 +198,45 @@ class ACLPermissionsView(AssignRemoveView):
def get_granted_list(self): def get_granted_list(self):
""" """
Merge or permissions we hold for this object and the permissions we 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 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) 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) return StoredPermission.objects.filter(pk__in=merged_pks)
def get_object(self): def get_object(self):
return get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk']) 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): def get_right_list_help_text(self):
if self.get_object().get_inherited_permissions(): if self.get_object().get_inherited_permissions():
return _( return _(
'Disabled permissions are inherited from a parent object.' '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 None return self.right_list_help_text
def left_list(self): def left_list(self):
Permission.refresh() Permission.refresh()
+3 -1
View File
@@ -98,7 +98,9 @@ class GrantAccessAction(WorkflowAction):
def get_form_schema(self, *args, **kwargs): def get_form_schema(self, *args, **kwargs):
self.fields['content_type']['kwargs']['queryset'] = ModelPermission.get_classes(as_content_type=True) self.fields['content_type']['kwargs']['queryset'] = ModelPermission.get_classes(as_content_type=True)
self.fields['permissions']['kwargs']['choices'] = Permission.all(as_choices=True) self.fields['permissions']['kwargs']['choices'] = Permission.all(
as_choices=True
)
return super(GrantAccessAction, self).get_form_schema(*args, **kwargs) return super(GrantAccessAction, self).get_form_schema(*args, **kwargs)
def get_execute_data(self): def get_execute_data(self):