Skip to content

Commit

Permalink
Massive refactoring and deep logical change
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-tresontani committed Jul 11, 2012
2 parents b37fbea + 5fb0a91 commit 5b37494
Show file tree
Hide file tree
Showing 24 changed files with 465 additions and 405 deletions.
43 changes: 43 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Rules
=====

Flexible ACL library for Django

Install
=======

Install::

pip install django-rules

Add ``rules`` to ``INSTALLED_APPS`` and run ``python manage.py syncdb`` to
create the appropriate database tables.

Use
===

...

Contribute
==========

Install for contributing::

git clone git://github.com/anthony-tresontani/rules.git
mkvirtualenv rules
cd rules
python setup.py develop
pip install -r requirements.txt

Run tests::

./run_tests.py

Understand
==========

Automatic group creation
------------------------

Any object is automatically assigned a group by default containing the all objects of the same model.
For ex, any Product instead will belong to the group group_product_model.
File renamed without changes.
2 changes: 1 addition & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ update-env:
pip install -r requirements.txt

test:
python rules_engine/manage.py test
./run_tests.py
10 changes: 5 additions & 5 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
django
peak-rules
django-nose
pyhamcrest
django_dynamic_fixture
Django==1.4
PyHamcrest==1.6
django-dynamic-fixture==1.6.3
django-nose==1.1
nose==1.1.2
3 changes: 1 addition & 2 deletions rules_engine/__init__.py → rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.contrib.admin.sites import site
from django.db.models.signals import class_prepared
from django.dispatch.dispatcher import receiver
from rules_engine.rules import Group
from rules.base import Group

logger = logging.getLogger("rules")

Expand All @@ -25,7 +25,6 @@ def create_group_model(sender, **kwargs):
if not Group.get_by_name(class_name):
group = create_group_class(sender)
Group.register(group)
logger.info("Creating and registering automatic group %s" , class_name)


def autodiscover():
Expand Down
108 changes: 71 additions & 37 deletions rules_engine/rules.py → rules/base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import logging
from peak.rules.core import abstract, when
from django.db.models.query import QuerySet
from django.db.models.query import QuerySet, Q

from rules_engine.ACL.models import ACL
from rules.models import Rule

logger = logging.getLogger("rules")

def get_permissions(for_, action, groups):
apply_permissions = ACL.objects.filter(group__in=groups, action=action, type=ACL.ALLOW)
deny_permissions = ACL.objects.filter(action=action, type=ACL.DENY)
def get_permissions(action, groups):
logger.info("All rules for action '%s':", action)
for rule in Rule.objects.filter(action=action):
logger.info(" %s", rule)

apply_permissions = Rule.objects.filter(action=action, deny=False)
apply_permissions= apply_permissions.filter(groups__name__in=groups, exclusive=False) | apply_permissions.filter(exclusive=True).exclude(groups__name__in=groups)

deny_permissions = Rule.objects.filter(action=action, deny=True)
inclusive_deny_permissions = deny_permissions.filter(groups__name__in=groups, exclusive=False)
exclusive_deny_permissions = deny_permissions.filter(exclusive=True).exclude(groups__name__in=groups)
deny_for_all = deny_permissions.filter(groups=None, exclusive=True)
deny_permissions = inclusive_deny_permissions | exclusive_deny_permissions | deny_for_all

# Remove Deny rule if an apply rule matches
for permission in deny_permissions:
if permission.groups.exists():
counter_exists = apply_permissions.filter(predicate=permission.predicate, action=permission.action, exclusive=permission.exclusive).exists()
else:
counter_exists = apply_permissions.filter(predicate=permission.predicate, action=permission.action).exists()
if counter_exists:
logger.info("Exclude deny rule as an apply has been found")
deny_permissions = deny_permissions.exclude(id=permission.id)
return apply_permissions, deny_permissions


Expand All @@ -19,13 +39,14 @@ def __new__(meta, classname, bases, classDict):
cls.register(cls)
return cls

# Create your models here.

class Group(object):
__metaclass__ = GroupMetaClass
groups = set([])

@classmethod
def register(cls, group_class):
logger.info("GROUP \'%s\' registered", group_class.name)
cls.groups.add(group_class)

@classmethod
Expand All @@ -45,7 +66,9 @@ def get_groups(cls, obj):
groups_in.append(group)
except AttributeError, e:
pass
return [group.name for group in groups_in]
groups_names = [group.name for group in groups_in]
logger.info("Obj %s belong to %s", obj, groups_names)
return groups_names

@classmethod
def get_by_name(cls, name):
Expand All @@ -68,28 +91,23 @@ def _apply_qs(cls, qs):
def _apply_obj(cls, qs):
return cls.apply_obj(qs)


class RuleMetaClass(type):
class PredicateMetaClass(type):
def __new__(meta, classname, bases, classDict):
cls = type.__new__(meta, classname, bases, classDict)
if classname != "Rule":
if classname != "Predicate":
for attr in classDict:
if attr.startswith("apply_") and callable(classDict[attr]) and not getattr(classDict[attr], "im_self",
None):
if attr.startswith("apply_") and callable(classDict[attr]) and not getattr(classDict[attr], "im_self", None):
raise AttributeError("method %s of class %s should be a classmethod" % (attr, classname))
if not "name" in classDict:
raise AttributeError("Rule %s should have a name attribute" % classname)
if not "group_name" in classDict:
raise AttributeError("Rule %s should have a group_name attribute" % classname)
#if not "group_name" in classDict:
# raise AttributeError("Rule %s should have a group_name attribute" % classname)
cls.register(cls)

for group, type_ in getattr(cls, "auto_on_groups", []):
ACL.deferred(group=group, rule=cls.name, action=cls.group_name, type=type_, auto=True)
return cls


class Rule(object):
__metaclass__ = RuleMetaClass
class Predicate(object):
__metaclass__ = PredicateMetaClass
rules = set([])

def __init__(self, next_=None):
Expand Down Expand Up @@ -140,7 +158,8 @@ def __init__(self, on, action, for_):
self.action = action
self.for_ = for_
self.groups = Group.get_groups(self.for_)
self.apply_permissions, self.deny_permissions = get_permissions(self.for_, self.action, self.groups)
logger.info("GROUPS %s", self.groups)
self.apply_permissions, self.deny_permissions = get_permissions(self.action, self.groups)

def no_perm_value(self):
if hasattr(self, "get_no_permission_value"):
Expand All @@ -149,10 +168,10 @@ def no_perm_value(self):
return self.no_permission_value

def check(self):
if not self.apply_permissions:
logger.info("No permission found")
self.reason = "No permission found"
return self.no_perm_value()
#if not self.apply_permissions:
# logger.info("No permission found")
# self.reason = "No permission found"
# return self.no_perm_value()
return self._check()

def _check(self):
Expand All @@ -168,24 +187,32 @@ def get_no_permission_value(self):
return self.model.objects.none()

def apply_perm(self, perm, method):
rule = Rule.get_by_name(perm.rule)
rule = Predicate.get_by_name(perm.predicate)
filters = rule.apply(obj=self.on)
filter_method = getattr(self.model.objects, method)
filter_method = getattr(self.on, method)
if isinstance(filters, dict):
on = filter_method(**filters)
else:
on = filter_method(**filters) # == self.on.filter(..)
else: # or self.on.exclude(..)
on = filter_method(filters)
return on

def _check(self):
on = None
logger.info("-"*8 + "CHECKING RULES" + "-"*8)
logger.info(" Rules applied on %d objects: %s", len(self.on), self.on)
logger.info(" Permissions to apply: %s", self.apply_permissions)
for permission in self.apply_permissions:
on = self.apply_perm(permission, method="filter")
self.on = self.apply_perm(permission, method="filter")
logger.info(" After filter %s: %s", permission, self.on)

logger.info(" Permissions to deny: %s", self.deny_permissions)
for permission in self.deny_permissions:
if not ACL.objects.filter(action=self.action, group__in=self.groups, rule=permission.rule).exists():
on = self.apply_perm(permission, method="exclude")
return on
# if not Rule.objects.filter(action=self.action, group__in=self.groups, rule=permission.rule).exists():
self.on = self.apply_perm(permission, method="exclude")
logger.info(" After filter: %s", self.on)
logger.info(" %d allowed objects: %s", len(self.on), self.on)
logger.info("-"*8 + "RULES CHECKED" + "-"*8)
return self.on


class IsRuleMatching(RuleHandler):
Expand All @@ -194,21 +221,28 @@ def __init__(self, on, action, for_):
self.reason = None

def _check(self):
logger.info("-"*8 + "CHECKING RULES" + "-"*8)
logger.info(" Rules applied on %s", self.on)
logger.info(" Permissions to apply: %s", self.apply_permissions)
result = True
for permission in self.apply_permissions:
rule = Rule.get_by_name(permission.rule)
rule = Predicate.get_by_name(permission.predicate)
result = rule.apply(obj=self.on)
if not result:
logger.info("Allow rule %s failed", rule)
logger.info(" rule '%s' failed", rule)
self.reason = rule.get_message()
return False

logger.info(" Permissions to deny: %s", self.deny_permissions)
for permission in self.deny_permissions:
if not ACL.objects.filter(action=self.action, group__in=self.groups, rule=permission.rule).exists():
rule = Rule.get_by_name(permission.rule)
# if not Rule.objects.filter(action=self.action, group__in=self.groups, rule=permission.rule).exists():
rule = Predicate.get_by_name(permission.predicate)
exclude = rule.apply(obj=self.on)
if exclude:
logger.info("Deny rule %s failed", rule)
logger.info(" rule '%s' failed", permission)
self.reason = rule.get_message()
return False
logger.info(" Return %s", result)
logger.info("-"*8 + "RULES CHECKED" + "-"*8)
return result

88 changes: 88 additions & 0 deletions rules/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import logging
import collections

from django.db import models
from django.db.models.signals import post_syncdb
from django.dispatch.dispatcher import receiver

logger = logging.getLogger("rules")

class RuleManager(models.Manager):
def create_rule(self, predicate, groups_in=None, not_groups_in=None, can_do=None, cant_do=None):
if groups_in:
groups = groups_in
exclusive=False
elif not_groups_in:
groups = not_groups_in
exclusive=True
else:
groups = []
exclusive = True
self.create_rules_for_groups(groups, can_do, cant_do, predicate, exclusive=exclusive)

def create_rules_for_groups(self, groups, can_do, cant_do, predicate, exclusive):
if not isinstance(groups, collections.Iterable) or isinstance(groups, str):
groups = [groups]
if can_do:
rule, created = self.get_or_create(action=can_do, predicate=predicate, exclusive=exclusive)
elif cant_do:
rule, created = self.get_or_create(action=cant_do, predicate=predicate, deny=True, exclusive=exclusive)
rule.save()
for group in groups:
group, created = GroupName.objects.get_or_create(name=group)
rule.groups.add(group)

class GroupName(models.Model):
name = models.CharField(max_length=80, null=True, blank=False)

class Rule(models.Model):
deferred_rules = []

ALLOW, DENY = "ALLOW", "DENY"
action_type = (("Allow", ALLOW), ("Deny", DENY))

action = models.CharField(max_length=20, null=False)
groups = models.ManyToManyField(GroupName)
predicate = models.CharField(max_length=80)
deny = models.BooleanField(default=False)
exclusive = models.BooleanField()
auto = models.BooleanField(default=False)

objects = RuleManager()

def save(self, *args, **kwargs):
from rules.base import Group, Predicate
if self.pk:
for group in self.groups.all():
if group not in Group.get_group_names():
raise ValueError("Group %s has not been registered" % group)
if self.predicate not in Predicate.get_rule_names() and self.predicate:
raise ValueError("Predicate %s has not been registered" % self.predicate)
super(Rule, self).save(*args, **kwargs)

def __repr__(self):
group = "for groups in %s"
if self.exclusive:
group = "for groups NOT in %s"
groups_list = " AND ".join([g.name for g in self.groups.all()])
if not groups_list:
group = "for %s"
groups_list = "ALL"
action = "CAN %s"
if self.deny:
action = "CAN'T %s"
msg= action + " " + "%s " + group
return msg % (self.action, self.predicate, groups_list )

def __str__(self):
return self.__repr__()

@classmethod
def deferred(self, **kwargs):
self.deferred_rules.append(kwargs)

@receiver(post_syncdb, )
def create_deferred_rules(sender, **kwargs):
for rule in Rule.deferred_rules:
Rule.objects.get_or_create(**rule)

Binary file removed rules_engine/ACL/.models.py.swp
Binary file not shown.
Loading

0 comments on commit 5b37494

Please sign in to comment.