Object-Level Permissions¶
Django’s permissions framework has the foundation for, but no implementation of, object-level permissions. For example, using the standard Django “polls” application to illustrate, you can use the Django permissions framework to determine if any given user can change Questions
, but not to determine if they can change a given Question
in particular.
Djem provides a very simple implementation of an object-level permissions system with the following features:
- As with model-level permissions, an object-level permission may be granted to a user based on the
User
object itself, or based on aGroup
to which the user belongs. - Permissions are only checked at the object level if the user has the model-level permission. That is, a user must be able to change
Questions
in general if they are to be granted permission to change a particularQuestion
. - Methods on the object itself grant or deny the permission, based on self-contained logic. The database is not required to store links between users/groups and individual model objects.
Note
Just as with the Django permissions framework, the object-level permissions systems expects the User
model to have certain attributes and methods. If you are using a custom user model, it will need to include the PermissionsMixin
to be compatible. See the Django documentation for custom user models and permissions.
Enabling¶
Use of object-level permissions is enabled simply by including the custom djem.auth.ObjectPermissionsBackend
authentication backend in the AUTHENTICATION_BACKENDS
setting:
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'djem.auth.ObjectPermissionsBackend'
]
See the Django documentation on authentication backends for more information.
New in version 0.4: ObjectPermissionsBackend
Supported permissions¶
Any existing permission can be used with the object-level permissions system, though it may not make sense for all of them. For example, Django provides default “add” permissions for all models. It doesn’t make sense for adding to involve object-level permissions, as no object would yet exist on which to check for an “add” permission. That being said, the object-level permissions system contains no logic preventing you from using any permission at the object level.
If the default Django-provided permissions (“add”, “change” and “delete”) aren’t enough, you can add custom permissions via the permissions
attribute of a model’s inner Meta
class, as per the Django documentation.
Any permissions added this way are automatically supported by the object-level permissions system. You just need to define the necessary methods on the model class, as described below. And remember: a user must have the standard, model-level permission before object-level permissions will even be checked. It does not matter how the user is granted the model-level permission, as long as they have it. That is, it may be granted to the user specifically, or to any one of the groups they belong to.
Defining permissions¶
Object-level permissions are ultimately determined by specially-named methods on the object in question. The two types of methods are:
_user_can_<permission_name>(self, user)
: Grant/deny permission based on the givenUser
instance by returningTrue
orFalse
, respectively._group_can_<permission_name>(self, groups)
: Grant/deny permission based on the givenGroup
queryset by returningTrue
orFalse
, respectively.
For the Django default “change” permission on the polls.Question
model, the method names would be: _user_can_change_question()
and _group_can_change_question()
.
When defining custom permissions, the permission name used in the method names must be the same as that provided in the permissions
attribute of the model’s Meta
class. If either of the methods returns True
, the user is granted the permission.
The following example demonstrates how to define a model that uses object-level permissions for a custom permission. It uses a modified version of the Question
model created in the Django tutorial that only allows voting by explicitly defined users.
from django.conf import settings
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
allowed_voters = models.ManyToManyField(settings.AUTH_USER_MODEL)
def _user_can_vote_on_question(self, user):
return self.allowed_voters.filter(pk=user.pk).exists()
class Meta:
permissions = (('vote_on_question', 'Can vote on question'),)
Note
The ObjectPermissionsBackend
handles calling these methods when necessary - they should never need to be called manually.
Note
These object-level access methods can raise PermissionDenied
and it will be treated as if they returned False
. Regardless of whether the user-based or group-based check raises the exception, the other could still grant the permission.
Permissions default open¶
An important concept in Djem’s object-level permissions system is that permissions default open at the object level. That is to say, unless explicit logic is given to dictate how an object-level permission should be granted/denied, it is assumed to be granted. As such, an object-level permission check on an object with no defined object-level access methods is equivalent to a model-level permission check for the same permission.
This makes the system interchangeable with the existing Django permissions system. Common code can check permissions at the object level and will be unaffected if no object-level access control exists for a given model - it doesn’t need to pick and choose whether to use object-level or model-level permission checking.
Checking permissions¶
The main ways of using the object-level permissions system to check a user’s permissions on a specific object are:
- the
permission_required
decorator for function-based views orPermissionRequiredMixin
mixin for class-based views - the
ifperm
andifnotperm
template tags - the
has_perm()
method on aUser
instance
All of these approaches use the standard Django permissions framework and rely on the custom ObjectPermissionsBackend
to call the appropriate _user_can_<permission_name>
/_group_can_<permission_name>
methods. In the examples below, each permission check will result in _user_can_<permission_name>
being called and provided the User
instance involved in the check, and _group_can_<permission_name>
being called and provided with a queryset of all Groups
to which that user belongs. Either method can return True
to grant the user permission.
Warning
The object on which a permission is checked is not verified. That is, you could check the polls.vote_on_question
permission on an instance of any random model and no warning would be given (checking validity would add unnecessary overhead to such a common operation). This is important because, if the instance provided does not define the appropriate object-level access methods (such as _user_can_vote_on_question()
), the permission is assumed to be granted at the object level, since permissions default open.
Protecting views¶
Protecting views that should only be accessed by users with certain object-level permissions is supported by Djem’s extensions of the standard Django permission_required
decorator for function-based views and PermissionRequiredMixin
mixin for class-based views.
See the Django documentation for the decorator and the mixin for the basic functionality these helpers provide.
Checking an object-level permission involves querying for an instance of the model the permission is for. If such an instance cannot be found, a Http404
exception is raised.
New in version 0.5: The permission_required()
decorator and the PermissionRequiredMixin
class-based view mixin.
Basic usage¶
Usage of Djem’s permission_required()
and PermissionRequiredMixin
is very similar to the originals except that specifying an object-level permission is done using a tuple of two strings: the first naming the permission, the second naming the view function keyword argument that contains the primary key of the object to test.
Model-level permissions can still be checked by specifying a plain string as per usual.
A mixture of multiple model-level and object-level permissions is also fully supported. In this case, permissions are checked in the order they are listed, and a user must pass every check in order to access the view.
from django.views import View
from djem.auth import PermissionRequiredMixin, permission_required
# Check a model-level permission on a function-based view
@permission_required('polls.view_questions')
def cast_vote(request, question):
...
# Check an object-level permission on a function-based view
@permission_required(('polls.vote_on_question', 'question'))
def cast_vote(request, question):
...
# Check a mixture of permissions on a function-based view
@permission_required('polls.view_questions', ('polls.vote_on_question', 'question'))
def cast_vote(request, question):
...
# Check a model-level permission on a class-based view
class CastVote(PermissionRequiredMixin, View):
permission_required = 'polls.view_questions'
...
# Check an object-level permission on a class-based view
class CastVote(PermissionRequiredMixin, View):
permission_required = [('polls.vote_on_question', 'question')]
...
# Check a mixture of permissions on a class-based view
class CastVote(PermissionRequiredMixin, View):
permission_required = ['polls.view_questions', ('polls.vote_on_question', 'question')]
...
Note
When specifying a single object-level permission using the permission_required
attribute of PermissionRequiredMixin
, it must be given as an item of an iterable (e.g. a list). While a single model-level permission can be provided as either a plain string or a single-item iterable (a feature inherited from Django’s own mixin class), because an object-level permission is defined in a tuple - which is itself an iterable - it would be treated as two model-level permissions (and would be invalid).
PermissionRequiredMixin
and the URLconf¶
Like all attributes of class-based views, the permission_required
attribute added by PermissionRequiredMixin
can be specified/overridden in the URLconf:
from django.conf.urls import url
from .views import CastVote
urlpatterns = [
url(
r'^question/(?P<question>\d+)/votes/cast/$',
CastVote.as_view(permission_required=['polls.view_questions', ('polls.vote_on_question', 'question')]),
name='cast-vote'
)
]
Controlling denied access¶
As with the originals, login_url
and raise_exception
are fully supported - as arguments to the permission_required()
decorator or as attributes of a class inheriting from PermissionRequiredMixin
. These allow additional control over what happens when a user is denied access to a view protected by the decorator/mixin. See the Django documentation for permission_required for more info on how these features work.
Djem extends this control slightly with the DJEM_DEFAULT_403
setting. This setting can be used to control the default value of raise_exception
. Django’s default is to NOT raise the PermissionDenied
exception, preferring to redirect to the login view, but allowing you to override this behaviour per-view using raise_exception
. Setting DJEM_DEFAULT_403
to True
allows you to configure all protected views to raise the PermissionDenied
exception by default, while still allowing per-view customisation with raise_exception
.
Argument replacement¶
When using either permission_required()
or PermissionRequiredMixin
to protect a view using object-level permissions, and the user passes all permission checks, any arguments named in an object-level permission two-tuple will be replaced with the appropriate instances.
These instances will already have been queried in order to check the user’s permissions against them, so they are injected into the view’s keyword arguments, replacing those that specified the primary key used in the queries. This allows the view to access such instances without needing to query for them again.
In the “cast vote” view examples used above, the view accepts a question
keyword argument. This argument is named as the source of the primary key of a Question
record, and used to check the user’s polls.vote_on_question
permission against that specific Question
. While the view was originally passed the primary key of a Question
(as controlled by the URLconf), this is used and replaced as part of the permissions check, and the view sees a Question
instance.
Checking in templates¶
Checking object-level permissions in a Django template can be done using the ifperm
and ifnotperm
template tags. These are block tags whose content is displayed if the permissions check passes. For ifperm
, it passes if the user has the permission. For ifnotperm
, it passes if the user does not have the permission. Each tag supports an else
block, whose content is displayed if the permissions check fails.
Each tag must be passed a user instance, the name of the permission to check and the object to check it on.
{% load djem %}
...
{% ifperm user 'polls.vote_on_question' question_obj %}
<a href="{% url 'vote' question_obj.pk %}">Vote Now</a>
{% else %}
You do not have permission to vote on this question.
{% endifperm %}
...
{% load djem %}
...
{% ifnotperm user 'polls.vote_on_question' question_obj %}
You do not have permission to vote on this question.
{% else %}
<a href="{% url 'vote' question_obj.pk %}">Vote Now</a>
{% endifnotperm %}
...
Checking via User
instances¶
The has_perm()
method provided by Django’s User
model (or by PermissionsMixin
if using a custom user model) accepts an optional obj
argument. Django does nothing with it by default, but passing it will invoke Djem’s object-level permissions system. Thus it can be used to check a user’s object-level permissions on a given object.
Continuing with the modified Question
model defined above:
>>> user = User.objects.get(username='alice')
>>> question = Question.objects.filter(voters=user).first()
>>> user.has_perm('polls.vote_on_question', question)
True
>>> question = Question.objects.exclude(voters=user).first()
>>> user.has_perm('polls.vote_on_question', question)
False
See has_perm()
documentation for User and PermissionsMixin.
Note
In addition to has_perm()
, the has_perms()
, get_group_permissions()
and get_all_permissions()
methods on User
/PermissionMixin
also accept the optional obj
argument and work with the object-level permissions system.
Caching¶
Like ModelBackend
does for model-level permissions, the ObjectPermissionsBackend
caches object-level permissions on the User
object after the first time they are checked. Unlike ModelBackend
, the user’s entire set of object-level permissions are not determined and cached on this first access, only the specific permission being tested, for the specific object given.
This caching system has the same advantages and disadvantages as that used for model-level permissions. Multiple checks of the same permission (on the same object) in the same request will only need to execute the (possibly expensive) logic in your _user_can_<permission_name>()
/_group_can_<permission_name>()
methods once. However, that means that if something changes within the request that would alter the state of a permission, and that permission has already been checked, the User
object will not immediately reflect the new state of the permission - a new instance of the User
would need to be queried from the database. Exactly what might affect the state of a permission depends entirely upon the logic implemented in the _user_can_<permission_name>()
/_group_can_<permission_name>()
methods, so this is something to be aware of both while writing these methods and while using them.
Other PermissionsMixin
methods¶
The object-level permissions system is fully compatible with Django’s PermissionsMixin
, meaning it supports more than just the has_perm()
method. Other supported methods include:
has_perms()
: For checking multiple permissions against aUser
instance at once.get_all_permissions()
: To obtain a list of all permissions accessible to the user, either directly or via their groups, with all necessary object-level logic applied.get_group_permissions()
: To obtain a list of all permissions accessible to the user via their groups only, with all necessary object-level logic applied.
While has_perms()
is a simple extension of has_perm()
to allow checking multiple permissions at once, some care should be taken with get_all_permissions()
and get_group_permissions()
.
Firstly, depending on the number of permissions your project uses, the amount that have object-level access methods defined, and the complexity of the logic used by those access methods, obtaining a list of available permissions could involve a lot of processing (compared to testing one at a time).
More subtly, get_group_permissions()
can potentially list permissions that would not actually be granted to the user via a standard permissions checking. This is a side-effect of the fact that object-level permissions default open. If a user-based object-level access method denied a certain permission, and no group-based access method was defined, a normal permissions check would return False
, on account of the user-based check. But a group-only check, such as performed by get_group_permissions()
would grant the permission, due to there being no object-level access method to indicate otherwise.
While not accessible via PermissionsMixin
, ObjectPermissionsBackend
also contains a get_user_permissions()
method which suffers from the same side-effect due to ignoring group-based access methods.