├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── drf_roles ├── __init__.py ├── mixins.py └── tests.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .venv 3 | .*.sw* 4 | dist/ 5 | *.egg-info/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Robert C Jensen 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-rest-framework-roles 2 | =========================== 3 | 4 | Simplifies `Role Based Access Control`_ in `django-rest-framework`_. 5 | 6 | Why would I use this? 7 | --------------------- 8 | 9 | You have more than one type of user in your data model and you have 10 | business logic that diverges depending on the type of user. You do not 11 | want to organize your API by role because that is not very RESTful. You 12 | do not want to manually type out a lot of conditional branching around 13 | user roles. 14 | 15 | Modeling Requirements 16 | --------------------- 17 | 18 | - You must have one **Group** for each role 19 | - A **User** cannot belong to more than one of the **Groups** 20 | corresponding to each role 21 | 22 | Installation 23 | ------------ 24 | 25 | .. code:: bash 26 | 27 | $ pip install django-rest-framework-roles 28 | 29 | Configuration 30 | ------------- 31 | 32 | - ``VIEWSET_METHOD_REGISTRY`` A tuple of DRF methods to override. 33 | Defaults to: 34 | 35 | .. code:: python 36 | 37 | ( 38 | "get_queryset", 39 | "get_serializer_class", 40 | "perform_create", 41 | "perform_update", 42 | "perform_destroy", 43 | ) 44 | 45 | - ``ROLE_GROUPS`` A tuple of Group names that correspond 1-to-1 with 46 | user roles. Defaults to: 47 | 48 | .. code:: python 49 | 50 | [group.name.lower() for group in Group.objects.all()] 51 | 52 | It's recommended to define ``ROLE_GROUPS`` in ``settings`` to avoid 53 | a database lookup on every request. 54 | 55 | Usage 56 | ----- 57 | 58 | Add the mixin to any ViewSet: 59 | 60 | .. code:: python 61 | 62 | from drf_roles.mixins import RoleViewSetMixin 63 | 64 | class MyViewSet(RoleViewSetMixin, ModelViewSet): 65 | # ... 66 | 67 | For each of the methods specified in ``VIEWSET_METHOD_REGISTRY`` a 68 | role-scoped method will be generated on your ViewSet. 69 | 70 | Parameterizing 71 | ~~~~~~~~~~~~~~ 72 | 73 | For example, let’s say you have three groups named *Takers*, *Leavers* & 74 | *Gods*. Let’s also say you included ``"get_queryset"`` in the 75 | ``VIEWSET_METHOD_REGISTRY``. 76 | 77 | When a *Taker* user hits an endpont on the ViewSet, the call to 78 | ``get_queryset`` will be rerouted to a call to 79 | ``get_queryset_for_takers``. 80 | 81 | When a *Leaver* user hits an endpont on the ViewSet, the call to 82 | ``get_queryset`` will be rerouted to a call to 83 | ``get_queryset_for_leavers``. 84 | 85 | When a *God* user hits an endpont on the ViewSet, the call to 86 | ``get_queryset`` will be rerouted to a call to 87 | ``get_queryset_for_gods``. 88 | 89 | You can implement each of these methods on your ViewSet to return a 90 | different queryset for each type of user. 91 | 92 | Not Parameterizing 93 | ~~~~~~~~~~~~~~~~~~ 94 | 95 | You can also *not* implement one or more of these methods, in which case 96 | the default call will be executed. For example, with our same set of 97 | groups and with ``"get_serializer_class"`` included in the role 98 | registry, let’s say you did not implement 99 | ``get_serializer_class_for_takers``. When a *Taker* user hits an 100 | endpoint on the ViewSet, the default implementation of 101 | ``get_serializer_class`` will be executed and return 102 | ``serializer_class``. 103 | 104 | In this case, you would want to be sure that you have a 105 | ``serializer_class`` defined on your ViewSet! Otherwise Django REST 106 | Framework will complain. It is a good idea to always define a default 107 | ``queryset`` and ``serializer_class`` with least privilege (e.g: 108 | Model.objects.none()). 109 | 110 | Roadmap 111 | ------- 112 | 113 | - Some projects require even further parameterization. For example, you may need 114 | to use a different `serializer_class` depending on the user's *role* **and** 115 | the *request method*. 116 | - There may be a more pleasant way to express the parameterization in code. For 117 | example, it may be more pleasing to use nested classes instead of renaming the 118 | methods. 119 | 120 | Further Reading 121 | --------------- 122 | 123 | - `Role-Based Access Control with Django Rest Framework`_ 124 | - `Computer Lab`_ 125 | 126 | .. _Role Based Access Control: https://en.wikipedia.org/wiki/Role-based_access_control 127 | .. _django-rest-framework: http://www.django-rest-framework.org/ 128 | .. _Role-Based Access Control with Django Rest Framework: http://computerlab.io/2016/08/17/django-rest-framework-roles/ 129 | .. _Computer Lab: http://computerlab.io 130 | -------------------------------------------------------------------------------- /drf_roles/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/computer-lab/django-rest-framework-roles/6c71da2e3285c8a91e8033442beda563de97dc44/drf_roles/__init__.py -------------------------------------------------------------------------------- /drf_roles/mixins.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib.auth.models import Group 3 | 4 | # Default settings 5 | DEFAULT_REGISTRY = ( 6 | "get_queryset", 7 | "get_serializer_class", 8 | "perform_create", 9 | "perform_update", 10 | "perform_destroy", 11 | ) 12 | 13 | 14 | class RoleError(Exception): 15 | """Base class for exceptions in this module.""" 16 | pass 17 | 18 | 19 | class RoleViewSetMixin(object): 20 | """A ViewSet mixin that parameterizes DRF methods over roles""" 21 | _viewset_method_registry = set(getattr(settings, "VIEWSET_METHOD_REGISTRY", DEFAULT_REGISTRY)) 22 | 23 | def __init__(self, **_kwargs): 24 | groups = getattr(settings, "ROLE_GROUPS", None) 25 | if groups is None: 26 | groups = [group.name.lower() for group in Group.objects.all()] 27 | self._role_groups = set(groups) 28 | 29 | def _call_role_fn(self, fn, *args, **kwargs): 30 | """Attempts to call a role-scoped method""" 31 | try: 32 | role_name = self._get_role(self.request.user) 33 | role_fn = "{}_for_{}".format(fn, role_name) 34 | return getattr(self, role_fn)(*args, **kwargs) 35 | except (AttributeError, RoleError): 36 | return getattr(super(RoleViewSetMixin, self), fn)(*args, **kwargs) 37 | 38 | def _get_role(self, user): 39 | """Retrieves the given user's role""" 40 | user_groups = set([group.name.lower() for group in user.groups.all()]) 41 | user_role = self._role_groups.intersection(user_groups) 42 | 43 | if len(user_role) < 1: 44 | raise RoleError("The user is not a member of any role groups") 45 | elif len(user_role) > 1: 46 | raise RoleError("The user is a member of multiple role groups") 47 | else: 48 | return user_role.pop() 49 | 50 | def register_fn(fn): 51 | """Dynamically adds fn to RoleViewSetMixin""" 52 | def inner(self, *args, **kwargs): 53 | return self._call_role_fn(fn, *args, **kwargs) 54 | setattr(RoleViewSetMixin, fn, inner) 55 | 56 | # Registers whitelist of ViewSet fns to override 57 | for fn in RoleViewSetMixin._viewset_method_registry: 58 | register_fn(fn) 59 | -------------------------------------------------------------------------------- /drf_roles/tests.py: -------------------------------------------------------------------------------- 1 | # XXX: Add tests! 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 5 | README = readme.read() 6 | 7 | # allow setup.py to be run from any path 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | setup( 11 | name='django-rest-framework-roles', 12 | version='0.6', 13 | packages=['drf_roles'], 14 | include_package_data=True, 15 | license='BSD License', 16 | description='Parameterizes Django REST Framework methods over user-defined roles', 17 | long_description=README, 18 | url='http://computerlab.io/', 19 | author='Robert C Jensen', 20 | author_email='rob@computerlab.io', 21 | classifiers=[ 22 | 'Development Status :: 3 - Alpha', 23 | 'Environment :: Web Environment', 24 | 'Framework :: Django', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Programming Language :: Python :: 2', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Topic :: Internet :: WWW/HTTP', 32 | 'Topic :: Software Development :: Libraries :: Python Modules', 33 | ], 34 | ) 35 | --------------------------------------------------------------------------------