Hello. My name is Nate. Myself and my friend Marco would like to upgrade django permissions. We would like to brainstorm here first to make sure that we design something that fits with django.
If there are any other discussions about upgrading django permissions, kindly point me to them. We have already some of the permissions threads on django-dev, but we thought that it would be best to make a new thread for our proposal. We are aware of the fact that django 1.2 added some hooks for row- level permissions, but this proposal covers an even broader scope. # The Proposal # In a nutshell, we would like to add a PermissionManager to django. It will be similar in concept to the 'objects' queryset Manager that already exists on models. Each model will have a default PermissionManager attached. People can override this PermissionManager with their own PermissionManager to change the permissions for a particular model. The default PermissionManager works like the current permissions system: it checks all of the Backends to see if the user has the given permission. (This, in turn, checks the records of auth.Permission by default.) The simplest use case of the PermissionManager is overriding it to add new permissions, equivalent to setting the Meta.permissions attribute. For example: class Poll(models.Model): permissions = PermissionManagerWith(vote='Can Vote') ... Note that PermissionManagerWith would be a factory function to create a new PermissionManager class with a vote permission. More on this later. Unlike the current system, PermissionManagers make a distinction between two types of permissions: object-level (row-level) and model- level permissions. Model-level permissions are similar to the current permissions that django has, while object-level permissions add more fine-grained control. ## Model Level Permissions ## Each PermissionManager has an 'allows_PERM' classmethod for each model- level permission. For example, a PermissionManager with the model- level permissions 'add', 'remove', and 'edit' will have the functions 'allows_add', 'allows_remove', and 'allows_edit'. These functions take a User object as a parameter and return a boolean as to whether or not the user has the given permission. So, for instance, a user could do the following: if not Poll.permissions.allows_vote(user): raise PermissionError By default, this will check all of the authentication Backends to see if the user has the given permission. Users could, of course, extend PermissionManager and override this behavior to use other methods of checking the permissions. Note, however, that these functions are defined on a Class level. Users overriding these functions can not do any computation concerning a model instance, only a model class. This does not allow people to do things like restrict edits to only models that the editor created. For that, you need object-level permissions. ## Object Level Permissions ## Each PermissionManager has a 'PERM_list' classmethod for each object- level permission. For example, a PermissionManager with the object- level permissions of 'remove' and 'edit' will have the functions 'remove_list' and 'edit_list'. These functions take a User object as a parameter and returns a queryset of all the objects for which the user has the specified permissions. So, for instance, you might see something like this in contrib.admin: queryset = Book.permissions.edit_list(user) By default, this is either all of the objects or none of the objects, depending upon the authentication backends. However, this is what is overridden to get more fine-grained permission control. For example, consider the following: class BookPermissionManager(PermissionManager): def edit_list(self, user): return super(BookPermissionManager, self).edit_list(user).filter(author=user) class Book(models.Model): author = models.ForeignKey(User) permissions = BookPermissionManager ... This is an example of using PermissionManagers to only allow users to edit books that they themselves authored. Note that this proposed permissions system does not require that we add any tracking or meta- data to the database. If users would like to base permissions on the meta-data of objects, then they need to store such meta-data themselves. This permissions system will by default use only the existing auth.Permission system for permissions. For symmetry and ease of use, each PermissionManager instance has an 'allows_PERM' method for each object-level permission. The default implementation of allows_PERM is this: def allows_PERM(user): return self.model in self.PERM_list(user) and it is strongly recommended that allows_PERM not be overridden for object-level permissions, because changes in allows_PERM will not be reflected in PERM_list. ### Adding Permissions ### The most common overriding of PermissionManagers will be adding new object-level or model-level permissions. This use case is currently the only use case supported by Django, and is done through the Meta.permissions attribute. The PermissionManager system would allow infinitely more flexibility, allowing users to roll their own permission-checking system if necessary. However, we need a simple shorthand for the common case. One possible method is to continue to configure permissions through Meta.permissions. However, it would be cleaner if we could find a way to combine extension (i.e. adding new permissions) with replacement (i.e. changing the PermissionManager like you change a queryset Manager). I'm still struggling to find a clean PermissionManager override syntax that easily allows the defining of new object-level and model-level permissions. One possible method would be through a factory method, such as the following: class Poll(models.Model): permissions = PermissionManagerWith( object={'vote': 'Can Vote'}, model={'create': 'Can Create'}) However, I am not convinced that this is the clearest method. Any other ideas? Also, you may notice that the tense of permission names has changed. The PermissionManager system implies that we give permissions names such as 'add', 'edit', 'remove', or 'vote' rather than 'can_add', 'can_edit', 'can_remove', or 'can_vote'. I personally think that these semantics are cleaner and more concise, but of course, it should be given some consideration. ## Module Level Permissions ## When you pass a string to has_perm or permission_required (or any similar function), you pass a permission as 'app_label.permisison_name'. This is all well and good, unless certain models have similar names. Consider for example an app 'polls' with two types of Poll: DemocratPoll and RepublicanPoll. Both models have a 'can_vote' permission defined in Meta.permissions. Both of these permissions will be confounded if you ask "user.has_perm('polls.can_vote')". Sometimes, this is what users want. Perhaps, in this example, 'can_vote' is meant to be a flag stating that the user is over 18 and is a registered voter. At other times, though, this is not what users want. Maybe 'can_vote' is supposed to be a flag stating that the user is registered as a member of the correct political party, and registered Democrats can only vote in the DemocratPolls while registered Republicans are confined to RepublicanPolls. In order to do this in django currently, you need to change the permissions to can_vote_democrat and can_vote_republican. (Note: you can still pass has_perm the actual Permission object in python code, but this is more difficult in a template.) This slightly violates the DRY principle and is a potential source of errors, especially as apps get larger (and people fail to realize that their permission names overlap). We propose, as such, that we depreciate the "app_label.permission_name" style of referring to permissions and switch to "app_label.Model.permission_name". This allows similarly named permissions between models to be easily differentiated. However, sometimes users want permissions that transcend models. In this example, people may want a permission that indicates a registered voter, regardless of political party. For this, we propose module- level permissions. We propose that each application has a PermissionManager attached to it. I'm not sure what the best way to do this would be, although the obvious way would be to use the first top-level PermissionManager instance found in the app as the module permission manager, similar to how we use the first queryset Manager instance found in a model as the default queryset manager. If none is found, the module will get a default PermissionManager that defines no module-level permissions. We would recommend that the module permission manager be named 'permissions' and be placed either in the application __inti__.py or models.py file. This may not be the cleanest of solutions, and I am open to other suggestions. This module-level PermissionManager would represent permissions attached to the module, but not to any specific model. We'd alter auth.Permission to allow a null Model, and those would be module-level permissions, accessed in the "app_label.permission_name" fashion. ## Superusers ## Any place that users override something like allows_PERM or PERM_list, they would have to consider whether or not the user is a superuser. This is a bit of boilerplate that is likely unnecessary. As such, let me describe how the allows_PERM and PERM_list functions work: The default allows_PERM(user) and PERM_list(user) methods simply call allows(permission, user) or list(permission, user) methods, where 'permission' is a string detailing the permission. These centralized methods always return true if the user is a superuser, and otherwise they look up the correct permission in auth.Permission and return the correct result. Thus, any override of allows_PERM or PERM_list need not check the user status so long as they use a super() call. This has the added benefit that a user can easily change the core workings of a PermissionManager by simply overriding the allows(permission, user) and edit(permission, user) functions. ## Backwards Compatibility ## As stated above, we suggest that referencing a model permission by string be changed to include the model name. "app.Model.perm" should be used to describe model permissions, while "app.perm" should be used for module permissions. That being said, in the interest of backwards compatibility, if "app.perm" fails, it should check "perm" on all modules. Whether or not such behavior should raise a PendingDepreciationWarning is up for discussion. Note that through the PermissionManager, ModelBackend will continue to be used without object permissions. "supports_object_permissions" will remain False. Rather, the default PermissionManager will check the permission with the ModelBackend, and then will filter the results accordingly through PERM_list. We feel that this is a better option than iterating over all available objects, asking "user.has_perm(perm, object)" for each object, and generating a list of all permitted objects. Using a PermissionManager and overriding PERM_list is both easier and more efficient than overriding the backend and checking with it for every object. That being said, users would still be welcome to make an object-managing backend and a specialized PermisisonManager that works with the new backend to make it's PERM_list methods. We would re-route User.has_perm to check with the PermissionManager instead of the backend, but this is trivial and would continue to work the same from all appearances. As far as the API goes, the old API would remain untouched (with pressure to move from app.perm to app.Model.perm), but the user would also be able to access permissions from the model side. In other words, the following will be equivalent: book.permissions.allows_edit(user) user.has_perm('writing.Book.edit', book) Author.permissions.allows_add(user) user.has_perm('writing.Author.add') Both styles have their place, I believe. ## Summary ## We would like to add a PermissionManager. It will be like a queryset Manager, and it will handle checking with the authentication Backend about the permission. It distinguishes between object-level and model- level permissions, providing "allows_PERM" (for model-level) and "PERM_list" (for object-level) methods for each permission at a class level, the former returning a bool and the latter returning a queryset. The common case will have users defining PermissionManagers with extra permissions, and overriding PERM_list to filter objects based upon user. We'll get started on writing a patch and some docs as soon as we get a green light. As you can tell, though, the idea is still not entirely fleshed out, and we'd like to bounce it around and clean it up a bit before we implement it. So, before we start logging hours, we'd like to know: is this sort of overhaul of the permission system something that the django-devs are interested in, and is this a sane direction to be taking? -- You received this message because you are subscribed to the Google Groups "Django developers" group. To post to this group, send email to django-develop...@googlegroups.com. To unsubscribe from this group, send email to django-developers+unsubscr...@googlegroups.com. For more options, visit this group at http://groups.google.com/group/django-developers?hl=en.