mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 08:14:16 +03:00
Fix AttributeError hiding on request authenticators (#5600)
* Update assertion style in user logout test * Apply middlewares to django request object * Fix test for request auth hiding AttributeErrors * Re-raise/wrap auth attribute errors * Fix test for py2k * Add docs for WrappedAttributeError
This commit is contained in:
parent
a91361dd2f
commit
c63e35cb09
|
@ -291,6 +291,12 @@ You *may* also override the `.authenticate_header(self, request)` method. If im
|
||||||
|
|
||||||
If the `.authenticate_header()` method is not overridden, the authentication scheme will return `HTTP 403 Forbidden` responses when an unauthenticated request is denied access.
|
If the `.authenticate_header()` method is not overridden, the authentication scheme will return `HTTP 403 Forbidden` responses when an unauthenticated request is denied access.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note:** When your custom authenticator is invoked by the request object's `.user` or `.auth` properties, you may see an `AttributeError` re-raised as a `WrappedAttributeError`. This is necessary to prevent the original exception from being suppressed by the outer property access. Python will not recognize that the `AttributeError` orginates from your custom authenticator and will instead assume that the request object does not have a `.user` or `.auth` property. These errors should be fixed or otherwise handled by your authenticator.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.
|
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.
|
||||||
|
|
|
@ -90,6 +90,10 @@ You won't typically need to access this property.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**Note:** You may see a `WrappedAttributeError` raised when calling the `.user` or `.auth` properties. These errors originate from an authenticator as a standard `AttributeError`, however it's necessary that they be re-raised as a different exception type in order to prevent them from being suppressed by the outer property access. Python will not recognize that the `AttributeError` orginates from the authenticator and will instaed assume that the request object does not have a `.user` or `.auth` property. The authenticator will need to be fixed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Browser enhancements
|
# Browser enhancements
|
||||||
|
|
||||||
REST framework supports a few browser enhancements such as browser-based `PUT`, `PATCH` and `DELETE` forms.
|
REST framework supports a few browser enhancements such as browser-based `PUT`, `PATCH` and `DELETE` forms.
|
||||||
|
|
|
@ -10,6 +10,9 @@ The wrapped request then offers a richer API, in particular :
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpRequest, QueryDict
|
from django.http import HttpRequest, QueryDict
|
||||||
from django.http.multipartparser import parse_header
|
from django.http.multipartparser import parse_header
|
||||||
|
@ -59,6 +62,24 @@ class override_method(object):
|
||||||
self.view.action = self.action
|
self.view.action = self.action
|
||||||
|
|
||||||
|
|
||||||
|
class WrappedAttributeError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def wrap_attributeerrors():
|
||||||
|
"""
|
||||||
|
Used to re-raise AttributeErrors caught during authentication, preventing
|
||||||
|
these errors from otherwise being handled by the attribute access protocol.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except AttributeError:
|
||||||
|
info = sys.exc_info()
|
||||||
|
exc = WrappedAttributeError(str(info[1]))
|
||||||
|
six.reraise(type(exc), exc, info[2])
|
||||||
|
|
||||||
|
|
||||||
class Empty(object):
|
class Empty(object):
|
||||||
"""
|
"""
|
||||||
Placeholder for unset attributes.
|
Placeholder for unset attributes.
|
||||||
|
@ -197,7 +218,8 @@ class Request(object):
|
||||||
by the authentication classes provided to the request.
|
by the authentication classes provided to the request.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_user'):
|
if not hasattr(self, '_user'):
|
||||||
self._authenticate()
|
with wrap_attributeerrors():
|
||||||
|
self._authenticate()
|
||||||
return self._user
|
return self._user
|
||||||
|
|
||||||
@user.setter
|
@user.setter
|
||||||
|
@ -220,7 +242,8 @@ class Request(object):
|
||||||
request, such as an authentication token.
|
request, such as an authentication token.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_auth'):
|
if not hasattr(self, '_auth'):
|
||||||
self._authenticate()
|
with wrap_attributeerrors():
|
||||||
|
self._authenticate()
|
||||||
return self._auth
|
return self._auth
|
||||||
|
|
||||||
@auth.setter
|
@auth.setter
|
||||||
|
@ -239,7 +262,8 @@ class Request(object):
|
||||||
to authenticate the request, or `None`.
|
to authenticate the request, or `None`.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_authenticator'):
|
if not hasattr(self, '_authenticator'):
|
||||||
self._authenticate()
|
with wrap_attributeerrors():
|
||||||
|
self._authenticate()
|
||||||
return self._authenticator
|
return self._authenticator
|
||||||
|
|
||||||
def _load_data_and_files(self):
|
def _load_data_and_files(self):
|
||||||
|
@ -322,7 +346,7 @@ class Request(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parsed = parser.parse(stream, media_type, self.parser_context)
|
parsed = parser.parse(stream, media_type, self.parser_context)
|
||||||
except:
|
except Exception:
|
||||||
# If we get an exception during parsing, fill in empty data and
|
# If we get an exception during parsing, fill in empty data and
|
||||||
# re-raise. Ensures we don't simply repeat the error when
|
# re-raise. Ensures we don't simply repeat the error when
|
||||||
# attempting to render the browsable renderer response, or when
|
# attempting to render the browsable renderer response, or when
|
||||||
|
|
|
@ -6,8 +6,10 @@ from __future__ import unicode_literals
|
||||||
import os.path
|
import os.path
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
|
from django.contrib.auth.middleware import AuthenticationMiddleware
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
@ -17,7 +19,7 @@ from django.utils import six
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.authentication import SessionAuthentication
|
from rest_framework.authentication import SessionAuthentication
|
||||||
from rest_framework.parsers import BaseParser, FormParser, MultiPartParser
|
from rest_framework.parsers import BaseParser, FormParser, MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request, WrappedAttributeError
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.test import APIClient, APIRequestFactory
|
from rest_framework.test import APIClient, APIRequestFactory
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
@ -197,7 +199,8 @@ class TestUserSetter(TestCase):
|
||||||
# available to login and logout functions
|
# available to login and logout functions
|
||||||
self.wrapped_request = factory.get('/')
|
self.wrapped_request = factory.get('/')
|
||||||
self.request = Request(self.wrapped_request)
|
self.request = Request(self.wrapped_request)
|
||||||
SessionMiddleware().process_request(self.request)
|
SessionMiddleware().process_request(self.wrapped_request)
|
||||||
|
AuthenticationMiddleware().process_request(self.wrapped_request)
|
||||||
|
|
||||||
User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow')
|
User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow')
|
||||||
self.user = authenticate(username='ringo', password='yellow')
|
self.user = authenticate(username='ringo', password='yellow')
|
||||||
|
@ -212,9 +215,9 @@ class TestUserSetter(TestCase):
|
||||||
|
|
||||||
def test_user_can_logout(self):
|
def test_user_can_logout(self):
|
||||||
self.request.user = self.user
|
self.request.user = self.user
|
||||||
self.assertFalse(self.request.user.is_anonymous)
|
assert not self.request.user.is_anonymous
|
||||||
logout(self.request)
|
logout(self.request)
|
||||||
self.assertTrue(self.request.user.is_anonymous)
|
assert self.request.user.is_anonymous
|
||||||
|
|
||||||
def test_logged_in_user_is_set_on_wrapped_request(self):
|
def test_logged_in_user_is_set_on_wrapped_request(self):
|
||||||
login(self.request, self.user)
|
login(self.request, self.user)
|
||||||
|
@ -227,22 +230,27 @@ class TestUserSetter(TestCase):
|
||||||
"""
|
"""
|
||||||
class AuthRaisesAttributeError(object):
|
class AuthRaisesAttributeError(object):
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
import rest_framework
|
self.MISSPELLED_NAME_THAT_DOESNT_EXIST
|
||||||
rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST
|
|
||||||
|
|
||||||
self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),))
|
request = Request(self.wrapped_request, authenticators=(AuthRaisesAttributeError(),))
|
||||||
SessionMiddleware().process_request(self.request)
|
|
||||||
|
|
||||||
login(self.request, self.user)
|
# The middleware processes the underlying Django request, sets anonymous user
|
||||||
try:
|
assert self.wrapped_request.user.is_anonymous
|
||||||
self.request.user
|
|
||||||
except AttributeError as error:
|
# The DRF request object does not have a user and should run authenticators
|
||||||
assert str(error) in (
|
expected = r"no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'"
|
||||||
"'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'", # Python < 3.5
|
with pytest.raises(WrappedAttributeError, match=expected):
|
||||||
"module 'rest_framework' has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'", # Python >= 3.5
|
request.user
|
||||||
)
|
|
||||||
else:
|
# python 2 hasattr fails for *any* exception, not just AttributeError
|
||||||
assert False, 'AttributeError not raised'
|
if six.PY2:
|
||||||
|
return
|
||||||
|
|
||||||
|
with pytest.raises(WrappedAttributeError, match=expected):
|
||||||
|
hasattr(request, 'user')
|
||||||
|
|
||||||
|
with pytest.raises(WrappedAttributeError, match=expected):
|
||||||
|
login(request, self.user)
|
||||||
|
|
||||||
|
|
||||||
class TestAuthSetter(TestCase):
|
class TestAuthSetter(TestCase):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user