Django Permission System
Permission system that comes with django.contrib.auth allows you to create and assign permissions for your models. Permissions can be assigned to users or groups. A user has permissions directly assigned to her as well as permissions inherited from her groups. Permission scope is always model classess (as opposed to individual model instances). That means if you are just checking FooModel.can_do_bar permission on a user; that user can do bar on all FooModel instances.
This built-in permission system is simple; as in you shouldn’t expect complex authorization schemes to be implemented easily. It is mainly used by django.contrib.admin and you can use it without any hassle for your very simple authorization requirements. Beauty of this permission system is it’s simplicity. Here are a few strong points:
- All your models get add, change and delete permissions created automatically.1 These three are all you need for most of your models.
- Admin uses permissions internally. But more importantly permissions can be granted/revoked dynamically via admin.
- Simple design encourages you to keep authorization scheme clean.2
The last point is very important; permissions shouldn’t leak into business logic. If you have a project that goes beyond a CMS, you probably have business objects. Business objects might have quite complex states and typically interact with each other in more than straightforward ways. On the other hand models in CMS style projects have simple states that are independent (of other models). Take a blog post; it is either published or not and its author is simply a ForeignKey to Users. If you depend heavily upon permissions for complex state transitions of your models you will soon find yourself in a dead end.
Permissions in Fixtures
There is a --natural argument to dumpdata command since Django 1.2, see documentation here. (Thanks Joseph)
One problem, and it’s a big problem, I’ve had with permissions is that you can’t store permissions in fixtures. Here is what happens:
- Relationships are stored in fixtures as primary keys.
- Permission are created by the framework programmatically in a specific order.
- When you add/delete a new model or add/delete a custom permission this order changes.
- Therefore primary keys of permissions change.
- As a result permission data in your fixture are invalid.
I’m a big fan of fixtures. More often than not you have some models that will be used as definitions (they’re partially fixed data) or you want to kickstart your project with placeholder data. It’s quite easy in Django. After creating your models, launch admin and create your data. Then simply dumpdata, maybe post-process a little bit and you have your initial_data! Not so easy if you have permissions in that basket.
Best solution I can come up with now is to hook a post_syncdb function and add your permissions programmatically.
Let’s define permissions problem in a more generic way. A permission determines if:
- An actor can
- Perform an action
- On an object
- Depending on arbitrary number of runtime conditions [optional]
In our little implementation actors will be auth.User instances and objects, naturally, models. Actions will simply be keys of string type. Because of the possibility that our permission can have conditions, it should be a callable. Then we will have all the power Python/Django has.
Let’s place our permissions on our models:
class FooPermissions(PermissionMixin): def __warg(self): return max(self.quux - self.parent.quux, 1) @staticmethod def allows_bar_for(actor): return actor.has_perm('app.bar_foo') and \ actor.status == 'barrable' def allows_baz_for(self, actor): if self.qux_set.count() > 10: return True else: return self.__warg() > 5 or actor.is_staff class Foo(models.Model, FooPermissions): # fields, managers, etc...
First we created a permissions mixin, subclassing a PermissionMixin I’ll show it to you in a minute, and mixed it with our model definition. This way we keep permissions seperate at the source level. Also, for instance __warg method will not be accessible from our model. You can do anything inside your permissions class, you can even have side effects if you like. all you have to do is to follow a naming convention for your permission methods. Here is the PermissionMixin class:
from django.contrib.auth.models import User class PermissionError(StandardError): pass class PermissionMixin(object): def attempt(self, action, actor, msg=None): return PermissionMixin._attempt(self, action, actor, msg=None) @classmethod def cls_attempt(cls, action, actor, msg=None): return PermissionMixin._attempt(cls, action, actor, msg=None) @staticmethod def _attempt(obj, action, actor, msg): if actor.__class__ != User or not isinstance(action, basestring): raise TypeError if getattr(obj, 'allows_%s_for' % action.lower().replace(' ', '_'))(actor): return True else: if msg is None: msg = u'%s doesn\'t have permission to %s %s' % (actor.username, action.lower(), repr(obj)) raise PermissionError(msg)
When you want to check for a permission you just need it’s key (name of the action) and have your User at hand:
# Check a permission on a model: Foo.cls_attempt('bar', user) # Check a permission on an instance: foo.attempt('baz', user)
Notice that we are still using Permissions from auth. Our PermissionMixin can do complex logic, but its hard-coded. We still need something like Permissions for dynamic behaviour. Why not use what’s built-in?
You can find PermissionMixin‘s code at Django snipplets.
Before employing a similar mechanism, you should think hard if you absolutely positively need it. Maybe you can simplify your UI? Maybe there is a nice tradeoff between code complexity and interactivity of your application? Maybe you can move this logic into model.save() or form.save() or form.clean()? Or maybe your code will be best organized if you use PermissionMixins.
I just wanted to show that you can build on auth‘s Permissions. Just don’t get discouraged when you find out you can’t do x out of the box. Be it permissions or some model magic or making something available in your templates; Django is Python, it can be extended easily.
1: Provided you have auth app installed of course.
2: Ask yourself; “Is this really a must?”, and not only for permission related complications.
If you have any questions, suggestions or corrections feel free to drop me a line.