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 returns True or False, 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 of permission_required. Note that if the callable is defined as a method, its signature should be method_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 three permission_required options, or None, 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, a PermissionDenied 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), where self and view both refer to the Django view being rendered, and obj 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.