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:

  .. sourcecode:: python

     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:

  .. sourcecode:: html

     {% 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:

  .. sourcecode:: python

     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:

  .. sourcecode:: python

     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``:

  .. sourcecode:: python

     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:

  .. sourcecode:: python

     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:

  .. sourcecode:: python

     from security.permissions import wrap

     protected_obj = wrap(obj, user)

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

  .. sourcecode:: python

     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``:

  .. sourcecode:: python

     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:

  .. sourcecode:: python

     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:

  .. sourcecode:: python

     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.
