authentication refactor : request.user + tests pass

This commit is contained in:
Sébastien Piquemal 2012-02-23 22:47:45 +02:00
parent 9da1ae81dc
commit afd490238a
6 changed files with 79 additions and 54 deletions

View File

@ -88,13 +88,14 @@ class UserLoggedInAuthentication(BaseAuthentication):
Otherwise returns :const:`None`. Otherwise returns :const:`None`.
""" """
request.DATA # Make sure our generic parsing runs first 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. # Enforce CSRF validation for session based authentication.
resp = CsrfViewMiddleware().process_view(request, None, (), {}) resp = CsrfViewMiddleware().process_view(request, None, (), {})
if resp is None: # csrf passed if resp is None: # csrf passed
return request.user return user
return None return None

View File

@ -3,7 +3,6 @@ The :mod:`mixins` module provides a set of reusable `mixin`
classes that can be added to a `View`. classes that can be added to a `View`.
""" """
from django.contrib.auth.models import AnonymousUser
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models.fields.related import ForeignKey from django.db.models.fields.related import ForeignKey
from urlobject import URLObject from urlobject import URLObject
@ -19,7 +18,7 @@ __all__ = (
# Base behavior mixins # Base behavior mixins
'RequestMixin', 'RequestMixin',
'ResponseMixin', 'ResponseMixin',
'AuthMixin', 'PermissionsMixin',
'ResourceMixin', 'ResourceMixin',
# Reverse URL lookup behavior # Reverse URL lookup behavior
'InstanceMixin', 'InstanceMixin',
@ -45,6 +44,13 @@ class RequestMixin(object):
Should be a tuple/list of classes as described in the :mod:`parsers` module. 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 request_class = Request
""" """
The class to use as a wrapper for the original request object. 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] 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): def create_request(self, request):
""" """
Creates and returns an instance of :class:`request.Request`. Creates and returns an instance of :class:`request.Request`.
@ -63,7 +75,9 @@ class RequestMixin(object):
parsers set on the view. parsers set on the view.
""" """
parsers = self.get_parsers() 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 @property
def _parsed_media_types(self): def _parsed_media_types(self):
@ -134,57 +148,32 @@ class ResponseMixin(object):
return [renderer.format for renderer in self.get_renderers()] 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 = () permissions_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.
"""
permissions = ()
""" """
The set of permissions that will be enforced on this view. 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. Should be a tuple/list of classes as described in the :mod:`permissions` module.
""" """
@property def get_permissions(self):
def user(self):
""" """
Returns the :obj:`user` for the current request, as determined by the set of Instantiates and returns the list of permissions that this view requires.
:class:`authentication` classes applied to the :class:`View`.
""" """
if not hasattr(self, '_user'): return [p(self) for p in self.permissions_classes]
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()
# TODO: wrap this behavior around dispatch() # 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. Check user permissions and either raise an ``ImmediateResponse`` or return.
""" """
user = self.user for permission in self.get_permissions():
for permission_cls in self.permissions:
permission = permission_cls(self)
permission.check_permission(user) permission.check_permission(user)

View File

@ -8,14 +8,15 @@ The wrapped request then offers a richer API, in particular :
- full support of PUT method, including support for file uploads - full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content - 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.response import ImmediateResponse
from djangorestframework import status from djangorestframework import status
from djangorestframework.utils.mediatypes import is_form_media_type from djangorestframework.utils.mediatypes import is_form_media_type
from djangorestframework.utils import as_tuple from djangorestframework.utils import as_tuple
from StringIO import StringIO
__all__ = ('Request',) __all__ = ('Request',)
@ -27,6 +28,7 @@ class Request(object):
Kwargs: Kwargs:
- request(HttpRequest). The original request instance. - request(HttpRequest). The original request instance.
- parsers(list/tuple). The parsers to use for parsing the request content. - 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 _USE_FORM_OVERLOADING = True
@ -34,10 +36,12 @@ class Request(object):
_CONTENTTYPE_PARAM = '_content_type' _CONTENTTYPE_PARAM = '_content_type'
_CONTENT_PARAM = '_content' _CONTENT_PARAM = '_content'
def __init__(self, request=None, parsers=None): def __init__(self, request, parsers=None, authentications=None):
self.request = request self.request = request
if parsers is not None: if parsers is not None:
self.parsers = parsers self.parsers = parsers
if authentications is not None:
self.authentications = authentications
@property @property
def method(self): def method(self):
@ -87,6 +91,16 @@ class Request(object):
self._load_data_and_files() self._load_data_and_files()
return self._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): def _load_data_and_files(self):
""" """
Parses the request content into self.DATA and self.FILES. Parses the request content into self.DATA and self.FILES.
@ -192,6 +206,27 @@ class Request(object):
parsers = property(_get_parsers, _set_parsers) 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): def __getattr__(self, name):
""" """
When an attribute is not present on the calling instance, try to get it When an attribute is not present on the calling instance, try to get it

View File

@ -12,7 +12,7 @@ import base64
class MockView(View): class MockView(View):
permissions = (permissions.IsAuthenticated,) permissions_classes = (permissions.IsAuthenticated,)
def post(self, request): def post(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3}) return HttpResponse({'a': 1, 'b': 2, 'c': 3})

View File

@ -13,17 +13,17 @@ from djangorestframework.resources import FormResource
from djangorestframework.response import Response from djangorestframework.response import Response
class MockView(View): class MockView(View):
permissions = ( PerUserThrottling, ) permissions_classes = ( PerUserThrottling, )
throttle = '3/sec' throttle = '3/sec'
def get(self, request): def get(self, request):
return Response('foo') return Response('foo')
class MockView_PerViewThrottling(MockView): class MockView_PerViewThrottling(MockView):
permissions = ( PerViewThrottling, ) permissions_classes = ( PerViewThrottling, )
class MockView_PerResourceThrottling(MockView): class MockView_PerResourceThrottling(MockView):
permissions = ( PerResourceThrottling, ) permissions_classes = ( PerResourceThrottling, )
resource = FormResource resource = FormResource
class MockView_MinuteThrottling(MockView): class MockView_MinuteThrottling(MockView):
@ -54,7 +54,7 @@ class ThrottlingTests(TestCase):
""" """
Explicitly set the timer, overriding time.time() 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): def test_request_throttling_expires(self):
""" """

View File

@ -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. Handles incoming requests and maps them to REST operations.
Performs request deserialization, response serialization, authentication and input validation. 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. List of parser classes the resource can parse the request with.
""" """
authentication = (authentication.UserLoggedInAuthentication, authentication_classes = (authentication.UserLoggedInAuthentication,
authentication.BasicAuthentication) authentication.BasicAuthentication)
""" """
List of all authenticating methods to attempt. List of all authenticating methods to attempt.
""" """
permissions = (permissions.FullAnonAccess,) permissions_classes = (permissions.FullAnonAccess,)
""" """
List of all permissions that must be checked. 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. # all other authentication is CSRF exempt.
@csrf_exempt @csrf_exempt
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.request = self.create_request(request) self.request = request = self.create_request(request)
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
try: try:
self.initial(request, *args, **kwargs) self.initial(request, *args, **kwargs)
# Authenticate and check request has the relevant permissions # check that user has the relevant permissions
self._check_permissions() self.check_permissions(request.user)
# Get the appropriate handler method # Get the appropriate handler method
if request.method.lower() in self.http_method_names: if request.method.lower() in self.http_method_names: