Wednesday, March 30, 2011

Django Pattern For Model Permissions

I think I've found an interesting way to set up a permissions system in Django.  This system allows you to specify granular permissions on specific object instances and is template friendly.

Setup The Permissions Infrastructure

First, you create an "abstract" Permissions class that models will use as a permissions base class.  This base permissions class sets up simple caching for the permissions and creates some common methods to be used by all permissions objects.  The current_user represents the logged in that you want to check permissions for a particular object.

Notice the use of threadlocals here.  If current_user is not supplied in the constructor, the class will try to lookup the current_user from a variable by the threadlocals middleware (see below).  Be warned, however, some people don't seem to like the threadlocals approach.  To be honest, I'm still trying to evaluate the pluses and minuses, but it sure makes certain things in the templates easier.  Comments are welcome.


from core.middleware import threadlocals                        
class Permissions(object):

    def __init__(self, obj, current_user=False):
        if not current_user:
        if not current_user:
            current_user = None
        self.current_user = current_user
        self.obj = obj

        self.cache = {}
    def clear_cache(self, key=None):
        if key:
               del self.cache[key]
            except KeyError:

    def get_current_user(self):
        return self.current_user

    def __unicode__(self):
        return "Permissions for: %s" % (self.obj)

The thredlocals middleware stores the current user in the local thread.   You need to include this middleware in my middleware settings.

core/middleware/ - Reference

import threading

_thread_locals = threading.local()

def get_current_user():
    return getattr(_thread_locals, 'user', None)

class ThreadLocals(object):
    """Middleware that gets various objects from the
    request object and saves them in thread local storage."""
    def process_request(self, request):
        _thread_locals.user = getattr(request, 'user', None)

Now you can create a Permission subclass within my model class.  This Permissions model inherits the abstract Permissions class that we defined earlier.   Now, you can create simple permissions methods within my Permissions child class.  For example, I've created is_creator() and can_edit() methods in this class.

Finally, inside of MyModel class, you create a permissions() method that instantiates a permissions instance and returns that instance.


from core import permissions 

class MyModel(models.Model):

    creator = models.ForeignKey('auth.User')
    field1 = models...
    field2 = models...  # define Django Model fields here 

    class Permissions(permissions.Permissions):

        def can_edit(self):
            if self.cache.has_key(KEY):
                return self.cache[KEY]

            if self.is_creator():
                return_value = True
                return_value = False

            return return_value

        def is_creator(self):
            if self.cache.has_key(KEY):
                return self.cache[KEY]
            if self.obj.creator == self.current_user:
                return_value = True
                return_value = False
            return return_value

    def permissions(self, current_user=None):
         return self.Permissions(self, current_user)

Using The Permissions

You can use the permissions system in a view as exemplified by the following code.  Because the abstract Permissions class contains some basic caching, multiple calls to the same permissions function will not hit the database multiple times.


from myapp import

def index(request, .... ):
    current_user = request.user 
    mymodel = MyModel.objects.get_mymodel_instance(.....)  

    mymodel_permissions = mymodel.permissions(current_user=current_user)

    if mymodel_permissions.can_edit():


Now what's really cool is how you can make use of this in templates.  Because of the threadlocals, you simply can make calls the my Permission instance methods directly in the template without using a templatetag to pass in the current user -- because of the framework that we setup above, the current user will be assumed automatically.   Using the "with" statement, you can instantiate the permissions object only once so you can take advantage of the caching in the template.

{% with mymodel.permissions as permissions %}
   <div>{% if permissions.can_edit %}<a href="">edit</a>{% endif %}</div>
{% endwith %}

You can still unittest this pattern easily by creating your tests similar to the way you create your views; you can simply pass in the current_user to the mymodel.permissions() function.

I hope this pattern is useful to someone.  I'm sure there are better approaches.  I'm glad to hear your comments.

No comments:

Post a Comment