Wednesday, December 13, 2006

Django Su

It's always a sign of good design when adding a new feature turns out to be easy.

In the Django authentication system, I wanted a way for an administrator to view the site as if they were a particular user; effectively an equivalent to su in UNIX-land.

The first easy step was to invent a URL to correspond to this action, which gets encoded in urls.py:

  (r'^su/(?P<username>.*)/$', 'qlockweb.accounts.views.su', {'redirect_url': '/qlockdata/'}),   

That done, the second and final step is to write some view code.

  @user_passes_test(lambda u: u.is_staff)
  def su(request, username, redirect_url='/'):
      su_user = get_object_or_404(User, username=username)
      if su_user.is_active:
          request.session[SESSION_KEY] = su_user.id
      return HttpResponseRedirect(redirect_url)

Seven lines of code and we're done (modulo a bunch of import statements).

Expanding what's going on here:

    (r'^su/(?P<username>.*)/$', 'qlockweb.accounts.views.su', {'redirect_url': '/qlockdata/'}), 
  • When an HTTP request arrives at the framework, Django goes through its list of URLs until it finds a match. In this case, going to http://mysite/accounts/su/fred/ ends hitting the urls.py line above; the /(?P<username>.*)/ part of the regexp pulls out "fred" and this gets passed as a parameter named username into the function su in qlockweb/accounts/views.py. This function also gets passed a parameter called redirect_url with value '/qlockdata/'.
  • @user_passes_test(lambda u: u.is_staff)
  • Actually, we need to rewind one step before we get into the su function. The line before the function definition is a Python decorator: some extra code wrapping the function that gets executed just before the function itself is executed. This decorator needs some expansion of its own:
      @user_passes_test(lambda u: u.is_staff)
    • The @ sign is the syntactic sugar that indicates that this line is a decorator for the function that comes immediately afterwards.
    • @user_passes_test(lambda u: u.is_staff)
    • This is the decorator function (from contrib/auth/decorators.py); its first argument test_func is a function that does the test. This test function is given a single parameter: the current User. If the test function returns true, the wrapped view code is called; if not, then the user gets redirected to a login page.
    • @user_passes_test(lambda u: u.is_staff)
    • More syntactic sugar. We want a function is_this_a_staff_user(u) that checks whether its argument u is an administrator. However, as this is the only place that the function is used, we don't bother to give it a name—we just use a lambda expression to give the definition right here and now.
    • @user_passes_test(lambda u: u.is_staff)
    • Finally, the body of the lambda expression just uses the method of the User class that indicates whether the user is an administrator or not.
  • def su(request, username, redirect_url='/'):
  • So now we're in the su function itself, and if we've got this far we're guaranteed that the person viewing the page is logged in as an is_staff user. The function has the username and redirect_url parameters mentioned earlier; it also has a request parameter that holds all of the information about the original web request (in a HttpRequest object).
  •   su_user = get_object_or_404(User, username=username)
  • The next line of code gets a User object for the username that was specified—fred in other words. If there isn't a user called fred, then a Http404 exception gets raised, which will percolate up the stack and display a (surprise, surprise) 404 page.
  •   if su_user.is_active:
  • This particular version of our code only allows impersonation of active users, helpfully provided by the is_active field in the standard User model.
  •     request.session[SESSION_KEY] = su_user.id
  • The next line of code is the one that actually does the work. The requesting user's session is modified so that its user ID is the impersonated user's.
  •   return HttpResponseRedirect(redirect_url)
  • The final line of code redirects the web browser off to the redirect_url page.

(Statutory disclaimer: I am not a security expert, nor do I play one on TV. Adding this to a production system is probably not a good idea.)


Another Django snippet: I finally discovered that the follow argument to the standard Manipulators allows you to list fields in the model that the form should leave untouched. Very helpful: the end result is more compact and less brittle than the code I'd put together to manually override all of the hidden fields.

[A:37385 B:3278 C:346 D:9187 E:62544 Total:112740]

3 comments:

Dead Man said...

You could probably add another view to switch back to the original user keeping the original user name in the session.

def su(...):
...
if user.is_active:
# add the current user name to the session
...


def su_exit(request):
# if original user name in the session, log in as that user

Just an idea :)

Adam Charnock said...

I have just wrapped all of this up into an egg here:

https://github.com/continuous/django-su

I have implemented the above comment, and removed the need for an authentication backend. I hope it helps someone :)

Anonymous said...

While I click a user in admin, sometimes I got the following "bad character in group name" error, but not always.

Django Version: 1.4
Exception Type: error
Exception Value:
bad character in group name
Exception Location: /usr/lib/python2.6/re.py in _compile, line 245
Python Executable: /usr/bin/python
Python Version: 2.6.4

Is this something related to django-su?

Thank you if you can help.