Skip to content

Instantly share code, notes, and snippets.

@bpereto
Last active November 28, 2019 09:19
Show Gist options
  • Save bpereto/de33b6988866fe04c3a1b2f77c21534f to your computer and use it in GitHub Desktop.
Save bpereto/de33b6988866fe04c3a1b2f77c21534f to your computer and use it in GitHub Desktop.
Django Restframework Rules Permission integration with HTTPMethod Map
"""
Django REST Framework Permission Class for Django Rules
"""
from rest_framework import permissions, exceptions
from rest_framework.viewsets import ModelViewSet
from django.http import Http404
class DjangoRulesMethodObjectPermissions(permissions.BasePermission):
"""
The request is authenticated using Django's object-level permissions.
It requires an object-permissions-enabled backend, such as django-rules
It ensures that the user is authenticated, and has the appropriate
`add`/`change`/`delete` permissions on the object using HTTP Methods
mapped to a django rule(s).
"""
# Map methods into required permission codes.
# Override this if you need to also provide 'view' permissions,
# or if you want to provide custom permission codes.
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': [],
}
object_perms_map = {}
authenticated_users_only = True
def get_required_permissions(self, method):
"""
Given a HTTP method, return the list of permission
codes that the user is required to have.
"""
if method not in self.perms_map:
raise exceptions.MethodNotAllowed(method)
return [perm for perm in self.perms_map[method]]
def get_required_object_permissions(self, method):
"""
Given a HTTP method, return the list of permission
codes that the user is required to have for an object
inherit object permission from global permission if not defined
"""
if (method not in self.object_perms_map) and \
(method not in self.perms_map):
raise exceptions.MethodNotAllowed(method)
# inherit global perm if not specified in object map
if method in self.object_perms_map:
object_map = self.object_perms_map[method]
else:
object_map = self.perms_map[method]
return [perm for perm in object_map]
def has_permission(self, request, view):
"""
check required perm codes with has_perms
"""
if not request.user or \
(not request.user.is_authenticated and self.authenticated_users_only):
return False
perms = self.get_required_permissions(request.method)
return request.user.has_perms(perms)
def has_object_permission(self, request, view, obj):
"""
check required perm codes for a specific object with has_perms
"""
# authentication checks have already executed via has_permission
user = request.user
perms = self.get_required_object_permissions(request.method)
if not user.has_perms(perms, obj):
# If the user does not have permissions we need to determine if
# they have read permissions to see 403, or not, and simply see
# a 404 response.
if request.method in permissions.SAFE_METHODS:
# Read permissions already checked and failed, no need
# to make another lookup.
raise Http404
read_perms = self.get_required_object_permissions('GET')
if not user.has_perms(read_perms, obj):
raise Http404
# Has read permissions.
return False
return True
"""
Django Rules
https://github.com/dfunckt/django-rules
dynamic function mapped to permission codes
"""
import rules
from rules.predicates import is_staff, is_authenticated
# pylint: disable=invalid-name
is_foo_user = rules.is_group_member('foo')
@rules.predicate
def tree_is_big_enough(_, obj):
"""checks if a tree is big enough"""
return obj.height > 10
rules.add_perm('tree.detail_rule_tree', is_foo_user)
rules.add_perm('tree.update_rule_tree', tree_is_big_enough)
rules.add_perm('tree.create_rule_tree', is_authenticated)
rules.add_perm('tree.delete_rule_tree', is_staff)
"""
REST API Permission Classes
"""
class TreePermission(DjangoRulesMethodObjectPermissions):
"""
example django rules object permission class
Hint: django checks global permission before object permission,
therefore for ex. PUT is an empty list in the perms_map, to allow
checking of the object permission in object_perms_map
"""
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': [],
'POST': ['tree.create_rule_tree'],
'PATCH': [],
'PUT': [],
'DELETE': []
}
object_perms_map = {
'GET': ['tree.detail_rule_tree'],
'PUT': ['tree.update_rule_tree'],
'PATCH': ['tree.update_rule_tree'],
'DELETE': ['tree.delete_rule_tree'],
}
class TreeViewSet(ModelViewSet):
'''
API endpoints for the Tree model.
'''
permission_classes = (TreePermission,)
"""
GNU General Public License v2
"""
@cdrandin
Copy link

cdrandin commented Feb 7, 2019

For those who want to be consistent with DRF permission exception PermissionDenied in place of Http404.

And thank you for this. I found this helpful over the drf-rules.

@nummersyv
Copy link

I looked at your code and the issue leading up to it, and I must say, this is a very nice way to do things!

However, isn't this code basically just a reimplementation of Django Rest Framework's default 'DjangoObjectPermissions' class? I've tried using the default class instead of this one, and it seems to work perfectly well -- also the code is very similar. Can you point me in the direction of the important differences between these two implementations?

Thanks a lot!

@highpost
Copy link

This looks great, but the GPL is a problem. Django, Django Rest Framework and Django Rules all use BSD-like licenses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment