diff --git a/example/templates/flatpages/default.html b/example/templates/flatpages/default.html index 0eafa3b..5276975 100644 --- a/example/templates/flatpages/default.html +++ b/example/templates/flatpages/default.html @@ -35,6 +35,17 @@ +
  • The permissions requested for this flatpage: + + {% get_permission_requests flatpage as "all_perm_requests" %} + + +
  • +
  • Permission form for adding a specific permission "add_flatpage" {% permission_form flatpage "flatpage_permission.add_flatpage" %}
  • @@ -47,6 +58,13 @@ {% add_url_for_obj flatpage %} +
  • Request a kind of access: +{% permission_request_form flatpage %} +
  • + +
  • Permission requuest form for adding a specific permission "add_flatpage" +{% permission_request_form flatpage "flatpage_permission.add_flatpage" %} +
  • Detailed tests

    diff --git a/src/authority/forms.py b/src/authority/forms.py index f756cb4..870dfda 100644 --- a/src/authority/forms.py +++ b/src/authority/forms.py @@ -2,6 +2,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User, Group +from django.forms.util import ErrorList from authority import permissions, get_choices_for from authority.models import Permission @@ -12,49 +13,76 @@ class BasePermissionForm(forms.ModelForm): class Meta: model = Permission - def __init__(self, perm=None, obj=None, *args, **kwargs): + def __init__(self, perm=None, obj=None, approved=False, *args, **kwargs): self.perm = perm self.obj = obj + self.approved = approved + if not self.approved: + self.base_fields['user'].widget = forms.HiddenInput() + else: + self.base_fields['user'].widget = forms.TextInput() if obj and perm: self.base_fields['codename'].widget = forms.HiddenInput() elif obj and not perm: perm_choices = get_choices_for(self.obj) - self.base_fields['codename'].widget = forms.Select(choices=perm_choices) + self.base_fields['codename'].widget = forms.Select( + choices=perm_choices) super(BasePermissionForm, self).__init__(*args, **kwargs) def save(self, request, commit=True, *args, **kwargs): + if not self.approved: + self.instance.user = request.user 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 + self.instance.approved = self.approved return super(BasePermissionForm, self).save(commit) + class UserPermissionForm(BasePermissionForm): 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 = permissions.BasePermission(user=user) - if check.has_perm(self.perm, self.obj): + def clean(self): + cleaned_data = self.cleaned_data + user = self.cleaned_data.get("user", None) + if user: + try: + user = User.objects.get(username__iexact=user) + except User.DoesNotExist: + raise forms.ValidationError( + _("A user with that username does not exist.")) + check = permissions.BasePermission(user=user) + error_msg = None if user.is_superuser: - error_msg = _("The super user %(user)s already has the permission '%(perm)s' for %(object_name)s '%(obj)s'") - else: - error_msg = _("The user %(user)s already has the permission '%(perm)s' for %(object_name)s '%(obj)s'") - raise forms.ValidationError(error_msg % { - 'object_name': self.obj._meta.object_name.lower(), - 'perm': self.perm, - 'obj': self.obj, - 'user': user, - }) - return user + error_msg = _("The user %(user)s do not need to request " \ + "access to any permission as it is a super user.") + elif check.has_perm(self.perm, self.obj): + error_msg = _("The user %(user)s already has the permission " \ + "'%(perm)s' for %(object_name)s '%(obj)s'") + elif check.has_request(self.perm, self.obj): + error_msg = _("The user %(user)s already has a permission " \ + "request '%(perm)s' for %(object_name)s '%(obj)s'") + + if error_msg: + msg = error_msg % { + 'object_name': self.obj._meta.object_name.lower(), + 'perm': self.perm, + 'obj': self.obj, + 'user': user, + } + # Only display the error for the user field when it is not hidden + if self.approved: + self._errors["user"] = ErrorList([msg]) + else: + raise forms.ValidationError(msg) + cleaned_data['user'] = user + + return cleaned_data + class GroupPermissionForm(BasePermissionForm): group = forms.CharField(label=_('Group')) diff --git a/src/authority/managers.py b/src/authority/managers.py index ab33710..218158a 100644 --- a/src/authority/managers.py +++ b/src/authority/managers.py @@ -10,10 +10,10 @@ class PermissionManager(models.Manager): def get_for_model(self, obj): return self.filter(content_type=self.get_content_type(obj)) - def for_object(self, obj): + def for_object(self, obj, approved=True): return self.get_for_model(obj).select_related( 'user', 'creator', 'group', 'content_type' - ).filter(object_id=obj.id) + ).filter(object_id=obj.id,approved=approved) def for_user(self, user, obj, check_groups=True): perms = self.get_for_model(obj) @@ -22,15 +22,17 @@ class PermissionManager(models.Manager): return perms.select_related('user', 'user__groups', 'creator').filter( Q(user=user) | Q(group__in=user.groups.all())) - def user_permissions(self, user, perm, obj, check_groups=True): - return self.for_user(user, obj, check_groups).filter(codename=perm) + def user_permissions(self, user, perm, obj, approved=True, check_groups=True): + return self.for_user(user, obj, check_groups).filter(codename=perm, + approved=approved) - def group_permissions(self, group, perm, obj): + def group_permissions(self, group, perm, obj, approved=True): """ Get objects that have Group perm permission on """ return self.get_for_model(obj).select_related( - 'user', 'group', 'creator').filter(group=group, codename=perm) + 'user', 'group', 'creator').filter(group=group, codename=perm, + approved=approved) def delete_objects_permissions(self, obj): """ @@ -48,3 +50,5 @@ class PermissionManager(models.Manager): return perms = self.user_permissions(user, perm, obj).filter(object_id=obj.id) perms.delete() + + \ No newline at end of file diff --git a/src/authority/models.py b/src/authority/models.py index 4b943e6..55e5cbc 100644 --- a/src/authority/models.py +++ b/src/authority/models.py @@ -21,15 +21,29 @@ class Permission(models.Model): group = models.ForeignKey(Group, null=True, blank=True) creator = models.ForeignKey(User, null=True, blank=True, related_name='created_permissions') + approved = models.BooleanField(default=False) + objects = PermissionManager() def __unicode__(self): return self.codename class Meta: + unique_together = ("codename", "object_id", "content_type", "user", "group") verbose_name = _('permission') verbose_name_plural = _('permissions') permissions = ( ('change_foreign_permissions', 'Can change foreign permissions'), ('delete_foreign_permissions', 'Can delete foreign permissions'), + ('approve_permission_request', 'Can approve permission requests'), ) + + + def approve_perm_request(self, creator): + """ + Approve granular permission request setting a Permission entry as + approved=True for a specific action from an user on an object instance. + """ + self.approved=True + self.creator=creator + self.save() diff --git a/src/authority/permissions.py b/src/authority/permissions.py index c4f22f8..544bdc8 100644 --- a/src/authority/permissions.py +++ b/src/authority/permissions.py @@ -35,7 +35,7 @@ class BasePermission(object): self.group = group super(BasePermission, self).__init__(*args, **kwargs) - def has_user_perms(self, perm, obj, check_groups=True): + def has_user_perms(self, perm, obj, approved, check_groups=True): if self.user: if self.user.is_superuser: return True @@ -43,15 +43,16 @@ class BasePermission(object): return False # check if a Permission object exists for the given params return Permission.objects.user_permissions(self.user, perm, obj, - check_groups).filter(object_id=obj.id) + approved, check_groups).filter(object_id=obj.id) return False - def has_group_perms(self, perm, obj): + def has_group_perms(self, perm, obj, approved): """ Check if group has the permission for the given object """ if self.group: - perms = Permission.objects.group_permissions(self.group, perm, obj) + perms = Permission.objects.group_permissions(self.group, perm, obj, + approved) return perms.filter(object_id=obj.id) return False @@ -60,12 +61,24 @@ class BasePermission(object): Check if user has the permission for the given object """ if self.user: - if self.has_user_perms(perm, obj, check_groups): + if self.has_user_perms(perm, obj, True, check_groups): return True if self.group: - return self.has_group_perms(perm, obj) + return self.has_group_perms(perm, obj, True) return False + def has_request(self, perm, obj, check_groups=True): + """ + Check if user has the permission request for the given object + """ + if self.user: + if self.has_user_perms(perm, obj, False, check_groups): + return True + if self.group: + return self.has_group_perms(perm, obj, False) + return False + + def can(self, check, generic=False, *args, **kwargs): if not args: args = [self.model] diff --git a/src/authority/templates/authority/permission_delete_link.html b/src/authority/templates/authority/permission_delete_link.html index baec867..ef83fec 100644 --- a/src/authority/templates/authority/permission_delete_link.html +++ b/src/authority/templates/authority/permission_delete_link.html @@ -1,2 +1,2 @@ {% load i18n %} -{% if delete_url %}{% trans "Revoke permission" %}{% endif %} \ No newline at end of file +{% if url %}{% trans "Revoke permission" %}{% endif %} \ No newline at end of file diff --git a/src/authority/templates/authority/permission_request_approve_link.html b/src/authority/templates/authority/permission_request_approve_link.html new file mode 100644 index 0000000..32a32af --- /dev/null +++ b/src/authority/templates/authority/permission_request_approve_link.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% if url %}{% trans "Approve request" %}{% endif %} \ No newline at end of file diff --git a/src/authority/templates/authority/permission_request_delete_link.html b/src/authority/templates/authority/permission_request_delete_link.html new file mode 100644 index 0000000..660354a --- /dev/null +++ b/src/authority/templates/authority/permission_request_delete_link.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% if url %}{% trans "Deny request" %}{% endif %} \ No newline at end of file diff --git a/src/authority/templates/authority/permission_request_form.html b/src/authority/templates/authority/permission_request_form.html new file mode 100644 index 0000000..ad5f810 --- /dev/null +++ b/src/authority/templates/authority/permission_request_form.html @@ -0,0 +1,10 @@ +{% load i18n %} +{% if form %} +
    + + {{ form.as_p }} +

    + +

    +
    +{% endif %} diff --git a/src/authority/templatetags/permissions.py b/src/authority/templatetags/permissions.py index 267d8f1..3b53582 100644 --- a/src/authority/templatetags/permissions.py +++ b/src/authority/templatetags/permissions.py @@ -10,6 +10,20 @@ from authority.forms import UserPermissionForm register = template.Library() +def _base_link(context, perm, view_name): + return { + 'next': context['request'].build_absolute_uri(), + 'url': reverse(view_name, kwargs={'permission_pk': perm.pk,}), + } + +def _base_permission_form(context, obj, perm, user, approved, view_name): + return { + 'form': UserPermissionForm(perm, obj, approved, + initial=dict(codename=perm, user=user)), + 'form_url': url_for_obj(view_name, obj), + 'next': context['request'].build_absolute_uri(), + } + def next_bit_for(bits, key, if_none=None): try: return bits[bits.index(key)+1] @@ -30,12 +44,16 @@ class ResolverNode(template.Node): return template.Variable(var).resolve(context) @register.simple_tag -def add_url_for_obj(obj): - return reverse('authority-add-permission', kwargs={ +def url_for_obj(view_name, obj): + return reverse(view_name, kwargs={ 'app_label': obj._meta.app_label, 'module_name': obj._meta.module_name, 'pk': obj.pk}) +@register.simple_tag +def add_url_for_obj(obj): + return url_for_obj('authority-add-permission', obj) + class ComparisonNode(ResolverNode): """ Implements a node to provide an "if user/group has permission on object" @@ -124,14 +142,11 @@ def permission_delete_link(context, perm): """ user = context['request'].user if user.is_authenticated(): - if user.has_perm('delete_foreign_permissions') or user.pk == perm.creator.pk: - return { - 'next': context['request'].build_absolute_uri(), - 'delete_url': reverse('authority-delete-permission', kwargs={ - 'permission_pk': perm.pk, - }) - } - return {'delete_url': None} + if user.has_perm('authority.delete_foreign_permissions') \ + or user.pk == perm.creator.pk: + return _base_link(context, perm, 'authority-delete-permission') + return {'url': None} + @register.inclusion_tag('authority/permission_form.html', takes_context=True) def permission_form(context, obj, perm=None): @@ -148,19 +163,37 @@ def permission_form(context, obj, perm=None): user = context['request'].user if user.is_authenticated(): if user.has_perm('authority.add_permission'): - return { - 'form': UserPermissionForm(perm, obj, initial=dict(codename=perm)), - 'form_url': add_url_for_obj(obj), - 'next': context['request'].build_absolute_uri(), - } + return _base_permission_form(context, obj, perm, None, True, + 'authority-add-permission') return {'form': None} + +@register.inclusion_tag('authority/permission_request_form.html', takes_context=True) +def permission_request_form(context, obj, perm=None): + """ + Renders an "add permission requests" form for the given object. If no perm + is given it will render a select box to choose from. + + Syntax:: + + {% permission_request_form [obj] [permission_label].[check_name] %} + {% permission_request_form lesson "lesson_permission.add_lesson" %} + + """ + user = context['request'].user + if user.is_authenticated() and not user.is_superuser: + return _base_permission_form(context, obj, perm, user, False, + 'authority-add-request') + return {'form': None} + + class PermissionsForObjectNode(ResolverNode): - def __init__(self, obj, user, var_name, perm=None, objs=None): + def __init__(self, obj, user, var_name, approved, perm=None, objs=None): self.obj = obj self.user = user self.perm = perm self.var_name = var_name + self.approved = approved def render(self, context): obj = self.resolve(self.obj, context) @@ -168,7 +201,7 @@ class PermissionsForObjectNode(ResolverNode): user = self.resolve(self.user, context) perms = [] if not isinstance(user, AnonymousUser): - perms = Permission.objects.for_object(obj) + perms = Permission.objects.for_object(obj, self.approved) if isinstance(user, User): perms = perms.filter(user=user) context[var_name] = perms @@ -196,6 +229,34 @@ def get_permissions(parser, token): 'obj': next_bit_for(bits, 'get_permissions'), 'user': next_bit_for(bits, 'for'), 'var_name': next_bit_for(bits, 'as', '"permissions"'), + 'approved': True, + } + return PermissionsForObjectNode(**kwargs) + + +@register.tag +def get_permission_requests(parser, token): + """ + Retrieves all permissions requests associated with the given obj and user + and assigns the result to a context variable. + + Syntax:: + + {% get_permission_requests obj %} + {% for perm in permissions %} + {{ perm }} + {% endfor %} + + {% get_permission_requests obj as "my_permissions" %} + {% get_permission_requests obj for request.user as "my_permissions" %} + + """ + bits = token.contents.split() + kwargs = { + 'obj': next_bit_for(bits, 'get_permission_requests'), + 'user': next_bit_for(bits, 'for'), + 'var_name': next_bit_for(bits, 'as', '"permission_requests"'), + 'approved': False, } return PermissionsForObjectNode(**kwargs) @@ -247,3 +308,31 @@ def get_permission(parser, token): 'var_name': next_bit_for(bits, 'as', '"permission"'), } return PermissionForObjectNode(**kwargs) + + +@register.inclusion_tag('authority/permission_request_delete_link.html', takes_context=True) +def permission_request_delete_link(context, perm): + """ + Renders a html link to the delete view of the given permission request. + Returns no content if the request-user has no permission to delete foreign + permissions. + """ + user = context['request'].user + if user.is_authenticated(): + if user.has_perm('authority.delete_permission'): + return _base_link(context, perm, 'authority-delete-request') + return {'url': None} + + +@register.inclusion_tag('authority/permission_request_approve_link.html', takes_context=True) +def permission_request_approve_link(context, perm): + """ + Renders a html link to the approve view of the given permission request. + Returns no content if the request-user has no permission to delete foreign + permissions. + """ + user = context['request'].user + if user.is_authenticated(): + if user.has_perm('authority.approve_permission_request'): + return _base_link(context, perm, 'authority-approve-request') + return {'url': None} \ No newline at end of file diff --git a/src/authority/urls.py b/src/authority/urls.py index 5a69132..08278f5 100644 --- a/src/authority/urls.py +++ b/src/authority/urls.py @@ -1,7 +1,11 @@ from django.conf.urls.defaults import * -from authority.views import add_permission, delete_permission +from authority.views import (add_permission, delete_permission, + approve_permission_request) 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"), + url(r'^add/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', add_permission, name="authority-add-permission", kwargs = {'approved': True }), + url(r'^add-request/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$', add_permission, name="authority-add-request", kwargs = {'approved': False }), + url(r'^approve-request/(?P\d+)/$', approve_permission_request, name="authority-approve-request"), + url(r'^delete-request/(?P\d+)/$', delete_permission, name="authority-delete-request", kwargs = {'approved': False }), + url(r'^delete/(?P\d+)/$', delete_permission, name="authority-delete-permission", kwargs = {'approved': True }), ) diff --git a/src/authority/views.py b/src/authority/views.py index 58f5bc6..bf93f7e 100644 --- a/src/authority/views.py +++ b/src/authority/views.py @@ -2,56 +2,84 @@ 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.utils.translation import ugettext +from django.utils.translation import ugettext as _ from django.template.context import RequestContext from django.template import loader from django.contrib.auth.decorators import login_required from authority.models import Permission from authority.forms import UserPermissionForm -from authority.templatetags.permissions import add_url_for_obj +from authority.templatetags.permissions import url_for_obj -@require_POST @login_required -def add_permission(request, app_label, module_name, pk, extra_context={}, - template_name='authority/permission_form.html'): +def add_permission(request, app_label, module_name, pk, approved=False, + extra_context={}): 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 permission_denied(request) 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) + + if approved: + template_name = 'authority/permission_form.html' + view_name = 'authority-add-permission' 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)) + template_name = 'authority/permission_request_form.html' + view_name = 'authority-add-request' + + if request.method == 'POST': + if codename is None: + return HttpResponseForbidden(next) + form = UserPermissionForm(data=request.POST, obj=obj, approved=approved, + perm=codename, initial=dict(codename=codename)) + if form.is_valid(): + form.save(request) + request.user.message_set.create( + message=_('You added a permission request.')) + return HttpResponseRedirect(next) + else: + form = UserPermissionForm(obj=obj, perm=codename, approved=approved, + initial=dict(codename=codename)) + + context = { + 'form': form, + 'form_url': url_for_obj(view_name, 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 == permission.creator: - permission.delete() +def approve_permission_request(request, permission_pk): + perm_request = get_object_or_404(Permission, pk=permission_pk) + if request.user.has_perm('authority.approve_permission_request'): + perm_request.approve_perm_request(request.user) request.user.message_set.create( - message=ugettext('You removed the permission.')) + message=_('You approved the permission request.')) next = request.REQUEST.get('next') or '/' return HttpResponseRedirect(next) + +@login_required +def delete_permission(request, permission_pk, approved): + permission = get_object_or_404(Permission, + pk=permission_pk, approved=approved) + if request.user.has_perm('authority.delete_foreign_permissions') \ + or request.user == permission.creator: + permission.delete() + if approved: + msg=_('You removed the permission.') + else: + msg=_('You removed the permission request.') + request.user.message_set.create(message=msg) + next = request.REQUEST.get('next') or '/' + return HttpResponseRedirect(next) + + def permission_denied(request, template_name=None, extra_context={}): """ Default 403 handler.