Version 0.4
authorEric Mc Sween <eric.mcsween@auf.org>
Tue, 24 Jul 2012 19:40:28 +0000 (15:40 -0400)
committerEric Mc Sween <eric.mcsween@auf.org>
Tue, 24 Jul 2012 19:41:27 +0000 (15:41 -0400)
Changement d'API: introduction des rôles

30 files changed:
.coveragerc [new file with mode: 0644]
.gitignore
CHANGES
auf/django/permissions/__init__.py
auf/django/permissions/admin.py
auf/django/permissions/forms.py [deleted file]
auf/django/permissions/migrations/0001_initial.py [deleted file]
auf/django/permissions/migrations/__init__.py [deleted file]
auf/django/permissions/models.py
auf/django/permissions/predicates.py [deleted file]
auf/django/permissions/templates/403.html [new file with mode: 0644]
auf/django/permissions/templatetags/permissions.py
buildout.cfg
doc/conf.py
doc/index.rst
doc/installation.rst
doc/roles.rst [new file with mode: 0644]
doc/utilisation.rst
setup.py
tests/food/__init__.py [deleted file]
tests/food/models.py [deleted file]
tests/food/tests.py [deleted file]
tests/permissions.py [deleted file]
tests/settings.py
tests/simple_tests/__init__.py [deleted file]
tests/simple_tests/models.py [deleted file]
tests/simple_tests/tests.py [deleted file]
tests/simpletests/__init__.py [new file with mode: 0644]
tests/simpletests/models.py [new file with mode: 0644]
tests/simpletests/tests.py [new file with mode: 0644]

diff --git a/.coveragerc b/.coveragerc
new file mode 100644 (file)
index 0000000..395a542
--- /dev/null
@@ -0,0 +1,2 @@
+[run]
+source = auf.django.permissions
index 92ddee3..823f272 100644 (file)
@@ -1,6 +1,7 @@
 *.egg-info
 *.pyc
 /.installed.cfg
+/.coverage
 /bin
 /build
 /develop-eggs
diff --git a/CHANGES b/CHANGES
index a95e953..cccb5c9 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,11 @@
+0.4 (2012-07-23)
+================
+
+* API complètement différent: abandon des règles et des prédicats. On utilise
+  plutôt des rôles pour donner des permissions.
+
+* Ajout de la méthod with_perm() aux querysets Django.
+
 0.3 (2012-03-13)
 ================
 
index effbf7f..afa0012 100644 (file)
 # encoding: utf-8
 
-from collections import defaultdict
+import itertools
+import re
+import urlparse
 
 from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
+from django.contrib.auth.views import redirect_to_login
+from django.core.exceptions import ImproperlyConfigured, PermissionDenied
+from django.db.models import Q
+from django.shortcuts import render
 from django.utils.importlib import import_module
 
-from auf.django.permissions.models import GlobalGroupPermission
 
+# Roles and role providers
 
-class Predicate(object):
+class Role(object):
     """
-    Wrapper pour une fonction ``f(user, obj, cls)``.
+    Base class for roles.
 
-    Le paramètre ``user`` est l'utilisateur pour lequel la permission est
-    testée.
-
-    Si le prédicat est testé sur un seul objet, cet objet est passé dans
-    le paramètre ``obj`` et le paramètre ``cls`` est ``None``. Inversement,
-    si le prédicat sert à filtrer un queryset, le modèle du queryset est
-    passé dans le paramètre ``cls`` et le paramètre ``obj`` est ``None``.
-
-    La fonction peut retourner un booléen ou un objet ``Q``. Un booléen
-    indique si le test a passé ou pas. Un objet ``Q`` indique les filtres à
-    appliquer pour ne garder que les objets qui passent le test. Si un objet
-    ``Q`` est retourné lors d'un test sur un seul objet, le test retournera
-    un booléen indiquant si l'objet satisfait au filtre.
-
-    Les prédicats peuvent être combinés à l'aide des opérateurs booléens
-    ``&``, ``|`` et ``~``.
+    Doesn't give any permission.
     """
 
-    def __init__(self, func_or_value):
+    def has_perm(self, perm):
         """
-        On peut initialiser un prédicat avec une fonction ayant la signature
-        ``f(user, obj, cls)`` ou avec une valeur constante.
+        Checks if this role gives the permission ``perm``.
+
+        Returns True if it does, False if it doesn't.
         """
-        if callable(func_or_value):
-            self.func = func_or_value
-        else:
-            self.func = lambda user, obj, cls: func_or_value
+        return False
 
-    def __call__(self, user, obj=None, cls=None):
+    def get_filter_for_perm(self, perm, model):
         """
-        Appelle la fonction encapsulée.
+        Returns a filter for instances of ``model`` for which this roles
+        grants permission ``perm``.
+
+        If the permission is granted on all instances of the model, the
+        method returns True.
+
+        If the permission is granted only on some instances of the model, the
+        method returns a Q object that selects only those instances.
+
+        If the permission is granted on no instances of the model, the
+        method return False.
         """
-        if self.func.func_code.co_argcount == 1:
-            return self.func(user)
-        else:
-            return self.func(user, obj, cls)
-
-    def __and__(self, other):
-        def func(user, obj, cls):
-            my_result = self(user, obj, cls)
-            if my_result is False:
-                return False
-            other_result = other(user, obj, cls)
-            if my_result is True:
-                return other_result
-            elif other_result is True:
-                return my_result
-            else:
-                return my_result & other_result
-        return Predicate(func)
+        return False
+
+
+def get_role_providers():
+    """
+    Gathers the role providers configured in the settings.
+    """
+    global _role_providers
+    if _role_providers is None:
+        _role_providers = []
+        for path in getattr(settings, 'ROLE_PROVIDERS', []):
+            module_path, sep, provider_name = path.rpartition('.')
+            try:
+                module = import_module(module_path)
+            except ImportError, e:
+                raise ImproperlyConfigured(
+                    "Error importing role provider %s: %s" % path, e
+                )
+            try:
+                provider = getattr(module, provider_name)
+            except AttributeError:
+                raise ImproperlyConfigured(
+                    'Module "%s" does not define a role provider named "%s"' %
+                    (module_path, provider_name)
+                )
+            _role_providers.append(provider)
+    return _role_providers
+_role_providers = None
+
+
+def get_roles(user):
+    """
+    Returns the roles given to a user.
+    """
+    return list(itertools.chain.from_iterable(
+        p(user) for p in get_role_providers()
+    ))
+
+
+# Permission checking
 
-    def __or__(self, other):
-        def func(user, obj, cls):
-            my_result = self(user, obj, cls)
-            if my_result is True:
+def user_has_perm(user, perm, obj=None):
+    """
+    Checks whether a user has the given permission.
+    """
+    roles = get_roles(user)
+    if obj is None:
+        return any(role.has_perm(perm) for role in roles)
+    else:
+        for role in roles:
+            q = role.get_filter_for_perm(perm, type(obj))
+            if q is True or (isinstance(q, Q) and qeval(obj, q)):
                 return True
-            other_result = other(user, obj, cls)
-            if my_result is False:
-                return other_result
-            elif other_result is False:
-                return my_result
-            else:
-                return my_result | other_result
-        return Predicate(func)
-
-    def __invert__(self):
-        def func(user, obj, cls):
-            result = self(user, obj, cls)
-            if isinstance(result, bool):
-                return not result
-            else:
-                return ~result
-        return Predicate(func)
-
-
-class Rules(object):
-
-    def __init__(self, allow_default=None, deny_default=None):
-        self.allow_rules = defaultdict(
-            lambda: allow_default or Predicate(lambda user: user.is_superuser)
-        )
-        self.deny_rules = defaultdict(lambda: deny_default or Predicate(False))
-
-    def allow(self, perm, cls, predicate):
-        if not isinstance(perm, basestring):
-            raise TypeError(
-                "the first argument to allow() must be a string"
-            )
-        if not isinstance(predicate, Predicate):
-            raise TypeError(
-                "the third argument to allow() must be a Predicate"
-            )
-        self.allow_rules[(perm, cls)] |= predicate
-
-    def deny(self, perm, cls, predicate):
-        if not isinstance(perm, basestring):
-            raise TypeError(
-                "the first argument to deny() must be a string"
-            )
-        if not isinstance(predicate, Predicate):
-            raise TypeError("the third argument to deny() must be a Predicate")
-        self.deny_rules[(perm, cls)] |= predicate
-
-    def allow_global(self, perm, predicate):
-        self.allow(perm, None, predicate)
-
-    def deny_global(self, perm, predicate):
-        self.deny(perm, None, predicate)
-
-    def predicate_for_perm(self, perm, cls):
-        return self.allow_rules[(perm, cls)] & ~self.deny_rules[(perm, cls)]
-
-    def user_has_perm(self, user, perm, obj):
-        cls = None if obj is None else obj.__class__
-        result = self.predicate_for_perm(perm, cls)(user, obj)
-        if isinstance(result, bool):
-            return result
-        else:
-            return obj._default_manager \
-                    .filter(pk=obj.pk) \
-                    .filter(result) \
-                    .exists()
-
-    def filter_queryset(self, user, perm, queryset):
-        predicate = self.predicate_for_perm(perm, queryset.model)
-        result = predicate(user, cls=queryset.model)
-        if result is True:
+        return False
+
+
+def queryset_with_perm(queryset, user, perm):
+    """
+    Filters ``queryset``, leaving only objects on which ``user`` has the
+    permission ``perm``.
+    """
+    roles = get_roles(user)
+    query = None
+    for role in roles:
+        q = role.get_filter_for_perm(perm, queryset.model)
+        if q is True:
             return queryset
-        elif result is False:
-            return queryset.none()
+        elif q is not False:
+            if query:
+                query |= q
+            else:
+                query = q
+    if query:
+        return queryset.filter(query)
+    else:
+        return queryset.none()
+
+
+def qeval(obj, q):
+    """
+    Evaluates a Q object on an instance of a model.
+    """
+
+    # Evaluate all children
+    for child in q.children:
+        if isinstance(child, Q):
+            result = qeval(child)
         else:
-            return queryset.filter(result)
+            filter, value = child
+            bits = filter.split('__')
+            path = bits[:-1]
+            lookup = bits[-1]
+            for attr in path:
+                obj = getattr(obj, attr)
+            if lookup == 'exact':
+                result = obj == value
+            elif lookup == 'iexact':
+                result = obj.lower() == value.lower()
+            elif lookup == 'contains':
+                result = value in obj
+            elif lookup == 'icontains':
+                result = value.lower() in obj.lower()
+            elif lookup == 'in':
+                result = obj in value
+            elif lookup == 'gt':
+                result = obj > value
+            elif lookup == 'gte':
+                result = obj >= value
+            elif lookup == 'lt':
+                result = obj < value
+            elif lookup == 'lte':
+                result = obj <= value
+            elif lookup == 'startswith':
+                result = obj.startswith(value)
+            elif lookup == 'istartswith':
+                result = obj.lower().istartswith(value.lower())
+            elif lookup == 'endswith':
+                result = obj.lower().iendswith(value.lower())
+            elif lookup == 'range':
+                result = value[0] <= obj <= value[1]
+            elif lookup == 'year':
+                result = obj.year == value
+            elif lookup == 'month':
+                result = obj.month == value
+            elif lookup == 'day':
+                result = obj.day == value
+            elif lookup == 'week_day':
+                result = (obj.weekday() + 1) % 7 + 1 == value
+            elif lookup == 'isnull':
+                result = (obj is None) if value else (obj is not None)
+            elif lookup == 'search':
+                raise NotImplementedError(
+                    'qeval does not implement "__search"'
+                )
+            elif lookup == 'regex':
+                result = bool(re.search(value, obj))
+            elif lookup == 'iregex':
+                result = bool(re.search(value, obj, re.I))
+            else:
+                obj = getattr(obj, lookup)
+                result = obj == value
+
+        # See if we can shortcut
+        if result and q.connector == Q.OR:
+            return True
+        if not result and q.connector == Q.AND:
+            return False
+
+    # We could'nt shortcut
+    if q.connector == Q.OR:
+        result = False
+    if q.connector == Q.AND:
+        result = True
+
+    # Negate if necessary
+    if q.negated:
+        return not result
+    else:
+        return result
 
-    def clear(self):
-        self.allow_rules.clear()
-        self.deny_rules.clear()
 
+# Authentication backend
 
 class AuthenticationBackend(object):
     supports_anonymous_user = True
@@ -157,33 +212,10 @@ class AuthenticationBackend(object):
     supports_object_permissions = True
 
     def has_perm(self, user, perm, obj=None):
-        if not user.is_active:
-            return False
-        if obj is None and perm in self.get_all_permissions(user):
-            return True
-        return get_rules().user_has_perm(user, perm, obj)
-
-    def get_group_permissions(self, user, obj=None):
-        if user.is_anonymous() or obj is not None:
-            return set()
-        if not hasattr(user, '_auf_global_group_perm_cache'):
-            user._auf_global_group_perm_cache = set(
-                p.codename
-                for p in GlobalGroupPermission.objects.filter(group__user=user)
-            )
-        return user._auf_global_group_perm_cache
-
-    def get_all_permissions(self, user, obj=None):
-        if user.is_anonymous() or obj is not None:
-            return set()
-        if not hasattr(user, '_auf_global_user_perm_cache'):
-            user._auf_global_user_perm_cache = set(
-                p.codename for p in user.global_permissions.all()
-            )
-            user._auf_global_user_perm_cache.update(
-                self.get_group_permissions(user)
-            )
-        return user._auf_global_user_perm_cache
+        return user_has_perm(user, perm, obj)
+
+    def has_module_perms(self, user, package_name):
+        return user_has_perm(user, package_name)
 
     def authenticate(self, username=None, password=None):
         # We don't authenticate
@@ -194,16 +226,32 @@ class AuthenticationBackend(object):
         return None
 
 
-def get_rules():
-    global _rules
-    if _rules is None:
-        if not hasattr(settings, 'AUF_PERMISSIONS_RULES'):
-            raise ImproperlyConfigured(
-                'Vous devez configurer la variable AUF_PERMISSIONS_RULES'
-            )
-        module_name, dot, attr = settings.AUF_PERMISSIONS_RULES.rpartition('.')
-        module = import_module(module_name)
-        _rules = getattr(module, attr)
-    return _rules
+# View protection and PermissionDenied management
 
-_rules = None
+def require_permission(user, perm, obj=None):
+    if not user.has_perm(perm, obj):
+        raise PermissionDenied
+
+
+class PermissionDeniedMiddleware(object):
+
+    def process_exception(self, request, exception):
+        if isinstance(exception, PermissionDenied):
+            if request.user.is_anonymous():
+
+                # Code de redirection venant de
+                # django.contrib.auth.decorators.permission_required()
+                path = request.build_absolute_uri()
+                login_url = settings.LOGIN_URL
+                # If the login url is the same scheme and net location then
+                # just use the path as the "next" url.
+                login_scheme, login_netloc = urlparse.urlparse(login_url)[:2]
+                current_scheme, current_netloc = urlparse.urlparse(path)[:2]
+                if ((not login_scheme or login_scheme == current_scheme) and
+                    (not login_netloc or login_netloc == current_netloc)):
+                    path = request.get_full_path()
+                return redirect_to_login(path, login_url, 'next')
+            else:
+                return render(request, '403.html')
+        else:
+            return None
index 19f0e09..e1787f5 100644 (file)
@@ -1,6 +1,5 @@
 # encoding: utf-8
 
-from auf.django.permissions import get_rules
 from django.contrib.admin import ModelAdmin
 
 
@@ -14,8 +13,5 @@ class GuardedModelAdmin(ModelAdmin):
                     .has_change_permission(request, obj)
 
     def queryset(self, request):
-        return get_rules().filter_queryset(
-            request.user,
-            'change',
-            super(GuardedModelAdmin, self).queryset(request)
-        )
+        return super(GuardedModelAdmin, self).queryset(request) \
+                .with_perm(request.user, 'change')
diff --git a/auf/django/permissions/forms.py b/auf/django/permissions/forms.py
deleted file mode 100644 (file)
index 84bca48..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-from django import forms
-
-
-def make_global_permissions_form(base_form, global_permissions,
-                                 prefix='global.'):
-
-    class _GlobalPermissionsForm(forms.ModelForm):
-
-        def __init__(self, *args, **kwargs):
-            instance = kwargs.get('instance')
-            initial = kwargs.get('initial', {})
-            if instance:
-                perms = instance.global_permissions.get_permissions()
-            else:
-                perms = []
-            permission_data = {}
-            for field, label in global_permissions:
-                perm = prefix + field
-                permission_data[field] = perm in perms
-            permission_data.update(initial)
-            kwargs['initial'] = permission_data
-            super(_GlobalPermissionsForm, self).__init__(*args, **kwargs)
-
-        def save(self, commit=True):
-            instance = super(_GlobalPermissionsForm, self).save(commit=commit)
-            old_save_m2m = getattr(self, 'save_m2m')
-
-            def save_m2m():
-                if old_save_m2m:
-                    old_save_m2m()
-                for field, label in global_permissions:
-                    perm = prefix + field
-                    if self.cleaned_data[field]:
-                        instance.global_permissions.add_permission(perm)
-                    else:
-                        instance.global_permissions.remove_permission(perm)
-
-            if commit:
-                save_m2m()
-            else:
-                self.save_m2m = save_m2m
-            return instance
-
-    fields = {}
-    for field, label in global_permissions:
-        fields[field] = forms.BooleanField(
-            label=label, required=False
-        )
-    form = type('UserOrGroupForm', (_GlobalPermissionsForm, base_form), fields)
-    return form
diff --git a/auf/django/permissions/migrations/0001_initial.py b/auf/django/permissions/migrations/0001_initial.py
deleted file mode 100644 (file)
index 241fd0f..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-# encoding: utf-8
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-class Migration(SchemaMigration):
-
-    def forwards(self, orm):
-        
-        # Adding model 'GlobalUserPermission'
-        db.create_table('permissions_globaluserpermission', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='global_permissions', to=orm['auth.User'])),
-            ('codename', self.gf('django.db.models.fields.CharField')(max_length=100)),
-        ))
-        db.send_create_signal('permissions', ['GlobalUserPermission'])
-
-        # Adding model 'GlobalGroupPermission'
-        db.create_table('permissions_globalgrouppermission', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('group', self.gf('django.db.models.fields.related.ForeignKey')(related_name='global_permissions', to=orm['auth.Group'])),
-            ('codename', self.gf('django.db.models.fields.CharField')(max_length=100)),
-        ))
-        db.send_create_signal('permissions', ['GlobalGroupPermission'])
-
-
-    def backwards(self, orm):
-        
-        # Deleting model 'GlobalUserPermission'
-        db.delete_table('permissions_globaluserpermission')
-
-        # Deleting model 'GlobalGroupPermission'
-        db.delete_table('permissions_globalgrouppermission')
-
-
-    models = {
-        'auth.group': {
-            'Meta': {'object_name': 'Group'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
-        },
-        'auth.permission': {
-            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
-        },
-        'auth.user': {
-            'Meta': {'object_name': 'User'},
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
-        },
-        'contenttypes.contenttype': {
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
-        },
-        'permissions.globalgrouppermission': {
-            'Meta': {'object_name': 'GlobalGroupPermission'},
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'global_permissions'", 'to': "orm['auth.Group']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
-        },
-        'permissions.globaluserpermission': {
-            'Meta': {'object_name': 'GlobalUserPermission'},
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'global_permissions'", 'to': "orm['auth.User']"})
-        }
-    }
-
-    complete_apps = ['permissions']
diff --git a/auf/django/permissions/migrations/__init__.py b/auf/django/permissions/migrations/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
index 97d681f..ff27086 100644 (file)
@@ -1,26 +1,15 @@
-from django.contrib.auth.models import User, Group
-from django.db import models
+# encoding: utf-8
 
+from django.db.models import Manager
+from django.db.models.query import QuerySet
 
-class GlobalPermissionManager(models.Manager):
+from auf.django.permissions import queryset_with_perm
 
-    def add_permission(self, codename):
-        self.get_or_create(codename=codename)
 
-    def remove_permission(self, codename):
-        self.filter(codename=codename).delete()
+# Add nice methods to the Django Queryset
 
-    def get_permissions(self):
-        return set(p.codename for p in self.all())
+def _manager_with_perm(manager, *args, **kwargs):
+    return manager.get_query_set().with_perm(*args, **kwargs)
 
-
-class GlobalUserPermission(models.Model):
-    user = models.ForeignKey(User, related_name='global_permissions')
-    codename = models.CharField(max_length=100)
-    objects = GlobalPermissionManager()
-
-
-class GlobalGroupPermission(models.Model):
-    group = models.ForeignKey(Group, related_name='global_permissions')
-    codename = models.CharField(max_length=100)
-    objects = GlobalPermissionManager()
+Manager.with_perm = _manager_with_perm
+QuerySet.with_perm = queryset_with_perm
diff --git a/auf/django/permissions/predicates.py b/auf/django/permissions/predicates.py
deleted file mode 100644 (file)
index 74af53d..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-# encoding: utf-8
-
-"""
-Builtin predicates.
-"""
-
-from auf.django.permissions import Predicate, get_rules
-
-
-def has_global_perm(perm):
-    def p(user):
-        return user.has_perm(perm)
-    return Predicate(p)
-
-def has_object_perm(perm):
-    def p(user, obj, cls):
-        return get_rules().predicate_for_perm(perm, cls or obj.__class__)(user, obj, cls)
-    return Predicate(p)
diff --git a/auf/django/permissions/templates/403.html b/auf/django/permissions/templates/403.html
new file mode 100644 (file)
index 0000000..1ef883c
--- /dev/null
@@ -0,0 +1 @@
+<h1>Permission denied</h1>
index 53f64cb..96e9dbe 100644 (file)
@@ -69,12 +69,15 @@ def ifhasperm(parser, token):
         nodelist_false = template.NodeList()
     return IfHasPermNode(perm, obj, nodelist_true, nodelist_false)
 
+
 @register.tag
 def withperms(parser, token):
     bits = token.split_contents()[1:]
     if len(bits) != 3 or bits[1] != 'as':
-        raise template.TemplateSyntaxError("%r tag takes two arguments separated by 'as'" %
-                                           token.contents.split()[0])
+        raise template.TemplateSyntaxError(
+            "%r tag takes two arguments separated by 'as'" %
+            token.contents.split()[0]
+        )
     obj = bits[0]
     var = bits[2]
     nodelist = parser.parse(('endwithperms',))
index 4b7651c..d1250aa 100644 (file)
@@ -12,6 +12,4 @@ recipe = auf.recipe.django
 project = tests
 settings = settings
 eggs = ${buildout:eggs}
-test =
-    food
-    simple_tests
+test = simpletests
index a0756da..895d073 100644 (file)
@@ -16,7 +16,7 @@ import sys, os
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
+sys.path.append(os.path.abspath('..'))
 
 # -- General configuration -----------------------------------------------------
 
@@ -48,9 +48,9 @@ copyright = u'2012, Eric Mc Sween'
 # built documents.
 #
 # The short X.Y version.
-version = '0.2'
+version = '0.4'
 # The full version, including alpha/beta/rc tags.
-release = '0.2'
+release = '0.4'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
index 90f5c4b..30c86ad 100644 (file)
@@ -10,10 +10,10 @@ Documentation d'auf.django.permissions
    :maxdepth: 2
 
    installation
+   roles
    utilisation
 
 
-
 Indices and tables
 ==================
 
index e5ccc7c..86aaffe 100644 (file)
@@ -1,13 +1,15 @@
+.. _installation:
+
 Installation
 ============
 
 Installez l'app dans votre configuration Django::
 
-    INSTALLED_APPS = {
+    INSTALLED_APPS = (
         ...
         'auf.django.permissions',
         ...
-    }
+    )
 
 Puis, configurez le backend d'authentification::
 
@@ -17,15 +19,10 @@ Puis, configurez le backend d'authentification::
         ...
     )
 
-Vous devez maintenant créer un module qui contiendra vos règles d'accès. Par
-exemple: ``monprojet/permissions.py``. Au départ, il contiendra un jeu de règles
-vide::
-
-    from auf.django.permissions import Rules
-
-    rules = Rules()
+Et le middleware pour gérer les échecs d'autorisation::
 
-Vous pouvez maintenant retourner dans votre configuration Django et indiquer où
-se trouve votre jeu de règles::
-
-    AUF_PERMISSIONS_RULES = 'monprojet.permissions.rules'
+    MIDDLEWARE_CLASSES = (
+        ...
+        'auf.django.permissions.PermissionDeniedMiddleware',
+        ...
+    )
diff --git a/doc/roles.rst b/doc/roles.rst
new file mode 100644 (file)
index 0000000..2d8ba6d
--- /dev/null
@@ -0,0 +1,140 @@
+Rôles
+=====
+
+Dans le cadre d':mod:`auf.django.permissions`, les permissions sont attribuées aux
+utilisateurs par des rôles. Un rôle est un objet qui détermine si l'utilisateur
+a une permission donnée et sur quels objets cette permission s'applique. Un
+utilisateur peut avoir plusieurs rôles et se verra attribuer le cumul des
+permissions données par chacun de ses rôles.
+
+Un rôle doit fournir les deux méthodes suivantes:
+
+.. function:: has_perm(self, perm)
+
+   Retourne un booléen qui indique si un utilisateur muni de ce rôle a la
+   permission *perm*.
+
+.. function:: get_filter_for_perm(self, perm, model)
+
+   Retourne un filtre qui indique pour quelles instances du modèle Django
+   *model* un utilisateur muni de ce rôle a la permission *perm*.  Ce filtre
+   peut être un booléen ou un objet Q.
+
+   Si le filtre est ``True``, l'utilisateur a la permission pour toutes les
+   instances.
+
+   Si le filtre est ``False``, l'utilisateur n'a la permission pour aucune des
+   instances.
+
+   Si le filtre est un objet Q, l'utilisateur a la permission pour les
+   instances qui satisfont l'objet Q.
+
+On pourrait, par exemple, définir des rôles d'éditeur pour chaque section d'un
+journal de la façon suivante::
+
+    class Editeur(object):
+
+        def __init__(self, section):
+            self.section = section
+
+        def has_perm(self, perm):
+            if perm == 'lire_le_journal':
+                return True
+            return False
+
+        def get_filter_for_perm(self, perm, model):
+            if model is Article:
+                if perm == 'voir':
+                    return True
+                if perm == 'editer':
+                    return Q(section=self.section)
+            return False
+
+    editeur_sports = Editeur('sports')
+    editeur_voyages = Editeur('voyages')
+
+Ainsi, le rôle ``editeur_sports`` donne à ses utilisateurs la permission globale
+``lire_le_journal``, la permission ``voir`` pour tous les articles, et la
+permission ``editer`` pour les articles de la section des sports seulement.
+
+Fournisseurs de rôles
+---------------------
+
+Une fois que les rôles sont définis, il faut indiquer à
+:mod:`auf.django.permissions` quels rôles sont attribués à quels utilisateurs.
+Pour ce faire, on définit des fournisseurs de rôles. Un fournisseur de rôles est
+simplement une fonction qui prend un utilisateur en paramètre et qui retourne
+une liste de rôles. Par exemple::
+
+    def mon_fournisseur(user):
+        if user.username == 'alice':
+            return [Editeur('sports')]
+        elif user.username == 'bob':
+            return [Editeur('voyages'), Editeur('affaires')]
+        else:
+            return []
+
+Ce fournisseur de rôles fait d'Alice l'éditrice des sports et de Bob l'éditeur
+des sections voyages et affaires. Pour enregistrer cette fonction comme
+fournisseur de rôles, il suffit de l'ajouter aux settings Django comme ceci::
+
+    ROLE_PROVIDERS = (
+        'chemin.du.module.mon_fournisseur',
+        ...
+    )
+
+Un rôle peut être un modèle
+---------------------------
+
+Dans bien des cas, on voudra faire des rôles un modèle Django, histoire de
+pouvoir associer les rôles aux utilisateurs via l'admin. Reprenons notre exemple
+de journal et définissons deux rôles: les journalistes pourront rédiger un
+article dans n'importe quelle section, et les éditeurs pourront éditer un
+article dans leur section::
+
+    from django.contrib.auth.models import User
+    from django.db import models
+    from django.db.models import Q
+
+
+    class JournalRole(models.Model):
+        ROLE_CHOICES = (
+            (u'journaliste', u'Journaliste'),
+            (u'editeur', u'Éditeur'),
+        )
+        SECTION_CHOICES = (
+            (u'sports', u'Sports'),
+            (u'affaires', u'Affaires'),
+            (u'voyages', u'Voyages'),
+        )
+
+        user = models.ForeignKey(User, related_name='roles')
+        type = models.CharField(max_length=15, choices=ROLE_CHOICES)
+        section = models.CharField(max_length=15, choices=SECTION_CHOICES)
+
+        def has_perm(self, perm):
+            return False
+
+        def get_filter_for_model(self, perm, model):
+            if self.type == 'journaliste':
+                if model is Article and perm == 'rediger':
+                    return True
+
+            if self.type == 'editeur':
+                if model is Article and perm == 'editer':
+                    return Q(section=self.section)
+
+            return False
+
+On pourra, par exemple, permettre l'association de ces rôles aux utilisateurs en
+ajoutant un "inline" à l'admin des utilisateurs.
+
+Puisqu'on a mis en place la relation inverse des utilisateurs vers les rôles à
+l'aide du paramètre ``related_name`` du champ ``user``, on peut créer facilement
+un fournisseur de rôles::
+
+    def fournisseur(user):
+        if user.is_anonymous():
+            return []
+        else:
+            return user.roles.all()
index e39d3cf..971b977 100644 (file)
 Utilisation
 ===========
 
-Permissions globales
---------------------
+Une fois qu'on a défini nos rôles, :mod:`auf.django.permissions` nous offre
+toutes sortes de façons d'en faire usage.
 
-``auf.django.permissions`` ajoute un système de permissions globales distinct de
-celui de Django. C'est un système très simple qui permet d'associer globalement
-une permission à un utilisateur. Ces permissions globales sont identifiées par
-une chaîne de caractères et, contrairement aux permissions Django, ne sont
-associées ni à une app, ni à un modèle.
+Système de permissions de Django
+--------------------------------
 
-Ces permissions globales sont disponibles à partir du manager
-``global_permissions`` des objets ``User`` et ``Group`` de Django. Les objets
-gérés par ce manager ont un seul attribut, ``codename``, qui contient le nom de
-la permission à donner à l'utilisateur ou au groupe. On gère donc les
-permissions globales de la façon suivante::
+Grâce au backend d'authentification configuré à la section
+:ref:`installation`, les permissions données par les rôles seront disponibles
+dans le système de permissions de Django. On pourra donc vérifier si un
+utilisateur a la permission de lire le journal::
 
-    user = User.objects.get(username='alice')
+    if user.has_perm('lire_le_journal'):
+        ...
 
-    # Ajouter une permission
-    user.global_permissions.create(codename='monapp.voir_les_documents_confidentiels')
+ou s'il a la permission d'éditer un article en particulier::
 
-    # Supprimer une permission
-    user.global_permissions.filter(codename='monapp.voir_les_fichiers_top_secret').delete()
+    if user.has_perm('editer', article):
+        ...
 
-    # Afficher une liste de toutes les permissions
-    for p in user.global_permissions.all():
-        print p.codename
+Protection des vues
+-------------------
 
+:mod:`auf.django.permissions` offre une alternative au décorateur
+:func:`permission_required` de Django. Au lieu de décorer la vue à protéger, on
+appelera la fonction :func:`require_permission`.
 
+.. function:: require_permission(user, perm, obj=None)
+
+    Vérifie si *user* a la permission *perm* pour l'objet *obj*. Si *obj* n'est
+    pas donné, vérifie simplement si *user* a la permission globale *perm*. Si
+    l'utilisateur n'a pas la permission demandée, l'exception
+    :exc:`django.core.exceptions.PermissionDenied` est lancée.
+
+Lorsque :func:`require_permission` refuse l'accès, l'exception
+:exc:`PermissionDenied` est lancée, ce qui fait réagir le
+middleware :class:`PermissionDeniedMiddleware` configuré à la section
+:ref:`installation`. Ce middleware réagit d'une des deux façons suivantes:
+
+* Si l'utilisateur n'est pas connecté (anonyme), il redirige à la page de
+  connexion définie par le *setting* ``LOGIN_URL``.
+* Si l'utilisateur est connecté, il génère une page 403 en utilisant le template
+  :file:`403.html`.
+
+Filtrage des querysets
+----------------------
+
+:mod:`auf.django.permissions` ajoute la méthode ``with_perm(user, perm)``
+aux managers et querysets de Django. Cette méthode filtre le queryset en ne
+gardant que les objets pour lesquels *user* a la permission *perm*. Par exemple,
+pour avoir une liste des articles qu'Alice peut éditer, on écrira simplement::
+
+    Article.objects.with_perm(alice, 'editer')
+
+Il est à noter que cette fonctionnalité n'est pas intégrée avec le système de
+permissions de Django et que seules les permissions définies par des rôles
+peuvent être utilisées.
+
+Templates
+---------
+
+Puisque les permissions d':mod:`auf.django.permissions` sont intégrées dans le
+système de permissions de Django, on peut se servir du *context processor*
+standard de Django pour tester les permissions:
+
+.. code-block:: django
+
+    {% if perms.lire_le_journal %}
+    ...
+    {% endif %}
+
+Pour que l'objet *perms* soit disponible, il faut que le *context processor*
+soit enregistré dans les settings::
+
+    TEMPLATE_CONTEXT_PROCESSORS = (
+        'django.contrib.auth.context_processors.auth',
+        ...
+    )
+
+Ce mécanisme fonctionne bien pour les permissions générales, mais pour les
+permissions liées à un objet, il faut des mécanismes supplémentaires.
+:mod:`auf.django.permissions` offre les *template tags* suivants:
+
+{% ifhasperm *perm* *obj* %}
+    Ce *template tag* exécute son contenu seulement si l'utilisateur courant a
+    la permission *perm* pour l'objet *obj*.
+
+    Par exemple:
+
+    .. code-block:: django
+
+        {% ifhasperm "editer" article %}
+            Vous pouvez éditer cet article.
+        {% else %}
+            Vous ne pouvez pas éditer cet article.
+        {% endif %}
+
+{% withperms *obj* as *obj_perms* %}
+    Ce *template tag* crée une nouvelle variable *obj_perms* qui se comporte un peu
+    comme la variable *perms* de Django, mais qui permet de tester les
+    permissions de l'objet en question. C'est utile lorsqu'on veut tester
+    plusieurs permissions sur un même objet:
+
+    .. code-block:: django
+
+        {% withperms article as article_perms %}
+        {% if article_perms.editer or article_perms.rediger %}
+            Vous pouvez agir sur cet article
+        {% elif article_perms.voir %}
+            Vous pourrez au moins voir cet article
+        {% endif %}
+
+Pour que ces *template tags* fonctionnent, le template doit avoir accès à la
+requête. Il faut donc ajouter ceci aux settings::
+
+    TEMPLATE_CONTEXT_PROCESSORS = (
+        'django.core.context_processors.request',
+        ...
+    )
index 571345f..10f1f1d 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@
 from setuptools import setup, find_packages
 
 name = 'auf.django.permissions'
-version = '0.3'
+version = '0.4'
 
 setup(name=name,
       version=version,
diff --git a/tests/food/__init__.py b/tests/food/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/tests/food/models.py b/tests/food/models.py
deleted file mode 100644 (file)
index 99906ba..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-from django.contrib.auth.models import User
-from django.db import models
-
-
-class Food(models.Model):
-    name = models.CharField(max_length=100)
-    allergic_users = models.ManyToManyField(User, related_name='allergies')
-
-    def __repr__(self):
-        return "<Food %r>" % self.name
diff --git a/tests/food/tests.py b/tests/food/tests.py
deleted file mode 100644 (file)
index b57cdaa..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-from __future__ import absolute_import
-
-from django.contrib.auth.models import User
-from django.db.models import Q
-from django.http import HttpRequest
-from django.template import Template, RequestContext
-from django.utils import unittest
-
-from auf.django.permissions import Predicate
-
-from tests.food.models import Food
-from tests.permissions import rules
-
-
-@Predicate
-def is_allergic(user, obj, cls):
-    return Q(allergic_users=user)
-
-
-class FoodTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.alice = User.objects.create_user('alice', 'alice@example.com')
-        self.bob = User.objects.create_user('bob', 'bob@example.com')
-        self.apple = Food.objects.create(name='apple')
-        self.banana = Food.objects.create(name='banana')
-        self.banana.allergic_users.add(self.alice)
-        rules.clear()
-
-    def tearDown(self):
-        self.alice.delete()
-        self.bob.delete()
-        self.apple.delete()
-        self.banana.delete()
-
-
-class RulesTestCase(FoodTestCase):
-
-    def test_global_perms(self):
-        rules.allow_global('sing', Predicate(lambda user: user is self.alice))
-        self.assertTrue(self.alice.has_perm('sing'))
-        self.assertFalse(self.alice.has_perm('dance'))
-
-    def test_global_deny(self):
-        rules.allow_global('eat', Predicate(True))
-        rules.deny_global('eat', Predicate(lambda user: user is self.bob))
-        self.assertTrue(self.alice.has_perm('eat'))
-        self.assertFalse(self.bob.has_perm('eat'))
-
-    def test_object_perms(self):
-        rules.allow('eat', Food, ~is_allergic)
-        self.assertTrue(self.alice.has_perm('eat', self.apple))
-        self.assertFalse(self.alice.has_perm('eat', self.banana))
-
-    def test_object_deny(self):
-        rules.allow('eat', Food, Predicate(True))
-        rules.deny('eat', Food, is_allergic)
-        self.assertTrue(self.alice.has_perm('eat', self.apple))
-        self.assertFalse(self.alice.has_perm('eat', self.banana))
-
-    def test_no_rules(self):
-        self.assertFalse(self.alice.has_perm('climb'))
-        rules.allow('eat', Food, Predicate(True))
-        self.assertTrue(self.alice.has_perm('eat', self.apple))
-        self.assertFalse(self.alice.has_perm('eat', self.bob))
-
-    def test_q_rules(self):
-        rules.allow('eat', Food, ~is_allergic)
-        self.assertListEqual(
-            list(rules.filter_queryset(self.alice, 'eat', Food.objects.all())),
-            [self.apple]
-        )
-        self.assertListEqual(
-            list(rules.filter_queryset(self.bob, 'eat', Food.objects.all())),
-            [self.apple, self.banana]
-        )
-        self.assertTrue(self.alice.has_perm('eat', self.apple))
-        self.assertFalse(self.alice.has_perm('eat', self.banana))
-
-
-class TemplateTagsTestCase(FoodTestCase):
-
-    def setUp(self):
-        FoodTestCase.setUp(self)
-        rules.allow('eat', Food, ~is_allergic)
-        rules.allow('throw', Food, Predicate(True))
-
-    def test_ifhasperm(self):
-        template = Template("""{% load permissions %}
-{% for fruit in food %}
-{% ifhasperm "eat" fruit %}
-Eat the {{ fruit.name }}
-{% else %}
-Don't eat the {{ fruit.name }}
-{% endifhasperm %}
-{% endfor %}""")
-        request = HttpRequest()
-        request.user = self.alice
-        context = RequestContext(request, {'food': Food.objects.all()})
-        self.assertRegexpMatches(template.render(context).strip(),
-                                 r'\s+'.join([
-                                     "Eat the apple",
-                                     "Don't eat the banana"
-                                 ])
-                                )
-
-    def test_withperms(self):
-        template = Template("""{% load permissions %}
-{% for fruit in food %}
-{% withperms fruit as fruit_perms %}
-Eat {{ fruit.name }}: {{ fruit_perms.eat|yesno }}
-Throw {{ fruit.name }}: {{ fruit_perms.throw|yesno }}
-Smoke {{ fruit.name }}: {{ fruit_perms.smoke|yesno }}
-{% endwithperms %}
-{% endfor %}""")
-        request = HttpRequest()
-        request.user = self.alice
-        context = RequestContext(request, {'food': Food.objects.all()})
-        self.assertRegexpMatches(template.render(context).strip(),
-                                 r'\s+'.join([
-                                     "Eat apple: yes",
-                                     "Throw apple: yes",
-                                     "Smoke apple: no",
-                                     "Eat banana: no",
-                                     "Throw banana: yes",
-                                     "Smoke banana: no"
-                                 ])
-                                )
diff --git a/tests/permissions.py b/tests/permissions.py
deleted file mode 100644 (file)
index a62a047..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-from auf.django.permissions import Rules
-
-rules = Rules()
index eacd61b..27238f4 100644 (file)
@@ -9,8 +9,7 @@ INSTALLED_APPS = (
     'django.contrib.auth',
     'django.contrib.contenttypes',
     'auf.django.permissions',
-    'tests.food',
-    'tests.simple_tests'
+    'tests.simpletests',
 )
 
 AUTHENTICATION_BACKENDS = (
@@ -25,4 +24,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
     'django.core.context_processors.request',
 )
 
-AUF_PERMISSIONS_RULES = 'tests.permissions.rules'
+ROLE_PROVIDERS = (
+    'tests.simpletests.role_provider',
+)
+
+SECRET_KEY = 'not-very-secret'
diff --git a/tests/simple_tests/__init__.py b/tests/simple_tests/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/tests/simple_tests/models.py b/tests/simple_tests/models.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/tests/simple_tests/tests.py b/tests/simple_tests/tests.py
deleted file mode 100644 (file)
index 03c78d7..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-from __future__ import absolute_import
-
-from django.contrib.auth.models import User, Group
-from django.utils import unittest
-
-
-class GlobalPermsTestCase(unittest.TestCase):
-
-    def setUp(self):
-        self.alice = User.objects.create_user('alice', 'alice@example.com')
-
-    def test_add_remove_user_permissions(self):
-        self.alice.global_permissions.add_permission('open_door')
-        self.assertSetEqual(
-            self.alice.global_permissions.get_permissions(),
-            set(['open_door'])
-        )
-        self.alice.global_permissions.add_permission('close_door')
-        self.assertSetEqual(
-            self.alice.global_permissions.get_permissions(),
-            set(['open_door', 'close_door'])
-        )
-        self.alice.global_permissions.remove_permission('open_door')
-        self.assertSetEqual(
-            self.alice.global_permissions.get_permissions(),
-            set(['close_door'])
-        )
-        self.alice.global_permissions.add_permission('close_door')
-        self.assertSetEqual(
-            self.alice.global_permissions.get_permissions(),
-            set(['close_door'])
-        )
-        self.alice.global_permissions.remove_permission('open_door')
-        self.assertSetEqual(
-            self.alice.global_permissions.get_permissions(),
-            set(['close_door'])
-        )
diff --git a/tests/simpletests/__init__.py b/tests/simpletests/__init__.py
new file mode 100644 (file)
index 0000000..61af87f
--- /dev/null
@@ -0,0 +1,24 @@
+from __future__ import absolute_import
+
+from auf.django.permissions import Role
+from django.db.models import Q
+
+from tests.simpletests.models import Food
+
+
+def role_provider(user):
+    if user.username == 'alice':
+        return [VegetarianRole()]
+    else:
+        return []
+
+
+class VegetarianRole(Role):
+
+    def has_perm(self, perm):
+        return perm in ('eat', 'pray', 'love')
+
+    def get_filter_for_perm(self, perm, model):
+        if model is Food and perm == 'eat':
+            return Q(is_meat=False)
+        return False
diff --git a/tests/simpletests/models.py b/tests/simpletests/models.py
new file mode 100644 (file)
index 0000000..0da9de0
--- /dev/null
@@ -0,0 +1,5 @@
+from django.db import models
+
+
+class Food(models.Model):
+    is_meat = models.BooleanField()
diff --git a/tests/simpletests/tests.py b/tests/simpletests/tests.py
new file mode 100644 (file)
index 0000000..db4c441
--- /dev/null
@@ -0,0 +1,29 @@
+from __future__ import absolute_import
+
+from django.contrib.auth.models import User
+from django.test import TransactionTestCase
+
+from tests.simpletests.models import Food
+
+
+class HasPermTestCase(TransactionTestCase):
+
+    def setUp(self):
+        self.alice = User.objects.create(username='alice')
+        self.carrot = Food.objects.create(is_meat=False)
+        self.celery = Food.objects.create(is_meat=False)
+        self.steak = Food.objects.create(is_meat=True)
+
+    def test_global_permissions(self):
+        self.assertTrue(self.alice.has_perm('eat'))
+        self.assertFalse(self.alice.has_perm('sleep'))
+
+    def test_object_permissions(self):
+        self.assertTrue(self.alice.has_perm('eat', self.carrot))
+        self.assertFalse(self.alice.has_perm('eat', self.steak))
+
+    def test_queryset_filtering(self):
+        actual = Food.objects.with_perm(self.alice, 'eat') \
+                .values_list('id', flat=True)
+        expected = [x.id for x in (self.carrot, self.celery)]
+        self.assertItemsEqual(actual, expected)