Friday, November 4, 2011

Dealing With Permissions In Generic Views

Generic views in Django cover a vast range of usage scenarios. That's why they're called generic views — because they implement the abstract patterns common to most applications that are using views. This a huge savings on code-writing cost because there isn't as much of it to write — or maintain thereafter. So, this all well and good, but what about the pre-generic days of writing Django views? Is there nothing of value there we can carry forward and exploit in our newly-fashioned generic views?

Of course, before we used class based views, there were still some degree of generic capabilities that could be attached to our view behavior. They came in the form of decorators. Decorators in Python are a realization of the decorator pattern — we're attaching additional responsibilities to the function. Or, in this case, our view function. But things aren't so straight forward in the brave new generic world of writing views that use classes. How can we deal with things like permissions in our generic views while still honoring the DRY principle?

The decorated permission approach
The authentication system that ships with Django includes the ability to only expose certain views to specific users.  If we're writing views as functions, the authentication framework has decorators we can use to ensure the current user has the appropriate permissions.  Like this...

from django.shortcuts import render_to_response
from django.contrib.auth.decorators import permission_required

@permission_required('my_app.can_do_stuff')
def my_view(request):
    return render_to_response('my_template.html', dict())

This is an elegant approach to ensuring that only authorized user have access to my_view.  Since the permission_required decorator is provided by the authentication framework, it's available for every view in our application.  And, we only need to implement one line of code where we need to handle permissions. One per view.

The trouble with permission_required in modern Django applications is that they don't fit in nicely with the newer class-based generic view methodology.  So how then, can we exploit the power of the generic views in Django while keeping the simplicity of the decorated permission handling?

The inherited permission approach
One thing that class-based views offer that decorators don't is the ability to define defaults that the rest of the view hierarchy in our application can inherit.  These defaults include both data attributes and behavior.  This is one approach we can use with our class based views to simplify permission handling...

from django.views.generic import TemplateView
from django.http import HttpResponseForbidden

class MyAppTemplateView(TemplateView):
    
    perms = dict()
    
    def dispatch(self, request, *args, **kwargs):
        
        perms = self.perms.get(request.method.lower(), None)
        
        if perms and not request.user.has_perms(perms):
            return HttpResponseForbidden()
        
        parent = super(MyAppTemplateView, self)
        return parent.dispatch(request, *args, **kwargs)
        
class MyAppNews(MyAppTemplateView):
    
    template_name = 'news.html'
    perms = dict(
        get = ('myapp.can_see_news',)
    )

With this approach, able to control permission-based access based on the HTTP method — all within a dictionary overridden by descendant classes.  Here is how it works.

First, we're re-creating the base TemplateView class — called MyAppTemplateView.  Any other template views used in my application are now going to inherit from this class instead of the standard TemplateView defined by Django.  This is how we decorate each view with the added responsibility of ensuring access control.

The dispatch() method is the first call to action for any generic view.  So it is here that we want to approve of any additional execution.  What were doing here is really simple.  We're checking if the user has the permissions defined by the perms attribute.  And this is all we need to override.  By default, this attribute is an empty dictionary — so no permissions will be validated.

The MyAppNews view simply overrides the perms attribute to ensure the requesting user has the myapp.can_see_news permission.  It's not exactly a decorator, but we're retaining some flexibility while staying true to the DRY principle.