-
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
Tomáš Rychlik
committed
Apr 24, 2014
0 parents
commit 1ffbfff
Showing
10 changed files
with
330 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,22 @@ | ||
# Python bytecode: | ||
*.py[co] | ||
|
||
# Packaging files: | ||
*.egg* | ||
|
||
# Editor temp files: | ||
*.swp | ||
*~ | ||
|
||
# SQLite3 database files: | ||
*.db | ||
|
||
# Celerybeat scheduler file: | ||
celerybeat-schedule | ||
|
||
*.pot | ||
.idea | ||
venv | ||
|
||
# Mac OS crap | ||
.DS_Store |
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 @@ | ||
from views import JSONApiResponseMixin, JSONApiLoginRequiredMixin, JSONPResponseMixin, JSONApiFormView | ||
from tests import JSONTestCase | ||
from jwt import JWTAuthorizationMixin |
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,41 @@ | ||
from __future__ import absolute_import | ||
|
||
import jwt | ||
|
||
from django.conf import settings | ||
from django.contrib.auth import get_user_model | ||
from django.utils.decorators import method_decorator | ||
from django.views.decorators.csrf import csrf_exempt | ||
|
||
from .views import JSONApiResponseMixin | ||
|
||
|
||
class JWTAuthorizationMixin(JSONApiResponseMixin): | ||
"""JSON API mixin that enables JSON Web Token authorization""" | ||
@method_decorator(csrf_exempt) | ||
def dispatch(self, request, *args, **kwargs): | ||
if request.user.is_authenticated(): | ||
self.user = request.user | ||
else: | ||
try: | ||
auth_prefix, auth_token = request.META['HTTP_AUTHORIZATION'].split(' ') | ||
if auth_prefix != 'Bearer': | ||
raise ValueError | ||
token_data = jwt.decode(auth_token, settings.FIREBASE_SECRET) | ||
except KeyError: | ||
return self.render_api_error_response('Not authenticated - Missing authorization header', status=401) | ||
except ValueError: | ||
return self.render_api_error_response('Not authenticated - Bad authorization header', status=401) | ||
except jwt.DecodeError: | ||
return self.render_api_error_response('Not authenticated - Bad authorization header (decode failed)', | ||
status=401) | ||
except jwt.ExpiredSignature: | ||
return self.render_api_error_response('Not authenticated - Expired authorization header', status=401) | ||
|
||
try: | ||
self.user = get_user_model().objects.get(pk=token_data['d']['id']) | ||
except (TypeError, KeyError): | ||
return self.render_api_error_response('Not authenticated - Bad authorization header data', status=401) | ||
except get_user_model().DoesNotExist: | ||
return self.render_api_error_response('Not authenticated - User not found', status=401) | ||
return super(JWTAuthorizationMixin, self).dispatch(request, *args, **kwargs) |
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,79 @@ | ||
import logging | ||
from django.conf import settings | ||
from django.middleware.csrf import CsrfViewMiddleware as DjangoCsrfViewMiddleware, get_token, _sanitize_token, _get_new_csrf_key, REASON_NO_REFERER, REASON_NO_CSRF_COOKIE, REASON_BAD_TOKEN | ||
from django.utils.crypto import constant_time_compare | ||
|
||
logger = logging.getLogger('django.request') | ||
|
||
class CsrfViewMiddleware(DjangoCsrfViewMiddleware): | ||
"""CSRF view middleware that will ensure csrf token is always sent in cookie | ||
Strict Referer checking of https requests is removed""" | ||
|
||
def process_view(self, request, callback, callback_args, callback_kwargs): | ||
if getattr(request, 'csrf_processing_done', False): | ||
return None | ||
|
||
try: | ||
csrf_token = _sanitize_token( | ||
request.COOKIES[settings.CSRF_COOKIE_NAME]) | ||
# Use same token next time | ||
request.META['CSRF_COOKIE'] = csrf_token | ||
except KeyError: | ||
csrf_token = None | ||
# Generate token and store it in the request, so it's | ||
# available to the view. | ||
request.META["CSRF_COOKIE"] = _get_new_csrf_key() | ||
|
||
# Wait until request.META["CSRF_COOKIE"] has been manipulated before | ||
# bailing out, so that get_token still works | ||
if getattr(callback, 'csrf_exempt', False): | ||
return None | ||
|
||
# Assume that anything not defined as 'safe' by RFC2616 needs protection | ||
if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): | ||
if getattr(request, '_dont_enforce_csrf_checks', False): | ||
# Mechanism to turn off CSRF checks for test suite. | ||
# It comes after the creation of CSRF cookies, so that | ||
# everything else continues to work exactly the same | ||
# (e.g. cookies are sent, etc.), but before any | ||
# branches that call reject(). | ||
return self._accept(request) | ||
|
||
if csrf_token is None: | ||
# No CSRF cookie. For POST requests, we insist on a CSRF cookie, | ||
# and in this way we can avoid all CSRF attacks, including login | ||
# CSRF. | ||
logger.warning('Forbidden (%s): %s', | ||
REASON_NO_CSRF_COOKIE, request.path, | ||
extra={ | ||
'status_code': 403, | ||
'request': request, | ||
} | ||
) | ||
return self._reject(request, REASON_NO_CSRF_COOKIE) | ||
|
||
# Check non-cookie token for match. | ||
request_csrf_token = "" | ||
if request.method == "POST": | ||
request_csrf_token = request.POST.get('csrfmiddlewaretoken', '') | ||
|
||
if request_csrf_token == "": | ||
# Fall back to X-CSRFToken, to make things easier for AJAX, | ||
# and possible for PUT/DELETE. | ||
request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') | ||
|
||
if not constant_time_compare(request_csrf_token, csrf_token): | ||
logger.warning('Forbidden (%s): %s', | ||
REASON_BAD_TOKEN, request.path, | ||
extra={ | ||
'status_code': 403, | ||
'request': request, | ||
} | ||
) | ||
return self._reject(request, REASON_BAD_TOKEN) | ||
|
||
resp = self._accept(request) | ||
|
||
request.META["CSRF_COOKIE_USED"] = True | ||
return resp |
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,12 @@ | ||
from django.conf import settings | ||
from django.template import Library | ||
|
||
from ..utils import MyJSONEncoder | ||
|
||
register = Library() | ||
|
||
encoder = MyJSONEncoder(ensure_ascii=False, encoding=settings.DEFAULT_CHARSET) | ||
|
||
@register.filter | ||
def jsonify(obj): | ||
return encoder.encode(obj) |
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 @@ | ||
import json | ||
|
||
from .utils import JSONEncoder | ||
from django.test import Client, TestCase | ||
|
||
class JSONTestClient(Client): | ||
"""Test client for JSON requests""" | ||
|
||
def _encode_data(self, data, content_type, **kwargs): | ||
"""Encode POST data to JSON for json content type""" | ||
if content_type.find('application/json') == 0: | ||
return json.dumps(data, cls=JSONEncoder) | ||
else: | ||
return super(JSONTestClient, self)._encode_data(data, content_type, **kwargs) | ||
|
||
def post(self, path, content_type='application/json; charset=utf-8', **kwargs): | ||
"""Only changing content_type default value""" | ||
return super(JSONTestClient, self).post(path, content_type=content_type, **kwargs) | ||
|
||
def put(self, path, content_type='application/json; charset=utf-8', **kwargs): | ||
"""Only changing content_type default value""" | ||
return super(JSONTestClient, self).put(path, content_type=content_type, **kwargs) | ||
|
||
class JSONTestCase(TestCase): | ||
client_class = JSONTestClient |
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,12 @@ | ||
import decimal | ||
from json import JSONEncoder, JSONDecoder | ||
|
||
|
||
class MyJSONEncoder(JSONEncoder): | ||
def default(self, o): | ||
if isinstance(o, decimal.Decimal): | ||
return float(o) | ||
return super(MyJSONEncoder, self).default(o) | ||
|
||
class MyJSONDecoder(JSONDecoder): | ||
pass |
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,111 @@ | ||
from django.conf import settings | ||
|
||
from django.http import HttpResponse | ||
from django.views.generic.edit import BaseFormView | ||
|
||
from .utils import MyJSONEncoder, MyJSONDecoder | ||
from libs.lazy import lazyprop | ||
|
||
class CharsetHttpResponse(HttpResponse): | ||
"""HttpResponse that will append charset info into Content-Type header""" | ||
charset = settings.DEFAULT_CHARSET | ||
|
||
def __init__(self, *args, **kwargs): | ||
if 'content_type' in kwargs and kwargs['content_type'].find('charset=') == -1: | ||
kwargs['content_type'] = '%s; charset=%s' % (kwargs['content_type'], self.charset) | ||
|
||
super(CharsetHttpResponse, self).__init__(*args, **kwargs) | ||
|
||
class JSONResponse(CharsetHttpResponse): | ||
"""Simple response class that will automatically serialize encode data to JSON""" | ||
|
||
def __init__(self, content_data, json_encoder=MyJSONEncoder, *args, **kwargs): | ||
encoder = json_encoder(ensure_ascii=False, encoding=self.charset) | ||
self.content_data = content_data | ||
kwargs.setdefault('content_type', 'application/json') | ||
|
||
super(JSONResponse, self).__init__(encoder.iterencode(self.content_data), *args, **kwargs) | ||
|
||
class JSONResponseMixin(object): | ||
"""Mixin providing render_json_response returning the JSONResponse""" | ||
def render_json_response(self, content_data): | ||
return JSONResponse(content_data) | ||
|
||
class JSONPResponse(CharsetHttpResponse): | ||
def __init__(self, content_data, callback, json_encoder=MyJSONEncoder, *args, **kwargs): | ||
encoder = json_encoder(ensure_ascii=False, encoding=self.charset) | ||
self.content_data = content_data | ||
kwargs.setdefault('content_type', 'application/javascript') | ||
|
||
super(JSONPResponse, self).__init__('%s(%s)' % (callback, encoder.encode(self.content_data)), *args, **kwargs) | ||
|
||
class JSONPResponseMixin(object): | ||
"""JSONP Response mixin that will fallback to JSON response if there is no callback supplied""" | ||
def render_jsonp_response(self, data): | ||
try: | ||
return JSONPResponse(data, self.request.REQUEST['callback']) | ||
except KeyError: | ||
return JSONResponse(data) | ||
|
||
class JSONApiResponse(JSONResponse): | ||
def __init__(self, content_data=None, status_msg='ok', json_encoder=MyJSONEncoder, *args, **kwargs): | ||
if content_data is None: | ||
content_data = {} | ||
content_data['status'] = status_msg | ||
super(JSONApiResponse, self).__init__(content_data, json_encoder, *args, **kwargs) | ||
|
||
class JSONApiErrorResponse(JSONApiResponse): | ||
status_code = 400 | ||
|
||
def __init__(self, error_msg, content_data=None, status_msg='fail', json_encoder=MyJSONEncoder, *args, **kwargs): | ||
if content_data is None: | ||
content_data = {} | ||
content_data['error'] = error_msg | ||
super(JSONApiErrorResponse, self).__init__(content_data, status_msg, json_encoder, *args, **kwargs) | ||
|
||
class JSONApiResponseMixin(object): | ||
"""Mixin providing JSON Api view functionality returning JSONApiResponse""" | ||
|
||
@lazyprop | ||
def payload_data(self): | ||
"""Decode received request data""" | ||
if self.request.META.get('CONTENT_TYPE', '').find('application/json') == 0: | ||
decoder = MyJSONDecoder(encoding='utf-8') | ||
return decoder.decode(self.request.body) | ||
else: | ||
return self.request.POST | ||
|
||
def render_api_response(self, content_data, **kwargs): | ||
return JSONApiResponse(content_data, **kwargs) | ||
|
||
def render_api_error_response(self, error_message, content_data=None, **kwargs): | ||
return JSONApiErrorResponse(error_message, content_data, **kwargs) | ||
|
||
class JSONApiLoginRequiredMixin(JSONApiResponseMixin): | ||
"""JSONApiResponseMixin that will check user authorization""" | ||
def dispatch(self, request, *args, **kwargs): | ||
if not request.user.is_authenticated(): | ||
return self.user_unauthorized() | ||
return super(JSONApiLoginRequiredMixin, self).dispatch(request, *args, **kwargs) | ||
|
||
def user_unauthorized(self): | ||
return self.render_api_error_response('Not authenticated', status=401) | ||
|
||
class JSONApiFormView(JSONApiResponseMixin, BaseFormView): | ||
def dispatch(self, request, *args, **kwargs): | ||
return super(JSONApiFormView, self).dispatch(request, *args, **kwargs) | ||
|
||
def get_form_kwargs(self): | ||
kwargs = super(JSONApiFormView, self).get_form_kwargs() | ||
if 'data' in kwargs: | ||
kwargs['data'] = self.payload_data | ||
return kwargs | ||
|
||
def get(self, request, *args, **kwargs): | ||
return self.render_api_error_response('Method not allowed', status=405) | ||
|
||
def form_valid(self, form): | ||
raise NotImplementedError | ||
|
||
def form_invalid(self, form): | ||
return self.render_api_error_response(dict(form.errors.iteritems())) |
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 @@ | ||
#!/usr/bin/env python | ||
|
||
from distutils.core import setup | ||
|
||
setup( | ||
name='django-jsonis', | ||
version='0.1.0', | ||
description='Django JSON Utils', | ||
author='Tomas Rychlik', | ||
author_email='[email protected]', | ||
packages=['jsonis'], | ||
license='MIT', | ||
install_requires=[ | ||
'Django==1.6.3', | ||
'PyJWT==0.1.6' | ||
], | ||
classifiers=[ | ||
'Intended Audience :: Developers', | ||
'Operating System :: OS Independent', | ||
'Programming Language :: Python', | ||
'Programming Language :: Python :: 2.7', | ||
'Topic :: Software Development :: Libraries', | ||
'Topic :: Software Development :: Libraries :: Python Modules', | ||
], | ||
) |