Commit initial: version 0.1 v0.1
authorEric Mc Sween <eric.mcsween@auf.org>
Thu, 8 Dec 2011 15:58:30 +0000 (10:58 -0500)
committerEric Mc Sween <eric.mcsween@auf.org>
Thu, 8 Dec 2011 15:58:30 +0000 (10:58 -0500)
12 files changed:
.gitignore [new file with mode: 0644]
auf/__init__.py [new file with mode: 0644]
auf/django/__init__.py [new file with mode: 0644]
auf/django/permissions/__init__.py [new file with mode: 0644]
auf/django/permissions/backends.py [new file with mode: 0644]
auf/django/permissions/decorators.py [new file with mode: 0644]
auf/django/permissions/models.py [new file with mode: 0644]
auf/django/permissions/templatetags/__init__.py [new file with mode: 0644]
auf/django/permissions/templatetags/permissions.py [new file with mode: 0644]
auf/django/permissions/tests.py [new file with mode: 0644]
runtests.py [new file with mode: 0755]
setup.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..77eda72
--- /dev/null
@@ -0,0 +1,3 @@
+*.egg-info
+*.pyc
+/build
diff --git a/auf/__init__.py b/auf/__init__.py
new file mode 100644 (file)
index 0000000..3ad9513
--- /dev/null
@@ -0,0 +1,2 @@
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
diff --git a/auf/django/__init__.py b/auf/django/__init__.py
new file mode 100644 (file)
index 0000000..3ad9513
--- /dev/null
@@ -0,0 +1,2 @@
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
diff --git a/auf/django/permissions/__init__.py b/auf/django/permissions/__init__.py
new file mode 100644 (file)
index 0000000..05ae6c1
--- /dev/null
@@ -0,0 +1,92 @@
+from collections import defaultdict
+
+class Rules(object):
+
+    def __init__(self):
+        self.tests = defaultdict(list)
+        self.qs = defaultdict(list)
+        self.test_qs = defaultdict(list)
+
+    def clear(self):
+        self.tests.clear()
+        self.qs.clear()
+        self.test_qs.clear()
+
+    def add(self, perm, cls=None, test=None, q=None):
+        if test:
+            self.tests[(perm, cls)].append(test)
+        if q:
+            self.qs[(perm, cls)].append(q)
+            if not test:
+                self.test_qs[(perm, cls)].append(q)
+
+    def test(self, user, perm, obj=None):
+        if obj is None:
+            tests = self.tests[(perm, None)]
+            return any(test(user) for test in tests)
+
+        cls = type(obj)
+        tests = self.tests[(perm, cls)]
+        if any(test(user, obj) for test in tests):
+            return True
+        qs = self.test_qs[(perm, cls)]
+        if qs:
+            q = reduce(lambda x, y: x | y, (q(user) for q in qs))
+            if cls._default_manager.filter(q).filter(pk=obj.pk).exists():
+                return True
+        return False
+
+    def get_q(self, user, perm, cls):
+        qs = self.qs[(perm, cls)]
+        return reduce(lambda x, y: x | y, (q(user) for q in qs)) if qs else None
+
+allow_rules = Rules()
+deny_rules = Rules()
+
+def allow(perm, cls=None, test=None, q=None):
+    allow_rules.add(perm, cls, test, q)
+
+def deny(perm, cls=None, test=None, q=None):
+    deny_rules.add(perm, cls, test, q)
+
+def user_has_perm(user, perm, obj=None):
+    return not deny_rules.test(user, perm, obj) and allow_rules.test(user, perm, obj)
+
+def filter(user, perm, queryset):
+    deny_q = deny_rules.get_q(user, perm, queryset.model)
+    if deny_q:
+        queryset = queryset.exclude(deny_q)
+    allow_q = allow_rules.get_q(user, perm, queryset.model)
+    if allow_q:
+        queryset = queryset.filter(allow_q)
+    else:
+        queryset = queryset.none()
+    return queryset
+
+def clear_rules():
+    allow_rules.clear()
+    deny_rules.clear()
+
+def autodiscover():
+    """
+    Auto-discover INSTALLED_APPS permissions.py modules and fail silently when
+    not present. This forces an import on them to register permission rules.
+    This is freely adapted from django.contrib.admin.autodiscover()
+    """
+
+    import copy
+    from django.conf import settings
+    from django.utils.importlib import import_module
+    from django.utils.module_loading import module_has_submodule
+
+    for app in settings.INSTALLED_APPS:
+        mod = import_module(app)
+        # Attempt to import the app's permissions module.
+        try:
+            import_module('%s.permissions' % app)
+        except:
+            # Decide whether to bubble up this error. If the app just
+            # doesn't have a permissions module, we can ignore the error
+            # attempting to import it, otherwise we want it to bubble up.
+            if module_has_submodule(mod, 'permissions'):
+                raise
diff --git a/auf/django/permissions/backends.py b/auf/django/permissions/backends.py
new file mode 100644 (file)
index 0000000..3dc43b4
--- /dev/null
@@ -0,0 +1,9 @@
+from auf.django.permissions import user_has_perm
+
+class AuthenticationBackend(object):
+    supports_anonymous_user = True
+    supports_inactive_user = True
+    supports_object_permissions = True
+
+    def has_perm(self, user, perm, obj=None):
+        return user_has_perm(user, perm, obj)
diff --git a/auf/django/permissions/decorators.py b/auf/django/permissions/decorators.py
new file mode 100644 (file)
index 0000000..d3dc705
--- /dev/null
@@ -0,0 +1,17 @@
+from django.contrib.auth.decorators import user_passes_test
+from django.shortcuts import get_object_or_404
+
+def get_object(cls, perm=None, arg=0, kwarg=None):
+    def decorator(view_func):
+        def wrapped_view(request, *args, **kwargs):
+            pk = kwargs[kwarg] if kwarg else args[arg]
+            obj = get_object_or_404(cls, pk=pk)
+            if kwarg:
+                kwargs[kwarg] = obj
+            else:
+                args = list(args)      # Make args mutable
+                args[arg] = obj
+            f = user_passes_test(lambda user: user.has_perm(perm, obj))(view_func) if perm else view_func
+            return f(request, *args, **kwargs)
+        return wrapped_view
+    return decorator
diff --git a/auf/django/permissions/models.py b/auf/django/permissions/models.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/auf/django/permissions/templatetags/__init__.py b/auf/django/permissions/templatetags/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/auf/django/permissions/templatetags/permissions.py b/auf/django/permissions/templatetags/permissions.py
new file mode 100644 (file)
index 0000000..53f64cb
--- /dev/null
@@ -0,0 +1,82 @@
+from django import template
+
+register = template.Library()
+
+
+class IfHasPermNode(template.Node):
+
+    def __init__(self, perm, obj, nodelist_true, nodelist_false):
+        self.perm = template.Variable(perm)
+        self.obj = template.Variable(obj)
+        self.nodelist_true = nodelist_true
+        self.nodelist_false = nodelist_false
+
+    def render(self, context):
+        if 'user' in context:
+            user = context['user']
+        else:
+            user = context['request'].user
+        perm = self.perm.resolve(context)
+        obj = self.obj.resolve(context)
+        if user.has_perm(perm, obj):
+            return self.nodelist_true.render(context)
+        else:
+            return self.nodelist_false.render(context)
+
+
+class WithPermsNode(template.Node):
+
+    def __init__(self, obj, var, nodelist):
+        self.obj = template.Variable(obj)
+        self.var = var
+        self.nodelist = nodelist
+
+    def render(self, context):
+        if 'user' in context:
+            user = context['user']
+        else:
+            user = context['request'].user
+        obj = self.obj.resolve(context)
+        context.update({self.var: PermWrapper(user, obj)})
+        output = self.nodelist.render(context)
+        context.pop()
+        return output
+
+
+class PermWrapper(object):
+
+    def __init__(self, user, obj):
+        self.user = user
+        self.obj = obj
+
+    def __getitem__(self, perm):
+        return self.user.has_perm(perm, self.obj)
+
+
+@register.tag
+def ifhasperm(parser, token):
+    bits = token.split_contents()[1:]
+    if len(bits) != 2:
+        raise template.TemplateSyntaxError("%r tag requires two arguments" %
+                                           token.contents.split()[0])
+    perm, obj = bits
+    nodelist_true = parser.parse(('else', 'endifhasperm'))
+    token = parser.next_token()
+    if token.contents == 'else':
+        nodelist_false = parser.parse(('endifhasperm',))
+        parser.delete_first_token()
+    else:
+        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])
+    obj = bits[0]
+    var = bits[2]
+    nodelist = parser.parse(('endwithperms',))
+    parser.delete_first_token()
+    return WithPermsNode(obj, var, nodelist)
diff --git a/auf/django/permissions/tests.py b/auf/django/permissions/tests.py
new file mode 100644 (file)
index 0000000..8537d7f
--- /dev/null
@@ -0,0 +1,127 @@
+import re
+
+from django.contrib.auth.models import User
+from django.db import models
+from django.db.models import Q
+from django.http import HttpRequest
+from django.template import Template, RequestContext
+from django.utils import unittest
+from django.utils.text import normalize_newlines
+
+import auf.django.permissions as perms
+
+
+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
+    
+
+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)
+
+    def tearDown(self):
+        self.alice.delete()
+        self.bob.delete()
+        self.apple.delete()
+        self.banana.delete()
+        perms.clear_rules()
+
+
+class RulesTestCase(FoodTestCase):
+
+    def test_global_perms(self):
+        perms.allow('sing', test=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):
+        perms.allow('eat', test=lambda user: True)
+        perms.deny('eat', test=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):
+        perms.allow('eat', Food, test=lambda user, obj: obj not in user.allergies.all())
+        self.assertTrue(self.alice.has_perm('eat', self.apple))
+        self.assertFalse(self.alice.has_perm('eat', self.banana))
+
+    def test_object_deny(self):
+        perms.allow('eat', Food, test=lambda user, obj: True)
+        perms.deny('eat', Food, test=lambda user, obj: obj in user.allergies.all())
+        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'))
+        perms.allow('eat', Food, test=lambda user, obj: True)
+        self.assertTrue(self.alice.has_perm('eat', self.apple))
+        self.assertFalse(self.alice.has_perm('eat', self.bob))
+
+    def test_q_rules(self):
+        perms.allow('eat', Food, q=lambda user: ~Q(allergic_users=user))
+        self.assertListEqual(list(perms.filter(self.alice, 'eat', Food.objects.all())),
+                             [self.apple])
+        self.assertListEqual(list(perms.filter(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)
+        perms.allow('eat', Food, 
+                    test=lambda user, obj: obj not in user.allergies.all(),
+                    q=lambda user: ~Q(allergic_users=user))
+        perms.allow('throw', Food, test=lambda user, obj: 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/runtests.py b/runtests.py
new file mode 100755 (executable)
index 0000000..8cd3d66
--- /dev/null
@@ -0,0 +1,28 @@
+#!/usr/bin/python2
+
+from django.conf import settings
+from django.core.management import call_command
+
+settings.configure(
+    DATABASES={
+        'default': {
+            'ENGINE': 'django.db.backends.sqlite3',
+            'NAME': ':memory:'
+        }
+    },
+    INSTALLED_APPS=(
+        'django.contrib.auth', 
+        'django.contrib.contenttypes',
+        'auf.django.permissions'
+    ),
+    AUTHENTICATION_BACKENDS=(
+        'auf.django.permissions.backends.AuthenticationBackend',
+    ),
+    TEMPLATE_LOADERS=(
+        'django.template.loaders.app_directories.Loader',
+    ),
+    TEMPLATE_CONTEXT_PROCESSORS=(
+        'django.core.context_processors.request',
+    )
+)
+call_command('test', 'permissions')
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..80eeae5
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,15 @@
+# encoding: utf-8
+
+from distutils.core import setup
+
+name = 'auf.django.permissions'
+
+setup(name=name,
+      version='0.1',
+      description="Extensions au système de permissions de Django",
+      author='Éric Mc Sween',
+      author_email='eric.mcsween@auf.org',
+      url='http://pypi.auf.org/%s' % name,
+      license='GPL',
+      packages=['auf.django.permissions'],
+     )