Browse Source

ACL algebra helper.

Rafał Pitoń 11 years ago
parent
commit
c1bf07ccb5
3 changed files with 204 additions and 0 deletions
  1. 84 0
      docs/developers/acls.rst
  2. 37 0
      misago/acl/algebra.py
  3. 83 0
      misago/acl/tests/test_acl_algebra.py

+ 84 - 0
docs/developers/acls.rst

@@ -1,3 +1,87 @@
 ====
 ====
 ACLs
 ACLs
 ====
 ====
+
+
+Extending permissions system
+============================
+
+
+Algebra
+-------
+
+Consider those three simple ACLs::
+
+    acls = (
+        {'can_be_knight': False},
+        {'can_be_knight': True},
+        {'can_be_knight': False},
+    )
+
+In order to obtain final ACL, one or more ACLs have to be sum together. Such operation requires loop over ACLs which compares values of dicts keys and picks preffered ones.
+
+This problem can be solved using simple implementation::
+
+    final_acl = {'can_be_knight': False}
+
+    for acl in acls:
+        if acl['can_be_knight']:
+            final_acl['can_be_knight'] = True
+
+But what if there are 20 permissions in ACL? Or if we are comparing numbers? What if complex rules are involved like popular "greater beats lower, zero beats all" in comparisions? This brings need for more suffisticated solution and Misago provides one in forum of ``misago.acl.algebra`` module.
+
+This module provides utilities for summing two acls and supports three most common comparisions found in web apps:
+
+* **greater**: True beats False, 42 beats 13
+* **lower**: False beats True, 13 beats 42
+* **greater or zero**: 42 beats 13, zero beats everything
+
+
+.. function:: sum_acls(defaults, *acls, **permissions)
+
+This function sums ACLs provided as ``*args`` using callables accepting two arguments defined in kwargs used to compare permission values. Example usage is following::
+
+    from misago.acl import algebra
+
+    user_acls = [
+        {
+            'can_see': False,
+            'can_hear': False,
+            'max_speed': 10,
+            'min_age': 16,
+            'speed_limit': 50,
+        },
+        {
+            'can_see': True,
+            'can_hear': False,
+            'max_speed': 40,
+            'min_age': 20,
+            'speed_limit': 0,
+        },
+        {
+            'can_see': False,
+            'can_hear': True,
+            'max_speed': 80,
+            'min_age': 18,
+            'speed_limit': 40,
+        },
+    ]
+
+    defaults = {
+        'can_see': False,
+        'can_hear': False,
+        'max_speed': 30,
+        'min_age': 18,
+        'speed_limit': 60,
+    }
+
+    final_acl = algebra.sum_acls(
+        defaults, *user_acls,
+        can_see=algebra.greater,
+        can_hear=algebra.greater,
+        max_speed=algebra.greater,
+        min_age=algebra.lower,
+        speed_limit=algebra.greater_or_zero
+        )
+
+As you can see because tests are callables, its easy to extend ``sum_acls`` support for new tests specific for your ACLs.

+ 37 - 0
misago/acl/algebra.py

@@ -0,0 +1,37 @@
+def sum_acls(defaults, *acls, **permissions):
+    result_acl = {}
+
+    for permission, compare in permissions.items():
+        try:
+            permission_value = defaults[permission]
+        except KeyError:
+            message = 'Default value for permission "%s" is not provided.'
+            raise ValueError(message % permission)
+
+        for acl in acls:
+            try:
+                permission_value = compare(permission_value, acl[permission])
+            except KeyError:
+                pass
+        result_acl[permission] = permission_value
+
+    return result_acl
+
+
+# Common comparisions
+def greater(a, b):
+    return a if a > b else b
+
+
+def greater_or_zero(a, b):
+    if a == 0:
+        return a
+    elif b == 0:
+        return b
+    else:
+        return greater(a, b)
+
+
+def lower(a, b):
+    return a if a < b else b
+

+ 83 - 0
misago/acl/tests/test_acl_algebra.py

@@ -0,0 +1,83 @@
+from django.test import TestCase
+from misago.acl import algebra
+
+
+class ComparisionsTests(TestCase):
+    def test_greater(self):
+        """greater permission wins test"""
+
+        self.assertEqual(algebra.greater(1, 3), 3)
+        self.assertEqual(algebra.greater(4, 2), 4)
+        self.assertEqual(algebra.greater(2, 2), 2)
+        self.assertEqual(algebra.greater(True, False), True)
+
+    def test_greater_or_zero(self):
+        """greater or zero permission wins test"""
+
+        self.assertEqual(algebra.greater_or_zero(1, 3), 3)
+        self.assertEqual(algebra.greater_or_zero(4, 2), 4)
+        self.assertEqual(algebra.greater_or_zero(2, 2), 2)
+        self.assertEqual(algebra.greater_or_zero(True, False), False)
+        self.assertEqual(algebra.greater_or_zero(2, 0), 0)
+        self.assertEqual(algebra.greater_or_zero(0, 0), 0)
+        self.assertEqual(algebra.greater_or_zero(0, 120), 0)
+
+    def test_lower(self):
+        """lower permission wins test"""
+
+        self.assertEqual(algebra.lower(1, 3), 1)
+        self.assertEqual(algebra.lower(4, 2), 2)
+        self.assertEqual(algebra.lower(2, 2), 2)
+        self.assertEqual(algebra.lower(True, False), False)
+
+
+class SumACLTests(TestCase):
+    def test_sum_acls(self):
+        """acls are summed"""
+
+        test_acls = [
+            {
+                'can_see': False,
+                'can_hear': False,
+                'max_speed': 10,
+                'min_age': 16,
+                'speed_limit': 50,
+            },
+            {
+                'can_see': True,
+                'can_hear': False,
+                'max_speed': 40,
+                'min_age': 20,
+                'speed_limit': 0,
+            },
+            {
+                'can_see': False,
+                'can_hear': True,
+                'max_speed': 80,
+                'min_age': 18,
+                'speed_limit': 40,
+            },
+        ]
+
+        defaults = {
+            'can_see': False,
+            'can_hear': False,
+            'max_speed': 30,
+            'min_age': 18,
+            'speed_limit': 60,
+        }
+
+        acl = algebra.sum_acls(
+            defaults, *test_acls,
+            can_see=algebra.greater,
+            can_hear=algebra.greater,
+            max_speed=algebra.greater,
+            min_age=algebra.lower,
+            speed_limit=algebra.greater_or_zero
+            )
+
+        self.assertEqual(acl['can_see'], True)
+        self.assertEqual(acl['can_hear'], True)
+        self.assertEqual(acl['max_speed'], 80)
+        self.assertEqual(acl['min_age'], 16)
+        self.assertEqual(acl['speed_limit'], 0)