I think I've found an interesting way to set up a permissions system in Django. This system allows you to specify granular permissions on specific object instances and is template friendly.
Setup The Permissions Infrastructure
First, you create an "abstract" Permissions class that models will use as a permissions base class. This base permissions class sets up simple caching for the permissions and creates some common methods to be used by all permissions objects. The current_user represents the logged in that you want to check permissions for a particular object.
Notice the use of threadlocals here. If current_user is not supplied in the constructor, the class will try to lookup the current_user from a variable by the threadlocals middleware (see below). Be warned, however, some people don't seem to like the threadlocals approach. To be honest, I'm still trying to evaluate the pluses and minuses, but it sure makes certain things in the templates easier. Comments are welcome.
core/permissions.py
The thredlocals middleware stores the current user in the local thread. You need to include this middleware in my settings.py middleware settings.
core/middleware/threadlocals.py - Reference
Now you can create a Permission subclass within my model class. This Permissions model inherits the abstract Permissions class that we defined earlier. Now, you can create simple permissions methods within my Permissions child class. For example, I've created is_creator() and can_edit() methods in this class.
Finally, inside of MyModel class, you create a permissions() method that instantiates a permissions instance and returns that instance.
myapp/models.py
Using The Permissions
You can use the permissions system in a view as exemplified by the following code. Because the abstract Permissions class contains some basic caching, multiple calls to the same permissions function will not hit the database multiple times.
myapp/views.py
Now what's really cool is how you can make use of this in templates. Because of the threadlocals, you simply can make calls the my Permission instance methods directly in the template without using a templatetag to pass in the current user -- because of the framework that we setup above, the current user will be assumed automatically. Using the "with" statement, you can instantiate the permissions object only once so you can take advantage of the caching in the template.
templates/myapp/index.html
You can still unittest this pattern easily by creating your tests similar to the way you create your views; you can simply pass in the current_user to the mymodel.permissions() function.
I hope this pattern is useful to someone. I'm sure there are better approaches. I'm glad to hear your comments.
Thanks!
Joe
Setup The Permissions Infrastructure
First, you create an "abstract" Permissions class that models will use as a permissions base class. This base permissions class sets up simple caching for the permissions and creates some common methods to be used by all permissions objects. The current_user represents the logged in that you want to check permissions for a particular object.
Notice the use of threadlocals here. If current_user is not supplied in the constructor, the class will try to lookup the current_user from a variable by the threadlocals middleware (see below). Be warned, however, some people don't seem to like the threadlocals approach. To be honest, I'm still trying to evaluate the pluses and minuses, but it sure makes certain things in the templates easier. Comments are welcome.
core/permissions.py
from core.middleware import threadlocals class Permissions(object): def __init__(self, obj, current_user=False): if not current_user: current_user=threadlocals.get_current_user() if not current_user: current_user = None self.current_user = current_user self.obj = obj self.cache = {} def clear_cache(self, key=None): if key: try: del self.cache[key] except KeyError: pass else: self.cache.clear() def get_current_user(self): return self.current_user def __unicode__(self): return "Permissions for: %s" % (self.obj)
The thredlocals middleware stores the current user in the local thread. You need to include this middleware in my settings.py middleware settings.
core/middleware/threadlocals.py - Reference
import threading _thread_locals = threading.local() def get_current_user(): return getattr(_thread_locals, 'user', None) class ThreadLocals(object): """Middleware that gets various objects from the request object and saves them in thread local storage.""" def process_request(self, request): _thread_locals.user = getattr(request, 'user', None)
Now you can create a Permission subclass within my model class. This Permissions model inherits the abstract Permissions class that we defined earlier. Now, you can create simple permissions methods within my Permissions child class. For example, I've created is_creator() and can_edit() methods in this class.
Finally, inside of MyModel class, you create a permissions() method that instantiates a permissions instance and returns that instance.
myapp/models.py
from core import permissions class MyModel(models.Model): creator = models.ForeignKey('auth.User') field1 = models... field2 = models... # define Django Model fields here class Permissions(permissions.Permissions): def can_edit(self): KEY='cache_can_edit' if self.cache.has_key(KEY): return self.cache[KEY] if self.is_creator(): return_value = True else: return_value = False self.cache.update({KEY:return_value}) return return_value def is_creator(self): KEY="cache_is_creator" if self.cache.has_key(KEY): return self.cache[KEY] if self.obj.creator == self.current_user: return_value = True else: return_value = False self.cache.update({KEY:return_value}) return return_value def permissions(self, current_user=None): return self.Permissions(self, current_user)
Using The Permissions
You can use the permissions system in a view as exemplified by the following code. Because the abstract Permissions class contains some basic caching, multiple calls to the same permissions function will not hit the database multiple times.
myapp/views.py
from myapp import models.py @login_required def index(request, .... ): current_user = request.user mymodel = MyModel.objects.get_mymodel_instance(.....) mymodel_permissions = mymodel.permissions(current_user=current_user) if mymodel_permissions.can_edit(): do_something_special() ....
Now what's really cool is how you can make use of this in templates. Because of the threadlocals, you simply can make calls the my Permission instance methods directly in the template without using a templatetag to pass in the current user -- because of the framework that we setup above, the current user will be assumed automatically. Using the "with" statement, you can instantiate the permissions object only once so you can take advantage of the caching in the template.
templates/myapp/index.html
{% with mymodel.permissions as permissions %} <div>{% if permissions.can_edit %}<a href="">edit</a>{% endif %}</div> {% endwith %}
You can still unittest this pattern easily by creating your tests similar to the way you create your views; you can simply pass in the current_user to the mymodel.permissions() function.
I hope this pattern is useful to someone. I'm sure there are better approaches. I'm glad to hear your comments.
Thanks!
Joe
Comments
Post a Comment