Django Middleware Munging

We’ve been hitting the code pretty hard of late at Loggly, and the beta is really starting to take shape on the development servers.  There’s lots to do, of course, so we’ve taken to using Unfuddle to track tickets, host our repository for code commits.  Later on we’ll use Unfuddle’s APIs to help track customer’s feature requests and tickets.  Here’s a screen cap of our latest commit timeline:

commits

One of the things you’ll notice when you use Unfuddle is the presence of a subdomain in the URLs you use on the site.  Our subdomain is ‘loggly’ on Unfuddle, and we  log into our project area by going to http://loggly.unfuddle.com/ (no, you can’t check our code out).  This type of customer segmentation allows for multiple unique usernames per customer, but doesn’t require a unique username site-wide.  For non-SEO sections of the site, this is a perfect solution.

We are taking a similar approach with Loggly, where a user will sign up for an account and define a unique customer identifier (we’re kicking around calling this a “mill”), which will then be mapped to a subdomain on the system.  So, for example, if Foobar, Inc. were to sign up for a Loggly account, they would access the site via http://foobar.loggly.com/, and then could create any number of user/pass combinations they wanted to access their company’s log resources.

The only problem with this approach is that we use Django, and their built in auth system (which is fantastic, BTW) doesn’t really have facilities for this type of functionality.  While we could certainly hack the Django auth system by writing our own multi-tenant auth module, it would take away from more pressing issues – like launching the beta!

Enter the Middleware Solution

One way to solve this is by munging the subdomain and username together, which provides a unique system-wide username. If, for example, you were to log in as steve under foobar.loggly.com, then we’d stick them together to be something like “foobar_steve”. Obviously we can’t have everyone remembering this long monstrosity for their username, so we’ll need to munge the subdomain off the URL and the username the user types in to get the correct combination to send off to the auth system.

Thankfully Django provides a super-easy way to add middleware to a project. By injecting a small piece of code into the request from the user’s browser, we are able to do our on-the-fly transformation before the auth system takes over. Nobody is the wiser because we can modify the display name code in the profile model to show the “normal” username to the user. Here’s what the result looks like:

settings.py:
...
MIDDLEWARE_CLASSES = (
'loggly.profile.MungeMiddle.MungeForMillMiddleware',
...
)
...

MungeMiddle.py:
class MungeForMillMiddleware:
    def process_request(self, request):
        if request.POST.has_key('username'):
            data = request.POST.copy()
            user = "%s_%s" % (request.META['HTTP_HOST'].split('.')[0], data['username'])
            data['username'] = user
            request.POST =  data

When a request comes in, we pull out the POST data and make a copy of it with .copy(). We then munge up the username with the subdomain out of HTTP_HOST, and then set the POST data to forward on to the rest of the stack. We don’t do this for all requests, just ones with the username set, so it’s lightweight enough for production use. We end up sticking the shorteded version of the username into the profile table, and use it for display.

So there you have it. A 5 minute fix for a 5 hour problem. I’m sure there are more elegant solutions to doing subdomain segmentation with Django’s out-of-the-box auth system, but frankly we don’t have time to stop and code them up. We’re bent on getting our beta out as soon as possible, and if it requires hacks like these to do it, then so be it! Release early, release often.

One Response to “Django Middleware Munging”

  1. zrlram 07. Dec, 2009 at 09:24 #

    After investigating the middleware solution for rewriting the usernames a little more, I found a slightly cleaner solution that intercepts the login view. It is nicer because you are not monitoring each Web POST to see whether a username is submitted.
    Here is the basic idea:

    1. You redirect the login view to your own code.
    2. If you are not dealing with any POST data, you just show the authentication form from django.contrib.auth (auth.views.login())
    3. If you get POST data, you create the new username as a combination of the subdomain and the original username. Also extract the password from the post.
    4. Verify the authentication credentials through the authenticate() call. If you are dealing with correct credentials, you actually log the user in (auth.login()) and redirect the user to either the main page or the URL provided inside of the login call (redirect_to field).
    5. Upon a failed login, just log the failed login and call the auth.views.login() method.
    6. That’s it. Here are the code snippets:

      urls.py:

      url(r'^login/$',
              login,          #this is your own view. By default you used auth.views.login() here.
             {'template_name': 'templates/login.html'}, name='login'),
      


      views.py:

      def login(request, template_name, redirect_field_name=REDIRECT_FIELD_NAME):
      
          if request.method == "POST":
              subdomain = request.META['HTTP_HOST'].split('.')[0]
              authuser = "%s_%s" % (subdomain, request.POST['username'])
              password = request.POST['password']
              user = authenticate(username=authuser, password=password)
              if user:
                  auth.login(request, user)
                  redirect_to = request.REQUEST.get(redirect_field_name, '')
                  if not redirect_to or '//' in redirect_to or ' ' in redirect_to:
                      redirect_to = settings.LOGIN_REDIRECT_URL
                  return HttpResponseRedirect(redirect_to)
              else:
                  logs.error('action=authentication,status=failure,user=' + authuser);
      
          return auth.views.login(request, template_name)
      

      Thanks to Seth for helping getting this to run.