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:
A custom user model including
OLPMixin
, as described above.The
DJEM_UNIVERSAL_OLP
setting set toTrue
.
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 logging1
: Logs are automatically created for each permission check, with minimal automatic entries2
: 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
)