diff --git a/.hgignore b/.hgignore index f4be7f0..7bc71b8 100644 --- a/.hgignore +++ b/.hgignore @@ -20,3 +20,4 @@ downloads/* .installed.cfg bin/* develop-eggs/*.egg-link +src/authority/_models/* \ No newline at end of file diff --git a/src/authority/__init__.py b/src/authority/__init__.py new file mode 100644 index 0000000..824442b --- /dev/null +++ b/src/authority/__init__.py @@ -0,0 +1,31 @@ +from inspect import isfunction, getmembers +from django.utils.importlib import import_module +from django.core.exceptions import ImproperlyConfigured + +LOADING = False + +def autodiscover(): + """ + Goes and imports the permissions submodule of every app in INSTALLED_APPS + to make sure the permission set classes are registered correctly. + """ + global LOADING + if LOADING: + return + LOADING = True + + import imp + from django.conf import settings + + for app in settings.INSTALLED_APPS: + print "checking %s" % app + try: + app_path = import_module(app).__path__ + except AttributeError: + continue + try: + imp.find_module('permissions', app_path) + except ImportError: + continue + import_module("%s.permissions" % app) + LOADING = False diff --git a/src/authority/admin.py b/src/authority/admin.py new file mode 100644 index 0000000..0872cdc --- /dev/null +++ b/src/authority/admin.py @@ -0,0 +1,32 @@ +from django.contrib.admin import site, ModelAdmin +from django.contrib.contenttypes import generic +from authority.models import Permission + +class PermissionInline(generic.GenericTabularInline): + model = Permission + extra = 1 + #exclude = ('creator',) + +class PermissionAdmin(ModelAdmin): + model = Permission + list_display = ('codename', 'content_type', 'user', 'group') + list_filter = ('codename', 'content_type') + search_fields = ['object_id', 'content_type', 'user', 'group'] + raw_id_fields = ['user', 'group'] + fieldsets = ( + (None, { + 'fields': ('codename', ('content_type', 'object_id')) + }), + ('granted for', { + 'fields': ('user', 'group', 'creator') + }), + ) + + def queryset(self, request): + user = request.user + if user.is_superuser or \ + user.has_perm('permissions.change_foreign_permissions'): + return super(PermissionAdmin, self).queryset(request) + return super(PermissionAdmin, self).queryset(request).filter(creator=user) + +site.register(Permission, PermissionAdmin) diff --git a/src/authority/forms.py b/src/authority/forms.py new file mode 100644 index 0000000..d3ba843 --- /dev/null +++ b/src/authority/forms.py @@ -0,0 +1,94 @@ +from django import forms +from django.forms.models import ModelForm +from django.utils.translation import ugettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import User, Group + +from authority.models import Permission +from authority.permissions import BasePermission + +class BasePermissionForm(ModelForm): + + class Meta: + model = Permission + + def __init__(self, perm=None, obj=None, *args, **kwargs): + self.perm = perm + self.obj = obj + super(BasePermissionForm, self).__init__(*args, **kwargs) + + def save(self, request, commit=True, *args, **kwargs): + self.instance.creator = request.user + self.instance.content_type = ContentType.objects.get_for_model(self.obj) + self.instance.object_id = self.obj.id + self.instance.codename = self.perm + return super(BasePermissionForm, self).save(commit) + +class UserPermissionForm(BasePermissionForm): + codename = forms.CharField(widget=forms.HiddenInput()) + user = forms.CharField(label=_('User')) + + class Meta(BasePermissionForm.Meta): + fields = ('user',) + + def clean_user(self): + username = self.cleaned_data["user"] + try: + user = User.objects.get(username__iexact=username) + except User.DoesNotExist: + raise forms.ValidationError( + _("A user with that username does not exist.")) + check = BasePermission(user) + if check.has_perm(self.perm, self.obj): + raise forms.ValidationError( + _("This user already has permission '%(perm)s' on %(obj)s") % { + 'perm': self.perm, + 'obj': self.obj + }) + return user + +class GroupPermissionForm(BasePermissionForm): + name = forms.CharField(label=_('Group')) + + class Meta(BasePermissionForm.Meta): + fields = ('group',) + + def clean_group(self): + name = self.cleaned_data["name"] + try: + group = Group.objects.get(name__iexact=name) + except Group.DoesNotExist: + raise forms.ValidationError( + _("A group with that name does not exist.")) + self.instance.group = group + return name + + def save(self, request, commit=True): + group=self.cleaned_data.get("group", None) + check = BasePermission(group=group) + if check.has_perm(self.perm, self.obj): + raise forms.ValidationError( + _("This group already has permission '%(perm)s' on %(obj)s") % { + 'perm': self.perm, + 'obj': self.obj, + }) + self.instance.group = group + return super(GroupPermissionForm, self).save(request, self.obj, commit) + + # def del_row_perm(self, instance, perm, check_groups=False, + # fail_silently=False): + # """ + # Remove granular permission perm from user on an object instance + # """ + # if not self.has_row_perm(instance, perm, not check_groups): + # if not fail_silently: + # raise DoesNotHavePermission(self, perm, instance) + # else: + # return + # content_type = ContentType.objects.get_for_model(instance) + # objects = Permission.objects.filter(user=self, + # content_type__pk=content_type.id, + # object_id=instance.id, name=perm) + # objects.delete() + # + # diff --git a/src/authority/models.py b/src/authority/models.py new file mode 100644 index 0000000..711e8f8 --- /dev/null +++ b/src/authority/models.py @@ -0,0 +1,76 @@ +from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.contrib.auth.models import User, Group +from django.utils.translation import ugettext_lazy as _ + +class AlreadyHasPermission(Exception): + """Defining exception for an already existing permission""" + + def __init__(self, user_or_group, name, obj=None): + self.user_or_group = user_or_group + self.perm_name = name + self.obj = obj + + def __str__(self): + if self.obj: + return "%s has already permission:\"%s\" on %s" % (self.user_or_group, + self.perm_name, + self.obj) + return "%s has already permission:\"%s\"" % (self.user_or_group, + self.perm_name) + +class DoesNotHavePermission(Exception): + """Defining exception for an already existing permission""" + + def __init__(self, user_or_group, name, obj=None): + self.user_or_group = user_or_group + self.perm_name = name + self.obj = obj + + def __str__(self): + if self.obj: + return "%s has not permission:\"%s\" on %s" % (self.user_or_group, + self.perm_name, + self.obj) + return "%s has not permission:\"%s\" " % (self.user_or_group, + self.perm_name) + +class PermissionManager(models.Manager): + + def permissions_for_object(self, obj): + object_type = ContentType.objects.get_for_model(obj) + return self.filter(content_type__pk=object_type.id, + object_id=obj.id) + + +class Permission(models.Model): + """ + A granular permission model, per-object permission in other words. + This kind of permission is associated with a user/group and an object + of any content type. + """ + codename = models.CharField(_('codename'), max_length=100) + content_type = models.ForeignKey(ContentType, related_name="row_permissions") + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + user = models.ForeignKey(User, null=True, blank=True, related_name='granted_permissions') + group = models.ForeignKey(Group, null=True, blank=True) + creator = models.ForeignKey(User, null=True, blank=True, related_name='created_permissions') + + objects = PermissionManager() + + def __unicode__(self): + return "%s.%s" % (self.content_type.app_label, self.codename) + + class Meta: + verbose_name = _('permission') + verbose_name_plural = _('permissions') + permissions = ( + ('change_foreign_permissions', 'Can change foreign permissions'), + ('delete_foreign_permissions', 'Can delete foreign permissions'), + ) + +from authority import autodiscover +autodiscover() diff --git a/src/authority/permissions.py b/src/authority/permissions.py new file mode 100644 index 0000000..21abd55 --- /dev/null +++ b/src/authority/permissions.py @@ -0,0 +1,171 @@ +from inspect import isfunction, getmembers +from django.utils.importlib import import_module +from django.core.exceptions import ImproperlyConfigured +from django.db.models import Q +from django.db.models.base import ModelBase +from django.contrib.contenttypes.models import ContentType +from authority.models import Permission + +registry = {} + +class AlreadyRegistered(Exception): + pass + +class NotRegistered(Exception): + pass + +class PermissionMetaclass(type): + """ + Used to generate the default set of permission checks "add", "change" and + "delete". + """ + def __new__(cls, name, bases, attrs): + new_class = super( + PermissionMetaclass, cls).__new__(cls, name, bases, attrs) + if new_class.__name__ == "BasePermission": + return new_class + if not new_class.model: + raise ImproperlyConfigured( + "Permission %s requires a model attribute." % new_class) + if not new_class.label: + new_class.label = "%s_permission" % new_class.__name__.lower() + if new_class in registry: + raise AlreadyRegistered( + "The permission %s is already registered" % new_class) + if new_class.label in registry.values(): + raise ImproperlyConfigured( + "The name of %s conflicts with %s" % \ + (new_class, registry[new_class.label])) + # registry[PollPermission] = Poll + registry[new_class] = new_class.label + if new_class.checks is None: + new_class.checks = () + # automatically add following default checks and the other + all_checks = ['add', 'change', 'delete'] + list(new_class.checks) + for check_name in all_checks: + def func(self, obj=None): + if obj is None: + obj = self.model + # first check Django's permission system + perm = '%s.%s_%s' % (obj._meta.app_label, # polls.add_poll + check_name.lower(), + obj._meta.object_name.lower()) + perms = self.has_perm(perm) + if obj is not None and not isinstance(obj, ModelBase): + # only check the authority if not model instance + return (perms or + self.can_admin(obj) or + self.has_perm(perm, obj)) + return perms + setattr(new_class, 'can_%s' % check_name.lower(), func) + return new_class + +def get_permission_by_label(label): + for perm_cls, perm_label in registry.items(): + if perm_label == label: + return perm_cls + return None + +class BasePermission(object): + """ + Base Permission class to be used to define app permissions. + + check = BasePermission(request.user) + """ + __metaclass__ = PermissionMetaclass + + checks = () + label = None + model = None + + def __init__(self, user=None, group=None, *args, **kwargs): + self.user = user + self.group = group + super(BasePermission, self).__init__(*args, **kwargs) + + @classmethod + def signature(cls): + """ + Used to determine the name of the permission class which then can be + used in the template tag to check. + + Format: . + e.g. "polls.poll" + """ + return '%s.%s' % (cls.model._meta.app_label, + cls.model._meta.object_name.lower()) + + def has_user_perms(self, perm, obj, check_groups=True): + if self.user: + if self.user.is_superuser: + return True + if not self.user.is_active: + return False + # check if a Permission object exists for the given params + print obj + content_type = ContentType.objects.get_for_model(obj) + perms = Permission.objects.filter(user=self.user, codename=perm, + content_type=content_type, + object_id=obj.id) + if perms: + return True + if check_groups: + # look if one of the user's group have the permissions + for group in self.user.groups.all(): + if self.has_group_perms(perm, obj): + return True + return False + + def has_group_perms(self, perm, obj): + """ + Check if group has the permission for the given object + """ + perms = self.get_group_perms(perm, obj).filter(object_id=obj.id) + if perms: + return True + return False + + def has_perm(self, perm, obj, check_groups=True): + """ + Check if user has the permission for the given object + """ + if self.user: + if self.has_user_perms(perm, obj, check_groups): + return True + if self.group: + return self.has_group_perms(perm, obj) + return False + + def get_perms(self, obj): + content_type = ContentType.objects.get_for_model(obj) + perms = Permission.objects.filter( + Q(user=self.user) | Q( + group__in=self.user.groups.all() + ), content_type=content_type) + return perms + + def get_user_perms(self, perm, obj): + """ + Get objects that User perm permission on + """ + perms = self.get_perms(obj) + return perms.filter(codename=perm) + + def get_group_perms(self, perm, obj): + """ + Get objects that Group perm permission on + """ + content_type = ContentType.objects.get_for_model(obj) + perms = Permission.objects.filter(group=self.group, codename=perm, + content_type=content_type) + return perms + + def clean_perms(self, obj): + """ + Delete permissions related to an object instance + """ + perms = self.get_perms(obj) + perms.delete() + + def can_admin(self, obj): + return self.has_perm('%s.admin' % obj._meta.app_label, obj) diff --git a/src/authority/templates/authority/permission_delete_link.html b/src/authority/templates/authority/permission_delete_link.html new file mode 100644 index 0000000..baec867 --- /dev/null +++ b/src/authority/templates/authority/permission_delete_link.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% if delete_url %}{% trans "Revoke permission" %}{% endif %} \ No newline at end of file diff --git a/src/authority/templates/authority/permission_form.html b/src/authority/templates/authority/permission_form.html new file mode 100644 index 0000000..6d49e75 --- /dev/null +++ b/src/authority/templates/authority/permission_form.html @@ -0,0 +1,10 @@ +{% load i18n %} +{% if form %} +
+ + {{ form.as_p }} +

+ +

+
+{% endif %} diff --git a/src/authority/templatetags/__init__.py b/src/authority/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/authority/templatetags/permissions_tags.py b/src/authority/templatetags/permissions_tags.py new file mode 100644 index 0000000..e1d7974 --- /dev/null +++ b/src/authority/templatetags/permissions_tags.py @@ -0,0 +1,174 @@ +from django import template +from django.db.models import get_app +from django.utils.importlib import import_module +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse +from django.template import Library, Node, Variable + +from authority import permissions +from authority.views import add_url_for_obj +from authority.models import Permission +from authority.forms import UserPermissionForm + +register = template.Library() + +class ComparisonNode(template.Node): + """ + Implements a node to provide an "if user/group has permission on object" + """ + def __init__(self, user, permission, nodelist_true, nodelist_false, *objs): + self.user = user + self.objs = objs + # poll_permission.can_change + self.perm_label, self.check_name = permission.strip('"').split('.') + self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false + + def render(self, context): + try: + user = template.Variable(self.user).resolve(context) + if self.objs: + objs = [] + for obj in self.objs: + if obj is not None: + objs.append( + template.Variable(obj).resolve(context)) + else: + objs = None + # get permission set first + perm_cls = permissions.get_permission_by_label(self.perm_label) + if perm_cls is not None: + # create a permission instance + perm_instance = perm_cls(user) + # and try to find the correct check method + check = getattr(perm_instance, self.check_name, None) + if check is not None: + # check + if check(*objs): + # profit + return self.nodelist_true.render(context) + # If the app couldn't be found + except (ImproperlyConfigured, ImportError): + return '' + # If either variable fails to resolve, return nothing. + except template.VariableDoesNotExist: + return '' + # If the types don't permit comparison, return nothing. + except (TypeError, AttributeError): + return '' + return self.nodelist_false.render(context) + +@register.tag('ifhasperm') +def do_if_has_perm(parser, token): + """ + This function provides funcitonality for the 'ifhasperm' template tag + + {% ifhasperm [permission_label].[check_name] [user] [*objs] %} + lalala + {% else %} + meh + {% endifhasperm %} + + {% if hasperm poll_permission.can_change request.user %} + lalala + {% else %} + meh + {% endifhasperm %} + """ + bits = token.contents.split() + if 5 < len(bits) < 3: + raise template.TemplateSyntaxError("'%s' tag takes three,\ + four or five arguments" % bits[0]) + end_tag = 'endifhasperm' + nodelist_true = parser.parse(('else', end_tag)) + token = parser.next_token() + if token.contents == 'else': # there is an 'else' clause in the tag + nodelist_false = parser.parse((end_tag,)) + parser.delete_first_token() + else: + nodelist_false = template.NodeList() + + if len(bits) == 3: # this tag requires at most 2 objects . None is given + objs = (None, None) + elif len(bits) == 4:# one is given + objs = (bits[3], None) + else: #two are given + objs = (bits[3], bits[4]) + return ComparisonNode(bits[2], bits[1], nodelist_true, nodelist_false, *objs) + +@register.inclusion_tag('authority/permission_delete_link.html', takes_context=True) +def permission_delete_link(context, perm): + """ + Renders a html link to the delete view of the given permission. Returns + no content if the request-user has no permission to delete foreign + permissions. + """ + if context['request'].user.has_perm('delete_foreign_permissions') \ + or context['request'].user == perm.creator: + return { + 'next': context['request'].build_absolute_uri(), + 'delete_url': reverse('authority-delete-permission', kwargs={ + 'permission_pk': perm.pk}) + } + return {'delete_url': None} + +@register.inclusion_tag('authority/permission_form.html', takes_context=True) +def permission_form(context, obj, perm): + """ + Renders an "add permissions" form + + {% permission_form [obj] add_lesson %} + {% permission_form lesson add_lesson %} + """ + if context['request'].user.is_authenticated(): + return { + 'form': UserPermissionForm(perm, initial={'codename': perm}), + 'form_url': add_url_for_obj(obj), + 'next': context['request'].build_absolute_uri(), + } + return {'form': None} + +class PermissionForObjectNode(Node): + def __init__(self, obj, var_name): + self.obj = obj + self.var_name = var_name + + def resolve(self, var, context): + """Resolves a variable out of context if it's not in quotes""" + if var[0] in ('"', "'") and var[-1] == var[0]: + return var[1:-1] + else: + return Variable(var).resolve(context) + + def render(self, context): + obj = self.resolve(self.obj, context) + var_name = self.resolve(self.var_name, context) + #check = permissions.BasePermission(user=user) + perms = Permission.objects.permissions_for_object(obj) + context[var_name] = perms + return '' + +@register.tag +def get_permissions_for(parser, token): + """ + Syntax:: + + {% get_permissions_for obj %} + {% for perm in permissions %} + {{ perm }} + {% endfor %} + + {% get_permissions_for obj as "my_permissions" %} + + """ + def next_bit_for(bits, key, if_none=None): + try: + return bits[bits.index(key)+1] + except ValueError: + return if_none + + bits = token.contents.split() + kwargs = { + 'obj': next_bit_for(bits, 'get_permissions_for'), + 'var_name': next_bit_for(bits, 'as', '"permissions"'), + } + return PermissionForObjectNode(**kwargs) diff --git a/src/authority/urls.py b/src/authority/urls.py new file mode 100644 index 0000000..5a69132 --- /dev/null +++ b/src/authority/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import * +from authority.views import add_permission, delete_permission + +urlpatterns = patterns('', + url(r'^add/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', add_permission, name="authority-add-permission"), + url(r'^delete/(?P\d+)/$', delete_permission, name="authority-delete-permission"), +) diff --git a/src/authority/views.py b/src/authority/views.py new file mode 100644 index 0000000..ed709f7 --- /dev/null +++ b/src/authority/views.py @@ -0,0 +1,59 @@ +from django.shortcuts import render_to_response, get_object_or_404 +from django.views.decorators.http import require_POST +from django.http import HttpResponseRedirect, HttpResponseForbidden +from django.db.models.loading import get_model +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext, ugettext_lazy as _ +from django.template.context import RequestContext +from django.contrib.auth.decorators import login_required + +from authority.models import Permission +from authority.forms import UserPermissionForm + +def add_url_for_obj(obj): + return reverse('authority-add-permission', + kwargs={'app_label': obj._meta.app_label, + 'module_name': obj._meta.module_name, + 'pk': obj.pk}) + +@require_POST +@login_required +def add_permission(request, app_label, module_name, pk, extra_context={}, + template_name='authority/permission_form.html'): + + next = request.POST.get('next', '/') + codename = request.POST.get('codename', None) + if codename is None: + return HttpResponseForbidden(next) + model = get_model(app_label, module_name) + if model is None: + return HttpResponseRedirect(next) + obj = get_object_or_404(model, pk=pk) + form = UserPermissionForm(data=request.POST, obj=obj, + perm=codename, initial={'codename': codename}) + if form.is_valid(): + form.save(request) + request.user.message_set.create( + message=ugettext('You added a permission.')) + return HttpResponseRedirect(next) + else: + context = { + 'form': form, + 'form_url': add_url_for_obj(obj), + 'next': next, + 'perm': codename, + } + context.update(extra_context) + return render_to_response(template_name, context, + context_instance=RequestContext(request)) + +@login_required +def delete_permission(request, permission_pk): + permission = get_object_or_404(Permission, pk=permission_pk) + if request.user.has_perm('delete_foreign_permissions') \ + or request.user == g.creator: + permission.delete() + request.user.message_set.create( + message=ugettext('You removed the permission.')) + next = request.REQUEST.get('next') or '/' + return HttpResponseRedirect(next)