REST Framework Roles
====================
[![rest-framework-roles](https://circleci.com/gh/Pithikos/rest-framework-roles.svg?style=svg)](https://circleci.com/gh/Pithikos/rest-framework-roles) [![PyPI version](https://badge.fury.io/py/rest-framework-roles.svg)](https://badge.fury.io/py/rest-framework-roles)
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
```python
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.
```python
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*
```python
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](https://www.django-rest-framework.org/api-guide/permissions/).
Next we need to define permissions for the views with `view_permissions`.
*views.py*
```python
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 whitelist 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
-----------------------
To fallback to the default DRF behaviour simply add `permission_classes` to a class like below.
```python
class MyViewSet():
permission_classes = [AllowAny] # Default DRF (dangerous) behaviour
```
Granting permission
-------------------
You can use the helper functions `allof` or `anyof` when deciding if a matched role should
be granted access
```python
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.
```python
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).
Raw data
{
"_id": null,
"home_page": "https://github.com/Pithikos/rest-framework-roles",
"name": "rest-framework-roles",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3",
"maintainer_email": null,
"keywords": "permissions, roles, users, django, rest, drf, acl, security, rest framework, access control",
"author": "Johan Hanssen Seferidis",
"author_email": "manossef@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/a0/17/efe67bd5b728b4da847e89629c3918c476dc438380880112ef8cadfe6273/rest_framework_roles-1.0.5.tar.gz",
"platform": null,
"description": "REST Framework Roles\n====================\n\n[![rest-framework-roles](https://circleci.com/gh/Pithikos/rest-framework-roles.svg?style=svg)](https://circleci.com/gh/Pithikos/rest-framework-roles) [![PyPI version](https://badge.fury.io/py/rest-framework-roles.svg)](https://badge.fury.io/py/rest-framework-roles)\n\nA Django REST Framework security-centric plugin aimed at decoupling permissions from your models and views in an easy intuitive manner.\n\nFeatures:\n\n - Least privilege by default.\n - Guard your application before a request reaches a view.\n - Secure chained views (redirections), removing potential vulnerabilities.\n - Backwards compatibility with DRF's `permission_classes`.\n - Enforce decoupled and abstracted permission logic, away from models and views.\n\nThe 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**.\n\nNote that `DEFAULT_PERMISSIONS_CLASSES` is patched so by default all endpoints will be denied access by simply installing this.\n\n\nInstallation\n============\n\nInstall\n\n pip install rest-framework-roles\n\nEdit your *settings.py* file\n\n```python\nINSTALLED_APPS = {\n ..\n 'rest_framework',\n 'rest_framework_roles', # Must be after rest_framework\n}\n\nREST_FRAMEWORK_ROLES = {\n 'ROLES': 'myproject.roles.ROLES',\n}\n```\n\nNow all your endpoints default to *403 Forbidden* unless you specifically use `view_permissions` or DRF's `permission_classes` in view classes.\n\nBy 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.\n\n```python\nREST_FRAMEWORK_ROLES = {\n 'ROLES': 'myproject.roles.ROLES',\n 'SKIP_MODULES': [\n 'django.*',\n 'myproject.myapp55.*',\n ],\n}\n```\n\n\nSetting permissions\n===================\n\n\nFirst you need to define some roles like below\n\n*roles.py*\n```python\nfrom rest_framework_roles.roles import is_user, is_anon, is_admin\n\n\ndef is_buyer(request, view):\n return is_user(request, view) and request.user.usertype == 'buyer'\n\ndef is_seller(request, view):\n return is_user(request, view) and request.user.usertype == 'seller'\n\n\nROLES = {\n # Django out-of-the-box\n 'admin': is_admin,\n 'user': is_user,\n 'anon': is_anon,\n\n # Some custom role examples\n 'buyer': is_buyer,\n 'seller': is_seller,\n}\n```\n\n`is_admin`, `is_user`, etc. are simple functions that take `request` and `view` as parameters, similar to [DRF's behaviour](https://www.django-rest-framework.org/api-guide/permissions/).\n\n\nNext we need to define permissions for the views with `view_permissions`.\n\n*views.py*\n```python\nfrom rest_framework.viewsets import ModelViewSet\nfrom rest_framework.decorators import action\nfrom rest_framework_roles.granting import is_self\n\n\nclass UserViewSet(ModelViewSet):\n serializer_class = UserSerializer\n queryset = User.objects.all()\n view_permissions = {\n 'create': {'anon': True}, # only anonymous visitors allowed\n 'list': {'admin': True}, \n 'retrieve,me': {'user': is_self},\n 'update,update_partial': {'user': is_self, 'admin': True},\n }\n\n @action(detail=False, methods=['get'])\n def me(self, request):\n self.kwargs['pk'] = request.user.pk\n return self.retrieve(request)\n```\n\nBy default everyone is denied access to everything. So we need to 'whitelist' any views\nwe want to give permission explicitly.\n\nFor redirections like `me` (which redirects to `retrieve`), we need to whitelist both or else we'll get 403 Forbidden.\n\n> In a view you can always check `_view_permissions` to see what permissions are in effect.\n\n> A request keeps track of all permissions checked so far. So redirections don't affect performance since the same permissions are never checked twice.\n\n\nAdvanced setup\n==============\n\nBypassing the framework\n-----------------------\nTo fallback to the default DRF behaviour simply add `permission_classes` to a class like below.\n\n```python\nclass MyViewSet():\n permission_classes = [AllowAny] # Default DRF (dangerous) behaviour\n```\n\n\nGranting permission\n-------------------\n\nYou can use the helper functions `allof` or `anyof` when deciding if a matched role should\nbe granted access\n\n```python\nfrom rest_framework_roles.granting import allof\n\ndef not_updating_email(request, view):\n return 'email' not in request.data\n\nclass UserViewSet(ModelViewSet):\n view_permissions = {\n 'update,partial_update': {\n 'user': allof(is_self, not_updating_email),\n 'admin': True,\n },\n }\n```\n\nIn the above example the user can only update their information only while not trying to update their email.\n\n> 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. \n\n> 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`).\n\n\nOptimizing role checking\n------------------------\n\nYou can change the order of how roles are checked. This makes sense if you want\nless frequent or expensive checks to happen prior to infrequent and slower ones.\n\n\n```python\nfrom rest_framework_roles.decorators import role_checker\n\n\n@role_checker(cost=0)\ndef is_freebie_user(request, view):\n return request.user.is_authenticated and request.user.plan == 'freebie'\n\n\n@role_checker(cost=0)\ndef is_payed_user(request, view):\n return request.user.is_authenticated and not request.user.plan\n\n\n@role_checker(cost=50)\ndef is_creator(request, view):\n obj = view.get_object()\n if hasattr(obj, 'creator'):\n return request.user == obj.creator\n return False\n```\n\nIn this example, roles with cost 0 would be checked first, and lastly the *creator* role would be checked since it has the highest cost.\n\n> 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).\n\n\n",
"bugtrack_url": null,
"license": "LICENSE",
"summary": "Role-based permissions for Django REST Framework and vanilla Django.",
"version": "1.0.5",
"project_urls": {
"Homepage": "https://github.com/Pithikos/rest-framework-roles"
},
"split_keywords": [
"permissions",
" roles",
" users",
" django",
" rest",
" drf",
" acl",
" security",
" rest framework",
" access control"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "595c64a43b9b87bc040cee4f0388dafdd9e3ff41a952906ee2e72ef940953249",
"md5": "d8f99c2a3713cd5ca4e5fd9869734dc8",
"sha256": "31df23cf15f9b3891705838bcc9abb89cf3bdb8e9fa282f0bbee8c89c9e81858"
},
"downloads": -1,
"filename": "rest_framework_roles-1.0.5-py3-none-any.whl",
"has_sig": false,
"md5_digest": "d8f99c2a3713cd5ca4e5fd9869734dc8",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3",
"size": 13258,
"upload_time": "2024-03-22T10:49:36",
"upload_time_iso_8601": "2024-03-22T10:49:36.955612Z",
"url": "https://files.pythonhosted.org/packages/59/5c/64a43b9b87bc040cee4f0388dafdd9e3ff41a952906ee2e72ef940953249/rest_framework_roles-1.0.5-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "a017efe67bd5b728b4da847e89629c3918c476dc438380880112ef8cadfe6273",
"md5": "cc726ac364e5e689e988cf4c25b49c77",
"sha256": "6342179c553bdd47bca37e9bd5eff9506f77e70872f22fa02c708380e7c11454"
},
"downloads": -1,
"filename": "rest_framework_roles-1.0.5.tar.gz",
"has_sig": false,
"md5_digest": "cc726ac364e5e689e988cf4c25b49c77",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3",
"size": 13573,
"upload_time": "2024-03-22T10:49:38",
"upload_time_iso_8601": "2024-03-22T10:49:38.062813Z",
"url": "https://files.pythonhosted.org/packages/a0/17/efe67bd5b728b4da847e89629c3918c476dc438380880112ef8cadfe6273/rest_framework_roles-1.0.5.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-03-22 10:49:38",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Pithikos",
"github_project": "rest-framework-roles",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"circle": true,
"requirements": [
{
"name": "Django",
"specs": [
[
"==",
"4.1.13"
]
]
},
{
"name": "djangorestframework",
"specs": [
[
"==",
"3.14.0"
]
]
},
{
"name": "pytest",
"specs": [
[
"==",
"7.2.0"
]
]
},
{
"name": "pytest-django",
"specs": [
[
"==",
"4.5.2"
]
]
},
{
"name": "pytest-forked",
"specs": [
[
"==",
"1.4.0"
]
]
},
{
"name": "pytest-xdist",
"specs": [
[
"==",
"3.0.2"
]
]
},
{
"name": "twine",
"specs": [
[
"==",
"4.0.1"
]
]
}
],
"lcname": "rest-framework-roles"
}