Skip to content

Commit 6953cf7

Browse files
Merge pull request #173 from brack3t/pr/145
Pr/145
2 parents da19aaf + 00db6e9 commit 6953cf7

File tree

3 files changed

+148
-56
lines changed

3 files changed

+148
-56
lines changed

braces/views/_access.py

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import inspect
12
import datetime
23
import re
34
import six
@@ -7,20 +8,29 @@
78
from django.contrib.auth.views import redirect_to_login, logout_then_login
89
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
910
from django.http import (HttpResponseRedirect, HttpResponsePermanentRedirect,
10-
Http404)
11+
Http404, HttpResponse)
1112
from django.shortcuts import resolve_url
1213
from django.utils.encoding import force_text
1314
from django.utils.timezone import now
1415

16+
# StreamingHttpResponse has been added in 1.5, and gets used for verification
17+
# only.
18+
try:
19+
from django.http import StreamingHttpResponse
20+
except ImportError:
21+
class StreamingHttpResponse(object):
22+
pass
23+
1524

1625
class AccessMixin(object):
1726
"""
1827
'Abstract' mixin that gives access mixins the same customizable
1928
functionality.
2029
"""
2130
login_url = None
22-
raise_exception = False # Default whether to raise an exception to none
31+
raise_exception = False
2332
redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth
33+
redirect_unauthenticated_users = False
2434

2535
def get_login_url(self):
2636
"""
@@ -46,6 +56,30 @@ def get_redirect_field_name(self):
4656
self.__class__.__name__))
4757
return self.redirect_field_name
4858

59+
def handle_no_permission(self, request):
60+
if self.raise_exception and not self.redirect_unauthenticated_users:
61+
if (inspect.isclass(self.raise_exception)
62+
and issubclass(self.raise_exception, Exception)):
63+
raise self.raise_exception
64+
if callable(self.raise_exception):
65+
ret = self.raise_exception(request)
66+
if isinstance(ret, (HttpResponse, StreamingHttpResponse)):
67+
return ret
68+
raise PermissionDenied
69+
70+
return self.no_permissions_fail(request)
71+
72+
def no_permissions_fail(self, request=None):
73+
"""
74+
Called when the user has no permissions and no exception was raised.
75+
This should only return a valid HTTP response.
76+
77+
By default we redirect to login.
78+
"""
79+
return redirect_to_login(request.get_full_path(),
80+
self.get_login_url(),
81+
self.get_redirect_field_name())
82+
4983

5084
class LoginRequiredMixin(AccessMixin):
5185
"""
@@ -56,17 +90,9 @@ class LoginRequiredMixin(AccessMixin):
5690
combined with CsrfExemptMixin - which in that case should
5791
be the left-most mixin.
5892
"""
59-
redirect_unauthenticated_users = False
60-
6193
def dispatch(self, request, *args, **kwargs):
6294
if not request.user.is_authenticated():
63-
if (self.raise_exception and
64-
not self.redirect_unauthenticated_users):
65-
raise PermissionDenied # return a forbidden response
66-
else:
67-
return redirect_to_login(request.get_full_path(),
68-
self.get_login_url(),
69-
self.get_redirect_field_name())
95+
return self.handle_no_permission(request)
7096

7197
return super(LoginRequiredMixin, self).dispatch(
7298
request, *args, **kwargs)
@@ -157,28 +183,15 @@ def check_permissions(self, request):
157183
perms = self.get_permission_required(request)
158184
return request.user.has_perm(perms)
159185

160-
def no_permissions_fail(self, request=None):
161-
"""
162-
Called when the user has no permissions. This should only
163-
return a valid HTTP response.
164-
165-
By default we redirect to login.
166-
"""
167-
return redirect_to_login(request.get_full_path(),
168-
self.get_login_url(),
169-
self.get_redirect_field_name())
170-
171186
def dispatch(self, request, *args, **kwargs):
172187
"""
173188
Check to see if the user in the request has the required
174189
permission.
175190
"""
176191
has_permission = self.check_permissions(request)
177192

178-
if not has_permission: # If the user lacks the permission
179-
if self.raise_exception:
180-
raise PermissionDenied # Return a 403
181-
return self.no_permissions_fail(request)
193+
if not has_permission:
194+
return self.handle_no_permission(request)
182195

183196
return super(PermissionRequiredMixin, self).dispatch(
184197
request, *args, **kwargs)
@@ -318,13 +331,8 @@ def dispatch(self, request, *args, **kwargs):
318331
in_group = self.check_membership(self.get_group_required())
319332

320333
if not in_group:
321-
if self.raise_exception:
322-
raise PermissionDenied
323-
else:
324-
return redirect_to_login(
325-
request.get_full_path(),
326-
self.get_login_url(),
327-
self.get_redirect_field_name())
334+
return self.handle_no_permission(request)
335+
328336
return super(GroupRequiredMixin, self).dispatch(
329337
request, *args, **kwargs)
330338

@@ -354,13 +362,9 @@ def get_test_func(self):
354362
def dispatch(self, request, *args, **kwargs):
355363
user_test_result = self.get_test_func()(request.user)
356364

357-
if not user_test_result: # If user don't pass the test
358-
if self.raise_exception: # *and* if an exception was desired
359-
raise PermissionDenied
360-
else:
361-
return redirect_to_login(request.get_full_path(),
362-
self.get_login_url(),
363-
self.get_redirect_field_name())
365+
if not user_test_result:
366+
return self.handle_no_permission(request)
367+
364368
return super(UserPassesTestMixin, self).dispatch(
365369
request, *args, **kwargs)
366370

@@ -370,13 +374,8 @@ class SuperuserRequiredMixin(AccessMixin):
370374
Mixin allows you to require a user with `is_superuser` set to True.
371375
"""
372376
def dispatch(self, request, *args, **kwargs):
373-
if not request.user.is_superuser: # If the user is a standard user,
374-
if self.raise_exception: # *and* if an exception was desired
375-
raise PermissionDenied # return a forbidden response.
376-
else:
377-
return redirect_to_login(request.get_full_path(),
378-
self.get_login_url(),
379-
self.get_redirect_field_name())
377+
if not request.user.is_superuser:
378+
return self.handle_no_permission(request)
380379

381380
return super(SuperuserRequiredMixin, self).dispatch(
382381
request, *args, **kwargs)
@@ -387,13 +386,8 @@ class StaffuserRequiredMixin(AccessMixin):
387386
Mixin allows you to require a user with `is_staff` set to True.
388387
"""
389388
def dispatch(self, request, *args, **kwargs):
390-
if not request.user.is_staff: # If the request's user is not staff,
391-
if self.raise_exception: # *and* if an exception was desired
392-
raise PermissionDenied # return a forbidden response
393-
else:
394-
return redirect_to_login(request.get_full_path(),
395-
self.get_login_url(),
396-
self.get_redirect_field_name())
389+
if not request.user.is_staff:
390+
return self.handle_no_permission(request)
397391

398392
return super(StaffuserRequiredMixin, self).dispatch(
399393
request, *args, **kwargs)

docs/access.rst

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,22 @@ These mixins all control a user's access to a given view. Since many of them ext
66
::
77

88
login_url = settings.LOGIN_URL
9-
redirect_field_name = REDIRECT_FIELD_NAME
109
raise_exception = False
10+
redirect_field_name = REDIRECT_FIELD_NAME
11+
redirect_unauthenticated_users = False
12+
13+
The ``raise_exception`` attribute allows for these scenarios, in case a
14+
permission is denied:
15+
16+
* ``False`` (default): redirects to the provided login view.
17+
* ``True``: raises a ``PermissionDenied`` exception.
18+
* A subclass of ``Exception``: raises this exception.
19+
* A callable: gets called with the ``request`` argument.
20+
The function has to return a ``HttpResponse`` or
21+
``StreamingHttpResponse`` (Django 1.5+), otherwise a ``PermissionDenied``
22+
exception gets raised.
1123

12-
The ``raise_exception`` attribute will cause the view to raise a ``PermissionDenied`` exception if it is set to ``True``, otherwise the view will redirect to the login view provided.
24+
This gets done in ``handle_no_permission``, which can be overridden itself.
1325

1426
.. contents::
1527

tests/test_access_mixins.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.test.utils import override_settings
1010
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
1111
from django.core.urlresolvers import reverse_lazy
12+
from django.http import Http404, HttpResponse
1213

1314
from .compat import force_text
1415
from .factories import GroupFactory, UserFactory
@@ -70,6 +71,75 @@ def test_raise_permission_denied(self):
7071
with self.assertRaises(PermissionDenied):
7172
self.dispatch_view(req, raise_exception=True)
7273

74+
def test_raise_custom_exception(self):
75+
"""
76+
Http404 should be raised if user is not authorized and
77+
raise_exception attribute is set to Http404.
78+
"""
79+
user = self.build_unauthorized_user()
80+
req = self.build_request(user=user, path=self.view_url)
81+
82+
with self.assertRaises(Http404):
83+
self.dispatch_view(req, raise_exception=Http404)
84+
85+
def test_raise_func_pass(self):
86+
"""
87+
An exception should be raised if user is not authorized and
88+
raise_exception attribute is set to a function that returns nothing.
89+
"""
90+
user = self.build_unauthorized_user()
91+
req = self.build_request(user=user, path=self.view_url)
92+
93+
def func(request):
94+
pass
95+
96+
with self.assertRaises(PermissionDenied):
97+
self.dispatch_view(req, raise_exception=func)
98+
99+
def test_raise_func_response(self):
100+
"""
101+
A custom response should be returned if user is not authorized and
102+
raise_exception attribute is set to a function that returns a response.
103+
"""
104+
user = self.build_unauthorized_user()
105+
req = self.build_request(user=user, path=self.view_url)
106+
107+
def func(request):
108+
return HttpResponse("CUSTOM")
109+
110+
resp = self.dispatch_view(req, raise_exception=func)
111+
assert resp.status_code == 200
112+
assert force_text(resp.content) == 'CUSTOM'
113+
114+
def test_raise_func_false(self):
115+
"""
116+
PermissionDenied should be raised, if a custom raise_exception
117+
function does not return HttpResponse or StreamingHttpResponse.
118+
"""
119+
user = self.build_unauthorized_user()
120+
req = self.build_request(user=user, path=self.view_url)
121+
122+
def func(request):
123+
return False
124+
125+
with self.assertRaises(PermissionDenied):
126+
self.dispatch_view(req, raise_exception=func)
127+
128+
def test_raise_func_raises(self):
129+
"""
130+
A custom exception should be raised if user is not authorized and
131+
raise_exception attribute is set to a callable that raises an
132+
exception.
133+
"""
134+
user = self.build_unauthorized_user()
135+
req = self.build_request(user=user, path=self.view_url)
136+
137+
def func(request):
138+
raise Http404
139+
140+
with self.assertRaises(Http404):
141+
self.dispatch_view(req, raise_exception=func)
142+
73143
def test_custom_login_url(self):
74144
"""
75145
Login url should be customizable.
@@ -128,6 +198,22 @@ def test_overridden_login_url(self):
128198
self.assertRedirects(resp, u'/auth/login/?next={0}'.format(
129199
self.view_url))
130200

201+
def test_redirect_unauthenticated(self):
202+
resp = self.dispatch_view(
203+
self.build_request(path=self.view_url),
204+
raise_exception=True,
205+
redirect_unauthenticated_users=True)
206+
assert resp.status_code == 302
207+
assert resp['Location'] == '/accounts/login/?next={0}'.format(
208+
self.view_url)
209+
210+
def test_redirect_unauthenticated_false(self):
211+
with self.assertRaises(PermissionDenied):
212+
self.dispatch_view(
213+
self.build_request(path=self.view_url),
214+
raise_exception=True,
215+
redirect_unauthenticated_users=False)
216+
131217

132218
class TestLoginRequiredMixin(TestViewHelper, test.TestCase):
133219
"""

0 commit comments

Comments
 (0)