Generic permissions added, allowed_methods and anon_allowed_methods now defunct, dispatch now mirrors View.dispatch more nicely

This commit is contained in:
Tom Christie 2011-04-25 01:03:23 +01:00
parent cb4b4f6be6
commit 4692374e0d
8 changed files with 99 additions and 131 deletions

View File

@ -13,10 +13,10 @@ import base64
class BaseAuthenticator(object): class BaseAuthenticator(object):
"""All authenticators should extend BaseAuthenticator.""" """All authenticators should extend BaseAuthenticator."""
def __init__(self, mixin): def __init__(self, view):
"""Initialise the authenticator with the mixin instance as state, """Initialise the authenticator with the mixin instance as state,
in case the authenticator needs to access any metadata on the mixin object.""" in case the authenticator needs to access any metadata on the mixin object."""
self.mixin = mixin self.view = view
def authenticate(self, request): def authenticate(self, request):
"""Authenticate the request and return the authentication context or None. """Authenticate the request and return the authentication context or None.
@ -61,11 +61,13 @@ class BasicAuthenticator(BaseAuthenticator):
class UserLoggedInAuthenticator(BaseAuthenticator): class UserLoggedInAuthenticator(BaseAuthenticator):
"""Use Djagno's built-in request session for authentication.""" """Use Django's built-in request session for authentication."""
def authenticate(self, request): def authenticate(self, request):
if getattr(request, 'user', None) and request.user.is_active: if getattr(request, 'user', None) and request.user.is_active:
# Temporarily request.POST with .RAW_CONTENT, so that we use our more generic request parsing # Temporarily set request.POST to view.RAW_CONTENT,
request._post = self.mixin.RAW_CONTENT # so that we use our more generic request parsing,
# in preference to Django's form-only request parsing.
request._post = self.view.RAW_CONTENT
resp = CsrfViewMiddleware().process_view(request, None, (), {}) resp = CsrfViewMiddleware().process_view(request, None, (), {})
del(request._post) del(request._post)
if resp is None: # csrf passed if resp is None: # csrf passed

View File

@ -396,9 +396,9 @@ class ResponseMixin(object):
########## Auth Mixin ########## ########## Auth Mixin ##########
class AuthMixin(object): class AuthMixin(object):
"""Mixin class to provide authentication and permissions.""" """Mixin class to provide authentication and permission checking."""
authenticators = () authenticators = ()
permitters = () permissions = ()
@property @property
def auth(self): def auth(self):
@ -406,6 +406,14 @@ class AuthMixin(object):
self._auth = self._authenticate() self._auth = self._authenticate()
return self._auth return self._auth
def _authenticate(self):
for authenticator_cls in self.authenticators:
authenticator = authenticator_cls(self)
auth = authenticator.authenticate(self.request)
if auth:
return auth
return None
# TODO? # TODO?
#@property #@property
#def user(self): #def user(self):
@ -421,15 +429,11 @@ class AuthMixin(object):
if not self.permissions: if not self.permissions:
return return
auth = self.auth for permission_cls in self.permissions:
for permitter_cls in self.permitters: permission = permission_cls(self)
permitter = permission_cls(self) if not permission.has_permission(self.auth):
permitter.permit(auth) raise ErrorResponse(status.HTTP_403_FORBIDDEN,
{'detail': 'You do not have permission to access this resource. ' +
'You may need to login or otherwise authenticate the request.'})
def _authenticate(self):
for authenticator_cls in self.authenticators:
authenticator = authenticator_cls(self)
auth = authenticator.authenticate(self.request)
if auth:
return auth
return None

View File

@ -410,13 +410,13 @@ class ModelResource(Resource):
class RootModelResource(ModelResource): class RootModelResource(ModelResource):
"""A Resource which provides default operations for list and create.""" """A Resource which provides default operations for list and create."""
allowed_methods = ('GET', 'POST')
queryset = None queryset = None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.model.objects.all() queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filter(**kwargs) return queryset.filter(**kwargs)
put = delete = http_method_not_allowed
class QueryModelResource(ModelResource): class QueryModelResource(ModelResource):
"""Resource with default operations for list. """Resource with default operations for list.
@ -424,10 +424,8 @@ class QueryModelResource(ModelResource):
allowed_methods = ('GET',) allowed_methods = ('GET',)
queryset = None queryset = None
def get_form(self, data=None):
return None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
queryset = self.queryset if self.queryset else self.model.objects.all() queryset = self.queryset if self.queryset else self.model.objects.all()
return queryset.filer(**kwargs) return queryset.filer(**kwargs)
post = put = delete = http_method_not_allowed

View File

@ -4,7 +4,7 @@ from django.views.decorators.csrf import csrf_exempt
from djangorestframework.compat import View from djangorestframework.compat import View
from djangorestframework.response import Response, ErrorResponse from djangorestframework.response import Response, ErrorResponse
from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin from djangorestframework.mixins import RequestMixin, ResponseMixin, AuthMixin
from djangorestframework import emitters, parsers, authenticators, validators, status from djangorestframework import emitters, parsers, authenticators, permissions, validators, status
# TODO: Figure how out references and named urls need to work nicely # TODO: Figure how out references and named urls need to work nicely
@ -19,11 +19,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
"""Handles incoming requests and maps them to REST operations, """Handles incoming requests and maps them to REST operations,
performing authentication, input deserialization, input validation, output serialization.""" performing authentication, input deserialization, input validation, output serialization."""
# List of RESTful operations which may be performed on this resource. http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace', 'patch']
# These are going to get dropped at some point, the allowable methods will be defined simply by
# which methods are present on the request (in the same way as Django's generic View)
allowed_methods = ('GET',)
anon_allowed_methods = ()
# List of emitters the resource can serialize the response with, ordered by preference. # List of emitters the resource can serialize the response with, ordered by preference.
emitters = ( emitters.JSONEmitter, emitters = ( emitters.JSONEmitter,
@ -37,12 +33,15 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
parsers.FormParser, parsers.FormParser,
parsers.MultipartParser ) parsers.MultipartParser )
# List of validators to validate, cleanup and type-ify the request content # List of validators to validate, cleanup and normalize the request content
validators = ( validators.FormValidator, ) validators = ( validators.FormValidator, )
# List of all authenticating methods to attempt. # List of all authenticating methods to attempt.
authenticators = ( authenticators.UserLoggedInAuthenticator, authenticators = ( authenticators.UserLoggedInAuthenticator,
authenticators.BasicAuthenticator ) authenticators.BasicAuthenticator )
# List of all permissions required to access the resource
permissions = ( permissions.DeleteMePermission, )
# Optional form for input validation and presentation of HTML formatted responses. # Optional form for input validation and presentation of HTML formatted responses.
form = None form = None
@ -53,52 +52,14 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
name = None name = None
description = None description = None
# Map standard HTTP methods to function calls @property
callmap = { 'GET': 'get', 'POST': 'post', def allowed_methods(self):
'PUT': 'put', 'DELETE': 'delete' } return [method.upper() for method in self.http_method_names if hasattr(self, method)]
def get(self, request, *args, **kwargs): def http_method_not_allowed(self, request, *args, **kwargs):
"""Must be subclassed to be implemented.""" """Return an HTTP 405 error if an operation is called which does not have a handler method."""
self.not_implemented('GET') raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % self.method})
def post(self, request, *args, **kwargs):
"""Must be subclassed to be implemented."""
self.not_implemented('POST')
def put(self, request, *args, **kwargs):
"""Must be subclassed to be implemented."""
self.not_implemented('PUT')
def delete(self, request, *args, **kwargs):
"""Must be subclassed to be implemented."""
self.not_implemented('DELETE')
def not_implemented(self, operation):
"""Return an HTTP 500 server error if an operation is called which has been allowed by
allowed_methods, but which has not been implemented."""
raise ErrorResponse(status.HTTP_500_INTERNAL_SERVER_ERROR,
{'detail': '%s operation on this resource has not been implemented' % (operation, )})
def check_method_allowed(self, method, auth):
"""Ensure the request method is permitted for this resource, raising a ResourceException if it is not."""
if not method in self.callmap.keys():
raise ErrorResponse(status.HTTP_501_NOT_IMPLEMENTED,
{'detail': 'Unknown or unsupported method \'%s\'' % method})
if not method in self.allowed_methods:
raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % method})
if auth is None and not method in self.anon_allowed_methods:
raise ErrorResponse(status.HTTP_403_FORBIDDEN,
{'detail': 'You do not have permission to access this resource. ' +
'You may need to login or otherwise authenticate the request.'})
def cleanup_response(self, data): def cleanup_response(self, data):
@ -111,6 +72,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
the EmitterMixin and Emitter classes.""" the EmitterMixin and Emitter classes."""
return data return data
# Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt. # Session based authentication is explicitly CSRF validated, all other authentication is CSRF exempt.
@csrf_exempt @csrf_exempt
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
@ -125,57 +87,54 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
4. cleanup the response data 4. cleanup the response data
5. serialize response data into response content, using standard HTTP content negotiation 5. serialize response data into response content, using standard HTTP content negotiation
""" """
self.request = request
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
set_script_prefix(prefix)
try: try:
# Authenticate the request, and store any context so that the resource operations can self.request = request
# do more fine grained authentication if required. self.args = args
# self.kwargs = kwargs
# Typically the context will be a user, or None if this is an anonymous request,
# but it could potentially be more complex (eg the context of a request key which
# has been signed against a particular set of permissions)
auth_context = self.auth
# If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
# self.method, self.content_type, self.CONTENT appropriately.
self.perform_form_overloading()
# Ensure the requested operation is permitted on this resource
self.check_method_allowed(self.method, auth_context)
# Get the appropriate create/read/update/delete function
func = getattr(self, self.callmap.get(self.method, None))
# Either generate the response data, deserializing and validating any request data # Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here.
# TODO: This is going to change to: func(request, *args, **kwargs) prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
# That'll work out now that we have the lazily evaluated self.CONTENT property. set_script_prefix(prefix)
response_obj = func(request, *args, **kwargs)
try:
# Allow return value to be either Response, or an object, or None # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
if isinstance(response_obj, Response): # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
response = response_obj self.perform_form_overloading()
elif response_obj is not None:
response = Response(status.HTTP_200_OK, response_obj) # Authenticate and check request is has the relevant permissions
else: self.check_permissions()
response = Response(status.HTTP_204_NO_CONTENT)
# Get the appropriate handler method
# Pre-serialize filtering (eg filter complex objects into natively serializable types) if self.method.lower() in self.http_method_names:
response.cleaned_content = self.cleanup_response(response.raw_content) handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
else:
except ErrorResponse, exc: handler = self.http_method_not_allowed
response = exc.response
response_obj = handler(request, *args, **kwargs)
# Always add these headers.
# # Allow return value to be either Response, or an object, or None
# TODO - this isn't actually the correct way to set the vary header, if isinstance(response_obj, Response):
# also it's currently sub-obtimal for HTTP caching - need to sort that out. response = response_obj
response.headers['Allow'] = ', '.join(self.allowed_methods) elif response_obj is not None:
response.headers['Vary'] = 'Authenticate, Accept' response = Response(status.HTTP_200_OK, response_obj)
else:
return self.emit(response) response = Response(status.HTTP_204_NO_CONTENT)
# Pre-serialize filtering (eg filter complex objects into natively serializable types)
response.cleaned_content = self.cleanup_response(response.raw_content)
except ErrorResponse, exc:
response = exc.response
# Always add these headers.
#
# TODO - this isn't actually the correct way to set the vary header,
# also it's currently sub-obtimal for HTTP caching - need to sort that out.
response.headers['Allow'] = ', '.join(self.allowed_methods)
response.headers['Vary'] = 'Authenticate, Accept'
return self.emit(response)
except:
import traceback
traceback.print_exc()

View File

@ -18,10 +18,13 @@ class UserAgentMungingTest(TestCase):
http://www.gethifi.com/blog/browser-rest-http-accept-headers""" http://www.gethifi.com/blog/browser-rest-http-accept-headers"""
def setUp(self): def setUp(self):
class MockResource(Resource): class MockResource(Resource):
anon_allowed_methods = allowed_methods = ('GET',) permissions = ()
def get(self, request): def get(self, request):
return {'a':1, 'b':2, 'c':3} return {'a':1, 'b':2, 'c':3}
self.req = RequestFactory() self.req = RequestFactory()
self.MockResource = MockResource self.MockResource = MockResource
self.view = MockResource.as_view() self.view = MockResource.as_view()

View File

@ -13,8 +13,6 @@ except ImportError:
import simplejson as json import simplejson as json
class MockResource(Resource): class MockResource(Resource):
allowed_methods = ('POST',)
def post(self, request): def post(self, request):
return {'a':1, 'b':2, 'c':3} return {'a':1, 'b':2, 'c':3}

View File

@ -16,7 +16,7 @@ class UploadFilesTests(TestCase):
file = forms.FileField file = forms.FileField
class MockResource(Resource): class MockResource(Resource):
allowed_methods = anon_allowed_methods = ('POST',) permissions = ()
form = FileForm form = FileForm
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):

View File

@ -12,7 +12,7 @@ except ImportError:
class MockResource(Resource): class MockResource(Resource):
"""Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified""" """Mock resource which simply returns a URL, so that we can ensure that reversed URLs are fully qualified"""
anon_allowed_methods = ('GET',) permissions = ()
def get(self, request): def get(self, request):
return reverse('another') return reverse('another')
@ -28,5 +28,9 @@ class ReverseTests(TestCase):
urls = 'djangorestframework.tests.reverse' urls = 'djangorestframework.tests.reverse'
def test_reversed_urls_are_fully_qualified(self): def test_reversed_urls_are_fully_qualified(self):
response = self.client.get('/') try:
response = self.client.get('/')
except:
import traceback
traceback.print_exc()
self.assertEqual(json.loads(response.content), 'http://testserver/another') self.assertEqual(json.loads(response.content), 'http://testserver/another')