Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Tomáš Rychlik committed Apr 24, 2014
0 parents commit 1ffbfff
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .gitignore
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
3 changes: 3 additions & 0 deletions jsonis/__init__.py
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
41 changes: 41 additions & 0 deletions jsonis/jwt.py
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)
79 changes: 79 additions & 0 deletions jsonis/middleware.py
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 added jsonis/templatetags/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions jsonis/templatetags/jsonify.py
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)
25 changes: 25 additions & 0 deletions jsonis/tests.py
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
12 changes: 12 additions & 0 deletions jsonis/utils.py
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
111 changes: 111 additions & 0 deletions jsonis/views.py
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()))
25 changes: 25 additions & 0 deletions setup.py
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',
],
)

0 comments on commit 1ffbfff

Please sign in to comment.