Skip to content

Commit

Permalink
Merge 811994c into ea89022
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicolas Delaby committed Sep 4, 2014
2 parents ea89022 + 811994c commit 48f2a45
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 47 deletions.
11 changes: 6 additions & 5 deletions rules/permissions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .predicates import NOT_GIVEN
from .rulesets import RuleSet


Expand All @@ -16,16 +17,16 @@ def perm_exists(name):
return permissions.rule_exists(name)


def has_perm(name, obj=None, target=None):
return permissions.test_rule(name, obj, target)
def has_perm(name, *args):
return permissions.test_rule(name, *args)


class ObjectPermissionBackend(object):
def authenticate(self, username, password):
return None
def has_perm(self, user, perm, obj=None):

def has_perm(self, user, perm, obj=NOT_GIVEN):
return has_perm(perm, user, obj)

def has_module_perms(self, user, app_label):
return has_perm(app_label, user)
58 changes: 30 additions & 28 deletions rules/predicates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from functools import update_wrapper


NOT_GIVEN = set() # object that have a False boolean evaluation


class Predicate(object):
def __init__(self, fn, name=None):
# fn can be a callable with any of the following signatures:
Expand All @@ -23,61 +26,60 @@ def __init__(self, fn, name=None):
self.fn = fn
self.num_args = num_args
self.name = name or fn.__name__

def __repr__(self):
return '<%s:%s object at %s>' % (
type(self).__name__, str(self), hex(id(self)))

def __str__(self):
return self.name

def __call__(self, *args, **kwargs):
# this method is defined as variadic in order to not mask the
# underlying callable's signature that was most likely decorated
# as a predicate. internally we consistently call ``test`` that
# provides a single interface to the callable.
return self.fn(*args, **kwargs)

def __and__(self, other):
def AND(obj=None, target=None):
def AND(obj=NOT_GIVEN, target=NOT_GIVEN):
return self.test(obj, target) and other.test(obj, target)
return type(self)(AND, '(%s & %s)' % (self.name, other.name))

def __or__(self, other):
def OR(obj=None, target=None):
def OR(obj=NOT_GIVEN, target=NOT_GIVEN):
return self.test(obj, target) or other.test(obj, target)
return type(self)(OR, '(%s | %s)' % (self.name, other.name))

def __xor__(self, other):
def XOR(obj=None, target=None):
def XOR(obj=NOT_GIVEN, target=NOT_GIVEN):
return self.test(obj, target) ^ other.test(obj, target)
return type(self)(XOR, '(%s ^ %s)' % (self.name, other.name))

def __invert__(self):
def INVERT(obj=None, target=None):
def INVERT(obj=NOT_GIVEN, target=NOT_GIVEN):
return not self.test(obj, target)
if self.name.startswith('~'):
name = self.name[1:]
else:
name = '~' + self.name
return type(self)(INVERT, name)
def test(self, obj=None, target=None):

def test(self, *args):
# we setup a list of function args depending on the number of
# arguments accepted by the underlying callback.
if self.num_args == 2:
args = (obj, target)
elif self.num_args == 1:
args = (obj,)
else:
args = ()
return bool(self.fn(*args))
passed_args = args[:self.num_args]
diff_length = self.num_args - len(passed_args)
if diff_length:
# fill in missing argument with NOT_GIVEN marker
passed_args = passed_args + (NOT_GIVEN,) * diff_length
return bool(self.fn(*passed_args))


def predicate(fn=None, name=None):
"""
Decorator that constructs a ``Predicate`` instance from any function::
>>> @predicate
... def is_book_author(user, book):
... return user == book.author
Expand All @@ -86,14 +88,14 @@ def predicate(fn=None, name=None):
if not name and not callable(fn):
name = fn
fn = None

def inner(fn):
if isinstance(fn, Predicate):
return fn
p = Predicate(fn, name)
update_wrapper(p, fn)
return p

if fn:
return inner(fn)
else:
Expand All @@ -103,7 +105,7 @@ def inner(fn):
# Predefined predicates

always_allow = predicate(lambda: True, name='always_allow')
always_deny = predicate(lambda: False, name='always_deny')
always_deny = predicate(lambda: False, name='always_deny')


@predicate
Expand Down Expand Up @@ -136,20 +138,20 @@ def is_active(user):

def is_group_member(*groups):
assert len(groups) > 0, 'You must provide at least one group name'

if len(groups) > 3:
g = groups[:3] + ('...',)
else:
g = groups

name = 'is_group_member:%s' % ','.join(g)

@predicate(name)
def fn(user):
if not hasattr(user, 'groups'):
return False # swapped user model, doesn't support groups
if not hasattr(user, '_group_names_cache'):
user._group_names_cache = set(user.groups.values_list('name', flat=True))
return set(groups).issubset(user._group_names_cache)

return fn
16 changes: 8 additions & 8 deletions rules/rulesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@


class RuleSet(dict):
def test_rule(self, name, obj=None, target=None):
return name in self and self[name].test(obj, target)
def test_rule(self, name, *args):
return name in self and self[name].test(*args)

def rule_exists(self, name):
return name in self

def add_rule(self, name, pred):
if name in self:
raise KeyError('A rule with name `%s` already exists' % name)
self[name] = pred

def remove_rule(self, name):
del self[name]

def __setitem__(self, name, pred):
fn = predicate(pred)
super(RuleSet, self).__setitem__(name, fn)
Expand All @@ -38,5 +38,5 @@ def rule_exists(name):
return default_rules.rule_exists(name)


def test_rule(name, obj=None, target=None):
return default_rules.test_rule(name, obj, target)
def test_rule(name, *args):
return default_rules.test_rule(name, *args)
56 changes: 53 additions & 3 deletions tests/testsuite/test_permissions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from nose.tools import with_setup
from nose.tools import with_setup, assert_raises

from rules.predicates import predicate
from rules.predicates import predicate, NOT_GIVEN
from rules.permissions import (permissions, add_perm, remove_perm,
perm_exists, has_perm, ObjectPermissionBackend)

Expand All @@ -17,6 +17,27 @@ def always_true():
return True


@predicate
def expect_two_args(user, perm):
assert user is not NOT_GIVEN
assert perm is not NOT_GIVEN
return True


@predicate
def expect_one_arg(user, perm):
assert user is not NOT_GIVEN
assert perm is NOT_GIVEN
return True


@predicate
def expect_no_arg(user, perm):
assert user is NOT_GIVEN
assert perm is NOT_GIVEN
return True


@with_setup(reset_ruleset(permissions), reset_ruleset(permissions))
def test_permissions_ruleset():
add_perm('can_edit_book', always_true)
Expand All @@ -31,10 +52,39 @@ def test_permissions_ruleset():
def test_backend():
backend = ObjectPermissionBackend()
assert backend.authenticate('someuser', 'password') is None

add_perm('can_edit_book', always_true)
assert 'can_edit_book' in permissions
assert backend.has_perm(None, 'can_edit_book')
assert backend.has_module_perms(None, 'can_edit_book')
remove_perm('can_edit_book')
assert not perm_exists('can_edit_book')


@with_setup(reset_ruleset(permissions), reset_ruleset(permissions))
def test_backend_with_not_given():
backend = ObjectPermissionBackend()
user = object()
obj= object()

add_perm('can_edit_book_2', expect_two_args)
add_perm('can_edit_book_1', expect_one_arg)
assert 'can_edit_book_2' in permissions
assert 'can_edit_book_1' in permissions
assert backend.has_perm(user, 'can_edit_book_2', obj)
assert backend.has_perm(user, 'can_edit_book_1')
assert_raises(AssertionError, backend.has_perm, user, 'can_edit_book_2')


def test_not_given_argument():
add_perm('two_args', expect_two_args)
add_perm('one_arg', expect_one_arg)
add_perm('no_arg', expect_no_arg)

assert has_perm('two_args', 'a', 'a')
assert_raises(AssertionError, has_perm, 'two_args', 'a')
assert_raises(AssertionError, has_perm, 'two_args')
assert has_perm('one_arg', 'a')
assert_raises(AssertionError, has_perm, 'one_arg')
assert has_perm('no_arg')
assert_raises(AssertionError, has_perm, 'no_arg', 'a', 'b')
23 changes: 21 additions & 2 deletions tests/testsuite/test_predicates.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from rules.predicates import Predicate, predicate
from nose.tools import assert_raises

from rules.predicates import Predicate, predicate, NOT_GIVEN


def test_lambda_predicate():
Expand Down Expand Up @@ -86,7 +88,7 @@ def always_true():
return True
assert always_true.name == 'foo'
assert always_true.num_args == 0

@predicate(name='bar')
def always_false():
return False
Expand Down Expand Up @@ -221,3 +223,20 @@ def p(a=None, b=None, *args, **kwargs):
assert a == 'a'
assert b == 'b'
p('a', b='b', c='c')


def test_positional_args_not_given():
@predicate
def p(a, b):
if b is NOT_GIVEN:
assert a == 'a'
elif a is NOT_GIVEN:
raise TypeError
else:
assert a == b

p('a', NOT_GIVEN)
p('a', 'a')
assert not NOT_GIVEN
assert_raises(TypeError, p)
assert_raises(AssertionError, p, 'a', 'b')
38 changes: 37 additions & 1 deletion tests/testsuite/test_rulesets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from nose.tools import nottest, with_setup, assert_raises

from rules.predicates import predicate
from rules.predicates import predicate, NOT_GIVEN
from rules.rulesets import (RuleSet, default_rules, add_rule, remove_rule,
rule_exists, test_rule)

Expand All @@ -20,6 +20,27 @@ def always_true():
return True


@predicate
def expect_two_args(a, b):
assert a is not NOT_GIVEN
assert b is not NOT_GIVEN
return True


@predicate
def expect_one_arg(a, b):
assert a is not NOT_GIVEN
assert b is NOT_GIVEN
return True


@predicate
def expect_no_arg(a, b):
assert a is NOT_GIVEN
assert b is NOT_GIVEN
return True


@with_setup(reset_ruleset(default_rules), reset_ruleset(default_rules))
def test_shared_ruleset():
add_rule('somerule', always_true)
Expand All @@ -39,3 +60,18 @@ def test_ruleset():
assert_raises(KeyError, ruleset.add_rule, 'somerule', always_true)
ruleset.remove_rule('somerule')
assert not ruleset.rule_exists('somerule')


def test_not_given_argument():
ruleset = RuleSet()
ruleset.add_rule('two_args', expect_two_args)
ruleset.add_rule('one_arg', expect_one_arg)
ruleset.add_rule('no_arg', expect_no_arg)

assert ruleset.test_rule('two_args', 'a', 'a')
assert_raises(AssertionError, ruleset.test_rule, 'two_args', 'a')
assert_raises(AssertionError, ruleset.test_rule, 'two_args')
assert ruleset.test_rule('one_arg', 'a')
assert_raises(AssertionError, ruleset.test_rule, 'one_arg')
assert ruleset.test_rule('no_arg')
assert_raises(AssertionError, ruleset.test_rule, 'no_arg', 'a', 'b')

0 comments on commit 48f2a45

Please sign in to comment.