diff --git a/apps/acls/__init__.py b/apps/acls/__init__.py new file mode 100644 index 0000000000..81de093b2b --- /dev/null +++ b/apps/acls/__init__.py @@ -0,0 +1,21 @@ +from django.utils.translation import ugettext_lazy as _ + +#from documents.models import Document +from navigation.api import register_links, register_multi_item_links +from project_setup.api import register_setup + +from permissions.api import register_permission, set_namespace_title + + +ACLS_EDIT_ACL = {'namespace': 'acls', 'name': 'acl_edit', 'label': _(u'Edit ACLs')} +ACLS_VIEW_ACL = {'namespace': 'acls', 'name': 'acl_view', 'label': _(u'View ACLs')} + +set_namespace_title('acls', _(u'Access control lists')) +register_permission(ACLS_EDIT_ACL) +register_permission(ACLS_VIEW_ACL) + +acl_list = {'text': _(u'ACLs'), 'view': 'acl_list', 'famfam': 'lock', 'permissions': [ACLS_VIEW_ACL]} + + + +#register_links(Document, [acl_list], menu_name='form_header') diff --git a/apps/acls/admin.py b/apps/acls/admin.py new file mode 100644 index 0000000000..630f1d4afe --- /dev/null +++ b/apps/acls/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin + +from acls.models import AccessEntry + +from django.contrib.contenttypes import generic +from django.contrib.contenttypes.models import ContentType + +#class PermissionHolderInline(admin.StackedInline): +# model = PermissionHolder +# extra = 1 +# classes = ('collapse-open',) +# allow_add = True# +# +class AccessEntryAdmin(admin.ModelAdmin): + related_lookup_fields = { + 'generic': [['holder_type', 'holder_id'], ['content_type', 'object_id']], + } + #inlines = [PermissionHolderInline] + list_display = ('pk', 'holder_object', 'permission', 'content_object') + list_display_links = ('pk',) + model = AccessEntry + +admin.site.register(AccessEntry, AccessEntryAdmin) diff --git a/apps/acls/api.py b/apps/acls/api.py new file mode 100644 index 0000000000..13efbbed28 --- /dev/null +++ b/apps/acls/api.py @@ -0,0 +1,62 @@ +try: + from psycopg2 import OperationalError +except ImportError: + class OperationalError(Exception): + pass + +from django.core.exceptions import ImproperlyConfigured +from django.db import transaction +from django.db.utils import DatabaseError +from django.shortcuts import get_object_or_404 +from django.utils.translation import ugettext +from django.core.exceptions import PermissionDenied +from django.utils.translation import ugettext_lazy as _ + +from permissions import PERMISSION_ROLE_VIEW, PERMISSION_ROLE_EDIT, \ + PERMISSION_ROLE_CREATE, PERMISSION_ROLE_DELETE, \ + PERMISSION_PERMISSION_GRANT, PERMISSION_PERMISSION_REVOKE + +from permissions.models import Permission + +namespace_titles = { + 'permissions': _(u'Permissions') +} + + +def set_namespace_title(namespace, title): + namespace_titles.setdefault(namespace, title) + + +@transaction.commit_manually +def register_permission(permission): + try: + permission_obj, created = Permission.objects.get_or_create( + namespace=permission['namespace'], name=permission['name']) + permission_obj.label = unicode(permission['label']) + permission_obj.save() + except DatabaseError: + transaction.rollback() + # Special case for ./manage.py syncdb + except (OperationalError, ImproperlyConfigured): + transaction.rollback() + # Special for DjangoZoom, which executes collectstatic media + # doing syncdb and creating the database tables + else: + transaction.commit() + + +def check_permissions(requester, permission_list): + for permission_item in permission_list: + permission = get_object_or_404(Permission, + namespace=permission_item['namespace'], name=permission_item['name']) + if permission.has_permission(requester): + return True + + raise PermissionDenied(ugettext(u'Insufficient permissions.')) + +register_permission(PERMISSION_ROLE_VIEW) +register_permission(PERMISSION_ROLE_EDIT) +register_permission(PERMISSION_ROLE_CREATE) +register_permission(PERMISSION_ROLE_DELETE) +register_permission(PERMISSION_PERMISSION_GRANT) +register_permission(PERMISSION_PERMISSION_REVOKE) diff --git a/apps/acls/models.py b/apps/acls/models.py new file mode 100644 index 0000000000..82d921cc9e --- /dev/null +++ b/apps/acls/models.py @@ -0,0 +1,84 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.contrib.auth.models import User +#from django.shortcuts import get_object_or_404 +#from django.utils.translation import ugettext +from django.core.exceptions import PermissionDenied +from django.core.urlresolvers import reverse + + +from permissions.models import Permission + + +class AccessEntryManager(models.Manager): + def grant(self, permission, requester, obj): + """ + Grant a permission (what), (to) a requester, (on) a specific object + """ + access_entry, created = AccessEntry.objects.get_or_create( + permission=permission, + holder_type=ContentType.objects.get_for_model(requester), + holder_id=requester.pk, + content_type=ContentType.objects.get_for_model(obj), + object_id=obj.pk + ) + return created + + def revoke(self, permission, holder, obj): + try: + access_entry = AccessEntry.objects.get( + permission=permission, + holder_type=ContentType.objects.get_for_model(holder), + holder_id=holder.pk, + content_type=ContentType.objects.get_for_model(obj), + object_id=obj.pk + ) + access_entry.delete() + return True + except AccessEntry.DoesNotExist: + return False + + def check_accesses(self, permission_list, requester, obj): + for permission_item in permission_list: + permission = get_object_or_404(Permission, + namespace=permission_item['namespace'], name=permission_item['name']) + try: + access_entry = AccessEntry.objects.get( + permission=permission, + holder_type=ContentType.objects.get_for_model(requester), + holder_id=requester.pk, + content_type=ContentType.objects.get_for_model(obj), + object_id=obj.pk + ) + return True + except AccessEntry.DoesNotExist: + raise PermissionDenied(ugettext(u'Insufficient permissions.')) + + def get_acl_url(self, obj): + content_type = ContentType.objects.get_for_model(obj.__class__) + return reverse('acl_list', args=[content_type.app_label, content_type.model, obj.pk]) + + +class AccessEntry(models.Model): + permission = models.ForeignKey(Permission, verbose_name=_(u'permission')) + holder_type = models.ForeignKey(ContentType, + related_name='access_holder', + limit_choices_to={'model__in': ('user', 'group', 'role')}) + holder_id = models.PositiveIntegerField() + holder_object = generic.GenericForeignKey(ct_field='holder_type', fk_field='holder_id') + + content_type = models.ForeignKey(ContentType, + related_name='object_content_type') + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey(ct_field='content_type', fk_field='object_id') + + objects = AccessEntryManager() + + class Meta: + verbose_name = _(u'access entry') + verbose_name_plural = _(u'access entries') + + def __unicode__(self): + return u'%s: %s' % (self.content_type, self.content_object) diff --git a/apps/acls/tests.py b/apps/acls/tests.py new file mode 100644 index 0000000000..501deb776c --- /dev/null +++ b/apps/acls/tests.py @@ -0,0 +1,16 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +from django.test import TestCase + + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) diff --git a/apps/acls/urls.py b/apps/acls/urls.py new file mode 100644 index 0000000000..00baddf5ea --- /dev/null +++ b/apps/acls/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('acls.views', + url(r'^list_for/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/$', 'acl_list', (), 'acl_list'), + +# url(r'^role/list/$', 'role_list', (), 'role_list'), +# url(r'^role/create/$', 'role_create', (), 'role_create'), +# url(r'^role/(?P\d+)/permissions/$', 'role_permissions', (), 'role_permissions'), +# url(r'^role/(?P\d+)/edit/$', 'role_edit', (), 'role_edit'), +# url(r'^role/(?P\d+)/delete/$', 'role_delete', (), 'role_delete'), +# url(r'^role/(?P\d+)/members/$', 'role_members', (), 'role_members'), +# +# url(r'^permissions/multiple/grant/$', 'permission_grant', (), 'permission_multiple_grant'), +# url(r'^permissions/multiple/revoke/$', 'permission_revoke', (), 'permission_multiple_revoke'), +) diff --git a/apps/acls/views.py b/apps/acls/views.py new file mode 100644 index 0000000000..be8a154646 --- /dev/null +++ b/apps/acls/views.py @@ -0,0 +1,39 @@ +from django.utils.translation import ugettext_lazy as _ +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response, get_object_or_404 +from django.template import RequestContext +from django.contrib import messages +from django.views.generic.list_detail import object_list +from django.core.urlresolvers import reverse +from django.views.generic.create_update import create_object, delete_object, update_object +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import User, Group + +from permissions.api import check_permissions, namespace_titles + +from acls import ACLS_EDIT_ACL, ACLS_VIEW_ACL + + +def acl_list_for(request, obj, extra_context=None): + check_permissions(request.user, [ACLS_VIEW_ACL]) + + context = { + 'object_list': [], + 'title': _(u'access control lists for: %s' % obj), + #'multi_select_as_buttons': True, + #'hide_links': True, + } + + if extra_context: + context.update(extra_context) + + return render_to_response('generic_list.html', context, + context_instance=RequestContext(request)) + + +def acl_list(request, app_label, model_name, object_id): + ct = get_object_or_404(ContentType, app_label=app_label, model=model_name) + obj = get_object_or_404(ContentType.user_type.get_object_for_this_type, pk=object_id) + return acl_list_for(request, obj) + +