Policy composition and assignment

As described in the previous section, policies are effectively interpreted clause-by-clause, with later clauses overriding earlier ones. The same interpretation semantics also holds across multiple policies: the way that django-tutelary is intended to be used is that you associate a sequence of policies with each user, with the sequence usually going from less specific to more specific.

In this section, we’ll work through an example, then talk about the Policy and Role classes and User.assign_policies and assign_user_policies methods defined by django-tutelary to connect users to the policies that control their permissions. We’ll also look at some custom query methods for finding roles and retrieving the list of roles and policies assigned to a user.

A worked example

Suppose that we have an organization with “departments”, and each department has “sections”. Departments are labelled as dept/<dept-name> and sections as sect/<dept-name>/<sect-name>. The possible actions we’ll consider are dept.view, dept.create, dept.delete, sect.view, sect.create and sect.delete, respectively to view, create or delete departments and sections within departments.

First, let’s think about a default policy that we can apply to all users. Let’s assume that all users should be able to view all departments and all sections in all departments. The policy clauses in default-policy.json with then be something like:

[{"effect": "allow", "action": ["dept.view"],
  "object": ["dept/*"]},
 {"effect": "allow", "action": ["sect.view"],
  "object": ["sect/*/*"]}]

Now consider what additional permissions we might want to allow for overall administrators of the organization. Such users need to be able to create and delete departments and create and delete sections within any department, so the policy clauses in org-admin-policy.json would be:

[{"effect": "allow", "action": ["dept.create", "dept.delete"],
  "object": ["dept/*"]},
 {"effect": "allow", "action": ["sect.create", "sect.delete"],
  "object": ["sect/*/*"]}]

We may also have departmental administrators, who are able to create and delete sections within their own department, but not in any other departments. In order to represent this, we use the following policy clauses in dept-admin-policy.json:

[{"effect": "allow", "action": ["sect.create", "sect.delete"],
  "object": ["sect/$department/*"]}]

Note how we use a policy template variable ($department) to stand in for a particular department, rather than a wildcard to refer to all departments. A value for the $department variable must be supplied when making use of this policy, as we’ll see below.

In order to make use of these policies, we first read the policy files and create and save django-tutelary Policy objects from them:

from django.contrib.aith.models import User
from tutelary.models import Policy

...

default_policy = Policy(
  name='default',
  body=open('default-policy.json').read()
)
default_policy.save()
org_admin_policy = Policy(
  name='org-admin',
  body=open('org-admin-policy.json').read()
)
org_admin_policy.save()
dept_admin_policy = Policy(
  name='dept-admin',
  body=open('dept-admin-policy.json').read()
)
dept_admin_policy.save()

We then apply these policies to users:

user1 = User.objects.get(username='alex')
user1.assign_policies(
  default_policy,
  org_admin_policy
)

user2 = User.objects.get(username='bertie')
user2.assign_policies(
  default_policy,
  (dept_admin_policy, {'department': 'finance'})
)

user3 = User.objects.get(username='charlie')
user3.assign_policies(default_policy)

When we assign multiple policies to a user, the clauses from each of the policies are interpreted one by one to construct an overall set of permissions for the user. Note how we supply a value for the $department variable in the department administrator policy when we assign policies to user2 by providing a dictionary mapping between policy variable names and values. The end result of this is that users alex, bertie and charlie end up with the following sets of permissions:

User Actions Objects
alex
dept.*
sect.*
dept/*
sect/*/*
bertie
dept.view
sect.view
sect.*
dept/*
sect/*/*
sect/finance/*
charlie
dept.view
sect.view
dept/*
sect/*/*

The Policy model

Policies in django-tutelary are represented as JSON objects, but in order to use them within Django, we need to store them as Django model instances. The Policy model from tutelary.models is used for this. This is a simple model with a name and a body, which is used to hold the string representation of the JSON data defining the policy. A policy object can thus be created and saved to the database using code like this:

default_policy = Policy(
  name='default',
  body=open('default-policy.json').read()
)
default_policy.save()

Changes to Policy objects are audited using the django-audit-log package.

The Role model

As well as treating policies individually, it’s possible to bundle a sequence of policies sharing variable assigments into a role. These are represented by instances of the Django Role model in tutelary.models. If we have policies assigned to variables default_pol, org_pol and project_pol, we can create and save a role like this:

project_role = Role.objects.create(
    name='project_role',
    policies=[default_pol, org_pol, project_pol],
    variables={'organization': 'Cadasta', 'project': 'TestProj'}
)

If this role is subsequently assigned to a user, it’s precisely equivalent to assigning the individual policies, all with the same variable assignments. (The rules governing the template variable assignments in role definitions are the same as for assigning policies to users: briefly, all variables used in the body of the policy definitions must be given values, and any superfluous variable assignments are ignored.)

As for policy objects, changes to Role objects are audited using the django-audit-log package.

A common use case for roles is to have a named role (e.g. system-admin, project-manager, process-qa) using policies with variables that are filled in for particular assignments to users. (For instance, the policies for a project-manager roles will probably have a $project variable that needs to be filled in to instantiate the role for a particular project – i.e. to make a user a project manager for that particular project). This case is easy to deal with using the standard filter query method to find Role objects by name and variable assignment. Since role names are not constrained to be unique, you can give all the role instances you assign to project managers the same name and can do this to find the project manager roles for a particular project:

project_manager_roles = Role.objects.filter(
  name='project-manager',
  variables={'project': 'ExcitingNewProject'}
)

Assigning policies to users

To associate a sequence of policies with a user, thus assigning a set of permissions to the user, we use the User.assign_policies method (django-tutelary adds this method to whatever user model is set up in Django’s settings.AUTH_USER_MODEL configuration variable) or the assign_user_policies function from tutelary.models. The latter is usually only needed for assigning policies for unauthenticated users (see below).

The assign_user_policies function takes as arguments a user and a sequence of policies and just calls User.assign_policies on the supplied user, except in the case where the supplied user is None. In that case, the supplied sequence of policies is taken to define permissions for unauthenticated (i.e. anonymous) users. By default, unauthenticated users (like all other users) have no django-tutelary permissions, but it’s often useful to be able to assign a narrow set of permissions to unauthenticated users (to view all public data on the site, for example).

The sequence of policies passed to User.assign_policies (and assign_user_policies) contains either individual Policy or Role objects or 2-tuples of a Policy object and a dictionary of policy variable assignments. A typical use looks like this:

default_policy = Policy.objects.get(name='default')
editor_policy = Policy.objects.get(name='editor')
user = User.objects.get(username='iross')
user.assign_policies(
  default_policy,
  (editor_policy, {'organization': 'Cadasta',
                   'project': 'Kibera'})
)

This assumes that the JSON body of the Policy object named “editor” uses the policy variables $organization and $project. It’s important to note that values for all policy variables used within the body of a policy must be provided at the point of use of the policy – here, “point of use” means when the policy is assigned to a user using User.assign_policies. Superfluous variables are ignored, but any used within the policy body must be given values.

The list of policies and/or roles assigned to a user can be retrieved using the user_assigned_policies function and the User.assigned_policies method – passing None to the former retrieves the policies assigned to the anonymous user. The return value of both of these is a sequence in the same format as the arguments passed to User.assign_policies, i.e. a sequence of policy or role values or (policy/role, variables) pairs.