Jan. 19, 2007, 1:13 p.m.
Hacking the Django Admin UI: Hiding Rows
This week's project has been a multi-user blogging application. I've added pretty much every API I could find (that made sense) to allow users to upload posts, but it still seems like it would be good to have a user interface for managing these things. Lucky for me, Django comes with one. The problem, though, is that the security isn't granular enough to prevent users from getting into other users' stuff.
There is the row-level permissions branch that should be helping this, but it's not ready, and I'm already bleeding-edge enough on myproductionservers. So I tried to see what I could do more generically. My goal was to only display items relevant to the current logged in user, and only during the admin UI. There's a known hack for getting to the user using thread locals, which is half of the battle. The other half is knowing whether we're in the admin UI or not. I used the same technique and extended the thread locals mechanism as follows:
def is_admin_page(): return getattr(_thread_locals, 'is_admin_page', False) def process_request(self, request): _thread_locals.user = getattr(request, 'user', None) _thread_locals.is_admin_page = \ (request.path.find("/admin") == 0)Note that the default value is false. This is only used for limiting the display, there are still security checks in the save() methods for preventing users from manipulating data in ways they should not be able. Next, I installed custom manager objects. The manager objects override
get_query_set
to check whether the we're in an admin page and whether the user has access to view all rows of the type. For example, my BlogManager
looks like this:
class BlogManager(models.manager.Manager): """Specific manager to get only the blogs available to a given user.""" def get_query_set(self): rv=QuerySet(self.model) user=threadlocals.get_current_user() if threadlocals.is_admin_page() and _limitBlogs(user): rv=rv.filter(user=user) return rvThis optionally returns a filtered
QuerySet
when the user is browsing the admin UI and shouldn't have access to view all of the Blogs.
We install this in two locations in our Blog model:
class Blog(models.Model): [...] objects=BlogManager() [...] class Admin: manager=BlogManager()The base one is generally used when we need a list of
Blog
objects. With respect to the admin UI, this is whenever you are looking at an object that has a ForeignKey
reference to a Blog
. The one under the Admin
inner class is when the admin UI specifically wants to present the user with a list of Blog
objects.
I have another case where I don't generally want to limit the list, but when the user is given a selection of objects to edit, I want to make sure the user owns all of those objects. In that case, I only override the Admin
manager.
This seems to solve all of the remaining problems I had. I don't have to write custom code for allowing people to create and edit content, but users aren't shown lots of things that don't apply to them. Perhaps I'll throw it all away when the row-level permissions branch gets merged, but in the meantime, I learned a lot more about Django.