aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/api-guide/authentication.md4
-rw-r--r--docs/topics/release-notes.md1
-rw-r--r--rest_framework/serializers.py43
-rw-r--r--rest_framework/tests/authentication.py39
-rw-r--r--rest_framework/views.py13
5 files changed, 73 insertions, 27 deletions
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index ba7c0c58..6a259500 100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -10,7 +10,7 @@ Authentication is the mechanism of associating an incoming request with a set of
REST framework provides a number of authentication schemes out of the box, and also allows you to implement custom schemes.
-Authentication will run the first time either the `request.user` or `request.auth` properties are accessed, and determines how those properties are initialized.
+Authentication is always run at the very start of the view, before the permission and throttling checks occur, and before any other code is allowed to proceed.
The `request.user` property will typically be set to an instance of the `contrib.auth` package's `User` class.
@@ -259,7 +259,7 @@ In some circumstances instead of returning `None`, you may want to raise an `Aut
Typically the approach you should take is:
* If authentication is not attempted, return `None`. Any other authentication schemes also in use will still be checked.
-* If authentication is attempted but fails, raise a `AuthenticationFailed` exception. An error response will be returned immediately, without checking any other authentication schemes.
+* If authentication is attempted but fails, raise a `AuthenticationFailed` exception. An error response will be returned immediately, regardless of any permissions checks, and without checking any other authentication schemes.
You *may* also override the `.authenticate_header(self, request)` method. If implemented, it should return a string that will be used as the value of the `WWW-Authenticate` header in a `HTTP 401 Unauthorized` response.
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 06dc79a6..43499c9a 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`:
### Master
+* Request authentication is no longer lazily evaluated, instead authentication is always run, which results in more consistent, obvious behavior. Eg. Supplying bad auth credentials will now always return an error response, even if no permissions are set on the view.
* Bugfix for serializer data being uncacheable with pickle protocol 0.
* Bugfixes for model field validation edge-cases.
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 91af2af2..ba9e9e9c 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -418,6 +418,27 @@ class ModelSerializer(Serializer):
"""
_options_class = ModelSerializerOptions
+ field_mapping = {
+ models.AutoField: IntegerField,
+ models.FloatField: FloatField,
+ models.IntegerField: IntegerField,
+ models.PositiveIntegerField: IntegerField,
+ models.SmallIntegerField: IntegerField,
+ models.PositiveSmallIntegerField: IntegerField,
+ models.DateTimeField: DateTimeField,
+ models.DateField: DateField,
+ models.TimeField: TimeField,
+ models.EmailField: EmailField,
+ models.CharField: CharField,
+ models.URLField: URLField,
+ models.SlugField: SlugField,
+ models.TextField: CharField,
+ models.CommaSeparatedIntegerField: CharField,
+ models.BooleanField: BooleanField,
+ models.FileField: FileField,
+ models.ImageField: ImageField,
+ }
+
def get_default_fields(self):
"""
Return all the fields that should be serialized for the model.
@@ -515,28 +536,8 @@ class ModelSerializer(Serializer):
kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs)
- field_mapping = {
- models.AutoField: IntegerField,
- models.FloatField: FloatField,
- models.IntegerField: IntegerField,
- models.PositiveIntegerField: IntegerField,
- models.SmallIntegerField: IntegerField,
- models.PositiveSmallIntegerField: IntegerField,
- models.DateTimeField: DateTimeField,
- models.DateField: DateField,
- models.TimeField: TimeField,
- models.EmailField: EmailField,
- models.CharField: CharField,
- models.URLField: URLField,
- models.SlugField: SlugField,
- models.TextField: CharField,
- models.CommaSeparatedIntegerField: CharField,
- models.BooleanField: BooleanField,
- models.FileField: FileField,
- models.ImageField: ImageField,
- }
try:
- return field_mapping[model_field.__class__](**kwargs)
+ return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
return ModelField(model_field=model_field, **kwargs)
diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py
index 3ceab808..c2c23bcc 100644
--- a/rest_framework/tests/authentication.py
+++ b/rest_framework/tests/authentication.py
@@ -4,13 +4,21 @@ from django.contrib.auth.models import User
from django.http import HttpResponse
from django.test import Client, TestCase
from rest_framework import HTTP_HEADER_ENCODING
+from rest_framework import exceptions
from rest_framework import permissions
from rest_framework import status
from rest_framework.authtoken.models import Token
-from rest_framework.authentication import TokenAuthentication, BasicAuthentication, SessionAuthentication, OAuth2Authentication
+from rest_framework.authentication import (
+ BaseAuthentication,
+ TokenAuthentication,
+ BasicAuthentication,
+ SessionAuthentication,
+ OAuth2Authentication
+)
from rest_framework.compat import patterns, url, include
from rest_framework.compat import oauth2
from rest_framework.compat import oauth2_provider
+from rest_framework.tests.utils import RequestFactory
from rest_framework.views import APIView
import json
import base64
@@ -18,17 +26,21 @@ import datetime
import unittest
+factory = RequestFactory()
+
+
class MockView(APIView):
permission_classes = (permissions.IsAuthenticated,)
+ def get(self, request):
+ return HttpResponse({'a': 1, 'b': 2, 'c': 3})
+
def post(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
def put(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
- def get(self, request):
- return HttpResponse({'a': 1, 'b': 2, 'c': 3})
urlpatterns = patterns('',
(r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),
@@ -199,6 +211,27 @@ class TokenAuthTests(TestCase):
self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key)
+class IncorrectCredentialsTests(TestCase):
+ def test_incorrect_credentials(self):
+ """
+ If a request contains bad authentication credentials, then
+ authentication should run and error, even if no permissions
+ are set on the view.
+ """
+ class IncorrectCredentialsAuth(BaseAuthentication):
+ def authenticate(self, request):
+ raise exceptions.AuthenticationFailed('Bad credentials')
+
+ request = factory.get('/')
+ view = MockView.as_view(
+ authentication_classes=(IncorrectCredentialsAuth,),
+ permission_classes=()
+ )
+ response = view(request)
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+ self.assertEqual(response.data, {'detail': 'Bad credentials'})
+
+
class OAuth2Tests(TestCase):
"""OAuth 2.0 authentication"""
urls = 'rest_framework.tests.authentication'
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 69377bc0..81cbdcbb 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -8,7 +8,7 @@ from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
-from rest_framework.compat import View, apply_markdown, smart_text
+from rest_framework.compat import View, apply_markdown
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.settings import api_settings
@@ -257,6 +257,16 @@ class APIView(View):
return (renderers[0], renderers[0].media_type)
raise
+ def perform_authentication(self, request):
+ """
+ Perform authentication on the incoming request.
+
+ Note that if you override this and simply 'pass', then authentication
+ will instead be performed lazily, the first time either
+ `request.user` or `request.auth` is accessed.
+ """
+ request.user
+
def check_permissions(self, request):
"""
Check if the request should be permitted.
@@ -305,6 +315,7 @@ class APIView(View):
self.format_kwarg = self.get_format_suffix(**kwargs)
# Ensure that the incoming request is permitted
+ self.perform_authentication(request)
self.check_permissions(request)
self.check_throttles(request)