Browse Source

Refactored and documented ACL's framework.

Rafał Pitoń 11 years ago
parent
commit
44610f870c

+ 73 - 3
docs/developers/acls.rst

@@ -2,17 +2,87 @@
 ACLs
 ACLs
 ====
 ====
 
 
+Misago brings its own ACL framework for implementing permissions and this document explains to how to use and extend it with your own permissions.
+
+
+Checking permissions
+====================
+
+Permissions are stored on special models named "roles" and assigned to users either directly or trough ranks. Guest users always have permissions from "Guest" role, and users always have permissions form "Member" role.
+
+There are two kinds of objects in Misago: aware and unaware of their ACL's. Aware objects have "acl" attribute containing dict with their permissions for given ACL, while unaware objects don't. Instances of ``User`` and ``AnonymousUser`` classes are always ACL aware, while other objects need you to make them aware of their ACLs through use of ``add_acl`` functions ofered by ``misago.acl`` module. However this is not always needed (or possible), in which cases you have to introspect user's acl attribute directly.
+
+ACL's are simple dictionaries, and their contents differ depending on objects they are belonging to. This means that to see if forum is visible to user, you have to perform following check::
+
+    if user.acl['forums'].get(forum.pk, {}).get('can_see'):
+        # huzza, we can see forum!
+
+Above snippet is edge example of checking forum permission, and luckily we have few alternatives::
+
+    if forum.pk in userl.acl['visible_forums']:
+        # Not really shorter, but simpler to remember and works in django templates!
+
+    from misago.acl import add_acl
+
+    add_acl(user, forums)
+    for forum in forums
+        if forum.acl['can_see']:
+            # Now model instances in forums queryset are aware of their ACLs!
+            # ACL's are easy to check in templates too now!
+
+Because ACL framework is very flexible, different features can have different ways to check their permissions.
+
+
+Permissions cache
+-----------------
+
+Construction of User's ACLs can be costful process, especially once you start installing extensions adding new features to your site. Because of this, Misago is not assinging ACLs to Users, but to combinations of roles.
+
+This means that if one User has roles 1, 3 and 4 assigned to account, but no rank, while other User has roles 1 and 4 assigned to account and role 3 assigned to his rank, both will have same ACL. This means that ACL has to be generated once and cached for use by others.
+
+ACL's are cached in two places: in remote cache storage, for use between requests, and in thread memory, so you don't have to write your own caches and checks when you are checking multiple users ACL's during single request.
+
+ACL cache is versioned and rebuilded when cache version is different than current ACL version, which happens when models being part of ACL framework are edited or deleted.
+
 
 
 Extending permissions system
 Extending permissions system
 ============================
 ============================
 
 
+ACL framework extensions are modules registered in ``MISAGO_ACL_EXTENSIONS`` setting. By convention, those modules are either named "permissions", or they are located in "permissions" package.
+
+Misago checks module for following functions:
+
+
+.. function:: change_permissions_form(role)
+
+Mandatory. This function is called when change permissions form for role is being build for view. It's expected to return Form type or none, if provider is not recognizing role type (eg. there is no sense in adding profiles visibility permissions to forums role form).
+
+.. warning::
+   Make sure that all fields in your form have initial value, or your form will make tests suite fail because it will be unable to mock POST requests to admin forms correctly.
+
+
+.. function:: build_acl(acl, roles, key_name)
+
+Mandatory. Is used in process of building new ACL. Its supplied dict with incomplete ACL, list of user roles and name of key under which its permissions values are stored in roles ``permissions`` attributes. Its expected to access roles ``permissions`` attributes which are dicts of values coming from permission change forms and update provided ``acl`` dict accordingly.
+
+
+.. function:: add_acl_to_target(user, acl, target)
+
+Optional. Is called when Misago is trying to make ``target`` aware of its ACLs. Its provided with three arguments:
+
+* **user** - user asking to make target aware of its ACL's
+* **acl** - user ACLs
+* **target** - target instance, guaranteed to be an single object, not list or other iterable (like queryset)
+
+Value of ``target`` argument has ``acl`` attribute which is dict with incomplete ACL that function can change and update with new keys.
+
 
 
 Algebra
 Algebra
 -------
 -------
 
 
-Consider those three simple ACLs::
+Consider those three simple permission sets::
 
 
-    acls = (
+    roles_permissions = (
         {'can_be_knight': False},
         {'can_be_knight': False},
         {'can_be_knight': True},
         {'can_be_knight': True},
         {'can_be_knight': False},
         {'can_be_knight': False},
@@ -24,7 +94,7 @@ This problem can be solved using simple implementation::
 
 
     final_acl = {'can_be_knight': False}
     final_acl = {'can_be_knight': False}
 
 
-    for acl in acls:
+    for acl in roles_permissions:
         if acl['can_be_knight']:
         if acl['can_be_knight']:
             final_acl['can_be_knight'] = True
             final_acl['can_be_knight'] = True
 
 

+ 9 - 9
misago/acl/api.py

@@ -1,6 +1,6 @@
 from misago.core import threadstore
 from misago.core import threadstore
 from misago.core.cache import cache
 from misago.core.cache import cache
-from misago.acl import cachebuster
+from misago.acl import version
 from misago.acl.builder import build_acl
 from misago.acl.builder import build_acl
 from misago.acl.providers import providers
 from misago.acl.providers import providers
 
 
@@ -30,11 +30,11 @@ def get_user_acl(user):
     if not acl_cache:
     if not acl_cache:
         acl_cache = cache.get(acl_key)
         acl_cache = cache.get(acl_key)
 
 
-    if acl_cache and cachebuster.is_valid(acl_cache.get('_acl_version')):
+    if acl_cache and version.is_valid(acl_cache.get('_acl_version')):
         return acl_cache
         return acl_cache
     else:
     else:
         new_acl = build_acl(user.get_roles())
         new_acl = build_acl(user.get_roles())
-        new_acl['_acl_version'] = cachebuster.get_version()
+        new_acl['_acl_version'] = version.get_version()
 
 
         threadstore.set(acl_key, new_acl)
         threadstore.set(acl_key, new_acl)
         cache.set(acl_key, new_acl)
         cache.set(acl_key, new_acl)
@@ -42,23 +42,23 @@ def get_user_acl(user):
         return new_acl
         return new_acl
 
 
 
 
-def add_acl(acl, target):
+def add_acl(user, target):
     """
     """
     Add valid ACL to target (iterable of objects or single object)
     Add valid ACL to target (iterable of objects or single object)
     """
     """
     try:
     try:
         for item in target:
         for item in target:
-            _add_acl_to_target(acl, target)
+            _add_acl_to_target(user, target)
     except TypeError:
     except TypeError:
-        _add_acl_to_target(acl, target)
+        _add_acl_to_target(user, target)
 
 
 
 
-def _add_acl_to_target(acl, target):
+def _add_acl_to_target(user, target):
     """
     """
     Add valid ACL to single target, helper for add_acl function
     Add valid ACL to single target, helper for add_acl function
     """
     """
     target.acl = {}
     target.acl = {}
 
 
-    for provider, module in providers.list():
+    for extension, module in providers.list():
         if hasattr(module, 'add_acl_to_target'):
         if hasattr(module, 'add_acl_to_target'):
-            module.add_acl_to_target(acl, target)
+            module.add_acl_to_target(user, user.acl, target)

+ 2 - 2
misago/acl/builder.py

@@ -7,7 +7,7 @@ def build_acl(roles):
     """
     """
     acl = {}
     acl = {}
 
 
-    for provider, module in providers.list():
-        module.build_acl(acl, roles)
+    for extension, module in providers.list():
+        module.build_acl(acl, roles, extension)
 
 
     return acl
     return acl

+ 5 - 5
misago/acl/forms.py

@@ -19,21 +19,21 @@ def get_permissions_forms(role, data=None):
     role_permissions = role.permissions
     role_permissions = role.permissions
 
 
     forms = []
     forms = []
-    for provider, module in providers.list():
+    for extension, module in providers.list():
         try:
         try:
             module.change_permissions_form
             module.change_permissions_form
         except AttributeError:
         except AttributeError:
             message = "'%s' object has no attribute '%s'"
             message = "'%s' object has no attribute '%s'"
             raise AttributeError(
             raise AttributeError(
-                message % (provider, 'change_permissions_form'))
+                message % (extension, 'change_permissions_form'))
 
 
         FormType = module.change_permissions_form(role)
         FormType = module.change_permissions_form(role)
 
 
         if FormType:
         if FormType:
             if data:
             if data:
-                forms.append(FormType(data, prefix=provider))
+                forms.append(FormType(data, prefix=extension))
             else:
             else:
-                forms.append(FormType(initial=role_permissions.get(provider),
-                                      prefix=provider))
+                forms.append(FormType(initial=role_permissions.get(extension),
+                                      prefix=extension))
 
 
     return forms
     return forms

+ 3 - 3
misago/acl/models.py

@@ -1,7 +1,7 @@
 from django.db import models
 from django.db import models
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 from misago.admin import site
 from misago.admin import site
-from misago.acl import cachebuster
+from misago.acl import version as acl_version
 import base64
 import base64
 try:
 try:
     import cPickle as pickle
     import cPickle as pickle
@@ -22,11 +22,11 @@ class BaseRole(models.Model):
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
         if self.pk:
         if self.pk:
-            cachebuster.invalidate()
+            acl_version.invalidate()
         return super(BaseRole, self).save(*args, **kwargs)
         return super(BaseRole, self).save(*args, **kwargs)
 
 
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
-        cachebuster.invalidate()
+        acl_version.invalidate()
         return super(BaseRole, self).delete(*args, **kwargs)
         return super(BaseRole, self).delete(*args, **kwargs)
 
 
     @property
     @property

+ 1 - 1
misago/acl/providers.py

@@ -18,7 +18,7 @@ class PermissionProviders(object):
             self._initialized = True
             self._initialized = True
 
 
     def _import_providers(self):
     def _import_providers(self):
-        for namespace in settings.MISAGO_PERMISSION_PROVIDERS:
+        for namespace in settings.MISAGO_ACL_EXTENSIONS:
             self._providers.append((namespace, import_module(namespace)))
             self._providers.append((namespace, import_module(namespace)))
             self._providers_dict[namespace] = import_module(namespace)
             self._providers_dict[namespace] = import_module(namespace)
 
 

+ 6 - 6
misago/acl/tests/test_providers.py

@@ -35,11 +35,11 @@ class PermissionProvidersTests(TestCase):
         providers = PermissionProviders()
         providers = PermissionProviders()
         providers_list = providers.list()
         providers_list = providers.list()
 
 
-        providers_setting = settings.MISAGO_PERMISSION_PROVIDERS
+        providers_setting = settings.MISAGO_ACL_EXTENSIONS
         self.assertEqual(len(providers_list), len(providers_setting))
         self.assertEqual(len(providers_list), len(providers_setting))
 
 
-        for provider, module in providers_list:
-            self.assertTrue(isinstance(provider, basestring))
+        for extension, module in providers_list:
+            self.assertTrue(isinstance(extension, basestring))
             self.assertEqual(type(module), ModuleType)
             self.assertEqual(type(module), ModuleType)
 
 
     def test_dict(self):
     def test_dict(self):
@@ -47,9 +47,9 @@ class PermissionProvidersTests(TestCase):
         providers = PermissionProviders()
         providers = PermissionProviders()
         providers_dict = providers.dict()
         providers_dict = providers.dict()
 
 
-        providers_setting = settings.MISAGO_PERMISSION_PROVIDERS
+        providers_setting = settings.MISAGO_ACL_EXTENSIONS
         self.assertEqual(len(providers_dict), len(providers_setting))
         self.assertEqual(len(providers_dict), len(providers_setting))
 
 
-        for provider, module in providers_dict.items():
-            self.assertTrue(isinstance(provider, basestring))
+        for extension, module in providers_dict.items():
+            self.assertTrue(isinstance(extension, basestring))
             self.assertEqual(type(module), ModuleType)
             self.assertEqual(type(module), ModuleType)

+ 0 - 1
misago/acl/cachebuster.py → misago/acl/version.py

@@ -14,4 +14,3 @@ def is_valid(version):
 
 
 def invalidate():
 def invalidate():
     cb.invalidate(ACL_CACHE_NAME)
     cb.invalidate(ACL_CACHE_NAME)
-

+ 1 - 1
misago/conf/defaults.py

@@ -127,7 +127,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
     'misago.conf.context_processors.settings',
     'misago.conf.context_processors.settings',
 )
 )
 
 
-MISAGO_PERMISSION_PROVIDERS = (
+MISAGO_ACL_EXTENSIONS = (
     'misago.users.permissions.account',
     'misago.users.permissions.account',
     'misago.users.permissions.profiles',
     'misago.users.permissions.profiles',
     'misago.users.permissions.destroying',
     'misago.users.permissions.destroying',

+ 19 - 0
misago/forums/models.py

@@ -2,6 +2,7 @@ from django.db import models
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from mptt.managers import TreeManager
 from mptt.managers import TreeManager
 from mptt.models import MPTTModel, TreeForeignKey
 from mptt.models import MPTTModel, TreeForeignKey
+from misago.acl import version as acl_version
 from misago.acl.models import BaseRole
 from misago.acl.models import BaseRole
 from misago.admin import site
 from misago.admin import site
 from misago.core.utils import subset_markdown, slugify
 from misago.core.utils import subset_markdown, slugify
@@ -60,6 +61,15 @@ class Forum(MPTTModel):
         else:
         else:
             return self.name
             return self.name
 
 
+    def save(self, *args, **kwargs):
+        if self.pk:
+            acl_version.invalidate()
+        return super(Forum, self).save(*args, **kwargs)
+
+    def delete(self, *args, **kwargs):
+        acl_version.invalidate()
+        return super(Forum, self).delete(*args, **kwargs)
+
     def set_name(self, name):
     def set_name(self, name):
         self.name = name
         self.name = name
         self.slug = slugify(name)
         self.slug = slugify(name)
@@ -81,6 +91,15 @@ class RoleForumACL(models.Model):
     forum = models.ForeignKey('Forum')
     forum = models.ForeignKey('Forum')
     forum_role = models.ForeignKey(ForumRole)
     forum_role = models.ForeignKey(ForumRole)
 
 
+    def save(self, *args, **kwargs):
+        if not self.pk:
+            acl_version.invalidate()
+        return super(RoleForumACL, self).save(*args, **kwargs)
+
+    def delete(self, *args, **kwargs):
+        acl_version.invalidate()
+        return super(RoleForumACL, self).delete(*args, **kwargs)
+
 
 
 """register model in misago admin"""
 """register model in misago admin"""
 site.add_node(
 site.add_node(

+ 1 - 1
misago/forums/permissions.py

@@ -22,5 +22,5 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 ACL Builder
 """
 """
-def build_acl(acl, roles):
+def build_acl(acl, roles, key_name):
     pass
     pass

+ 3 - 3
misago/users/models/rankmodel.py

@@ -1,7 +1,7 @@
 from django.db import models, transaction
 from django.db import models, transaction
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 from misago.admin import site
 from misago.admin import site
-from misago.acl import cachebuster
+from misago.acl import version as acl_version
 from misago.core.utils import slugify
 from misago.core.utils import slugify
 
 
 
 
@@ -41,11 +41,11 @@ class Rank(models.Model):
         if not self.pk:
         if not self.pk:
             self.set_order()
             self.set_order()
         else:
         else:
-            cachebuster.invalidate()
+            acl_version.invalidate()
         return super(Rank, self).save(*args, **kwargs)
         return super(Rank, self).save(*args, **kwargs)
 
 
     def delete(self, *args, **kwargs):
     def delete(self, *args, **kwargs):
-        cachebuster.invalidate()
+        acl_version.invalidate()
         return super(Rank, self).delete(*args, **kwargs)
         return super(Rank, self).delete(*args, **kwargs)
 
 
     def set_name(self, name):
     def set_name(self, name):

+ 1 - 1
misago/users/models/usermodel.py

@@ -163,7 +163,7 @@ class User(AbstractBaseUser, PermissionsMixin):
                     roles_pks.append(role.pk)
                     roles_pks.append(role.pk)
                     roles_dict[role.pk] = role
                     roles_dict[role.pk] = role
 
 
-        return [roles_dict[r] for r in roles_pks]
+        return [roles_dict[r] for r in sorted(roles_pks)]
 
 
     def update_acl_key(self):
     def update_acl_key(self):
         pass
         pass

+ 1 - 1
misago/users/permissions/account.py

@@ -38,5 +38,5 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 ACL Builder
 """
 """
-def build_acl(acl, roles):
+def build_acl(acl, roles, key_name):
     pass
     pass

+ 1 - 1
misago/users/permissions/destroying.py

@@ -30,5 +30,5 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 ACL Builder
 """
 """
-def build_acl(acl, roles):
+def build_acl(acl, roles, key_name):
     pass
     pass

+ 1 - 1
misago/users/permissions/profiles.py

@@ -29,5 +29,5 @@ def change_permissions_form(role):
 """
 """
 ACL Builder
 ACL Builder
 """
 """
-def build_acl(acl, roles):
+def build_acl(acl, roles, key_name):
     pass
     pass