Putting Object Ownership in Django

Apr 22nd, 2007No Comments

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](http://www.djangoproject.com/) 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](http://code.djangoproject.com/wiki/RowLevelPermissions) 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\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.