Friday, 13 July 2012

User Fixtures in Django

At Glasses Direct, we are setting up an internal system which needs a simple web UI.  As we use Django for all of our HTTP needs, the admin interface was the obvious, quick solution.  All we need to do is write the requisite model, tie it in to the admin interface and we're done!  Right?  Wrong.

The above description is missing an important part of the puzzle: authentication.  Django's admin interface (sensibly) requires authentication by default.  However, this piece of our system will only be exposed internally, and we don't want to have to manage credentials for all of our internal users (as we are sadly lacking when it come to internal single sign-on).

The obvious course of action is to remove the authentication.  However, this seems to be easier said than done.  Firstly, there is no simple switch to disable authentication. Secondly, even were there, we wouldn't want everyone to have access to the full admin interface, largely because it would be confusing for the target users.  So we can't get rid of authentication.  What we really want is a default user.

A default user is easy enough, you can add it using a User fixture that looks something like this (the easiest way to do this is to create a User object and use the dumpdata management command):
[{"pk": 2,
  "model": "auth.user",
  "fields": {
    "username": "default",
    "is_staff": true,
    ...
  }}]
Voila! After running a syncdb, you'll have a user who can access the admin interface.  Unfortunately, they won't be able to do anything, because they don't have any permissions.  Let's fix that by adding some (again, easiest to do this using dumpdata):
[{"pk": 2,
  "model": "auth.user",
  "fields": {
    "username": "default",
    "is_staff": true,
    "user_permissions": [12, 13, 14],
    ...
  }}]
You can see here that we've granted this user three permissions.  The relevant entries will show up in the admin interface.  We're done!  Right?  Wrong.

Everything will seem to be proceeding happily, possibly for quite some time.  Then, in a few weeks or months, you'll add another model, or app or something and suddenly your default user will have permission to do really weird things.  The problem here is that Django will occasionally regenerate the primary keys of permissions (and other internal objects).  So what are we to do?  After a fair amount of swearing this afternoon, my colleague Ondrej pointed me in the direction of natural keys. With these, you can future-proof yourself against primary key oddities:
[{"pk": 2,
  "model": "auth.user",
  "fields": {
    "username": "default",
    "is_staff": true,
    "user_permissions": [
      ["add_mymodel", "myapp", "mymodel"],
      ["change_mymodel", "myapp", "mymodel"],
      ["delete_mymodel", "myapp", "mymodel"]
    ],
    ...
  }}]
As with the above examples, you should generate this output with dumpdata, passing in the --natural flag on the command line.

To conclude, we've looked at how we can use Django fixtures to give us a default user with a known username and password, with reliable, known permissions. Perfect!


N.B. One option for "auto-authentication" would be to use a middleware class that sets the user on the request to our default user, something along the lines of this.  This component is only meant to be a quick procedure fix, so we haven't taken the time to do that.

3 comments:

  1. Usage of fixtures (especially in tests) should be avoided due to the same reasons you mentioned that data changes and also due to rigged nature of it. This also has been talked over in quite a lot of blogs of django devs and in django con. cant find video though.

    Check out django-bootup, with few lines of settings you can have default user - that's simple and clean solution for dev and test environment. In live there should be no default user as such, at least with not the same password.

    ReplyDelete
    Replies
    1. Thanks for reading and responding. The requirement for this live system is that it is trivially accessible by all internal users. As we don't have single sign-on in any meaningful manner, we've opted for a default user (as having to create a user account for everyone who wants to access it would not count as trivial).

      I agree that this is less than ideal, but it's the best we can do without significant SSO work. I'll definitely take a look at django-bootup though, thanks for the heads-up!

      Delete
  2. Thanks for the post! Some other quick suggestions that I've ended up using - simple data migration if you're happy to require South, or set a signal handler for post_syncdb: https://docs.djangoproject.com/en/dev/ref/signals/#post-syncdb

    The reason being that either of these allows you to use Django's models directly, but not sure if this will help your wild permissions issue that required natural keys.

    ReplyDelete