diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index 00a6bce17d..5805a3122c 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -2,6 +2,7 @@ import urlparse from functools import wraps from django.conf import settings from django.contrib.auth import REDIRECT_FIELD_NAME +from django.core.exceptions import PermissionDenied from django.utils.decorators import available_attrs @@ -47,9 +48,20 @@ def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login return actual_decorator -def permission_required(perm, login_url=None): +def permission_required(perm, login_url=None, raise_exception=False): """ Decorator for views that checks whether a user has a particular permission - enabled, redirecting to the log-in page if necessary. + enabled, redirecting to the log-in page if neccesary. + If the raise_exception parameter is given the PermissionDenied exception + is raised. """ - return user_passes_test(lambda u: u.has_perm(perm), login_url=login_url) + def check_perms(user): + # First check if the user has the permission (even anon users) + if user.has_perm(perm): + return True + # In case the 403 handler should be called raise the exception + if raise_exception: + raise PermissionDenied + # As the last resort, show the login form + return False + return user_passes_test(check_perms, login_url=login_url) diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index a50e24c0df..9df2d11115 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -1167,7 +1167,7 @@ checks to make sure the user is logged in and has the permission return HttpResponse("You can't vote in this poll.") # ... -.. function:: user_passes_test() +.. function:: user_passes_test(func, [login_url=None]) As a shortcut, you can use the convenient ``user_passes_test`` decorator:: @@ -1205,7 +1205,7 @@ checks to make sure the user is logged in and has the permission The permission_required decorator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. function:: permission_required() +.. function:: permission_required([login_url=None, raise_exception=False]) It's a relatively common task to check whether a user has a particular permission. For that reason, Django provides a shortcut for that case: the @@ -1234,6 +1234,13 @@ The permission_required decorator As in the :func:`~decorators.login_required` decorator, ``login_url`` defaults to :setting:`settings.LOGIN_URL `. + .. versionchanged:: 1.4 + + Added ``raise_exception`` parameter. If given, the decorator will raise + :exc:`~django.core.exceptions.PermissionDenied`, prompting + :ref:`the 403 (HTTP Forbidden) view` instead of + redirecting to the login page. + .. currentmodule:: django.contrib.auth Limiting access to generic views @@ -1632,6 +1639,8 @@ the ``auth_permission`` table most of the time. .. _django/contrib/auth/backends.py: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/backends.py +.. _anonymous_auth: + Authorization for anonymous users ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index 16bdd2d29a..e7bb004ff4 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -370,6 +370,21 @@ class ClientTest(TestCase): # TODO: Log in with right permissions and request the page again + def test_view_with_permissions_exception(self): + "Request a page that is protected with @permission_required but raises a exception" + + # Get the page without logging in. Should result in 403. + response = self.client.get('/test_client/permission_protected_view_exception/') + self.assertEquals(response.status_code, 403) + + # Log in + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Log in with wrong permissions. Should result in 403. + response = self.client.get('/test_client/permission_protected_view_exception/') + self.assertEquals(response.status_code, 403) + def test_view_with_method_permissions(self): "Request a page that is protected with a @permission_required method" diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py index 57746d70ec..66019498e6 100644 --- a/tests/modeltests/test_client/urls.py +++ b/tests/modeltests/test_client/urls.py @@ -21,6 +21,7 @@ urlpatterns = patterns('', (r'^login_protected_method_view/$', views.login_protected_method_view), (r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect), (r'^permission_protected_view/$', views.permission_protected_view), + (r'^permission_protected_view_exception/$', views.permission_protected_view_exception), (r'^permission_protected_method_view/$', views.permission_protected_method_view), (r'^session_view/$', views.session_view), (r'^broken_view/$', views.broken_view), diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py index baa9525b37..6f597c0911 100644 --- a/tests/modeltests/test_client/views.py +++ b/tests/modeltests/test_client/views.py @@ -143,7 +143,7 @@ def login_protected_view_changed_redirect(request): return HttpResponse(t.render(c)) login_protected_view_changed_redirect = login_required(redirect_field_name="redirect_to")(login_protected_view_changed_redirect) -def permission_protected_view(request): +def _permission_protected_view(request): "A simple view that is permission protected." t = Template('This is a permission protected test. ' 'Username is {{ user.username }}. ' @@ -151,7 +151,8 @@ def permission_protected_view(request): name='Permissions Template') c = Context({'user': request.user}) return HttpResponse(t.render(c)) -permission_protected_view = permission_required('modeltests.test_perm')(permission_protected_view) +permission_protected_view = permission_required('modeltests.test_perm')(_permission_protected_view) +permission_protected_view_exception = permission_required('modeltests.test_perm', raise_exception=True)(_permission_protected_view) class _ViewManager(object): @method_decorator(login_required)