Advanced Permissions

100 views
Skip to first unread message

Nate

unread,
Jul 13, 2010, 12:08:03 PM7/13/10
to Django developers, marco....@gmail.com
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?

Andrew Ball

unread,
Jul 29, 2010, 5:24:23 PM7/29/10
to django-d...@googlegroups.com
I'm certainly interested in it, although I'm not a Django core developer.

Have you given any thought to allowing users to only read or update a
subset of the fields of a given model? We have a very complex
home-grown authorization system at the company I work for that I'm
very interested in replacing with something more standard/mainstream.

Also, with module-level permissions (which may be more like app-level
permissions if I understand what you're proposing correctly), is there
a possibility for one app to make use a permission defined in another?
For instance, I may have one app (say, united_states_polling) with
DemocratPoll and RepublicanPoll models as described in your
description and another app (say, great_britain_polling) with ToryPoll
and LabourPoll models. I would then possibly want to make use of a
module-level permission defined for the united_states_polling app in
the great_britain_polling app.

Thanks for working on this problem.

Peace,
Andrew

> --
> You received this message because you are subscribed to the Google Groups "Django developers" group.
> To post to this group, send email to django-d...@googlegroups.com.
> To unsubscribe from this group, send email to django-develop...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/django-developers?hl=en.
>
>

--
=======================
Andrew D. Ball
勃安
"Ὁ θεὸς ἀγάπη ἐστίν ..." (1 Jn 4:16)

Reply all
Reply to author
Forward
0 new messages