recluze

Row-level Permissions in Django Admin

So you’ve started working with Django and you love the admin interface that you get for free with your models. You deploy half of your app with the admin interface and are about to release when you figure out that anyone who can modify a model can do anything with it. There is no concept of “ownership” of records!

Let me give you an example. Let’s say we’re creating a little MIS for the computer science department where each faculty member can put in his courses and record the course execution (what was done per lecture). That would be a nice application. (In fact, it’s available open source on github and that is what this tutorial is referring to.) However, the issue is that all instructors can access all the course records and there is no way of ensuring that an instructor can modify only the courses that s/he taught. This isn’t easily possible because admin doesn’t not have “row-level permissions”.

Of course, we can create them by sub-classing some of the Admin classes and modifying a couple of functions. Here’s how:

Let’s assume we have the following in our models:

class Instructor(models.Model):
    name = models.CharField(max_length=200)
    # other stuff here 

class Course(models.Model):
    course_code = models.CharField(max_length=10, default='CS')
    instructor = models.ForeignKey(Instructor)
    course_name = models.CharField(max_length=200)
    # other stuff here 

class CourseOutline(models.Model):
    course = models.OneToOneField(Course)
    objectives = models.TextField(blank=True)
    # other stuff 

And in admin.py, we have the following:

class CourseAdmin(admin.ModelAdmin):
    list_display = ('course_name', 'instructor')
    # some other stuff 

admin.site.register(Course, CourseAdmin)

class CourseOutlineAdmin(admin.ModelAdmin):
    # nothing here of importance     

admin.site.register(CourseOutline, CourseOutlineAdmin)

So, when you open the page for Course, you can see the instructors and when you open the CourseOutline, you can see the Courses. Pretty good but how do we add row-level permissions? We do this by overriding a couple of functions.

Here’s the strategy I followed:

First, create a user account for each instructor. Then, take away these user’s rights to modify Instructor objects. (You can keep stuff in Instructor Profile so that they can modify their information.)

Next, add a field to the Instructor model that binds it to a particular user. The model now becomes:

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

class Instructor(models.Model):
    name = models.CharField(max_length=200)
    owner = models.ForeignKey(User)
    # other stuff here 

The other models can remain the same. We only need to modify their “querysets”. Let’s open up admin.py again and modify the *Admins.

We override two functions to make CourseAdmin look like the following:

class CourseAdmin(admin.ModelAdmin):
    # whatever was here 
 
    def queryset(self, request):
        qs = super(CourseAdmin, self).queryset(request)
        if request.user.is_superuser:
            return qs 
        
        # get instructor's "owner" 
        return qs.filter(instructor__owner=request.user)
    
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "instructor" and not request.user.is_superuser:
            kwargs["queryset"] = Instructor.objects.filter(owner=request.user)
            return db_field.formfield(**kwargs)
        return super(CourseAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
    
admin.site.register(Course, CourseAdmin)

What’s that? The first function basically modifies the queryset function and makes sure that if you’re not a super user, you will see only those courses in the course list where instructor__owner=request.user i.e. your own courses.

The second function is required so that you can’t add courses that belong to other instructors. It filters the foreign key dropdown box so that only owner=request.user objects are shown in the foreign key dropdown. That is, you only see yourself in that dropdown.

We can do the same thing for CourseOutline. That is here:

class CourseOutlineAdmin(admin.ModelAdmin):
    # whatever was here 

    def queryset(self, request):
        qs = super(CourseOutlineAdmin, self).queryset(request)
        if request.user.is_superuser:
            return qs 
        
        # get instructor's "owner" 
        return qs.filter(course__instructor__owner=request.user)
    
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "course" and not request.user.is_superuser:
            kwargs["queryset"] = Course.objects.filter(instructor__owner=request.user)
            return db_field.formfield(**kwargs)
        return super(CourseAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)

The only difference is that we’re now going two levels up the foreign key ladder. That’s all you need to have row-level permissions in admin. Questions?

3 thoughts on “Row-level Permissions in Django Admin

  1. Arjun

    Hi Nauman,

    Row level permissions for the django admin is something that I have been searching, for quite some time.I found Django-guardian but it does not work within the django admin…Your post is really helpful.I have not implemented it yet but after reading,it seems that I can only prevent an unauthorized user from viewing a row.Is is possible to only stop a user from changing the attributes of the row but allowing him to view it…

  2. recluze Post author

    Hi Arjun,

    It might be pssible but not really recommended to use the admin interface for such fine-grained permissions. This tutorial is mostly for illustrative purposes. If you need fine-grained controls over who gets to do what, it’s best to write your own views.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>