Putting Object Ownership in Django

I’m working on a Top Secret web project that I will talk more about in the near to medium future. Because I’m a Python nut and need to make simple web application that has complex business rules (i.e. the technology isn’t complex, but the business logic can be ) I chose Django as my engine of choice.

One of the issues I ran into is Django’s security model. By default, if a user or a group has permission to that object, the users have permissions on all those objects. This isn’t what I wanted — I wanted to give the idea of “ownership” to the object, so only the owner and/or the superusers can change that object.

This isn’t as easy as it appears. Django does have a branch called per-object-permissions that would do it, but it hasn’t been merged with the trunk for a while and I found it too late — I was already writing stuff for Django 0.96! So I had to come up with my own method. But, when that branch is merged, I would like to use it. I would do the merge myself, but it wouldn’t be an easy process and I have other things to do now.

I took some good, hard looks at the admin views and decided that I needed to either rewrite these views or intercept them before going to those functions. The first one that I wrote, the one that displays all the objects a user has permissions to change. This came from change_list view in admin/views/main

  from django.contrib.admin.views.main import ChangeList,IncorrectLookupParameters
 def my_objects(request):

     app_label = 'myapp'
     model_name = 'myobject'
     model = models.get_model(app_label, model_name)
     if model is None:
         raise Http404("App %r, model %r, not found" % (app_label, model_name))
     if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()):
         raise PermissionDenied
     try:
         cl = ChangeList(request, model)

         if not request.user.is_superuser:

             owner_objs=list(MyObject.objects.filter(owner=request.user))
             these_objs = cl.result_list

             new_objs = []
             for f in owner_objs:
                 if f in these_objs:
                     new_objs.append(f)

             cl.result_list=new_objs
             cl.result_count = len(cl.result_list)

     except IncorrectLookupParameters:
         if ERROR_FLAG in request.GET.keys():
             return render_to_response(’admin/invalid_setup.html’, {’title’: _(’Database error’)})
         return HttpResponseRedirect(request.path + ‘?’ + ERROR_FLAG + ‘=1′)

     c = template.RequestContext(request, {
         ‘title’: cl.title,
         ‘is_popup’: cl.is_popup,
         ‘cl’: cl,
     })
     c.update({’has_add_permission’: c['perms'][app_label][cl.opts.get_add_permission()]}),    

     return render_to_response(['admin/%s/%s/change_list.html' % (app_label, cl.opts.object_name.lower()),
                                'admin/%s/change_list.html' % app_label,
                                'admin/change_list.html'],
                                context_instance=c)

I’m sure that this could be better — in fact, I’m not sure I had to rewrite the whole thing. I could have just intercepted the objects before making the list, somehow. And I know that the queries could have been better. But this works well.

The view that displays the individual object for editing is much easier. In this function, I simply intercepted the call, checked the permissions, and if they were okay, the view sends them to the proper view, which is admin.views.main.change_stage. I like how this one turned out over the view above:

from django.contrib.admin.views.main import change_stage

def edit_object(request,object_id):

    if not request.user.is_superuser:    
        objects=list(MyObject.objects.filter(owner=request.user))
        this_obj = MyObject.objects.get(id=object_id)

        if not this_obj in objects:
            raise PermissionDenied

    return change_stage(request,'myapp','myobject',object_id)

After doing all this, you have to change your urls.py to go to your new views instead of the standard admin views. So I added these in my urlpatterns object there:

(r'^admin/myapp/myobject/$', 'myapp.views.my_objects'), 
(r'^admin/myapp/myobject/(?P<object_id>\d+)/$', 'myapp.views.edit_object'),

So that’s it — this is what my app will be using until the Per-Object-Permissions gets merged (or a recent trunk gets merged to that branch). I think that solution is better.

Leave a Reply

You must be logged in to post a comment.