Developer Reference

Permissions System

Overview

Permissions system is implemented by security.permissions module. It is designed to be as low-ceremony as possible, but some boilerplate is unavoidable.

Package django-guardian is used to handle per-object permissions, so it’s possible to grant user extra permissions for some specific objects instead of all objects of particular type.

Support for the concept of object ownership is provided. If a Django model defines is_owner(self, user_or_group) method, any user or group that passes this method (i.e. True or equivalent value is returned) will be considered that object’s owner. Object owners always have full permissions for the object, including the permission to manage other users’ permissions. Essentially they’re equivalent to superusers, but their authority only covers the objects they own. Also, owner status does not grant add permission (which is considered model-level permission, not object-level).

Here’s inventory.models.InventoryObject.is_owner() method as example:

def is_owner(self, applicant):
    if isinstance(applicant, Group):
        return applicant == self.assigned_group
    if isinstance(applicant, User):
        return (applicant == self.assigned_user) or \
               self.assigned_group and (applicant in self.assigned_group.user_set.all())
    return False

Essentially, user owns an InventoryObject if he’s recorded as InventoryObject.assigned_user, or alternatively, if he’s a member of a group that’s recorded as InventoryObject.assigned_group. A group must match InventoryObject.assigned_group to be considered it’s owner.

Object’s Permissions Management

Permissions management consists of two components: object permissions overview table and object permissions form.

To display object permissions in HTML format, use the following boilerplate in your Django template:

{% include 'common/permissions_view.html' with object=your_object %}

This template expects that object is available in template context. If your object is already available in context with this name, with object=your_object part can be skipped.

Permissions overview table can be placed anywhere, though it’s advised to put it on a separate tab.

Permissions management form is provided as extension of racks.crud package. Normally, it is automatically accessible for any model which explicitly defines manage_yourmodelname permission:

class InventoryObject(models.Model):

    class Meta:
        permissions = (
            ('view_ipmi', 'View IPMI'),
            ('edit_ipmi', 'Edit IPMI'),
            ('manage_inventoryobject', 'Manage Access'),
        )

Alternatively, you can add 'manage' to the model’s list of default permissions and let Django handle permission name generation:

class InventoryObject(models.Model):

    class Meta:
        default_permissions = ('add', 'change', 'delete', 'manage')

For as long as there’s a manage permission defined for a model, CRUD viewset for that model will automatically enable access page, and “Manage Permissions” button will be displayed below the permissions overview table (disabled unless authenticated user is superuser, current object’s owner or has manage permission set for current object).

For models which do not define their own manage permission, permissions management form can still be enabled by explicitly setting access_view attribute of a viewset to racks.crud.AccessView:

from racks import crud

class MyModelViewSet(crud.ViewSet):
   access_view = crud.AccessView

To disable permissions form for a model which does define it’s own manage permission, set viewset’s disable_access_view attribute to True:

class MyModelViewSet(crud.ViewSet):
   disable_access_view = True

Protected Objects and Protected Fields

Module security.permissions provides functionality to restrict access to model’s fields based on user’s permissions for specific model instance. That functionality is not fully transparent and must be used explicitly. Essentially, you’re creating a security wrapper object for your model’s instance, and use that wrapper like you would use the wrapped object itself.

To create a security-wrapped object, use the following syntax:

from security.permissions import wrap

protected_obj = wrap(obj, user)

And here’s an example of a model with protected fields:

from security.permissions import require_owner

class MyModel(models.Model):
    public_field = CharField(max_length=50)
    readonly_field = CharField(max_length=50)
    secret_field = CharField(max_length=50)
    owner_field = CharField(max_length=50)

    class Meta:
        permissions = (
            ('manage_mymodel', 'Manage Access'),
            ('view_secret', 'Read Secret Field'),
            ('edit_secret', 'Write Secret Field'),
        )

    protected_fields = {
        'readonly_field': (True, False),
        'secret_field': ('read_secret', 'write_secret'),
        'owner_field': (True, require_owner),
    }

This model defines four fields, standard manage permission (so permissions management form will be enabled), two custom permissions and a special protected_fields dictionary which contains the list of all protected fields and their read/write prerequisites.

First field in this model is public_field. It is not mentioned in protected_fields, and as such receives no protection. Any user will be able to access this field’s value through the security wrapper, and any user with change permission for the object will be able to edit this field’s value (unless it’s protected by other means, like declaring it read-only in viewset or form).

Second field is readonly_field. It is mentioned in protected_fields and has quite simple prerequisites: read is always True and write is always False.

Third field is secret_field and it defines string read/write prerequisites. These are interpreted as follows:

  • Only users with “read_secret” permission on MyModel object may read secret_field value of that object.
  • Only users with “edit_secret” permission on MyModel object may modify secret_field value of that object.

Finally, the owner_field provides an example of a function callback being used as access validator. It’s read permission is always True and thus available to any user. However to modify this field, user must pass require_owner(obj, user) check.

security.permissions module provides three predefined callbacks that can be used in field permissions:

  • require_auth returns True for all objects for as long as user is authenticated.
  • require_owner returns True if user owns the object.
  • require_superuser returns True for all objects for if user is a superuser.

Custom callback functions or callable objects can be used if necessary.

Managing Permissions Programmatically

Permissions for specific object can be set with shortcut functions provided by django-guardian:

from django.contrib.auth.models import User
from myapp.models import MyModel
from guardian.shortcuts import assign_perm, remove_perm

me = User.objects.get(username='myname')
obj = MyModel.objects.get(pk=11)
assign_perm('myapp.delete_mymodel', obj)
remove_perm('myapp.manage_mymodel', obj)

To check user’s permissions on particular object, it is recommended to use get_permissions() shortcut function from security.permissions module:

from django.contrib.auth.models import User
from myapp.models import MyModel
from security.permissions import wrap, get_permissions

me = User.objects.get(username='myname')
obj = MyModel.objects.get(pk=11)
perms = get_permissions(obj, me)
if perms.change:
    log.info('I can modify this object!')
if perms.secret_field.view:
    log.info('I can view secret field value of this object!')

# Note that permissions are also available from security-wrapped object:
protected_obj = wrap(obj, me)
if protected_obj.permissions.change:
    log.info('I can still modify this object!')

Finally, user’s permissions for specific object can be checked with standard Django syntax, monkey-patched by django-guardian package:

me = User.objects.get(username='myname')
obj = MyModel.objects.get(pk=11)
if me.has_perm('change_mymodel', obj):
    # (...)

The last method is not recommended though, as it doesn’t handle object ownership. Also, with get_permissions() you can check manage permission on all models, even those that do not define their own manage_modelname permission.