diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py index e326c15ae..00a61e3d8 100644 --- a/djangorestframework/authentication.py +++ b/djangorestframework/authentication.py @@ -88,13 +88,14 @@ class UserLoggedInAuthentication(BaseAuthentication): Otherwise returns :const:`None`. """ request.DATA # Make sure our generic parsing runs first + user = getattr(request.request, 'user', None) - if getattr(request, 'user', None) and request.user.is_active: + if user and user.is_active: # Enforce CSRF validation for session based authentication. resp = CsrfViewMiddleware().process_view(request, None, (), {}) if resp is None: # csrf passed - return request.user + return user return None diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py index 51c859cd2..398ed28ad 100644 --- a/djangorestframework/mixins.py +++ b/djangorestframework/mixins.py @@ -3,7 +3,6 @@ The :mod:`mixins` module provides a set of reusable `mixin` classes that can be added to a `View`. """ -from django.contrib.auth.models import AnonymousUser from django.core.paginator import Paginator from django.db.models.fields.related import ForeignKey from urlobject import URLObject @@ -19,7 +18,7 @@ __all__ = ( # Base behavior mixins 'RequestMixin', 'ResponseMixin', - 'AuthMixin', + 'PermissionsMixin', 'ResourceMixin', # Reverse URL lookup behavior 'InstanceMixin', @@ -45,6 +44,13 @@ class RequestMixin(object): Should be a tuple/list of classes as described in the :mod:`parsers` module. """ + authentication_classes = () + """ + The set of authentication types that this view can handle. + + Should be a tuple/list of classes as described in the :mod:`authentication` module. + """ + request_class = Request """ The class to use as a wrapper for the original request object. @@ -56,6 +62,12 @@ class RequestMixin(object): """ return [p(self) for p in self.parser_classes] + def get_authentications(self): + """ + Instantiates and returns the list of authentications the request will use. + """ + return [a(self) for a in self.authentication_classes] + def create_request(self, request): """ Creates and returns an instance of :class:`request.Request`. @@ -63,7 +75,9 @@ class RequestMixin(object): parsers set on the view. """ parsers = self.get_parsers() - return self.request_class(request, parsers=parsers) + authentications = self.get_authentications() + return self.request_class(request, parsers=parsers, + authentications=authentications) @property def _parsed_media_types(self): @@ -134,57 +148,32 @@ class ResponseMixin(object): return [renderer.format for renderer in self.get_renderers()] -########## Auth Mixin ########## +########## Permissions Mixin ########## -class AuthMixin(object): +class PermissionsMixin(object): """ - Simple :class:`mixin` class to add authentication and permission checking to a :class:`View` class. + Simple :class:`mixin` class to add permission checking to a :class:`View` class. """ - authentication = () - """ - The set of authentication types that this view can handle. - - Should be a tuple/list of classes as described in the :mod:`authentication` module. - """ - - permissions = () + permissions_classes = () """ The set of permissions that will be enforced on this view. Should be a tuple/list of classes as described in the :mod:`permissions` module. """ - @property - def user(self): + def get_permissions(self): """ - Returns the :obj:`user` for the current request, as determined by the set of - :class:`authentication` classes applied to the :class:`View`. + Instantiates and returns the list of permissions that this view requires. """ - if not hasattr(self, '_user'): - self._user = self._authenticate() - return self._user - - def _authenticate(self): - """ - Attempt to authenticate the request using each authentication class in turn. - Returns a ``User`` object, which may be ``AnonymousUser``. - """ - for authentication_cls in self.authentication: - authentication = authentication_cls(self) - user = authentication.authenticate(self.request) - if user: - return user - return AnonymousUser() + return [p(self) for p in self.permissions_classes] # TODO: wrap this behavior around dispatch() - def _check_permissions(self): + def check_permissions(self, user): """ Check user permissions and either raise an ``ImmediateResponse`` or return. """ - user = self.user - for permission_cls in self.permissions: - permission = permission_cls(self) + for permission in self.get_permissions(): permission.check_permission(user) diff --git a/djangorestframework/request.py b/djangorestframework/request.py index e8f2b8c3c..964231ba4 100644 --- a/djangorestframework/request.py +++ b/djangorestframework/request.py @@ -8,14 +8,15 @@ The wrapped request then offers a richer API, in particular : - full support of PUT method, including support for file uploads - form overloading of HTTP method, content type and content """ +from StringIO import StringIO + +from django.contrib.auth.models import AnonymousUser from djangorestframework.response import ImmediateResponse from djangorestframework import status from djangorestframework.utils.mediatypes import is_form_media_type from djangorestframework.utils import as_tuple -from StringIO import StringIO - __all__ = ('Request',) @@ -27,6 +28,7 @@ class Request(object): Kwargs: - request(HttpRequest). The original request instance. - parsers(list/tuple). The parsers to use for parsing the request content. + - authentications(list/tuple). The authentications used to try authenticating the request's user. """ _USE_FORM_OVERLOADING = True @@ -34,10 +36,12 @@ class Request(object): _CONTENTTYPE_PARAM = '_content_type' _CONTENT_PARAM = '_content' - def __init__(self, request=None, parsers=None): + def __init__(self, request, parsers=None, authentications=None): self.request = request if parsers is not None: self.parsers = parsers + if authentications is not None: + self.authentications = authentications @property def method(self): @@ -87,6 +91,16 @@ class Request(object): self._load_data_and_files() return self._files + @property + def user(self): + """ + Returns the :obj:`user` for the current request, authenticated + with the set of :class:`authentication` instances applied to the :class:`Request`. + """ + if not hasattr(self, '_user'): + self._user = self._authenticate() + return self._user + def _load_data_and_files(self): """ Parses the request content into self.DATA and self.FILES. @@ -192,6 +206,27 @@ class Request(object): parsers = property(_get_parsers, _set_parsers) + def _authenticate(self): + """ + Attempt to authenticate the request using each authentication instance in turn. + Returns a ``User`` object, which may be ``AnonymousUser``. + """ + for authentication in self.authentications: + user = authentication.authenticate(self) + if user: + return user + return AnonymousUser() + + def _get_authentications(self): + if hasattr(self, '_authentications'): + return self._authentications + return () + + def _set_authentications(self, value): + self._authentications = value + + authentications = property(_get_authentications, _set_authentications) + def __getattr__(self, name): """ When an attribute is not present on the calling instance, try to get it diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py index 25410b040..5debc79a6 100644 --- a/djangorestframework/tests/authentication.py +++ b/djangorestframework/tests/authentication.py @@ -12,7 +12,7 @@ import base64 class MockView(View): - permissions = (permissions.IsAuthenticated,) + permissions_classes = (permissions.IsAuthenticated,) def post(self, request): return HttpResponse({'a': 1, 'b': 2, 'c': 3}) diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py index 393c3ec89..73a4c02b1 100644 --- a/djangorestframework/tests/throttling.py +++ b/djangorestframework/tests/throttling.py @@ -13,17 +13,17 @@ from djangorestframework.resources import FormResource from djangorestframework.response import Response class MockView(View): - permissions = ( PerUserThrottling, ) + permissions_classes = ( PerUserThrottling, ) throttle = '3/sec' def get(self, request): return Response('foo') class MockView_PerViewThrottling(MockView): - permissions = ( PerViewThrottling, ) + permissions_classes = ( PerViewThrottling, ) class MockView_PerResourceThrottling(MockView): - permissions = ( PerResourceThrottling, ) + permissions_classes = ( PerResourceThrottling, ) resource = FormResource class MockView_MinuteThrottling(MockView): @@ -54,7 +54,7 @@ class ThrottlingTests(TestCase): """ Explicitly set the timer, overriding time.time() """ - view.permissions[0].timer = lambda self: value + view.permissions_classes[0].timer = lambda self: value def test_request_throttling_expires(self): """ diff --git a/djangorestframework/views.py b/djangorestframework/views.py index 6bfc41927..509d1471f 100644 --- a/djangorestframework/views.py +++ b/djangorestframework/views.py @@ -69,7 +69,7 @@ _resource_classes = ( ) -class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): +class View(ResourceMixin, RequestMixin, ResponseMixin, PermissionsMixin, DjangoView): """ Handles incoming requests and maps them to REST operations. Performs request deserialization, response serialization, authentication and input validation. @@ -91,13 +91,13 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): List of parser classes the resource can parse the request with. """ - authentication = (authentication.UserLoggedInAuthentication, + authentication_classes = (authentication.UserLoggedInAuthentication, authentication.BasicAuthentication) """ List of all authenticating methods to attempt. """ - permissions = (permissions.FullAnonAccess,) + permissions_classes = (permissions.FullAnonAccess,) """ List of all permissions that must be checked. """ @@ -206,15 +206,15 @@ class View(ResourceMixin, RequestMixin, ResponseMixin, AuthMixin, DjangoView): # all other authentication is CSRF exempt. @csrf_exempt def dispatch(self, request, *args, **kwargs): - self.request = self.create_request(request) + self.request = request = self.create_request(request) self.args = args self.kwargs = kwargs try: self.initial(request, *args, **kwargs) - - # Authenticate and check request has the relevant permissions - self._check_permissions() + + # check that user has the relevant permissions + self.check_permissions(request.user) # Get the appropriate handler method if request.method.lower() in self.http_method_names: