Skip to main content

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

Release files for django-cancan 0.4.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-cancan 0.4.2
File Size Uploaded
django-cancan-0.4.2.tar.gz 15.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-cancan 0.4.2
File Interpreter ABI Platform
django_cancan-0.4.2-py3-none-any.whl Python 3 none any Details

Total release size: 35.6 kB

Release files / django-cancan-0.4.2.tar.gz

Download URL django-cancan-0.4.2.tar.gz
Size 15.0 kB
Tags Source
SHA-256 checksum
How to use checksums
7fbff8968d8af78d5e42345d55f9d6b8ed0bc78ef66f2b900fad3c4d65b7ed48
BLAKE2b-256 checksum
How to use checksums
0f06748b1861d63ac63427132534b8641899b52a04d94890b1b76e822501b379
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/44.0.0 requests-toolbelt/0.9.1 tqdm/4.48.2 CPython/3.7.9

Release files / django_cancan-0.4.2-py3-none-any.whl

Download URL django_cancan-0.4.2-py3-none-any.whl
Size 20.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d0742f12540198906a4e22901704e66f047eb67e5779ad84ab2fc4f4540e26aa
BLAKE2b-256 checksum
How to use checksums
4de4a6c9f6ece44f082535a82b890a833c820ef345dfaf7fd52041972a5d1d97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/44.0.0 requests-toolbelt/0.9.1 tqdm/4.48.2 CPython/3.7.9

Release history Release notifications | RSS feed

This release

0.4.2 This release

2 release files

0.4.1

2 release files

0.4

2 release files

0.3.3

2 release files

0.3.2

2 release files

0.3.1

2 release files

0.3

2 release files

0.2

2 release files

0.1

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page