/django-keeper

Authorization library for Django, with ACL, not depends on models.

Primary LanguagePythonMIT LicenseMIT

django-keeper

Authorization library for Django, not depends on models.

  • Won't depend on models
  • Won't depend on datastores
  • Won't depend on Django's permission system
  • Won't depend on Django's User model

Supported versions:

  • Python3.8
  • Python 3.9
  • Django 2.2
  • Django 3.0
  • Django 3.1
  • Django 3.2
  • Django 4.1
  • Django 4.2

Install

$ pip install django-keeper

And add to INSTALLED_APPS

INSTALLED_APPS = [
    ...
    'keeper',
]

At A Glance

Declarative permission mapping for models.

from django.conf import settings
from keeper.security import Allow
from keeper.operators import Everyone, Authenticated, IsUser


class Issue(models.Model):
    author = models.ForeignKey(settings.AUTH_USER_MODEL)
    ...

    def __acl__(self):
        return [
            (Allow, Everyone, 'view'),
            (Allow, Authenticated, 'add_comment'),
            (Allow, IsUser(self.author), 'edit'),
        ]

Instances of model allow:

  • Every requests to view
  • Autheticated requests to add comments
  • it's author to edit

Then, apply @keeper for views.

from keeper.views import keeper


# Model Permissions
@keeper(
    'view',
    model=Issue,
    mapper=lambda request, issue_id: {'id': issue_id},
)
def issue_detail(request, issue_id):
    """ View requires 'view' permission of Issue model

    * An issue object will be retrieved
    * keeper will check whether the rquests has 'view' permission for the issue

    The third argument function can return keyword argument to retrieve the issue object.
    """
    request.k_context  # Will be instance of the issue object
    ...



@keeper(
    'add_comment',
    model=Issue,
    mapper=lambda request, issue_id: {'id': issue_id},
)
def add_comment(request, issue_id):
    ...

Global Permission

Not just for model permissions django-keeper can handle global permissions.

First, write class having __acl__ method in models.py.

class Root:
    def __acl__(self):
        return [
            (Allow, Authenticated, 'view_dashboard'),
            (Allow, Authenticated, 'add_issue'),
        ]

It's not necessary to put it in models.py, but easy to understand.

And specify it in settings.

KEEPER_GLOBAL_CONTEXT = 'myapp.models.Root'

Then you can use global permission in views. Simply just apply @keeper and permission names.

@keeper('add_issue')
def issue_list(request):
    """ View requires 'add_issue' permission of Root Context
    """

Operators

Operators is just Callable[[HttpRequest], bool]. By default django-keeper has these operators:

  • keeper.operators.Everyone
  • keeper.operators.Authenticated
  • keeper.operators.IsUser
  • keeper.operators.Staff

Also you can create your own operators easily.

from keeper.operators import Authenticated, Operator


class IsIP(Operator):
    def __init__(self, ip):
        self.ip = ip
        
    def __call__(self, request):
        return request.META.get('REMOTE_ADDR') == self.ip


class BelongsTeam(Authenticated):
    def __init__(self, team, role):
        self.team = team

    def __call__(self, request):
        if not super().__call__(request):
            return False
        return  request.user.team == self.team

Use it in ACL

class Article(models.Model):
    team = models.ForeignKey(Team)
    
    def __acl__(self):
        return [
            (Allow, Everyone, 'view'),
            (Allow, BelongsTeam(self.team), 'edit'),
            (Allow, IsIP(settings.COMPANY_IP_ADDRESS), 'edit'),
        ]

Combining operators

You can use bitwise operators to combine multiple "Operators".

class Article(models.Model):
    def __acl__(self):
        return [
            (Allow, Authenticated() & IsIP(settings.COMPANY_IP_ADDRESS), 'view'),
        ]

There operators can be used

  • a & b
  • a | b
  • a ^ b
  • ~a

On Fail Actions

You can change actions when requests can't pass ACLs.

from keeper.views import keeper, login_required

@keeper(
    'view_articles',
    on_fail=login_required(),
)
def dashboard(request):
    ...

This view will behave just like @login_required decorator of Django when requests don't have 'view' permission.

Also you can use other actions.

  • keeper.views.login_required
  • keeper.views.permission_denied
  • keeper.views.not_found
  • keeper.views.redirect

Use in template

Handling permissions in templates is also supported.

{% load keeper %}

{% has_permission issue 'edit' as can_edit %}

{% if can_edit %}
    <a href="...">Edit</a>
{% endif %}

When checking global permission, use has_global_permission.

{% load keeper %}

{% has_global_permission 'add_issue' as can_add_issue %}

{% if can_add_issue %}
    <a href="...">New Issue</a>
{% endif %}

With Django Core

Add the authentication backend:

AUTHENTICATION_BACKENDS = (
    'keeper.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend',
)

Now User.has_perm method will consider permissions of django-keeper.

Alternative

  • django-guardian
    • It depends on databases
    • Not way to handle global permissions, not just for a model
  • django-rules
    • Basiaclly, rules is used with Django's permissions, but keeper isn't (basiacally).
    • rules doesn't support view decorators or mixins for it's "rules" system (for Django's permission, yes).

FAQ

  • Can I filter models by using ACL?
    • Not supported