User guide

Here is the quick overview how to use the framework.

The theory

Experience by itself teaches nothing...
Without theory, experience has no meaning.
Without theory, one has no questions to ask.
Hence without theory there is no learning.
~ W. Edwards Deming

Yes, theories can be boring. However, we need to establish some common vocabulary before we can move forward. Therefore, please review the theory section before heading on to practical stuff.

Installation

pip install django-access-control

That’s it. No need to add anything to INSTALLED_APPS, configure the AUTHENTICATION_BACKENDS or inherit from our User model. Also, we do not create any objects in your database.

Implementing access control

Different systems need different granularity when it comes to access control. Let’s go over some examples moving from more broad categories to more specific ones.

We will implement a simplified version of the example project: a Q&A site. Let’s create the model Question as follows:

from django.db import models

class Question(models.Model):
    title = models.CharField(max_length=100)
    body = models.TextField()
    author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
    is_published = models.BooleanField(default=True)

    objects = QuestionQuerySet.as_manager()

    def __str__(self) -> str:
        return self.title

Above the model, let’s add the query set:

from django_access_control.querysets import ConfidentialQuerySet

class QuestionQuerySet(ConfidentialQuerySet):
    pass

Finally, let’s register oue model to the DJango admin site:

from django.contrib import admin
from django_access_control.admin import ConfidentialModelAdmin

from questions.models import Question


@admin.register(Question)
class QuestionAdmin(ConfidentialModelAdmin):

    def save_model(self, request, obj, form, change):
        """
        Automatically fill `question.author` with the current user
        when a new question is added.
        """
        if not change:  # `not change` means the obj is added, not modified
            obj.author = request.user
        super().save_model(request, obj, form, change)

Now everything should work in the same way as it normal would since our permission policy defaults are the same as Django’s to make it more intuitive to use.

Table level permissions

There are four methods to configure table level permissions. Here they are with their default implementations:

def has_table_wide_add_permission(self, user: AbstractUser) -> bool:
    return user.is_superuser or has_permission(user, "add", self.model)

def has_table_wide_view_permission(self, user: AbstractUser) -> bool:
    return user.is_superuser or has_permission(user, "view", self.model)

def has_table_wide_change_permission(self, user: AbstractUser) -> bool:
    return user.is_superuser or has_permission(user, "change", self.model)

def has_table_wide_delete_permission(self, user: AbstractUser) -> bool:
    return user.is_superuser or has_permission(user, "delete", self.model)

Let’s override one of them to give every logged in user the right to add questions:

def has_table_wide_add_permission(self, user: AbstractUser) -> bool:
    return user.is_authenticated

Row level permissions

If the user has some permission for the entire table, it will apply to all the rows. The methods below will give you more granular control by allowing to you give extra permission to a limited number or rows.

The methods you can use are:

def rows_with_extra_view_permission(self, user: AbstractUser) -> QuerySet:
    return self.none()

def rows_with_extra_change_permission(self, user: AbstractUser) -> QuerySet:
    return self.none()

def rows_with_extra_delete_permission(self, user: AbstractUser) -> QuerySet:
    return self.none()

By default they do not grant permission to any rows. Let’s adjust the view permissions so that

  • everybody can see published questions
  • staff members can see all questions
  • authors can see their questions even if they are not published
def rows_with_extra_view_permission(self, user: AbstractUser) -> QuerySet[Question]:
    if user.is_staff:
        return self
    return self.filter(is_published=True) | \
            (self.filter(author=user) if user.is_authenticated else self.none())

On the last line | is the bitwise OR operator which returns the union of the two querysets (this is why we learned set theory at school 🙂 ).

The if user.is_authenticated else is necessary because if the user is not logged in, they are represented by an AnonymousUser which has a more limited interface compared to the regular User. Without this check, AnonymousUser would be passed to self.filter(author=user) which would result in an exception in the lower layers (which was pretty hard to track down…).

Colum level permissions

There are three methods for fine-tuning field level permissions:

@classmethod
def addable_fields(cls, user: AbstractUser) -> FrozenSet[str]:
    """
    Control which fields the user can specify when creating a new object (row).
    NB! Make sure that all not nullable fields that are not specified here
    would be populated automatically
    to prevent IntegrityError for the database.
    """
    return frozenset(all_field_names(cls.model))

@staticmethod
def viewable_fields(user: AbstractUser, obj) -> FrozenSet[str]:
    """
    Control which wields the user can view.
    """
    return frozenset(all_field_names(obj))

@staticmethod
def changeable_fields(user: AbstractUser, obj) -> FrozenSet[str]:
    """
    Control which wields the user can edit.
    """
    return frozenset(all_field_names(obj))

By default the user has access to all the fields. Let’s update it so that

  • the question authors can update the body of the question
  • stuff members can publish and unpublish questions
  • superusers can do everything
@staticmethod
def changeable_fields(user: AbstractUser, obj: Question) -> FrozenSet[str]:
    if user.is_superuser: return frozenset(all_field_names(obj))
    fields = frozenset()
    if obj.author == user: fields |= frozenset({"body"})
    if user.is_staff: fields |= frozenset({"is_published"})
    return fields

The view layer

Django Access Control provides out-of-the-box integration with the Django admin site through the ConfidentialModelAdmin class. You can use its implementation as an example for your custom views and forms as well.

In addition to the methods already discussed above, ConfidentialQuerySet provides some special methods intended to be used in the view layer:

  • rows_with_view_permission(self, user: AbstractUser) -> QuerySet
  • rows_with_change_permission(self, user: AbstractUser) -> QuerySet
  • rows_with_delete_permission(self, user: AbstractUser) -> QuerySet
  • rows_with_some_permission(self, user: AbstractUser) -> QuerySet – returns all rows for which the user has at least some kind of permissions
  • has_some_permissions(self, user: AbstractUser) -> bool – indicates whether the user has any permission to any instance of the model
  • contains(self, obj: Model) -> bool – indicates whether the given queryset contains the obj
Last modified July 10, 2021: Working on homepage (9c46162)