Django: Generic Class Based Views with Object Level Permissions Checking

A common design pattern in Django Class Based Views (CBVs) is checking whether the user is logged-in (authenticated) or not. If the user is authenticated, the view proceeds. If not, the view throws a permission denied exception.

For example, take a look at this code:

models.py


from django.contrib.auth.models import User
from django.db import models

class Blog(models.Model):
    title = models.CharField(max_length=255)
    user = models.ForeignKey(User)
    #..one-million-lines-of-code-here..

views.py

from django.views.generic.edit import UpdateView

class BlogUpdateView(UpdateView):
    model = Blog
    #..one-million-lines-of-code-here..

urls.py

from django.conf.urls.defaults import *
from django.contrib.auth.decorators import login_required

urlpatterns = patterns("views",
    url(r"^blogs/update/(?P<pk>\d+)/$",
        login_required(BlogUpdateView.as_view()),
        name="update_blog"),
)

The urls.py is a common place to put these constraints. In here, only logged-in users are allowed to edit blog entries. Great!

The problem

Consider this scenario. Darwin logs-in, creates his blog, and then saves it. Mel happens to log-in, creates her blog, and saves it. Now two blog objects exist with IDs let’s say 1 for Darwin and 2 for Mel. Darwin tries to access Mel’s blog and types the following address in his browser.

http://my.blogsite.net/blogs/update/2

Remember that the codes above allow any logged-in user access to the view BlogUpdateView. Darwin could actually edit Mel’s blog entry and save his changes. Not cool!

Enter dslibpy.views.restricted:

As a solution to the problem, I have rolled-out dslibpy.views.restricted. A set of “secure” Class Based Views that subclass each of the view classes in django.views.generic. I compiled the modules as part of a larger library of Python reusable codes.

dslibpy.views.restricted

Description

No need for @login_required and @permission_required decorators. These reusable Django class-based-views will provide object-level permissions to its subclasses more than what those two can offer. These views take a step ahead by giving an option to make views accessible only to the owner of the object (via the usual model attributes .user, .owner, or .creator).

Classes

  • RestrictedCreateView – subclass of django.views.generic.CreateView
  • RestrictedDetailView – subclass of django.views.generic.DetailView
  • RestrictedListView – subclass of django.views.generic.ListView
  • RestrictedUpdateView – subclass of django.views.generic.UpdateView
  • RestrictedDeleteView – subclass of django.views.generic.DeleteView
  • RestrictedMixin – for use with views that inherit from other classes

Usage

.restriction attribute

There are 8 restriction levels that you can assign to your class-based-views:

  • 0 – Nobody has access (not even you? aw!)
  • 1 – Only super users have access.
  • 2 – Any staff with permissions has access.
  • 3 – The owner of the object has access (request.user == object.user, object.owner, etc.)
  • 4 – Any logged-in user with model-level permissions has access.
  • 5 – Any staff has access (request.user.is_staff == True).
  • 6 – Any logged-in user has access (request.user.is_authenticated() == True).
  • 7 – Anybody have access.

You set the restriction through the restriction attribute of the view class. This attribute is an integer (not string).

For example:


class MyClass(RestrictedDeleteView):
    model = Blog
    restriction = 1

will only allow superusers (admins) to delete Blog objects.

The restrictions filter-through, meaning a user is evaluated from the top (level 1) downwards until the user qualifies on a level which grants him/her access. When a user fails to pass the restriction level set for the view, a PermissionDenied() exception is thrown.

.owner_field attribute

For models in which objects belong to a user (i.e. owned by a user). set the owner_field attribute to the name of the field that is a ForeignKey to django.contrib.auth.models.User. This attribute is a string. It is mainly used in views that have restriction = 3.

You do not have to declare an owner_field attribute if you don’t need restriction level 3. It will simply be ignored in other restriction levels.

For example:

models.py

from django.db import models
from django.contrib.auth.models import User

class Blog(models.Model):
    title = models.CharField(max_length=255)
    user = models.ForeignKey(User)

views.py

from dslibpy.views.restricted import RestrictedUpdateView
from myproject.blogs.models import Blog
from myproject.blogs.forms import BlogModelForm

class BlogRestrictedUpdateView(RestrictedUpdateView):
    model = Blog
    form_class = BlogModelForm
    restriction = 3

will grant access to the owner of the Blog object (level 3). It will also grant access to any staff (is_staff == True) provided that they have “change” permission for Blog objects (level 2). It will also grant access to all superusers (level 1). Users who belong to level 4 and below are NOT granted access to this view.

Note here that the view does not have the owner_field attribute. You can also do this and the view will safely ignore objects that do not have owners in the first place. To be exact, this view will grant access to levels 1 and 2 users only although it indicates level 3.

More Examples

The following usage examples are based on the Blog model above.

Using RestrictedCreateView

This code:

from dslibpy.views.restricted import RestrictedCreateView
from myproject.blogs.models import Blog

class BlogRestrictedCreateView(RestrictedCreateView):
    model = Blog
    owner_field = 'user'
    restriction = 5

will make BlogRestrictedCreateView automatically save the logged-in user into the field user of the Blog object (i.e. object.user == request.user). Also it grants access not only to the owner and the admins but also to any authenticated user who is a staff (is_staff == True) and to any authenticated user who has “add” permissions on the Blog model.

Using RestrictedDetailView

This code:

from dslibpy.views.restricted import RestrictedDetailView
from myproject.blogs.models import Blog

class BlogRestrictedDetailView(RestrictedDetailView):
    model = Blog
    owner_field = "user"
    restriction = 2

will only allow admins, and staff users (is_staff == True) who have “view” permissions on the Blog model.

Using RestrictedListView

This code:

from dslibpy.views.restricted import RestrictedListView
from myproject.blogs.models import Blog

class BlogRestrictedListView(RestrictedListView):
    model = Blog
    template_name = "blogs/blog_list.html"
    owner_field = 'user'
    restriction = 7

    def get_queryset_perm(self, user):
        queryset = super(Blog, self).get_queryset_perm(user)
        # one-million-lines-of-code-here

will allow all users including guests and visitors who are not logged in to view the list of blog entries.

RestrictedListView deserves special mention here. The default get_queryset() method now is wrapped in get_queryset_perm() which requires passing of the user object (request.user). Aside from that, the usual attributes like template_name, success_url, etc. retain their default behaviors.

Using RestrictedUpdateView

from dslibpy.views.restricted import RestrictedUpdateView
from myproject.blogs.models import Blog
from myproject.blogs.forms import BlogModelForm

class BlogRestrictedUpdateView(RestrictedUpdateView):
    model = Blog
    form_class = BlogModelForm
    restriction = 3

Using RestrictedDeleteView

I will leave the usage of this view as an exercise for you. If you have reached this far, you can definitely nail it.

Using RestrictedMixin

This code:

from dslibpy.views.restricted import RestrictedMixin
from dslibpy.profiles.models import CustomerProfile, VendorProfile
from myproject.profiles.views import ProfileUpdateView

class CustomerProfileUpdateView(RestrictedMixin, ProfileUpdateView):
    model = CustomerProfile
    user = 'customer'
    restriction = 3

class VendorProfileUpdateView(RestrictedMixin, ProfileUpdateView):
    model = VendorProfile
    user = "vendor"
    restriction = 3

will allow you to have all the security features of dslibpy.views.restricted alongside the reusable features of your existing class-based-views. Just make sure that RestrictedMixin is the first in the list of base classes so that its methods are called first in the command chain.

Download

You can download the Python library from my GitHub repository:
Darwinian Software Library for Python.

References:

It worked!