diff --git a/MANIFEST.in b/MANIFEST.in index 100e670..a63229b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,3 @@ include LICENSE recursive-include src/authority/templates/authority * +recursive-include src/authority/templates/admin * diff --git a/example/exampleapp/permissions.py b/example/exampleapp/permissions.py index 8ed6166..72263a1 100644 --- a/example/exampleapp/permissions.py +++ b/example/exampleapp/permissions.py @@ -1,9 +1,10 @@ from django.contrib.flatpages.models import FlatPage from django.utils.translation import ugettext_lazy as _ -from authority import permissions +import authority +from authority.permissions import BasePermission -class FlatPagePermission(permissions.BasePermission): +class FlatPagePermission(BasePermission): """ This class contains a bunch of checks: @@ -39,12 +40,13 @@ class FlatPagePermission(permissions.BasePermission): {% endifhasperm %} """ - model = FlatPage label = 'flatpage_permission' checks = ('review', 'top_secret') - def top_secret(self, flatpage=None): + def top_secret(self, flatpage=None, lala=None): if flatpage and flatpage.registration_required: return self.browse_flatpage(obj=flatpage) return False top_secret.short_description=_('Is allowed to see top secret flatpages') + +authority.register(FlatPage, FlatPagePermission) diff --git a/example/exampleapp/views.py b/example/exampleapp/views.py index 2727727..8b02090 100644 --- a/example/exampleapp/views.py +++ b/example/exampleapp/views.py @@ -3,11 +3,11 @@ from django.contrib.flatpages.models import FlatPage from authority.decorators import permission_required, permission_required_or_403 -@permission_required_or_403('flatpage_permission.top_secret', # use this to return a 403 page - (FlatPage, 'url__contains', 'url'), (FlatPage, 'url__contains', 'lala')) # @permission_required('flatpage_permission.top_secret', # (FlatPage, 'url__contains', 'url'), (FlatPage, 'url__contains', 'lala')) -#@permission_required_or_403('flatpages.add_flatpage') +# use this to return a 403 page: +@permission_required_or_403('flatpage_permission.top_secret', + (FlatPage, 'url__contains', 'url'), 'lala') def top_secret(request, url, lala=None): """ A wrapping view that performs the permission check given in the decorator diff --git a/example/urls.py b/example/urls.py index 6c33bf8..86fc631 100644 --- a/example/urls.py +++ b/example/urls.py @@ -14,7 +14,7 @@ urlpatterns = patterns('', #('^admin/', include(admin.site.urls)), (r'^perms/', include('authority.urls')), (r'^accounts/login/$', 'django.contrib.auth.views.login'), - url(r'^(?P[\/0-9A-Za-z]+)$', 'example.exampleapp.views.top_secret'), + url(r'^(?P[\/0-9A-Za-z]+)$', 'example.exampleapp.views.top_secret', {'lala': 'oh yeah!'}), ) if settings.DEBUG: diff --git a/setup.py b/setup.py index 86a1000..57b4558 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,19 @@ +import os from setuptools import setup, find_packages +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + setup( name='django-authority', - version='0.0.1', + version='0.1', description="A Django app that provides generic per-object-permissions for Django's auth app.", - long_description=open('README').read(), + long_description=read('README'), author='Jannis Leidel', author_email='jannis@leidel.info', + license='BSD', url='http://bitbucket.org/jezdez/django-authority/', + download_url='http://bitbucket.org/jezdez/django-authority/downloads/', packages=find_packages('src'), package_dir = {'': 'src'}, classifiers=[ @@ -22,6 +28,8 @@ setup( package_data = { 'authority': [ 'templates/authority/*.html', + 'templates/admin/edit_inline/action_tabular.html', + 'templates/admin/permission_change_form.html', ] }, zip_safe=False, diff --git a/src/authority/__init__.py b/src/authority/__init__.py index bb2f74f..5db95f3 100644 --- a/src/authority/__init__.py +++ b/src/authority/__init__.py @@ -1,4 +1,6 @@ import sys +from authority.sites import site, get_check, get_choices_for, register, unregister + LOADING = False def autodiscover(): diff --git a/src/authority/actions.py b/src/authority/actions.py index 4dd8be0..a6d9726 100644 --- a/src/authority/actions.py +++ b/src/authority/actions.py @@ -6,6 +6,7 @@ from django.utils.safestring import mark_safe from django.utils.translation import ugettext from django.contrib.contenttypes.models import ContentType from django.forms.formsets import all_valid +from django.http import HttpResponseRedirect try: from django.contrib.admin import actions @@ -57,7 +58,8 @@ def edit_permissions(modeladmin, request, queryset): if all_valid(formsets): for formset in formsets: formset.save() - return None + # redirect to full request path to make sure we keep filter + return HttpResponseRedirect(request.get_full_path()) context = { 'errors': ActionErrorList(formsets), diff --git a/src/authority/admin.py b/src/authority/admin.py index b4d0b3a..34963f5 100644 --- a/src/authority/admin.py +++ b/src/authority/admin.py @@ -4,8 +4,8 @@ from django.contrib.contenttypes import generic from django.utils.translation import ugettext_lazy as _ from authority.models import Permission -from authority import permissions from authority.widgets import GenericForeignKeyRawIdWidget +from authority import get_choices_for class PermissionInline(generic.GenericTabularInline): model = Permission @@ -14,7 +14,7 @@ class PermissionInline(generic.GenericTabularInline): def formfield_for_dbfield(self, db_field, **kwargs): if db_field.name == 'codename': - perm_choices = permissions.registry.get_choices_for(self.parent_model) + perm_choices = get_choices_for(self.parent_model) kwargs['label'] = _('permission') kwargs['widget'] = forms.Select(choices=perm_choices) return db_field.formfield(**kwargs) diff --git a/src/authority/decorators.py b/src/authority/decorators.py index e587ac8..64e9b43 100644 --- a/src/authority/decorators.py +++ b/src/authority/decorators.py @@ -7,10 +7,10 @@ from django.shortcuts import get_object_or_404 from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME -from authority import permissions +from authority import permissions, get_check from authority.views import permission_denied -def permission_required(perm, *model_lookups, **kwargs): +def permission_required(perm, *lookup_variables, **kwargs): """ Decorator for views that checks whether a user has a particular permission enabled, redirecting to the log-in page if necessary. @@ -20,30 +20,36 @@ def permission_required(perm, *model_lookups, **kwargs): redirect_to_login = kwargs.pop('redirect_to_login', True) def decorate(view_func): def decorated(request, *args, **kwargs): - objs = [] if request.user.is_authenticated(): - for model, lookup, varname in model_lookups: - if varname not in kwargs: - continue - value = kwargs.get(varname, None) - if value is None: - continue - if isinstance(model, basestring): - model_class = get_model(*model.split(".")) - else: - model_class = model - if model_class is None: - raise ValueError( - "The given argument '%s' is not a valid model." % model) - if inspect.isclass(model_class) and \ - not issubclass(model_class, Model): - raise ValueError( - 'The argument %s needs to be a model.' % model) - objs.append(get_object_or_404(model_class, **{lookup: value})) - check = permissions.registry.get_check(request.user, perm) + params = [] + for lookup_variable in lookup_variables: + if isinstance(lookup_variable, basestring): + value = kwargs.get(lookup_variable, None) + if value is None: + continue + params.append(value) + elif isinstance(lookup_variable, (tuple, list)): + model, lookup, varname = lookup_variable + value = kwargs.get(varname, None) + if value is None: + continue + if isinstance(model, basestring): + model_class = get_model(*model.split(".")) + else: + model_class = model + if model_class is None: + raise ValueError( + "The given argument '%s' is not a valid model." % model) + if (inspect.isclass(model_class) and + not issubclass(model_class, Model)): + raise ValueError( + 'The argument %s needs to be a model.' % model) + obj = get_object_or_404(model_class, **{lookup: value}) + params.append(obj) + check = get_check(request.user, perm) granted = False if check is not None: - granted = check(*objs) + granted = check(*params) if granted or request.user.has_perm(perm): return view_func(request, *args, **kwargs) if redirect_to_login: diff --git a/src/authority/forms.py b/src/authority/forms.py index 293eb4e..21e811d 100644 --- a/src/authority/forms.py +++ b/src/authority/forms.py @@ -3,8 +3,8 @@ 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 import permissions, get_choices_for from authority.models import Permission -from authority import permissions class BasePermissionForm(forms.ModelForm): codename = forms.CharField(label=_('Permission')) @@ -18,7 +18,7 @@ class BasePermissionForm(forms.ModelForm): if obj and perm: self.base_fields['codename'].widget = forms.HiddenInput() elif obj and not perm: - perm_choices = permissions.registry.get_choices_for(self.obj) + perm_choices = get_choices_for(self.obj) self.base_fields['codename'].widget = forms.Select(choices=perm_choices) super(BasePermissionForm, self).__init__(*args, **kwargs) diff --git a/src/authority/models.py b/src/authority/models.py index 29d13e6..4b943e6 100644 --- a/src/authority/models.py +++ b/src/authority/models.py @@ -3,6 +3,7 @@ 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 _ + from authority.managers import PermissionManager class Permission(models.Model): diff --git a/src/authority/permissions.py b/src/authority/permissions.py index 3b85f15..27903a1 100644 --- a/src/authority/permissions.py +++ b/src/authority/permissions.py @@ -1,59 +1,11 @@ from inspect import getmembers, ismethod -from django.db.models.base import ModelBase -from django.db.models.fields import BLANK_CHOICE_DASH -from django.core.exceptions import ImproperlyConfigured +from django.db.models import Q +from django.db.models.base import Model, ModelBase from django.template.defaultfilters import slugify from django.utils.translation import ugettext_lazy as _ from authority.models import Permission -class AlreadyRegistered(Exception): - pass - -class NotRegistered(Exception): - pass - -class PermissionRegistry(dict): - """ - A dictionary that contains permission instances and their labels. - """ - _choices = {} - def get_permission_by_label(self, label): - for perm_cls, perm_label in self.items(): - if perm_label == label: - return perm_cls - return None - - def get_check(self, user, label): - perm_label, check_name = label.split('.') - perm_cls = self.get_permission_by_label(perm_label) - if perm_cls is None: - return None - perm_instance = perm_cls(user) - return getattr(perm_instance, check_name, None) - - def get_permissions_by_model(self, model): - return [perm for perm in self if perm.model == model] - - def get_choices_for(self, obj, default=BLANK_CHOICE_DASH): - model_cls = obj - if not isinstance(obj, ModelBase): - model_cls = obj.__class__ - if model_cls in self._choices: - choices = self._choices[model_cls] - else: - choices = [] + default - for perm in self.get_permissions_by_model(model_cls): - for name, check in getmembers(perm, ismethod): - if name in perm.checks: - signature = '%s.%s' % (perm.label, name) - label = getattr(check, 'short_description', signature) - choices.append((signature, label)) - self._choices[model_cls] = choices - return choices - -registry = PermissionRegistry() - class PermissionMetaclass(type): """ Used to generate the default set of permission checks "add", "change" and @@ -62,61 +14,15 @@ class PermissionMetaclass(type): 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() new_class.label = slugify(new_class.label) - 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[new_class] = new_class.label if new_class.checks is None: new_class.checks = [] # force check names to be lower case new_class.checks = [check.lower() for check in new_class.checks] - generic_checks = ['add', 'browse', 'change', 'delete'] - for check_name in new_class.checks: - check_func = getattr(new_class, check_name, None) - if check_func is not None: - func = new_class.create_check(check_name, check_func) - func.__name__ = check_name - func.short_description = getattr(check_func, 'short_description', - _("%(object_name)s permission '%(check)s'") % { - 'object_name': new_class.model._meta.object_name, - 'check': check_name}) - setattr(new_class, check_name, func) - else: - generic_checks.append(check_name) - for check_name in generic_checks: - func = new_class.create_check(check_name, generic=True) - object_name = new_class.model._meta.object_name - func_name = "%s_%s" % (check_name, object_name.lower()) - func.short_description = _("Can %(check)s this %(object_name)s") % { - 'object_name': new_class.model._meta.object_name.lower(), - 'check': check_name} - func.check_name = check_name - if func_name not in new_class.checks: - new_class.checks.append(func_name) - setattr(new_class, func_name, func) return new_class - def _create_check(cls, check_name, check_func=None, generic=False): - def check(self, *args, **kwargs): - granted = self.can(check_name, generic, *args, **kwargs) - if check_func and not granted: - return check_func(self, *args, **kwargs) - return granted - return check - create_check = classmethod(_create_check) - class BasePermission(object): """ Base Permission class to be used to define app permissions. @@ -125,7 +31,7 @@ class BasePermission(object): checks = () label = None - model = None + generic_checks = ['add', 'browse', 'change', 'delete'] def __init__(self, user=None, group=None, *args, **kwargs): self.user = user @@ -168,6 +74,9 @@ class BasePermission(object): args = [self.model] perms = False for obj in args: + # skip this obj if it's not a model class or instance + if not isinstance(obj, (ModelBase, Model)): + continue # first check Django's permission system if self.user: perm = '%s.%s' % (obj._meta.app_label, check.lower()) @@ -177,6 +86,7 @@ class BasePermission(object): perm = '%s.%s' % (self.label, check.lower()) if generic: perm = '%s_%s' % (perm, obj._meta.object_name.lower()) + # then check authority's per object permissions if not isinstance(obj, ModelBase) and isinstance(obj, self.model): # only check the authority if obj is not a model class perms = perms or self.has_perm(perm, obj) diff --git a/src/authority/sites.py b/src/authority/sites.py new file mode 100644 index 0000000..ebba324 --- /dev/null +++ b/src/authority/sites.py @@ -0,0 +1,128 @@ +from inspect import getmembers, ismethod +from django.db.models.base import ModelBase +from django.db.models.fields import BLANK_CHOICE_DASH +from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ImproperlyConfigured + +from authority.permissions import BasePermission + +class AlreadyRegistered(Exception): + pass + +class NotRegistered(Exception): + pass + +class PermissionSite(object): + """ + A dictionary that contains permission instances and their labels. + """ + _registry = {} + _choices = {} + + def get_permission_by_label(self, label): + for perm_cls in self._registry.values(): + if perm_cls.label == label: + return perm_cls + return None + + def get_permissions_by_model(self, model): + return [perm for perm in self._registry.values() if perm.model == model] + + def get_check(self, user, label): + perm_label, check_name = label.split('.') + perm_cls = self.get_permission_by_label(perm_label) + if perm_cls is None: + return None + perm_instance = perm_cls(user) + return getattr(perm_instance, check_name, None) + + def get_labels(self): + return [perm.label for perm in self._registry.values()] + + def get_choices_for(self, obj, default=BLANK_CHOICE_DASH): + model_cls = obj + if not isinstance(obj, ModelBase): + model_cls = obj.__class__ + if model_cls in self._choices: + return self._choices[model_cls] + choices = [] + default + for perm in self.get_permissions_by_model(model_cls): + for name, check in getmembers(perm, ismethod): + if name in perm.checks: + signature = '%s.%s' % (perm.label, name) + label = getattr(check, 'short_description', signature) + choices.append((signature, label)) + self._choices[model_cls] = choices + return choices + + def register(self, model_or_iterable, permission_class=None, **options): + if not permission_class: + permission_class = BasePermission + + if isinstance(model_or_iterable, ModelBase): + model_or_iterable = [model_or_iterable] + + if permission_class.label in self.get_labels(): + raise ImproperlyConfigured( + "The name of %s conflicts with %s" % (permission_class, + self.get_permission_by_label(permission_class.label))) + + for model in model_or_iterable: + if model in self._registry: + raise AlreadyRegistered( + 'The model %s is already registered' % model.__name__) + if options: + options['__module__'] = __name__ + permission_class = type("%sPermission" % model.__name__, + (permission_class,), options) + + permission_class.model = model + self.setup(model, permission_class) + self._registry[model] = permission_class + + def unregister(self, model_or_iterable): + if isinstance(model_or_iterable, ModelBase): + model_or_iterable = [model_or_iterable] + for model in model_or_iterable: + if model not in self._registry: + raise NotRegistered('The model %s is not registered' % model.__name__) + del self._registry[model] + + def setup(self, model, permission): + for check_name in permission.checks: + check_func = getattr(permission, check_name, None) + if check_func is not None: + func = self.create_check(check_name, check_func) + func.__name__ = check_name + func.short_description = getattr(check_func, 'short_description', + _("%(object_name)s permission '%(check)s'") % { + 'object_name': model._meta.object_name, + 'check': check_name}) + setattr(permission, check_name, func) + else: + permission.generic_checks.append(check_name) + for check_name in permission.generic_checks: + func = self.create_check(check_name, generic=True) + object_name = model._meta.object_name + func_name = "%s_%s" % (check_name, object_name.lower()) + func.short_description = _("Can %(check)s this %(object_name)s") % { + 'object_name': model._meta.object_name.lower(), + 'check': check_name} + func.check_name = check_name + if func_name not in permission.checks: + permission.checks.append(func_name) + setattr(permission, func_name, func) + + def create_check(self, check_name, check_func=None, generic=False): + def check(self, *args, **kwargs): + granted = self.can(check_name, generic, *args, **kwargs) + if check_func and not granted: + return check_func(self, *args, **kwargs) + return granted + return check + +site = PermissionSite() +get_check = site.get_check +get_choices_for = site.get_choices_for +register = site.register +unregister = site.unregister diff --git a/src/authority/templatetags/permissions.py b/src/authority/templatetags/permissions.py index 87802b7..59ffc38 100644 --- a/src/authority/templatetags/permissions.py +++ b/src/authority/templatetags/permissions.py @@ -3,7 +3,7 @@ from django.core.urlresolvers import reverse from django.core.exceptions import ImproperlyConfigured from django.contrib.auth.models import User, AnonymousUser -from authority import permissions +from authority import permissions, get_check from authority.models import Permission from authority.views import add_url_for_obj from authority.forms import UserPermissionForm @@ -51,7 +51,7 @@ class ComparisonNode(ResolverNode): objs.append(self.resolve(obj, context)) else: objs = None - check = permissions.registry.get_check(user, perm) + check = get_check(user, perm) if check is not None: if check(*objs): # return True if check was successful @@ -80,7 +80,7 @@ def do_if_has_perm(parser, token): meh {% endifhasperm %} - {% if hasperm "poll_permission.can_change" request.user %} + {% if hasperm "poll_permission.change_poll" request.user %} lalala {% else %} meh @@ -206,7 +206,7 @@ class PermissionForObjectNode(ResolverNode): user = self.resolve(self.user, context) granted = False if not isinstance(user, AnonymousUser): - check = permissions.registry.get_check(user, perm) + check = get_check(user, perm) if check is not None: granted = check(*objs) context[var_name] = granted @@ -222,8 +222,8 @@ def get_permission(parser, token): {% get_permission [permission_label].[check_name] for [user] and [objs] as [varname] %} - {% get_permission "poll_permission.can_change" for request.user and poll as "is_allowed" %} - {% get_permission "poll_permission.can_change" for request.user and poll,second_poll as "is_allowed" %} + {% get_permission "poll_permission.change_poll" for request.user and poll as "is_allowed" %} + {% get_permission "poll_permission.change_poll" for request.user and poll,second_poll as "is_allowed" %} {% if is_allowed %} I've got ze power to change ze pollllllzzz. Muahahaa.