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 my production servers. 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 rv
This 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.
blog comments powered by Disqus