Skip to main content

Authorization library for Django

Project description

django-cancan

Logo

Build Status PyPI version

django-cancan is an authorization library for Django. It works on top of default Django permissions and allows to restrict the resources (models and objects) a given user can access.

This library is inspired by cancancan for Ruby on Rails.

Key features

  • All of your permissions logic is kept in one place. User permissions are defined in a single function and not scattered across views, querysets, etc.

  • Same permissions logic is used to check permissions on a single model instance and to generate queryset containing all instances that the user can access

  • Easy unit testing

  • Integration with built-in Django default permissions system and Django admin (coming soon)

  • Intergration with Django Rest Framework (coming soon)

How to install

Using pip:

pip install django-cancan

Quick start

  1. Add cancan to your INSTALLED_APPS setting like this:
INSTALLED_APPS = [
    ...,
    'cancan',
]
  1. Create a function that define the access rules for a given user. For example, create abilities.py in myapp module:
def define_access_rules(user, rules):
    # Anybody can view published articles
    rules.allow('view', Article, published=True)

    if not user.is_authenticated:
        return 

    # Allow logged in user to view his own articles, regardless of the `published` status
    # allow accepts the same kwargs that you would provide to QuerySet.filter method
    rules.allow('view', Article, author=user)

    if user.has_perm('article.view_unpublished'):
        # You can also check for custom model permissions (i.e. view_unpublished)
        rules.allow('view', Article, published=False)


    if user.is_superuser:
        # Superuser gets unlimited access to all articles
        rules.allow('add', Article)
        rules.allow('view', Article)
        rules.allow('change', Article)
        rules.allow('delete', Article)
  1. In settings.py add CANCAN section, so that cancan library will know where to search for define_access_rules function from the previous step:
CANCAN = {
    'ABILITIES': 'myapp.abilities.define_access_rules'
}

The define_access_rules function will be executed automatically per each request by the cancan middleware. The middleware will call the function to determine the abilities of a current user.

Let's add cancan middleware, just after AuthenticationMiddleware:

MIDDLEWARE = [
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'cancan.middleware.CanCanMiddleware',
    ...
]

By adding the middleware you will also get an access to request.ability instance which you can use to:

  • check model permissions,
  • check object permissions,
  • generate model querysets (i.e. when inheriting from ListView)
  1. Check for abilities in views:
class ArticleListView(ListView):
    model = Article

    def get_queryset():
        # this is how you can retrieve all objects that current user can access
        qs = self.request.ability.queryset_for('view', Article)
        return qs


class ArticleDetailView(PermissionRequiredMixin, DetailView):
    queryset = Article.objects.all()

    def has_permission(self):
        article = self.get_object()
        # this is how you can check if user can access an object
        return self.request.ability.can('view', article)
  1. Check for abilities in templates

You can also check for abilities in template files, i. e. to show/hide/disable buttons or links.

First you need to add cancan processor to context_processors in TEMPLATES section of settings.py:

TEMPLATES = [
    {
        ...,
        "OPTIONS": {
            "context_processors": [
                ...,
                "cancan.context_processors.abilities",
            ],
        },
    },
]

This will give you access to ability object in a template. You also need add {% load cancan_tags %} at the beginning of the template file.

Next you can check for object permissions:

{% load cancan_tags %}

...

{% if ability|can:"change"|subject:article %}
    <a href="{% url 'article_edit' pk=article.id %}">Edit article</a>
{% endif %}

or model permissions:

{% if ability|can:"add"|"myapp.Article" %}
    <a href="{% url 'article_new' %}">Create new article</a>
{% endif %}

You can also use can template tag to create a reusable variable:

{% can "add" "core.Project" as can_add_project %}
...
{% if can_add_project %}
    ...
{% endif %}

Checking for abilities in Django Rest Framework

Let's start by creating a pemission class:

from rest_framework import permissions

def set_aliases_for_drf_actions(ability):
    """
    map DRF actions to default Django permissions
    """
    ability.access_rules.alias_action("list", "view")
    ability.access_rules.alias_action("retrieve", "view")
    ability.access_rules.alias_action("create", "add")
    ability.access_rules.alias_action("update", "change")
    ability.access_rules.alias_action("partial_update", "change")
    ability.access_rules.alias_action("destroy", "delete")


class AbilityPermission(permissions.BasePermission):
    def has_permission(self, request, view=None):
        ability = request.ability
        set_aliases_for_drf_actions(ability)
        return ability.can(view.action, view.get_queryset().model)

    def has_object_permission(self, request, view, obj):
        ability = request.ability
        set_aliases_for_drf_actions(ability)
        return ability.can(view.action, obj)

Next, secure the ViewSet with AbilityPermission and override get_queryset method to list objects based on the access rights.

class ArticleViewset(ModelViewSet):
    permission_classes = [AbilityPermission]

    def get_queryset(self):
        return self.request.ability.queryset_for(self.action, Article).distinct()

ability.queryset_for and rules.allow explained

When executing rules.allow you specify 2 positional arguments: action and subject. Any additional parameters passed to allow will filter the results in the same way as for Django QuerySet.fiter method.

Let's say that we have the following models in core.models.py:

class Project(models.Model):
    name = models.CharField(max_length=128)
    description = models.TextField(default="", blank=True)
    members = models.ManyToManyField(User, through="Membership")
    created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name="owner")

class Membership(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    project = models.ForeignKey(Project, on_delete=models.CASCADE)

If you have the following rules:

rules.allow('view', Project, name="Foo")

then executing:

ability.queryset_for('view', Project)

will result in the following query:

SELECT "core_project"."id", "core_project"."name", "core_project"."description", "core_project"."created_by_id" FROM "core_project" WHERE "core_project"."name" = Foo

Similarly, rules.allow('view', Project, name="Foo", description__contains="Bar")

will generate a query:

SELECT "core_project"."id", "core_project"."name", "core_project"."description", "core_project"."created_by_id" FROM "core_project" WHERE ("core_project"."description" LIKE %Bar% ESCAPE '\' AND "core_project"."name" = Foo)

Multiple rules for the same action and model will result in OR'ed queries, i.e.:

rules.allow('view', Project, name="Foo")
rules.allow('view', Project, description__contains="Bar")

will generate a query:

SELECT "core_project"."id", "core_project"."name", "core_project"."description", "core_project"."created_by_id" FROM "core_project" WHERE ("core_project"."description" LIKE %Bar% ESCAPE '\' OR "core_project"."name" = Foo)

See example_project/cancan_playground.ipynb for more examples.

Sponsors

Ermlab
Logo made by Freepik from www.flaticon.com

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django-cancan-0.4.2.tar.gz (15.0 kB view hashes)

Uploaded Source

Built Distribution

django_cancan-0.4.2-py3-none-any.whl (20.6 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page