Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

added authenticators

added more HTTP exception classes
added HTTP response shortcuts
added deletion view mixin
added OPTIONS support to views
  • Loading branch information...
commit 33c386d1286f033d5363b1a5027612e10718c4cd 1 parent 388a59c
@aehlke authored
View
15 catnap/__init__.py
@@ -1,4 +1,6 @@
'''
+Manifesto (just a draft)
+
catnap is a lightweight REST framework for Django.
@@ -11,17 +13,22 @@
It'll help you make an API which can be explored via hypertext.
-It won't help you document your API, though, because it doesn't make
+It won't help you document your API, though, because it's not pedantic about making
you define your resource models. You should document the API you want
first, then make your views behave accordingly. This isn't far off from
how you write Python code -- you document the behavior, and implement
that behavior, without predefined static type checking making sure
-you implemented it right.
+you implemented it right. catnap has "Resource" classes, but doesn't make
+you use them for everything when less abstraction is more suitable.
+
+catnap lets you scale your abstractions up or down as is suitable. You
+shouldn't have to fight your framework to do what you want, or change
+your API to fit the framework. So use catnap's conventions where they help,
+and abandon them where they get in the way.
If your REST resources correspond very closely to your Django ORM models
in a CRUD-like, 1:1 fashion, you're probably better off with another
-library, like django-piston, since catnap doesn't help you generate
-views from Django models.
+library, like django-tastypie or django-piston.
catnap recognizes that your HTTP resources are not necessarily identical
to your data models.
View
128 catnap/auth.py
@@ -0,0 +1,128 @@
+# This module mostly taken from django-tastypie (thanks!),
+# which is also BSD-licensed.
+# Also uses part of Django-Rest-Framework.
+
+import base64
+from django.contrib.auth import authenticate as django_authenticate
+from exceptions import HttpUnauthorizedException
+from django.middleware.csrf import CsrfViewMiddleware
+
+
+
+class Authentication(object):
+ '''
+ A simple base class to establish the protocol for auth.
+
+ By default, this indicates the user is always authenticated.
+ '''
+ def authenticate(self, request, **kwargs):
+ '''
+ Identifies if the user is authenticated to continue or not.
+
+ Should usually set `request.user`. Doesn't return anything,
+ but if authentication fails, it should raise an exception,
+ usually an `HttpException` subclass.
+ '''
+
+
+class BasicAuthentication(Authentication):
+ '''
+ Handles HTTP Basic auth against a specific auth backend if provided,
+ or against all configured authentication backends using the
+ ``authenticate`` method from ``django.contrib.auth``.
+
+ Optional keyword arguments:
+
+ ``backend``
+ If specified, use a specific ``django.contrib.auth`` backend instead
+ of checking all backends specified in the ``AUTHENTICATION_BACKENDS``
+ setting.
+ ``realm``
+ The realm to use in the ``HttpUnauthorized`` response. Default:
+ ``django-catnap``.
+ '''
+ def __init__(self, backend=None, realm='django-catnap'):
+ self.backend = backend
+ self.realm = realm
+
+ def _unauthorized(self):
+ #FIXME: Sanitize realm.
+ raise HttpUnauthorizedException("Basic Realm='%s'" % self.realm)
+ #raise HttpUnauthorizedException("Basic Realm='%s'" % self.realm)
+
+ def authenticate(self, request, **kwargs):
+ '''
+ Checks a user's basic auth credentials against the current
+ Django auth backend.
+
+ Should return either ``True`` if allowed, ``False`` if not or an
+ ``HttpResponse`` if you need something custom.
+ '''
+ if not request.META.get('HTTP_AUTHORIZATION'):
+ self._unauthorized()
+
+ try:
+ (auth_type, data) = request.META['HTTP_AUTHORIZATION'].split()
+ if auth_type.lower() != 'basic':
+ return self._unauthorized()
+ user_pass = base64.b64decode(data)
+ except:
+ self._unauthorized()
+
+ bits = user_pass.split(':')
+
+ if len(bits) != 2:
+ self._unauthorized()
+
+ if self.backend:
+ user = self.backend.authenticate(username=bits[0], password=bits[1])
+ else:
+ user = django_authenticate(username=bits[0], password=bits[1])
+
+ if user is None:
+ self._unauthorized()
+
+ request.user = user
+
+
+class DjangoContribAuthentication(Authentication):
+ '''Use Djagno's built-in request session for authentication.'''
+ def authenticate(self, request, **kwargs):
+ if getattr(request, 'user', None) and request.user.is_active:
+ resp = CsrfViewMiddleware().process_view(request, None, (), {})
+ if resp is None: # csrf passed
+ return request.user
+ return None
+
+
+class AuthenticationMixin(object):
+ '''
+ Mixin to use for catnap REST views.
+
+ You must override the `authenticator` property with whichever
+ authentication method you want. It defaults to a debug-mode
+ one which always authenticates successfully.
+ '''
+
+ # Which authentication to use.
+ authenticator = Authentication()
+ authenticators = None
+
+ def dispatch(self, request, *args, **kwargs):
+ if getattr(self, 'authenticators', None):
+ # Try the authenticators until one works.
+ for authenticator in self.authenticators:
+ try:
+ authenticator.authenticate(request)
+ except HttpUnauthorizedException:
+ pass
+ else:
+ break
+ else:
+ self.authenticator.authenticate(request)
+
+ return super(AuthenticationMixin, self).dispatch(
+ request, *args, **kwargs)
+
+
+
View
31 catnap/exceptions.py
@@ -1,25 +1,34 @@
from django.http import (HttpResponseBadRequest, HttpResponseNotFound,
HttpResponseForbidden, HttpResponseGone)
-
+from http import HttpResponseUnauthorized, HttpResponseTemporaryRedirect
class HttpException(Exception):
- def __init__(self, http_response):
+ def __init__(self, http_response=None):
self.response = http_response
+ super(HttpException, self).__init__(unicode(self.response))
+
+class _HttpException(Exception):
+ def __init__(self, *args, **kwargs):
+ self.response = self._HTTP_RESPONSE_CLASS(*args, **kwargs)
+ super(_HttpException, self).__init__(unicode(self.response))
# HttpException should really only be used for the cases listed below, in general.
# So instead of using it directly, use these instead.
-class HttpBadRequestException(HttpException):
- def __init__(self, *args, **kwargs):
- self.response = HttpResponseBadRequest(*args, **kwargs)
+class HttpBadRequestException(_HttpException):
+ _HTTP_RESPONSE_CLASS = HttpResponseBadRequest
-class HttpForbiddenException(HttpException):
- def __init__(self, *args, **kwargs):
- self.response = HttpResponseForbidden(*args, **kwargs)
+class HttpUnauthorizedException(_HttpException):
+ _HTTP_RESPONSE_CLASS = HttpResponseUnauthorized
-class HttpGoneException(HttpException):
- def __init__(self, *args, **kwargs):
- self.response = HttpResponseGone(*args, **kwargs)
+class HttpForbiddenException(_HttpException):
+ _HTTP_RESPONSE_CLASS = HttpResponseForbidden
+
+class HttpGoneException(_HttpException):
+ _HTTP_RESPONSE_CLASS = HttpResponseGone
+
+class HttpTemporaryRedirectException(_HttpException):
+ _HTTP_RESPONSE_CLASS = HttpResponseTemporaryRedirect
View
17 catnap/http.py
@@ -13,6 +13,9 @@ def __init__(self, location, **kwargs):
HttpResponse.__init__(self, **kwargs)
self['Location'] = iri_to_uri(location)
+class HttpResponseNoContent(HttpResponse):
+ status_code = 204
+
class HttpResponseSeeOther(HttpResponse):
status_code = 303
@@ -20,6 +23,20 @@ def __init__(self, redirect_to):
HttpResponse.__init__(self)
self['Location'] = iri_to_uri(redirect_to)
+class HttpResponseTemporaryRedirect(HttpResponse):
+ status_code = 307
+
+ def __init__(self, redirect_to):
+ HttpResponse.__init__(self)
+ self['Location'] = iri_to_uri(redirect_to)
+
+class HttpResponseUnauthorized(HttpResponse):
+ status_code = 401
+
+ def __init__(self, www_authenticate):
+ HttpResponse.__init__(self)
+ self['WWW-Authenticate'] = www_authenticate
+
class HttpResponseNotAcceptable(HttpResponse):
status_code = 406
View
6 catnap/middleware.py
@@ -1,6 +1,7 @@
# Some code adapted from the WebOb project: http://bitbucket.org/ianb/webob/
from webob.acceptparse import accept_property, Accept, MIMEAccept, NilAccept, MIMENilAccept, NoAccept
+from django.http import HttpResponse
ALL_HTTP_METHODS = ['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE']
@@ -26,7 +27,6 @@ def process_request(self, request):
request.accept = MIMEAccept('Accept', accept_val)
else:
request.accept = MIMENilAccept('Accept')
- return None
class HttpMethodsFallbackMiddleware(object):
@@ -46,7 +46,6 @@ def process_request(self, request):
request.method = request.POST[self.FALLBACK_PARAM]
else:
return HttpResponseNotAllowed(ALL_HTTP_METHODS)
- return None
class HttpExceptionMiddleware(object):
@@ -60,7 +59,6 @@ class HttpExceptionMiddleware(object):
'''
def process_exception(self, request, exception):
if (hasattr(exception, 'response')
- and isinstance(exception, HttpResponse)):
+ and isinstance(exception.response, HttpResponse)):
return exception.response
- return None
View
60 catnap/restviews.py
@@ -1,14 +1,17 @@
+from auth import AuthenticationMixin
+from catnap.http import (HttpResponseNotAcceptable, HttpResponseNoContent,
+ HttpResponseCreated, HttpResponseTemporaryRedirect)
+from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse
from django.http import HttpResponseNotAllowed, HttpResponseBadRequest
-from catnap.http import HttpResponseNotAcceptable
-from django.core.exceptions import ImproperlyConfigured
from django.utils import simplejson as json
+from django.utils.decorators import method_decorator
+from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View
from django.views.generic.detail import SingleObjectMixin, BaseDetailView
from django.views.generic.list import MultipleObjectMixin, BaseListView
-from serializers import json_serialize
from django_urls import UrlMixin
-
+from serializers import json_serialize
@@ -19,18 +22,29 @@ class _HttpResponseShortcuts(object):
def __init__(self, parent_view):
self.parent = parent_view
- def see_other(redirect_to):
+ def see_other(self, redirect_to):
return self.parent.get_response(redirect_to,
content_type='',
httpresponse_class=HttpResponseSeeOther)
- def created(location):
+ def created(self, location):
return self.parent.get_response(location,
content_type='',
httpresponse_class=HttpResponseCreated)
+ def temporary_redirect(self, redirect_to):
+ return self.parent.get_response(redirect_to,
+ content_type='',
+ httpresponse_class=HttpResponseTemporaryRedirect)
+
+ def no_content(self):
+ return self.parent.get_response(None,
+ content_type='',
+ httpresponse_class=HttpResponseNoContent)
+
+
-class RestView(View):
+class RestView(AuthenticationMixin, View):
'''
A base class view that cares a little more about RESTy things,
like strict content types and HTTP response codes.
@@ -42,14 +56,26 @@ class RestView(View):
'''
def __init__(self, *args, **kwargs):
- responses = _HttpResponseShortcuts(self)
+ self.responses = _HttpResponseShortcuts(self)
+ @method_decorator(csrf_exempt)
def dispatch(self, request, *args, **kwargs):
- # Make sure the `Accept` header matches our content type.
- if self.content_type not in request.accept:
- return HttpResponseNotAcceptable()
+ # Make sure the `Accept` header matches our content type.
+ #import pdb;pdb.set_trace()
+ if self.content_type not in request.accept:
+ return HttpResponseNotAcceptable()
+
+ return super(RestView, self).dispatch(request, *args, **kwargs)
+
+ def allowed_methods(self, request, *args, **kwargs):
+ '''Returns a list of allowed HTTP verbs.'''
+ return [m for m in self.http_method_names if hasattr(self, m)]
- return super(RestView, self).dispatch(request, *args, **kwargs)
+ def options(self, request, *args, **kwargs):
+ resp = self.get_response(None, content_type='')
+ resp['Access-Control-Allow-Methods'] = ', '.join(
+ self.allowed_methods(request, *args, **kwargs)).upper()
+ return resp
def get_response(self,
content,
@@ -218,6 +244,16 @@ class ListView(RestMultipleObjectMixin, BaseListView):
pass
+class DeletionMixin(object):
+ '''
+ A mixin providing the ability to delete objects.
+ '''
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ self.object.delete()
+ return HttpResponseNoContent()
+
+
class _BaseEmitterMixin(object):
@property
def serialize_context(self):
Please sign in to comment.
Something went wrong with that request. Please try again.