Permissions for views¶
It’s recommended that you use class-based views with django-tutelary. The permission handling API for class-based views is much more flexible than for function views.
Class-based views¶
Using django-tutelary for controlling access to Django views is
straightforward. There are two mixin classes defined in
tutelary.mixins
, PermissionRequiredMixin
for “normal” Django
views and APIPermissionRequiredMixin
for DRF views, and in most cases one
of these can simply be mixed into your view classes without any drama.
For example, suppose that you’ve defined a user.detail
action for
the User
model (using the permissioned_model
function). You
can then control access to a detail view for the User
model using
this action as follows:
import django.views.generic as generic
from tutelary.mixins import PermissionRequiredMixin
from django.contrib.auth.models import User
...
class UserDetail(PermissionRequiredMixin, generic.DetailView):
model = User
permission_required = 'user.detail'
The only additional element needed to activate django-tutelary
permissions is the permission_required
attribute that should be
added to the view. There are a number of possibilities for setting
this attribute, but the simplest option is either a single action name
or a sequence of action names. All of the actions listed in
permission_required
must be allowed for the user on the model
object for the view to succeed. (There are some exceptions to this
statement, related to “collective actions” – see below.)
The permission_required
attribute¶
As noted above, the simplest value that can be given for the
permission_required
attribute in the PermissionRequiredMixin
or APIPermissionRequiredMixin
mixins is a single action name. A
range of other possibilities allow for more flexible handling of
permissions lookup, dependent on characteristics of the request being
processed.
The options for permission_required
are:
- Single action
- This is the simplest case – a single action name is provided as a string. The permissions backend checks that the user making the request is allowed to perform the specified action on the object (or objects) associated with the view.
- Sequence of actions
- If a sequence of action names is provided for
permission_required
, then all of these actions must be permitted for the user for the request to be allowed. - Callable
- If the
permission_required
value is callable, it is called with the view and request as arguments. If the callable returnsTrue
orFalse
, the request is permitted or denied immediately. If the callable returns a single string or sequence of strings, these are interpreted as action names, and the permissions for the view are checked as if these action names had been provided directly as the value ofpermission_required
. Note that if the callable is defined as a method, its signature should bemethod_name(self, view, request)
, even if it’s already defined as a member of a view class. - Method dictionary
- The final option for
permission_required
is to provide a dictionary whose keys are HTTP method names (GET, POST, PUT, etc.). The values in the dictionary can be any of the preceding threepermission_required
options, orNone
, which indicates that requests for the given HTTP method are always permitted. This facility be useful for cases where it’s desirable for unpermissioned users to be able to access the forms to perform particular actions, even if the actions then subsequently fail when form data is POSTed. For example, you might want to allow any user to access the form for creation of new entities, and for permissioning only to be applied at the point where the user submits the form and the object is to be created. An example is presented below.
Some examples should make this clearer. Suppose that we have a
ListCreateView
which provides list of Board
entities via GET
requests, with an attached form to create a new Board
entity, and
processes form uploads on POST requests. If we want it to always be
possible to access the list and attached form, but to apply the
board.create
permission to form submissions, we can do this:
import views.generic.edit as edit
from tutelary.mixins import PermissionRequiredMixin
from manufacturing.models import Board
...
class BoardList(PermissionRequiredMixin, edit.ListCreateView):
model = Board
permission_required = {
'GET': None,
'POST': 'board.create'
}
The permission_required
method dictionary says that GET requests
are always permitted (because of the None
value) and POST requests
must have permissions to perform the board.create
action.
Using a callable for permission_required
allows us to make the
permissions required to fulfil a request depend on the state of a
model entity or body data in the request. Here’s an example where the
actions that need to be checked in a DRF RetrieveUpdateAPIView
depend on the state of a model entity (is the entity “archived”?) and
the request body (is the request trying to “archive” or “unarchive”
the entity?):
class OrganizationDetail(APIPermissionRequiredMixin,
generics.RetrieveUpdateAPIView):
def patch_actions(self, view, request):
is_archived = self.get_object().archived
new_archived = request.data.get('archived', is_archived)
if not is_archived and (is_archived != new_archived):
return ('org.update', 'org.archive')
elif is_archived and (is_archived != new_archived):
return ('org.update', 'org.unarchive')
else:
return 'org.update'
permission_required = {
'GET': 'org.view',
'PATCH': patch_actions
}
Special treatment of collective actions¶
There are a couple of extra features for annotating views that are
intended to make some common use cases with “collective actions” work
more smoothly. In this context, “collective actions” means actions
that refer to more than one object at a time. In normal Django
generic views, any views that use the SingleObjectMixin
class
don’t refer to collective actions, while those that use the
MultipleObjectMixin
class do. Similary, when using the Django
REST Framework, generic views that use the ListModelMixin
class
are collective actions and most others are not.
The essential issue with collective actions is that a user may have
permission to perform a particular action on only a subset of the
queryset of a view. Normally, django-tutelary checks that the
requested actions are permitted on all the objects in the queryset.
If any of these permission checks fail, then the entire attempt to
render the view will fail and a PermissionDenied
exception will be
raised. This behaviour is reasonable, but there is an alternative and
equally reasonable option, which is to filter the view’s queryset so
that only objects for which the action is permitted remain.
As a concrete example, suppose that we have models representing organizations and projects in our application. Each project belongs to a single organization. Our models look like this (all the code examples shown in this section are just sketches – you’d obviously need to add some things to fully functional working models and views, but we’ll show enough to illustrate the permissioning issues):
@permissioned_model
class Organization(models.Model):
name = models.CharField(max_length=100)
class TutelaryMeta:
perm_type = 'org'
path_fields = ('pk',)
actions = [('org.list', {'permissions_object': None}),
('org.create', {'permissions_object': None}),
'org.detail',
'org.delete']
@permissioned_model
class Project(models.Model):
name = models.CharField(max_length=100)
organization = models.ForeignKey(Organization)
class TutelaryMeta:
perm_type = 'project'
path_fields = ('organization', 'pk')
actions = [('project.list',
{'permissions_object': 'organization'}),
('project.create',
{'permissions_object': 'organization'}),
'project.detail',
'project.delete']
Suppose that we wish to provide a view to list all projects in the
database. Using a DRF ListAPIView
, our view might look something
like this:
class ProjectListView(APIPermissionRequiredMixin, ListAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_required = 'project.list'
Now, suppose that we process a request to render this view for a user
who has project.list
permissions for the Organization
with
name org1
, but not for org2
. As it stands, assuming that
projects exist in both of these organizations, this user’s request
will fail with a PermissionDenied
exception, because the
project.list
action has to be allowed for all of the objects in
the view’s queryset, which includes both projects in org1
(that
the user can list) and projects in org2
(that the user is not
permitted to list).
This is obviously inconvenient. To make this work, we would have to
override the get_queryset
method on our view and manually filter
out the objects for which the request is not permitted. Instead of
doing this, django-tutelary allows us to specify that we want the
view’s queryset to be filtered. We do this by adding a
permission_filter_queryset
attribute to the view class:
class ProjectListView(APIPermissionRequiredMixin, ListAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_required = 'project.list'
permission_filter_queryset = True
The permission_filter_queryset
attribute can be set to:
False
: gives the default (“all fail if one fails”) behaviour;True
: causes querysets for all collective actions to be filtered – in this case, aPermissionDenied
exception is never raised: if the action is denied for all objects in the queryset, then an empty queryset is used for the view;- a sequence of “associated” action names or a dictionary mapping from action names to sequences of “associated” action names: in this case, the queryset is filtered both on the “main” action and the “associated” action – this capability is intended primarily for list views, where it may be desirable to restrict the entities rendered to a subset where certain other actions can be performed.
- a callable: this is called as
fun(self, view, obj)
, whereself
andview
both refer to the Django view being rendered, andobj
is the object for which permissions are being determined. The return value of the callable should be a sequence of “associated” actions to add to the list of actions whose permissions are being checked for the object. (The slightly strange calling sequence of the callable is because Python treats it as a method within the view class.)
As an example of the last, more complex case, suppose that we want to display a list of all the projects that a user is allowed to delete. We can do this with a view like this:
class ProjectDeleteListView(APIPermissionRequiredMixin, ListAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_required = 'project.list'
permission_filter_queryset = ['project.delete']
This view will return all projects for which the requesting user has
the project.list
permission for the associated organization, and
for which the user has the project.delete
permission on the
project itself.
Collective action filtering using PermissionsFilterMixin
¶
Another method to filter querysets for collective actions (especially listing of objects) is to provide a list of permissions as a query filter with the request.
Suppose that we have a view that lists all projects in an organization that the user is allowed to view. The corresponding view would look like this:
class ProjectListView(APIPermissionRequiredMixin, ListAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_required = 'project.list'
permission_filter_queryset = ['project.view']
Clients can access this view via GET /projects
and the server
returns a list of all viewable projects. To get a list of all
projects the user can update and delete as well as view, clients can
add a permissions
query parameter to the request URL: GET
/projects/?permissions=project.update,project.delete
. This results
in a list of projects for which the user has project.view
,
project.update
and project.delete
permissions.
This functionality is provided by the PermissionsFilterMixin
mixin
class from tutelary.mixins
. Filtering based on the
permissions
URL query parameter can be enabled by adding this
mixin to the view:
class ProjectListView(PermissionsFilterMixin,
APIPermissionRequiredMixin,
ListAPIView):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_required = 'project.list'
permission_filter_queryset = ['project.view']
The get_perms_objects
method¶
Most of the time, django-tutelary can work out which objects are
associated with a view itself – for single-object views, it calls
get_object
, for multi-object views it calls get_queryset
, and
it also does something reasonable for object creation forms, which are
something of a special case.
However, there are some cases where it’s not possible to work out
which object or objects permissions should be tested on just from the
form of the view. One example where this is likely to be the case is
if you have a many-to-many relation from a base object class to a
subsidiary class, and you want to control addition and removal of the
subsidiary objects based on the base object. The views in this case
will be views of the subsidiary objects, so some mechanism is needed
to allow django-tutelary to identify the base object on which
permissions should be test. For this purpose, django-tutelary checks
to see whether a view has a method called get_perms_objects
, and
if it does, it uses this method to find the set of objects on which to
test permissions.
To see how this works, suppose we have a model representing organizations, and that organizations can have users as members, with this membership being represented by a many-to-many field between the organization model and the user model. Adding or removing users to an organization is an operation on the organization, not on the user list, but the user list is what the views will be managing. To deal with this, we write code like this:
class OrganizationUsersDelete(APIPermissionRequiredMixin,
OrganizationUsersQuerySet,
generics.DestroyAPIView):
permission_required = 'org.users.remove'
def get_perms_objects(self):
return [self.get_organization(self.kwargs['slug'])]
def destroy(self, request, *args, **kwargs):
user = self.get_object()
self.org.users.remove(user)
return Response(status=status.HTTP_204_NO_CONTENT)
Here, the get_perms_objects
method returns a reference to the
organization on which permissions should be tested.
Use of the get_perms_objects
method shortcuts all the other
mechanisms that django-tutelary uses to determine permissions objects,
so can easily be used to deal with any special cases of this kind.
Function views¶
For function views, there is a permission_required
decorator that
works in a similar way to the permission_required
decorator in
Django’s default permissions system – see the reference documentation
for details.