Package: django-rest-framework-rules
django-rest-framework-rulesaims to seamlessly integraterules-  a tiny but powerful app providing object-level permissions - into the Django REST framework.Parts of the originaldjango-rules documentationwere used to improve the reading experience of this document.Table of ContentsRequirementsRun testsHow to installUsing rest_framework_rulesPermissionRequiredMixin with the APIView and GenericAPIViewPermissionRequiredMixin with the ViewSet and GenericViewSetpermission_required decorator with APIView and ViewSet methodsusing list_route and detail_route decorator with the PermissionRequiredMixinChangelogLicenceRequirementsThis package was made to be used withDjango,django-rulesand thedjango-rest-framework.Build statusRun tests$gitclonehttp://github.com/escodebar/django-rest-framework-rules.git$cddjango-rest-framework-rules$python3-mvenv.&&sourcebin/activate$pipinstall-rrequirements_test.txt$(django-rest-framework-rules)./runtests.shHow to installUsing pip:$pipinstalldjango-rest-framework-rulesUsing rest_framework_rulesrest_framework_rulesaims to integraterulespermission system into the Django REST framework.rulesis based on the idea that you maintain a dict-like object that maps string keys used as identifiers of some kind, to callables, calledpredicates.
Predicates can do pretty much anything with the given arguments, but must always return True if the condition they check is true, False otherwise.
(Read more about rules in thedjango-rules documentation - Using Rules.)For a better illustration of the usage ofrest_framework_ruleslet’s assume the following setup:
We are proud owners of an app (climb-app!) which allows routesetters to advertise their newly created routes / boulders.
The climbers can use the app to review the routes / boulders and propose solutions for these.Given such an application, one could think of the following permissions:Routesetters may
- create new boulders
- retrieve boulders’ details
- update their boulders
- delete their boulders
- retrieve their boulders’ reviews
- retrieve their boulders’ solutionsClimbers may
- retrieve boulders’ details
- create boulder reviews
- retrieve boulders’ reviews
- update their reviews
- delete their reviews
- create a boulder solution
- retrieve boulders’ solutions
- update their boulder solutions
- delete their boulder solutionsLet’s define some predicates and the beforementioned permissions (this code usually resides inrules.pyin your application folder).fromclimb_app.modelsimportClimber,RouteSetterimportrules@rules.predicatedefis_a_climber(user):returnClimber.objects.filter(user=user).exists()@rules.predicatedefis_a_routesetter(user):returnRouteSetter.objects.filter(user=user).exists()@rules.predicatedefis_related_to_routesetters_boulder(user,content=None):ifcontentisNoneornothasattr(content,'boulder'):returnFalsereturncontent.boulder.routesetter==user@rules.predicatedefobject_is_none(user,obj=None):returnobjisNone@rules.predicatedefis_author(user,content):ifnothasattr(content,'author'):returnFalsereturncontent.author==userrules.add_perm('climb_app.create_boulder',is_a_routesetter)rules.add_perm('climb_app.retrieve_boulder',is_a_climber|is_a_routesetter&is_author)rules.add_perm('climb_app.update_boulder',is_a_routesetter&is_author)rules.add_perm('climb_app.delete_boulder',is_a_routesetter&is_author)rules.add_perm('climb_app.retrieve_reviews',is_a_routesetter)rules.add_perm('climb_app.retrieve_climbers',is_a_routesetter)rules.add_perm('climb_app.create_climber_content',is_a_climber)rules.add_perm('climb_app.retrieve_climber_content',(is_a_climber|is_a_routesetter&is_related_to_routesetters_boulder|is_a_routesetter&object_is_none))rules.add_perm('climb_app.update_climber_content',is_a_climber&is_author)rules.add_perm('climb_app.delete_climber_content',is_a_climber&is_author)PermissionRequiredMixin with the APIView and GenericAPIViewThePermissionRequiredMixinallows to define a required permission name (permission_required).
This permission name (or list of such) is needed by the request’s user to access the methods of the view.I could think of the following use case within climb-app!
Let’s allow climbers tocheckboulders once they were able to solve them.
(This is basically adding a solution without data.)fromclimb_app.modelsimportBoulder,Solutionfromrest_framework.responseimportResponsefromrest_framework.viewsimportAPIViewfromrest_framework_rules.mixinsimportPermissionRequiredMixinclassCheckmarkBoulderView(PermissionRequiredMixin,APIView):permission_required='climb_app.create_climber_content'defget(self,request,*args,**kwargs):solution,created=Solution.objects.get_or_create(user=request.user,boulder=Boulder.objects.get(pk=kwargs.get('boulder_pk')),)ifcreated:returnResponse(status=204)returnResponse(status=304)When used with aGenericAPIView, thePermissionRequiredMixinallows to define anobject_permission_required(defaults topermission_requiredif not set).
This permission (or list of permissions) is required by the request’s user to call theget_objectmethod of the view.In the context of climb-app! this could be used with the views for retrieving and updating boulder reviews:fromclimb_app.modelsimportReviewfromclimb_app.serializerimportReviewSerializerfromrest_framework.genericsimportGenericAPIViewfromrest_framework.responseimportResponsefromrest_framework_rules.mixinsimportPermissionRequiredMixinclassRetrieveReviewView(PermissionRequiredMixin,GenericAPIView):permission_required='climb_app.retrieve_climber_content'queryset=Review.objects.all()defget(self,request,*args,**kwargs):review=self.get_object()serializer=ReviewSerializer(review)returnResponse(data=serializer.data)classCreateOrUpdateReviewView(PermissionRequiredMixin,GenericAPIView):object_permission_required='climb_app.update_climber_content'permission_required='climb_app.create_climber_content'queryset=Review.objects.all()defpost(self,request,*args,**kwargs):solution,created=Review.objects.get_or_create(user=request.user,boulder=Boulder.objects.get(pk=kwargs.get('boulder_pk')),)ifcreated:returnResponse(status=204)returnResponse(status=304)defput(self,request,*args,**kwargs):review=self.get_object()# update review...returnResponse(status=204)PermissionRequiredMixin with the ViewSet and GenericViewSetThePermissionRequiredMixincan be used as well withViewSetandGenericViewSet.
The user need to have thepermission_requiredto call actions of a viewset andobject_permission_required(which defaults topermission_requiredif not set) to callget_object.Let’s use this in climb app! to allow routesetters to create, delete boulders and list the reviews of their boulders.fromclimb_app.modelsimportBoulder,Reviewfromclimb_app.serializersimportBoulderSerializer,ReviewSerializerfromrest_framework.responseimportResponsefromrest_framework.viewsetsimportGenericViewSet,ViewSetfromrest_framework_rules.mixinsimportPermissionRequiredMixinclassReviewViewSet(PermissionRequiredMixin,ViewSet):permission_required='climb_app.retrieve_reviews'deflist(self,request):queryset=Review.objects.filter(boulder__author=request.user)serializer=ReviewSerializer(queryset,many=True)returnResponse(serializer.data)classBoulderViewSet(PermissionRequiredMixin,GenericViewSet):object_permission_required='climb_app.delete_boulder'permission_required='climb_app.create_boulder'defcreate(self,request,*args,**kwargs):# create boulders...returnResponse(status=204)defdestroy(self,request,*args,**kwargs):instance=self.get_object()instance.delete()returnResponse(status=204)permission_required decorator with APIView and ViewSet methodsPermissions can be set using thepermission_requireddecorator.
The required permission(s) is passed as first argument to the decorator.
The decorator also has an optionalfnargument, which is either the context object itself or a callable returning the context object.
The arguments passed to the context object callable are the same as the ones of the decorated method.fromclimb_app.modelsimportBoulder,Solutionfromrest_framework.responseimportResponsefromrest_framework.viewsimportAPIViewfromrest_framework_rules.decoratorsimportpermission_requiredclassCheckmarkBoulderView(APIView):@permission_required('climb_app.create_climber_content')defget(self,request,*args,**kwargs):solution,created=Solution.objects.get_or_create(user=request.user,boulder=Boulder.objects.get(pk=kwargs.get('boulder_pk')),)ifcreated:returnResponse(status=204)returnResponse(status=304)classBoulderViewSet(ViewSet):@permission_required('climb_app.access_method')defcreate(self,request):# create boulder...returnResponse(status=204)@permission_required('someapp.access_method',fn=lambdarequest,pk:Boulder.objects.get(pk=pk))defdestroy(self,request,pk):boulder=Boulder.objects.get(pk=pk)boulder.delete()returnResponse(status=204)using list_route and detail_route decorator with the PermissionRequiredMixinrest_frameworkprovides the decoratorslist_routeanddetail_routeto define custom routes in viewsets.
These can be used as well withdjango-rest-framework-rulesunder the condition, that theViewSetis mixed with thePermissionRequiredMixin.Let’s add some custom routes to theBoulderViewSetdefined in climb-app! to allow routesetter to retrieve all reviews of a boulder and list all climbers which have solved the routesetter’s boulders.fromrest_framework.decoratorsimportlist_route,detail_routefromrest_framework.responseimportResponsefromrest_framework.viewsetsimportViewSetfromrest_framework_rules.mixinsimportPermissionRequiredMixinclassBoulderViewSet(PermissionRequiredMixin,GenericViewSet):object_permission_required='climb_app.delete_boulder'permission_required='climb_app.create_boulder'defcreate(self,request,*args,**kwargs):# create boulders...returnResponse(status=204)defdestroy(self,request,*args,**kwargs):instance=self.get_object()instance.delete()returnResponse(status=204)@detail_route(methods=['get'],permission_required='climb_app.retrieve_reviews')defreviews(self,request,pk):boulder=self.get_object()queryset=(Review.objects.filter(boulder=boulder).order_by('created'))serializer=ReviewSerializer(queryset=queryset,many=True)returnResponse(serializer.data)@list_route(methods=['get'],permission_required='climb_app.retrieve_climbers')defclimbers(self,request):queryset=Climber.objects.filter(solution__boulder__author=request.user).distinct()serializer=ClimberSerializer(queryset=queryset,many=True)returnResponse(serializer.data)Changelogv1.0.0- 2018/05/15Dropped python 2.7 support.v0.1.1- 2017/11/17Improve README and package setup.v0.1.0- 2017/11/13Implemented PermissionRequiredMdddixin, permission_required decorator and the django rest framework integration tests.v0.0.1- 2017/10/30Forked fromhttps://github.com/dfunckt/django-rulesLicencedjango-rest-framework-rulesis distributed under the MIT licence.Copyright (c) 2017 Pablo EscodebarPermission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the “Software”), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.