This repository has been archived by the owner on Jun 4, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
42 changed files
with
1,155 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
|
||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
*.py[cod] | ||
*$py.class | ||
|
||
# Distribution / packaging | ||
.Python | ||
env/ | ||
.env/ | ||
build/ | ||
develop-eggs/ | ||
dist/ | ||
downloads/ | ||
eggs/ | ||
.eggs/ | ||
lib/ | ||
lib64/ | ||
parts/ | ||
sdist/ | ||
var/ | ||
*.egg-info/ | ||
.installed.cfg | ||
*.egg | ||
|
||
# PyInstaller | ||
# Usually these files are written by a python script from a template | ||
# before PyInstaller builds the exe, so as to inject date/other infos into it. | ||
*.manifest | ||
*.spec | ||
|
||
# Installer logs | ||
pip-log.txt | ||
pip-delete-this-directory.txt | ||
|
||
# Unit test / coverage reports | ||
htmlcov/ | ||
.tox/ | ||
.coverage | ||
.coverage.* | ||
.cache | ||
nosetests.xml | ||
coverage.xml | ||
*,cover | ||
.hypothesis/ | ||
|
||
# Translations | ||
*.mo | ||
*.pot | ||
|
||
# Django stuff: | ||
*.log | ||
local_settings.py | ||
|
||
# Sphinx documentation | ||
docs/_build/ | ||
|
||
# PyBuilder | ||
target/ | ||
|
||
#Ipython Notebook | ||
.ipynb_checkpoints | ||
|
||
# pyenv | ||
.python-version | ||
|
||
.egg-info/ | ||
coverage/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
language: python | ||
python: | ||
- "3.4" | ||
- "3.5" | ||
install: | ||
- pip install -U pip | ||
- pip install coveralls | ||
- pip install -r requirements.txt | ||
- pip install -r dev-requirements.txt | ||
- pip install -e . | ||
script: | ||
- py.test -ra -vvv --cov-config .coveragerc --cov rest_jwt_permission rest_jwt_permission_tests | ||
after_success: coveralls |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
[![Build Status](https://travis-ci.org/chessbr/rest-jwt-permission.svg?branch=master)](https://travis-ci.org/chessbr/rest-jwt-permission) | ||
[![Coverage Status](https://coveralls.io/repos/github/chessbr/rest-jwt-permission/badge.svg?branch=master)](https://coveralls.io/github/chessbr/rest-jwt-permission?branch=master) | ||
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) | ||
|
||
|
||
# Django Rest Framework JWT permissions | ||
|
||
Module that check API View permissions from JWT payloads. | ||
|
||
## Installation | ||
|
||
``` | ||
pip install rest_jwt_permission | ||
``` | ||
|
||
## Using | ||
|
||
Add `rest_jwt_permission` in your `INSTALLED_APPS` and configure the `settings` as you wish. Here is an example: | ||
|
||
```python | ||
REST_JWT_PERMISSION = { | ||
"SCOPE_PROVIDERS": [ | ||
"rest_jwt_permission.providers.APIEndpointScopeProvider", | ||
"rest_jwt_permission.providers.AdminScopeProvider" | ||
], | ||
"GET_PAYLOAD_FROM_SCOPES_HANDLER": ( | ||
"rest_jwt_permission.handlers.get_payload_from_scopes" | ||
), | ||
"GET_SCOPES_FROM_PAYLOAD_HANDLER": ( | ||
"rest_jwt_permission.handlers.get_scopes_from_payload" | ||
), | ||
"GET_PAYLOAD_FROM_REQUEST_HANDLER": ( | ||
"rest_jwt_permission.handlers.get_jwt_payload_from_request" | ||
) | ||
} | ||
``` | ||
|
||
Now you can use `JWTAPIPermission` class in your API Views through `permission_classes` property or even setting it as the default permission class in your [settings](http://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy) | ||
|
||
## Motivation | ||
|
||
Inspired by GitHub [Personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) and by [Auth0 API Keys blog post](https://auth0.com/blog/using-json-web-tokens-as-api-keys/), this package provides a Django Rest Framework Permission object to check permissions from JWT payloads. | ||
|
||
This enables your API to check permissions avoiding an extra database hit. | ||
|
||
## How it works | ||
|
||
Basically, it extracts a list of all Rest API Views and generate an unique ID for each endpoint + action. Then, after authenticaton, your API should inject which permission identifiers the user has access. The JWT payload should look like the following: | ||
|
||
```json | ||
{ | ||
"scopes": [ | ||
"myviewset:get" | ||
"function_endpoint:get", | ||
"basicview:get", | ||
"simpleviewsetpermission:custom_action:put", | ||
"modelviewsetpermission:retrieve:get", | ||
"modelviewsetpermission:destroy:delete", | ||
"modelviewsetpermission:some_detail_metod:patch", | ||
] | ||
} | ||
``` | ||
|
||
On each authenticated request, the `JWTAPIPermission` permission class will generate the unique ID for the requested view and will check whether the JWT payloads contains the ID. If so, the user has access. | ||
|
||
:warning: This package does not automatically injects the scopes payload into the JWT, although we have helpers (`rest_jwt_permission.handlers.get_payload_from_scopes`) you can use to do that. We strongly recommend you to use [REST framework JWT Auth](https://github.com/GetBlimp/django-rest-framework-jwt) package as it provides all you need to make this eaiser. You can change the payload handler though it's `JWT_PAYLOAD_HANDLER` setting. | ||
|
||
|
||
You can also create some sort of admin page to select the permissions for user and/or groups like GitHub token scopes, and use that to inject the token into the JWT: | ||
|
||
![GitHub Token Page](https://help.github.com/assets/images/help/settings/token_scopes.gif) | ||
|
||
|
||
#### Scopes | ||
|
||
Scopes are basically what users can access (has permission to do). Each scope should has an unique identifier (see [`Scope`](rest_jwt_permission/scopes/base.py) base class). You can extend the base `Scope` class and add extra properties and methods. | ||
|
||
|
||
#### Providers | ||
|
||
`Providers` are objects that returns a list of existing scopes. We currently have 2 built-in providers: | ||
|
||
* `APIEndpointScopeProvider`: returns all scopes for Django REST Framework registered views. | ||
* `AdminScopeProvider`: returns admin related scopes. Currently, only returns `superuser` scope. | ||
|
||
You can develop new providers to your project as you need or even extend the built-ins. | ||
|
||
|
||
### Settings | ||
|
||
This project was build with extension in mind, so it is easy to extend, add or remove features. | ||
|
||
See the list of settings you can customize, all of them are inside the `REST_JWT_PERMISSION` setting key: | ||
|
||
|
||
**`SCOPE_PROVIDERS`**: List of providers used to extract the existing scopes. | ||
Defaults to: | ||
``` | ||
"SCOPE_PROVIDERS": [ | ||
"rest_jwt_permission.providers.APIEndpointScopeProvider", | ||
"rest_jwt_permission.providers.AdminScopeProvider" | ||
] | ||
``` | ||
|
||
**`GET_PAYLOAD_FROM_SCOPES_HANDLER`**: Handler function to get the payload with scopes to be injected into JWT. Defaults to: | ||
``` | ||
"GET_PAYLOAD_FROM_SCOPES_HANDLER": "rest_jwt_permission.handlers.get_payload_from_scopes" | ||
``` | ||
|
||
**`GET_SCOPES_FROM_PAYLOAD_HANDLER`**: Handler function to get the scopes from a JWT payload. Defaults to: | ||
``` | ||
"GET_SCOPES_FROM_PAYLOAD_HANDLER": "rest_jwt_permission.handlers.get_scopes_from_payload" | ||
``` | ||
|
||
**`GET_PAYLOAD_FROM_REQUEST_HANDLER`**: Handler function to get JWT payload from Request. Defaults to: | ||
``` | ||
"GET_PAYLOAD_FROM_REQUEST_HANDLER": "rest_jwt_permission.handlers.get_jwt_payload_from_request" | ||
``` | ||
|
||
|
||
### Showing all available roles | ||
|
||
You can use the `show_roles` management command to print all available roles according to your providers. | ||
|
||
``` | ||
python manage.py show_roles | ||
``` | ||
|
||
## Running tests | ||
|
||
Install dependencies from `dev-requirements.txt` and run `py.tets --cov`: | ||
|
||
``` | ||
pip install dev-requirements.txt && py.tets --cov | ||
``` | ||
|
||
# Compatibility | ||
|
||
* Python >= 3.4 | ||
* Django >= 1.10 | ||
* Django Rest Framework >= 3.6 | ||
|
||
# License | ||
|
||
MIT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
pytest | ||
isort | ||
flake8 | ||
pip-tools | ||
coverage | ||
pytest-cov | ||
pytest-django | ||
pytest-sugar |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# | ||
# This file is autogenerated by pip-compile | ||
# To update, run: | ||
# | ||
# pip-compile --output-file dev-requirements.txt dev-requirements.in | ||
# | ||
click==6.7 # via pip-tools | ||
coverage==4.4.1 | ||
first==2.0.1 # via pip-tools | ||
flake8==3.4.1 | ||
isort==4.2.15 | ||
mccabe==0.6.1 # via flake8 | ||
pip-tools==1.10.1 | ||
py==1.4.34 # via pytest | ||
pycodestyle==2.3.1 # via flake8 | ||
pyflakes==1.5.0 # via flake8 | ||
pytest-cov==2.5.1 | ||
pytest-django==3.1.2 | ||
pytest-sugar==0.9.0 | ||
pytest==3.2.2 | ||
six==1.11.0 # via pip-tools | ||
termcolor==1.1.0 # via pytest-sugar |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
django>=1.10,<1.12 | ||
djangorestframework>=3.6,<4.0 | ||
djangorestframework-jwt>=1.11,<2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# | ||
# This file is autogenerated by pip-compile | ||
# To update, run: | ||
# | ||
# pip-compile --output-file requirements.txt requirements.in | ||
# | ||
django==1.11.5 | ||
djangorestframework-jwt==1.11.0 | ||
djangorestframework==3.6.4 | ||
pyjwt==1.5.3 # via djangorestframework-jwt | ||
pytz==2017.2 # via django |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# -*- coding: utf-8 -*- | ||
from django.apps import AppConfig | ||
|
||
|
||
class RestApiPermissionConfig(AppConfig): | ||
name = 'rest_api_permission' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# -*- coding: utf-8 -*- | ||
import jwt | ||
from django.utils.encoding import smart_text | ||
from django.utils.translation import ugettext_lazy as _ | ||
from rest_framework import exceptions | ||
from rest_framework.authentication import get_authorization_header | ||
|
||
|
||
def get_payload_from_scopes(scopes): | ||
""" | ||
Get a dict to be used in JWT payload. | ||
Just merge this dict with the JWT payload. | ||
:type roles list[rest_jwt_permission.scopes.Scope] | ||
:return dictionary to be merged with the JWT payload | ||
:rtype dict | ||
""" | ||
return { | ||
"scopes": [scope.identifier for scope in scopes] | ||
} | ||
|
||
|
||
def get_scopes_from_payload(payload): | ||
""" | ||
Get a list of Scope from the JWT payload | ||
:type payload dict | ||
:rtype list[rest_jwt_permission.scopes.Scope] | ||
""" | ||
return payload.get("scopes", []) | ||
|
||
|
||
def get_jwt_payload_from_request(request): | ||
# lazy load this if one want to use other rest lib for JWT | ||
from rest_framework_jwt.settings import api_settings | ||
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER | ||
|
||
auth = get_authorization_header(request).split() | ||
auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower() | ||
|
||
if not auth: | ||
if api_settings.JWT_AUTH_COOKIE: | ||
return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE) | ||
return None | ||
|
||
if smart_text(auth[0].lower()) != auth_header_prefix: | ||
return None | ||
|
||
if len(auth) == 1: | ||
raise exceptions.PermissionDenied(_('No credentials provided.')) | ||
elif len(auth) > 2: | ||
raise exceptions.PermissionDenied(_('Invalid Authorization header.')) | ||
|
||
jwt_value = auth[1] | ||
if jwt_value is None: | ||
return None | ||
|
||
try: | ||
payload = jwt_decode_handler(jwt_value) | ||
except jwt.ExpiredSignature: | ||
raise exceptions.PermissionDenied(_('Signature has expired.')) | ||
except jwt.DecodeError: | ||
raise exceptions.PermissionDenied(_('Error decoding signature.')) | ||
except jwt.InvalidTokenError: | ||
raise exceptions.PermissionDenied(_('Invalid token.')) | ||
|
||
return payload |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# -*- coding: utf-8 -*- | ||
from django.core.management.base import BaseCommand | ||
|
||
from rest_jwt_permission.scopes import get_all_permission_providers_scopes | ||
|
||
|
||
class Command(BaseCommand): | ||
def handle(self, *args, **options): | ||
for x in get_all_permission_providers_scopes(): | ||
print(x) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# -*- coding: utf-8 -*- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# -*- coding: utf-8 -*- | ||
from rest_framework.permissions import BasePermission | ||
|
||
from .settings import get_imported_setting | ||
from .utils import get_role_for, get_view_role | ||
|
||
|
||
class JWTAPIPermission(BasePermission): | ||
""" | ||
Returns whether the scope is inside the JWT payload | ||
""" | ||
def has_permission(self, request, view): | ||
get_jwt_payload_from_request = get_imported_setting("GET_PAYLOAD_FROM_REQUEST_HANDLER") | ||
get_scopes_from_payload = get_imported_setting("GET_SCOPES_FROM_PAYLOAD_HANDLER") | ||
|
||
payload = get_jwt_payload_from_request(request) | ||
|
||
if not payload: | ||
return False | ||
|
||
payload_scopes = get_scopes_from_payload(payload) | ||
role = get_role_for(request.method.lower(), getattr(view, "action", None)) | ||
scope = get_view_role(view, role) | ||
|
||
return scope in payload_scopes |
Oops, something went wrong.