Skip to content

Commit c63e35c

Browse files
Ryan P Kilbycarltongibson
authored andcommitted
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
1 parent a91361d commit c63e35c

File tree

4 files changed

+64
-22
lines changed

4 files changed

+64
-22
lines changed

docs/api-guide/authentication.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ You *may* also override the `.authenticate_header(self, request)` method. If im
291291

292292
If the `.authenticate_header()` method is not overridden, the authentication scheme will return `HTTP 403 Forbidden` responses when an unauthenticated request is denied access.
293293

294+
---
295+
296+
**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.
297+
298+
---
299+
294300
## Example
295301

296302
The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.

docs/api-guide/requests.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ You won't typically need to access this property.
9090

9191
---
9292

93+
**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.
94+
95+
---
96+
9397
# Browser enhancements
9498

9599
REST framework supports a few browser enhancements such as browser-based `PUT`, `PATCH` and `DELETE` forms.

rest_framework/request.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
"""
1111
from __future__ import unicode_literals
1212

13+
import sys
14+
from contextlib import contextmanager
15+
1316
from django.conf import settings
1417
from django.http import HttpRequest, QueryDict
1518
from django.http.multipartparser import parse_header
@@ -59,6 +62,24 @@ def __exit__(self, *args, **kwarg):
5962
self.view.action = self.action
6063

6164

65+
class WrappedAttributeError(Exception):
66+
pass
67+
68+
69+
@contextmanager
70+
def wrap_attributeerrors():
71+
"""
72+
Used to re-raise AttributeErrors caught during authentication, preventing
73+
these errors from otherwise being handled by the attribute access protocol.
74+
"""
75+
try:
76+
yield
77+
except AttributeError:
78+
info = sys.exc_info()
79+
exc = WrappedAttributeError(str(info[1]))
80+
six.reraise(type(exc), exc, info[2])
81+
82+
6283
class Empty(object):
6384
"""
6485
Placeholder for unset attributes.
@@ -197,7 +218,8 @@ def user(self):
197218
by the authentication classes provided to the request.
198219
"""
199220
if not hasattr(self, '_user'):
200-
self._authenticate()
221+
with wrap_attributeerrors():
222+
self._authenticate()
201223
return self._user
202224

203225
@user.setter
@@ -220,7 +242,8 @@ def auth(self):
220242
request, such as an authentication token.
221243
"""
222244
if not hasattr(self, '_auth'):
223-
self._authenticate()
245+
with wrap_attributeerrors():
246+
self._authenticate()
224247
return self._auth
225248

226249
@auth.setter
@@ -239,7 +262,8 @@ def successful_authenticator(self):
239262
to authenticate the request, or `None`.
240263
"""
241264
if not hasattr(self, '_authenticator'):
242-
self._authenticate()
265+
with wrap_attributeerrors():
266+
self._authenticate()
243267
return self._authenticator
244268

245269
def _load_data_and_files(self):
@@ -322,7 +346,7 @@ def _parse(self):
322346

323347
try:
324348
parsed = parser.parse(stream, media_type, self.parser_context)
325-
except:
349+
except Exception:
326350
# If we get an exception during parsing, fill in empty data and
327351
# re-raise. Ensures we don't simply repeat the error when
328352
# attempting to render the browsable renderer response, or when

tests/test_request.py

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import os.path
77
import tempfile
88

9+
import pytest
910
from django.conf.urls import url
1011
from django.contrib.auth import authenticate, login, logout
12+
from django.contrib.auth.middleware import AuthenticationMiddleware
1113
from django.contrib.auth.models import User
1214
from django.contrib.sessions.middleware import SessionMiddleware
1315
from django.core.files.uploadedfile import SimpleUploadedFile
@@ -17,7 +19,7 @@
1719
from rest_framework import status
1820
from rest_framework.authentication import SessionAuthentication
1921
from rest_framework.parsers import BaseParser, FormParser, MultiPartParser
20-
from rest_framework.request import Request
22+
from rest_framework.request import Request, WrappedAttributeError
2123
from rest_framework.response import Response
2224
from rest_framework.test import APIClient, APIRequestFactory
2325
from rest_framework.views import APIView
@@ -197,7 +199,8 @@ def setUp(self):
197199
# available to login and logout functions
198200
self.wrapped_request = factory.get('/')
199201
self.request = Request(self.wrapped_request)
200-
SessionMiddleware().process_request(self.request)
202+
SessionMiddleware().process_request(self.wrapped_request)
203+
AuthenticationMiddleware().process_request(self.wrapped_request)
201204

202205
User.objects.create_user('ringo', '[email protected]', 'yellow')
203206
self.user = authenticate(username='ringo', password='yellow')
@@ -212,9 +215,9 @@ def test_user_can_login(self):
212215

213216
def test_user_can_logout(self):
214217
self.request.user = self.user
215-
self.assertFalse(self.request.user.is_anonymous)
218+
assert not self.request.user.is_anonymous
216219
logout(self.request)
217-
self.assertTrue(self.request.user.is_anonymous)
220+
assert self.request.user.is_anonymous
218221

219222
def test_logged_in_user_is_set_on_wrapped_request(self):
220223
login(self.request, self.user)
@@ -227,22 +230,27 @@ def test_calling_user_fails_when_attribute_error_is_raised(self):
227230
"""
228231
class AuthRaisesAttributeError(object):
229232
def authenticate(self, request):
230-
import rest_framework
231-
rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST
233+
self.MISSPELLED_NAME_THAT_DOESNT_EXIST
232234

233-
self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),))
234-
SessionMiddleware().process_request(self.request)
235+
request = Request(self.wrapped_request, authenticators=(AuthRaisesAttributeError(),))
235236

236-
login(self.request, self.user)
237-
try:
238-
self.request.user
239-
except AttributeError as error:
240-
assert str(error) in (
241-
"'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'", # Python < 3.5
242-
"module 'rest_framework' has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'", # Python >= 3.5
243-
)
244-
else:
245-
assert False, 'AttributeError not raised'
237+
# The middleware processes the underlying Django request, sets anonymous user
238+
assert self.wrapped_request.user.is_anonymous
239+
240+
# The DRF request object does not have a user and should run authenticators
241+
expected = r"no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'"
242+
with pytest.raises(WrappedAttributeError, match=expected):
243+
request.user
244+
245+
# python 2 hasattr fails for *any* exception, not just AttributeError
246+
if six.PY2:
247+
return
248+
249+
with pytest.raises(WrappedAttributeError, match=expected):
250+
hasattr(request, 'user')
251+
252+
with pytest.raises(WrappedAttributeError, match=expected):
253+
login(request, self.user)
246254

247255

248256
class TestAuthSetter(TestCase):

0 commit comments

Comments
 (0)