Authorization library for Django
Project description
django-cancan
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
- Add
cancan
to yourINSTALLED_APPS
setting like this:
INSTALLED_APPS = [
...,
'cancan',
]
- Create a function that define the access rules for a given user. For example, create
abilities.py
inmyapp
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)
- In
settings.py
addCANCAN
section, so thatcancan
library will know where to search fordefine_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
)
- 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)
- 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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for django_cancan-0.4.2-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | d0742f12540198906a4e22901704e66f047eb67e5779ad84ab2fc4f4540e26aa |
|
MD5 | ef33bf153482184b9f50e35498c266d5 |
|
BLAKE2b-256 | 4de4a6c9f6ece44f082535a82b890a833c820ef345dfaf7fd52041972a5d1d97 |