Skip to main content

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.

core/permissions.py

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

    def __init__(self, obj, current_user=False):
        if not current_user:
            current_user=threadlocals.get_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:
            try:
               del self.cache[key]
            except KeyError:
               pass
        else:
            self.cache.clear()

    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 settings.py middleware settings.

core/middleware/threadlocals.py - 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.

myapp/models.py

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):
            KEY='cache_can_edit'
            if self.cache.has_key(KEY):
                return self.cache[KEY]

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

            self.cache.update({KEY:return_value})
            return return_value


        def is_creator(self):
            KEY="cache_is_creator"
            if self.cache.has_key(KEY):
                return self.cache[KEY]
            if self.obj.creator == self.current_user:
                return_value = True
            else:
                return_value = False
            self.cache.update({KEY:return_value})
            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.

myapp/views.py

from myapp import models.py

@login_required
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():
        do_something_special()

    ....


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.

templates/myapp/index.html
{% 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.
Thanks!
Joe

Comments

Popular posts from this blog

Django Docker and Celery

I've finally had the time to create a Django+Celery project that can be completely run using Docker and Docker Compose. I'd like to share some of the steps that helped me achieve this. I've created an example project that I've used to demo this process. The example project can be viewed here on Github.

https://github.com/JoeJasinski/docker-django-demo/tree/blogpost

To run this example, you will need to have a recent version of Docker and Docker Compose installed. I'm using Docker 17.03.1 and Docker Compose 1.11.2.

Let's take a look at one of the core files, the docker-compose.yml.   You'll notice that I have defined the following services:
db - the service running the Postgres database container, needed for the Django apprabbitmq - service running the RabbitMQ container, needed for queuing jobs submitted by Celeryapp - the service containing Django app containerworker - the service that runs the Celery worker containerweb - the service that runs the Nginx con…

Django Admin Override Save for Model

Sometimes it's nice to be able to add custom code to the save method of objects in the Django Admin.  So, when editing an object on the Admin object detail page (change form), adding the following method override to your ModelAdmin in admin.py will allow you to add custom code to the save function.

In admin.py:  
class MyModelAdmin(admin.ModelAdmin): def save_model(self, request, obj, form, change): # custom stuff here obj.save()
This is documented in the Django Docs, but I found it particularly useful.

Django: Using Caching to Track Online Users

Recently I wanted a simple solution to track whether a user is online on a given Django site.  The definition of "online" on a site is kind of ambiguous, so I'll define that a user is considered to be online if they have made any request to the site in the last five minutes.

I found that one approach is to use Django's caching framework to track when a user last accessed the site.  For example, upon each request, I can have a middleware set the current time as a cache value associated with a given user.  This allows us to store some basic information about logged-in user's online state without having to hit the database on each request and easily retrieve it by accessing the cache.

My approach below.  Comments welcome.

In settings.py:
# add the middleware that you are about to create to settings MIDDLEWARE_CLASSES = ( .... 'middleware.activeuser_middleware.ActiveUserMiddleware', .... ) # Setup caching per Django docs. In actuality, you'd p…