dfunckt / django-rules

Awesome Django authorization, without the database

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Rule chain for if A: return B

BoPeng opened this issue · comments

commented

I have some rules like

@rules.predicate
def can_do_something(user, target):
    if user.is_anonymous:
        return test_A(user, target)
    elif user.is_admin:
        return test_B(user, target)
    else:
        return test_C(user, target)

which I would like to translate to

can_do_something = (rules.is_anonymous & test_A) | (rules.is_admin & test_B) | (rules.is_authenticated & test_C)

The problem is that in the original implementation if user.is_anonymous is True, the checker will return immediately with test_A, but in the new implementation the checker will go through all cases if is_anonymous & test_A is False. How do I avoid evaluating all three cases? It looks like I am missing something obvious here.

Make sure “test_A”, “test_B”, etc. are predicates themselves and it should work.

commented

Both version works but as I said I prefer the second cleaner version but it is less "efficient" (which perhaps does not matter) because it evaluate all three conditions.

I was asking if there is a trick for

(rules.is_anonymous & test_A) | ...

returning test_A if rules.is_anonumous is True, without going into the next condition, as the function version does.

Is test_A a predicate? As in, did you define it like so?

@rules.predicate
def test_A(...):
  return some_condition
commented

Yes, it is.

commented

Sorry, the demo for the my function version was not written correctly. I am updating it.

Can you reduce this down to a minimal example (that I can just copy/paste and run) that reproduces this please?

commented
import rules

user = {'anonymous': True, 'admin': False }
target = {'test_A': False, 'test_B': False }

@rules.predicate
def is_anonymous(user):
    print('tested for anonymous')
    return user['anonymous']

@rules.predicate
def is_admin(user):
    print('tested for admin')
    return user['admin']

@rules.predicate
def test_A(user, target):
    print('tested for A')
    return target['test_A']

@rules.predicate
def test_B(user, target):
    print('tested for B')
    return target['test_B']


@rules.predicate
def func_form(user, target):
    if is_anonymous(user):
        return test_A(user, target)
    elif is_admin(user):
        return test_B(user, target)

chain_form = (is_anonymous & test_A ) | (is_admin & test_B ) 


print('Function form')
func_form(user, target)

print('\nChain form')
chain_form(user, target)

with output

Function form
tested for anonymous
tested for A

Chain form
tested for anonymous
tested for A
tested for admin

Note that I know beforehand that is_anonymous, is_admin etc are mutually exclusive so there is no need to test other branches.

Thanks, I see now. Seems I misread this bit in your message:

The problem is that in the original implementation if user.is_anonymous is True, the checker will return immediately with test_A, but in the new implementation the checker will go through all cases if is_anonymous & test_A is False

Well, that is expected behaviour, you're not missing anything. Predicates combine and behave similar to regular boolean algebra:

def test_A():
  return False

def test_B():
  return True

test_A() or test_B()
# yields `True`

The two forms (ie. "function" and "chain" form) are not equivalent. To achieve the behaviour you describe, you should use the first one.

commented

Thanks for your clarification, at least I did not miss any trick from the documentation.

To clarify somewhat, the "chain" form is as if you wrote the "function" form like so:

@rules.predicate
def can_do_something(user, target):
    if user.is_anonymous and test_A(user, target):
        return True
    elif user.is_admin and test_B(user, target):
        return True
    else:
        return test_C(user, target)