Commit 4692374e by Tom Christie

Generic permissions added, allowed_methods and anon_allowed_methods now defunct,…

Generic permissions added, allowed_methods and anon_allowed_methods now defunct, dispatch now mirrors View.dispatch more nicely
parent cb4b4f6b
...@@ -13,10 +13,10 @@ import base64 ...@@ -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): ...@@ -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
......
...@@ -396,9 +396,9 @@ class ResponseMixin(object): ...@@ -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): ...@@ -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): ...@@ -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
...@@ -410,13 +410,13 @@ class ModelResource(Resource): ...@@ -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): ...@@ -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
\ No newline at end of file
...@@ -4,7 +4,7 @@ from django.views.decorators.csrf import csrf_exempt ...@@ -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): ...@@ -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,13 +33,16 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View): ...@@ -37,13 +33,16 @@ 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): ...@@ -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):
"""Must be subclassed to be implemented."""
self.not_implemented('GET')
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 http_method_not_allowed(self, request, *args, **kwargs):
def check_method_allowed(self, method, auth): """Return an HTTP 405 error if an operation is called which does not have a handler method."""
"""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, raise ErrorResponse(status.HTTP_405_METHOD_NOT_ALLOWED,
{'detail': 'Method \'%s\' not allowed on this resource.' % method}) {'detail': 'Method \'%s\' not allowed on this resource.' % self.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): ...@@ -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,36 +87,30 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View): ...@@ -125,36 +87,30 @@ 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
""" """
try:
self.request = request self.request = request
self.args = args
self.kwargs = kwargs
# Calls to 'reverse' will not be fully qualified unless we set the scheme/host/port here. # 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()) prefix = '%s://%s' % (request.is_secure() and 'https' or 'http', request.get_host())
set_script_prefix(prefix) set_script_prefix(prefix)
try: try:
# Authenticate the request, and store any context so that the resource operations can
# do more fine grained authentication if required.
#
# 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 # If using a form POST with '_method'/'_content'/'_content_type' overrides, then alter
# self.method, self.content_type, self.CONTENT appropriately. # self.method, self.content_type, self.RAW_CONTENT & self.CONTENT appropriately.
self.perform_form_overloading() self.perform_form_overloading()
# Ensure the requested operation is permitted on this resource # Authenticate and check request is has the relevant permissions
self.check_method_allowed(self.method, auth_context) self.check_permissions()
# Get the appropriate create/read/update/delete function # Get the appropriate handler method
func = getattr(self, self.callmap.get(self.method, None)) if self.method.lower() in self.http_method_names:
handler = getattr(self, self.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
# Either generate the response data, deserializing and validating any request data response_obj = handler(request, *args, **kwargs)
# TODO: This is going to change to: func(request, *args, **kwargs)
# That'll work out now that we have the lazily evaluated self.CONTENT property.
response_obj = func(request, *args, **kwargs)
# Allow return value to be either Response, or an object, or None # Allow return value to be either Response, or an object, or None
if isinstance(response_obj, Response): if isinstance(response_obj, Response):
...@@ -178,4 +134,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View): ...@@ -178,4 +134,7 @@ class Resource(RequestMixin, ResponseMixin, AuthMixin, View):
response.headers['Vary'] = 'Authenticate, Accept' response.headers['Vary'] = 'Authenticate, Accept'
return self.emit(response) return self.emit(response)
except:
import traceback
traceback.print_exc()
...@@ -18,10 +18,13 @@ class UserAgentMungingTest(TestCase): ...@@ -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()
......
...@@ -13,8 +13,6 @@ except ImportError: ...@@ -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}
......
...@@ -16,7 +16,7 @@ class UploadFilesTests(TestCase): ...@@ -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):
......
...@@ -12,7 +12,7 @@ except ImportError: ...@@ -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): ...@@ -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):
try:
response = self.client.get('/') 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')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment