Advanced Features

Basic use of Djem’s OLP system is a simple drop-in extension of Django’s own permissions system, enabled by ObjectPermissionsBackend. If your user model, no matter how it is defined, is compatible with Django’s default permissions system, it will be compatible with the OLP system as well.

However, more advanced features are available that require a higher level of configuration. Specifically, they require a custom user model - they will not be available if simply making use of Django’s included auth.User model. Django recommends using a custom user model anyway (for new projects, at least), even if it doesn’t actually customise anything.

To enable these advanced features, described below, your custom user model must include the OLPMixin.

If not looking to actually customise anything, a custom user model incorporating OLPMixin is as simple as:

from django.contrib.auth.models import AbstractUser

from djem.models import OLPMixin


class User(OLPMixin, AbstractUser):

    pass

If looking to customise the user model more heavily (for example, using an email address instead of a username as the user’s identification token), use something like the following:

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin

from djem.models import OLPMixin


class User(OLPMixin, AbstractBaseUser, PermissionsMixin):

    ...

New in version 0.7: OLPMixin

Important

OLPMixin must be listed ahead of AbstractUser/PermissionsMixin in order for it to work correctly.

Superusers

The Django permissions system automatically grants any and all permissions to a User instance with the is_superuser flag set to True. By default, this is how the OLP system operates as well: no object-level access methods are executed, the superuser is simply granted the permission.

There are, however, situations in which this is not desirable. For example, you may want to define a model that does not grant the “delete” permission to anyone but the user that created it, no matter how “super” the user is. It would be trivial to configure the model to achieve this for a standard user, but a superuser would bypass any custom object-level access methods and be granted the permission anyway.

Djem provides a means of forcing superusers to be subject to the same OLP logic as regular users. They are still implicitly granted all permissions at the model level, but any object-level access methods will be executed and can deny the user permission.

In order to enable this feature, two things are required:

With these two requirements met, object-level permissions will be applied “universally”, including for superusers.

Note

Enabling this feature will cause superusers to be subject to the OLP logic for all permissions that define some. If your project contains permissions which should still be granted to superusers regardless of the additional checks that standard users are subject to, the relevant access method can include a simple guard clause:

def _user_can_vote_on_question(self, user):

    if user.is_superuser:
        return True

    # Do custom logic
    ...

Clearing the permission cache

As described in Caching, the results of object-level permission checks are cached, which has the downside of the results potentially getting out-of-date if elements of the state used to determine the permission are changed.

By default, the only way to clear this cache is to re-query for a new user instance. This is particularly annoying if needing to replace the user instance on the request object. OLPMixin provides a clear_perm_cache() method, which, as the name suggests, clears the permissions cache on the user instance.

In addition to clearing the OLP cache, clear_perm_cache() also clears Django’s model-level permissions caches, for good measure.

Automatically logging permission checks

OLPMixin leverages instance-based logging to support automatically logging all permission checks made via its overridden has_perm() method - both model-level and object-level.

Read the documentation for the instance-based logging functionality provided by Loggable for an introduction to the system. OLPMixin inherits from Loggable, and thus offers all the same features, in addition to those specific to permissions.

There are multiple levels of automatic permission logging available, controlled via the DJEM_PERM_LOG_VERBOSITY setting:

  • 0: No automatic logging

  • 1: Logs are automatically created for each permission check, with minimal automatic entries

  • 2: Logs are automatically created for each permission check, with more informative automatic entries

Using a setting above 0 configures has_perm() to create an appropriately-named log and populate it with automated entries as appropriate (based on the verbosity level chosen). In addition to the automated entries, having a suitable log already created and active provides a simpler experience if utilising logging in object-level access methods. Revisiting the “delete_product” access method described in the instance-based logging examples, enabling automatic logging allows for a simpler method definition:

class Product(models.Model):

    code = models.CharField(max_length=20)
    name = models.CharField(max_length=100)
    active = models.BooleanField(default=True)
    supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT)

    def _user_can_delete_product(self, user):

        if self.active:
            user.log('Cannot delete active product lines')
            return False
        elif get_quantity_in_stock(self):
            user.log('Cannot delete products with stock on hand')
            return False
        elif not user.has_perm('inventory.manage_supplier', self.supplier):
            inner_log = user.get_last_log(raw=True)
            user.log(*inner_log)
            return False

        user.log('Product can be deleted')
        return True

Log output

Using the “delete_product” permission from the above Product model as a reference, the output of a permission check with a DJEM_PERM_LOG_VERBOSITY setting of 1 might look something like:

Model-level Result: Granted

Cannot delete active product lines

RESULT: Permission Denied

And with a DJEM_PERM_LOG_VERBOSITY of 2:

Permission: inventory.delete_product
User: user.name (54)
Object: PROD123 (1375)

Model-level Result: Granted

Cannot delete active product lines

RESULT: Permission Denied

Tags

Automatically generated log entries utilise tagging, and are given the 'auto' tag. This allows them to be easily identified and filtered out if desired. Reproducing the above high-verbosity log output, highlighting which lines are tagged, gives:

[tag:auto] Permission: inventory.delete_product
[tag:auto] User: user.name (54)
[tag:auto] Object: PROD123 (1375)
[tag:auto]
[tag:auto] Model-level Result: Granted
[tag:auto]
Cannot delete active product lines
[tag:auto]
[tag:auto] RESULT: Permission Denied

Log names

Retrieving automatically generated permission logs via get_log() requires knowing their name. As long as you know the name of the permission that was checked, and the primary key of the object it was checked against (where applicable), the name of the log can be easily determined:

For model-level permission checks: auto-<permission_name> (e.g. auto-inventory.delete_product)

For object-level permission checks: auto-<permission_name>-<object_id> (e.g. auto-inventory.delete_product-1375)