Skip to main content

Role-based permissions for Django REST Framework and vanilla Django.

Project description

REST Framework Roles

rest-framework-roles PyPI version

A Django REST Framework security-centric plugin aimed at decoupling permissions from your models and views in an easy intuitive manner.

Features:

  • Least privilege by default.
  • Guard your application before a request reaches a view.
  • Secure chained views (redirections), removing potential vulnerabilities.
  • Backwards compatibility with DRF's permission_classes.
  • Enforce decoupled and abstracted permission logic, away from models and views.

The framework provides view_permissions as an alternative to permission_classes, which in our opinion makes things much more intuitive and also provides greater security by adding the permission checking between the views and the middleware of Django. By protecting the views, redirections can be used without creating securityholes, and by using the least-privilege principle by default any new API endpoint you create is secure.

Note that DEFAULT_PERMISSIONS_CLASSES is patched so by default all endpoints will be denied access by simply installing this.

Installation

Install

pip install rest-framework-roles

Edit your settings.py file

INSTALLED_APPS = {
    ..
    'rest_framework',
    'rest_framework_roles',  # Must be after rest_framework
}

REST_FRAMEWORK_ROLES = {
  'ROLES': 'myproject.roles.ROLES',
}

Now all your endpoints default to 403 Forbidden unless you specifically use view_permissions or DRF's permission_classes in view classes.

By default endpoints from django.contrib won't be patched. If you wish to explicitly set what modules are skipped you can edit the SKIP_MODULES setting like below.

REST_FRAMEWORK_ROLES = {
  'ROLES': 'myproject.roles.ROLES',
  'SKIP_MODULES': [
    'django.*',
    'myproject.myapp55.*',
  ],
}

Setting permissions

First you need to define some roles like below

roles.py

from rest_framework_roles.roles import is_user, is_anon, is_admin


def is_buyer(request, view):
    return is_user(request, view) and request.user.usertype = 'buyer'

def is_seller(request, view):
    return is_user(request, view) and request.user.usertype = 'seller'


ROLES = {
    # Django out-of-the-box
    'admin': is_admin,
    'user': is_user,
    'anon': is_anon,

    # Some custom role examples
    'buyer': is_buyer,
    'seller': is_seller,
}

is_admin, is_user, etc. are simple functions that take request and view as parameters, similar to DRF's behaviour.

Next we need to define permissions for the views with view_permissions.

views.py

from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework_roles.granting import is_self


class UserViewSet(ModelViewSet):
    serializer_class = UserSerializer
    queryset = User.objects.all()
    view_permissions = {
        'create': {'anon': True},  # only anonymous visitors allowed
        'list': {'admin': True}, 
        'retrieve,me': {'user': is_self},
        'update,update_partial': {'user': is_self, 'admin': True},
    }

    @action(detail=False, methods=['get'])
    def me(self, request):
        self.kwargs['pk'] = request.user.pk
        return self.retrieve(request)

By default everyone is denied access to everything. So we need to 'whitelist' any views we want to give permission explicitly.

For redirections like me (which redirects to retrieve), we need to give the same permissions to both or else we'll get 403 Forbidden.

In a view you can always check _view_permissions to see what permissions are in effect.

A request keeps track of all permissions checked so far. So redirections don't affect performance since the same permissions are never checked twice.

Advanced setup

Bypassing the framework

If you want to bypass the framework in a specific view class just explicitly set the permission_classes.

class MyViewSet():
    permission_classes = [AllowAny]

By default when you install DRF, every class gets automatically populated permission_classes = [AllowAny] which is really a bad idea. If for some reason you wish to get the same behaviour, you'd need to add permission_classes = [AllowAny] on every individual class.

Granting permission

You can use the helper functions allof or anyof when deciding if a matched role should be granted access

from rest_framework_roles.granting import allof

def not_updating_email(request, view):
    return 'email' not in request.data

class UserViewSet(ModelViewSet):
    view_permissions = {
        'update,partial_update': {
            'user': allof(is_self, not_updating_email),
            'admin': True,
        },
    }

In the above example the user can only update their information only while not trying to update their email.

You can put all these functions inside a new file granting.py or just keep them close to the views, depending on what makes sense for your case. It's important to not mix them with the roles though to keeps things clean; (1) a role identifies someone making the request while (2) granting determines if the person fitting tha role should be granted permission for their request.

Keep in mind that someone can fit multiple roles. E.g. admin is also a user (unless you change the implementation of is_user and is_admin).

Optimizing role checking

You can change the order of how roles are checked. This makes sense if you want less frequent or expensive checks to happen prior to infrequent and slower ones.

from rest_framework_roles.decorators import role_checker


@role_checker(cost=0)
def is_freebie_user(request, view):
    return request.user.is_authenticated and request.user.plan == 'freebie'


@role_checker(cost=0)
def is_payed_user(request, view):
    return request.user.is_authenticated and not request.user.plan


@role_checker(cost=50)
def is_creator(request, view):
    obj = view.get_object()
    if hasattr(obj, 'creator'):
        return request.user == obj.creator
    return False

In this example, roles with cost 0 would be checked first, and lastly the creator role would be checked since it has the highest cost.

Note this is similar to Django REST's check_permissions and check_object_permissions but more generic & adjustable since you can have arbitrary number of costs (instead of 2).

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

rest_framework_roles-1.0.4.tar.gz (18.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rest_framework_roles-1.0.4-py3-none-any.whl (13.4 kB view details)

Uploaded Python 3

File details

Details for the file rest_framework_roles-1.0.4.tar.gz.

File metadata

  • Download URL: rest_framework_roles-1.0.4.tar.gz
  • Upload date:
  • Size: 18.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.11.8

File hashes

Hashes for rest_framework_roles-1.0.4.tar.gz
Algorithm Hash digest
SHA256 c78fa67d698adc7be77250304115db555ca5da01ac2911f4f9214e2dba52f07f
MD5 2eae1cb690fcd354acde7be4e11f188c
BLAKE2b-256 e72262e5f6dd90734b8e69b4bfdd7823dfce766fc55226024f5e7ed30b8d6b04

See more details on using hashes here.

File details

Details for the file rest_framework_roles-1.0.4-py3-none-any.whl.

File metadata

File hashes

Hashes for rest_framework_roles-1.0.4-py3-none-any.whl
Algorithm Hash digest
SHA256 067a2bd7883899dffc30790f97b3f4d2327b0fd32fa5aba14959d323d8535978
MD5 4bb8bc9e2f8272972d1fea1dc65bb327
BLAKE2b-256 162b2025b790e058e59b5a295532ee0a4740eb9c88fd5fe125184933b95a2972

See more details on using hashes here.

Supported by

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