Skip to content
Jon Staab edited this page Dec 3, 2015 · 9 revisions

Installation

Add "flexible_permissions" and "django.contrib.contenttypes" to INSTALLED_APPS:

    INSTALLED_APPS = (
        ...
        'django.contrib.contenttypes',
        'flexible_permissions',
    )

Run python manage.py migrate to create the models.

Dependencies:

  • django.contrib.contenttypes

Definitions

Reading this section will make the configuration section much easier. There are a few key terms to know.

  • A Target is any Django model. Flexible Permissions uses generic foreign keys, so all a Target needs is an id.
  • An Agent is any Django model to which you want to grant permissions. This probably will include User and Group, but it can include anything. A model can be a Target and Agent at the same time.
  • An Action is a label that is checked when permissions are enforced. These can be anything; consistent usage is up to you.
  • A Role is a label mapped to a number of actions. Multiple roles may be mapped to the same action. The reason for the distinction is that it's easy to consider Agents from a Role's perspective, while it's easier to consider application permission checks from the perspective of an Action.
  • A Permission is a mapping between a Role, an Agent, and a Target that grants access to all Actions associated with that Role to the Agent for the Target. Agent may be omitted to indicate that all Agents have access to that permission.
  • A Relation is a connection between two models, with any number of intermediate models, as defined by Django's fields, like ManyToManyField or ForeignKey.

Usage

Before diving into configuration, it might help to know what permission checks are supposed to look like. There are three main options for querying permissions: you can query the Permission model directly, use the shortcuts, or include the mixins in the inheritance tree of your models.

Querying Permissions directly is not super easy since it uses ContentType for handling generic relations. A Permission has a role, an agent (agent_type + agent_id), and a target (target_type + target_id). A Query might look something like this:

from django.contrib.contenttypes import ContentType
from flexible_permissions.models import Permission

user = User.objects.first()
site = Site.objects.first()

perm_granted = Permission.objects.filter(
    role='site.admin', 
    agent_id=user.id, 
    agent_type=ContentType.objects.get_for_model(user),
    target_id=site.id,
    target_type=ContentType.objects.get_for_model(site)
).exits()

if perm_granted:
    // etc...

Shortcuts take care of some of this by abstracting away the ContentType stuff. The convention for ordering arguments is role, agent, target, but you can use keyword arguments if you prefer. The same query above might look something like this:

from flexible_permissions.shortcuts import get_perms
from flexible_permissions.roles import actions_to_roles

user = User.objects.first()
site = Site.objects.first()

perm_granted = get_perms(actions_to_roles('site.update'), user, site)

if perm_granted:
    // etc...

You'll note that the actions_to_roles bit is necessary to turn an Action into a Role, since the shortcuts only work with roles. This is so that you can make up Actions depending on what exactly perm_granted enables an Agent to do, while managing far fewer, more meaningful roles in one place. More on that in the Configuration section below.

The final option takes a tiny bit more setup, but is well worth it. The mixins provide some abstract Model classes that provide methods for querying permissions. This is nice, since you can chain these checks with other queryset methods, and receive a queryset back. The PermTarget class is for querying targets, the PermAgent class is for querying agents. The above query would look like this:

user = User.objects.first()
site = Site.objects.first()

perm_granted = Site.objects.for_action('site.update', user)

if perm_granted:
    // etc...

You can do the same with a PermAgent class to get a list of Agents with a Role for a given Target. Note that this last option is read-only; if you want to update or remove permissions, you'll want to use the shortcuts.

Configuration

Out of the box, Flexible Permissions does very little. It is a configuration-heavy package, but it's well worth it. Configuration has the following aspects:

  • Registering action/role mappings
  • Registering agents and how each kind of agent is related to other agents.
  • Registering relations between target objects
  • Using provided QuerySets

I'll briefly describe each of these below. For a working example app, look at tests/models.py and tests/apps.py.

Note: Most of this configuration should happen within a Django AppConfig ready method.

Registering Roles and Actions

Roles and Actions are two sides of the same coin - Actions are statically mapped to Roles and configuration time, using something like the following:

from flexible_permissions.roles import register_role

register_role('site.admin', ['site.view', 'site.update'])

What this signifies is that an Agent with the site.admin role is allowed to view and update the site. This is not automatically enforced - implementing permission checks is up to you. See Usage above.

Registering Agents

This step is totally optional - an Agent can be any Model you put into the Permission table as an Agent. It's purely convention. Registering an Agent takes care of the use case where you want certain agents to be able to act as other agents - for example, a User acting with Permissions granted to a Group. How this relationship is defined is totally up to you. Here's an example:

from flexible_permissions.agents import register_agent

register_agent(User, lambda user: [user] + list(user.group_set.all()))

This will be enough to query the given user and all his groups when using the mixins. If you're using the shortcuts, though, be aware that you'll have to invoke this conversion yourself, using flexible_permissions.agents.normalize_agent.

Registering Relations

Relations are helpful when simple table-level permissions just aren't enough (which in my experience is pretty much always). This is useful for when you have a Model that belongs to another Model, which belongs to a... you get the picture. All you need to do is register these relations at app setup time, and they'll be traversed as part of the permissions checks. You can register them by providing the class of the Target that is inheriting permissions, along with a dictionary of underscore separated paths to the Target referenced in the Permission table:

from flexible_permissions.relations import register_relation

register_relation(Comment, {'site': 'thread__site'})

This allows you to do things like Comment.objects.for_action('view', agent) and get all the comments that an Agent can view by virtue of Permission records linking them and the agent with a relevant role.

You can also provide a list instead of a string to define paths, if two objects might be related in more than one way.

Subclassing Mixins

As I mentioned before the really nice features, like inferred agents and related models, aren't available unless you're using a PermQuerySet. To do this, you can grab either a PermTargetQuerySet or a PermAgentQuerySet from flexible_permissions.query and assign it as a manager to one of your models, e.g., objects = PermTargetQuerySet.as_manager(), and set up a GenericRelation on those Models. Or you could do the easy thing, and make your Models inherit from mixins.PermTarget or mixins.PermAgent, which will do all that for you:

from flexible_permissions.mixins import PermTarget, PermAgent

class Site(PermTarget):
    pass

class User(PermAgent):
    pass

This enables the nifty for_role/action and with_role/action methods on those models' querysets. If you already use a custom base model, you can just add the permissions mixins in at the end:

class Site(BaseModel, PermTarget):
    pass

Caveats

Performance has undergone some tuning, but I'm sure there's more to be done. The main thing to realize, performance wise, is that if registered relations require LEFT JOINs to traverse, queries can get prohibitively slow, very fast. This is mitigated in part by detecting when multiple relations need to be traversed, querying separately, and combining into a final queryset using id__in. This seems to work in most cases.

Development

Running tests

Please run tests when contributing a fix. You may optionally provide a dot-separated path to the tests you want to run, e.g., test.test_agents or tests.test_agents.AgentsTestCase.test_normalize_agent.

python runtests.py <test_name>

Releasing a new version

rm -r dist
python setup.py sdist
twine upload dist/*
Clone this wiki locally
You can’t perform that action at this time.