Skip to content
/ django Public

Commit e5cb4e1

Browse files
committed
Fixed #24914 -- Added authentication mixins for CBVs
Added the mixins LoginRequiredMixin, PermissionRequiredMixin and UserPassesTestMixin to contrib.auth as counterparts to the respective view decorators. The authentication mixins UserPassesTestMixin, LoginRequiredMixin and PermissionRequiredMixin have been inspired by django-braces <https://github.com/brack3t/django-braces/> Thanks Raphael Michel for the initial patch, tests and docs on the PR and Ana Balica, Kenneth Love, Marc Tamlyn, and Tim Graham for the review.
1 parent 2f615b1 commit e5cb4e1

File tree

6 files changed

+548
-35
lines changed

6 files changed

+548
-35
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ answer newbie questions, and generally made Django that much better:
583583
Ram Rachum <ram@rachum.com>
584584
Randy Barlow <randy@electronsweatshop.com>
585585
Raphaël Barrois <raphael.barrois@m4x.org>
586+
Raphael Michel <mail@raphaelmichel.de>
586587
Raúl Cumplido <raulcumplido@gmail.com>
587588
Remco Wendt <remco.wendt@gmail.com>
588589
Renaud Parent <renaud.parent@gmail.com>

django/contrib/auth/mixins.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from django.conf import settings
2+
from django.contrib.auth import REDIRECT_FIELD_NAME
3+
from django.contrib.auth.views import redirect_to_login
4+
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
5+
from django.utils import six
6+
from django.utils.encoding import force_text
7+
8+
9+
class AccessMixin(object):
10+
"""
11+
Abstract CBV mixin that gives access mixins the same customizable
12+
functionality.
13+
"""
14+
login_url = None
15+
permission_denied_message = ''
16+
raise_exception = False
17+
redirect_field_name = REDIRECT_FIELD_NAME
18+
19+
def get_login_url(self):
20+
"""
21+
Override this method to override the login_url attribute.
22+
"""
23+
login_url = self.login_url or settings.LOGIN_URL
24+
if not login_url:
25+
raise ImproperlyConfigured(
26+
'{0} is missing the login_url attribute. Define {0}.login_url, settings.LOGIN_URL, or override '
27+
'{0}.get_login_url().'.format(self.__class__.__name__)
28+
)
29+
return force_text(login_url)
30+
31+
def get_permission_denied_message(self):
32+
"""
33+
Override this method to override the permission_denied_message attribute.
34+
"""
35+
return self.permission_denied_message
36+
37+
def get_redirect_field_name(self):
38+
"""
39+
Override this method to override the redirect_field_name attribute.
40+
"""
41+
return self.redirect_field_name
42+
43+
def handle_no_permission(self):
44+
if self.raise_exception:
45+
raise PermissionDenied(self.get_permission_denied_message())
46+
return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name())
47+
48+
49+
class LoginRequiredMixin(AccessMixin):
50+
"""
51+
CBV mixin which verifies that the current user is authenticated.
52+
"""
53+
def dispatch(self, request, *args, **kwargs):
54+
if not request.user.is_authenticated():
55+
return self.handle_no_permission()
56+
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
57+
58+
59+
class PermissionRequiredMixin(AccessMixin):
60+
"""
61+
CBV mixin which verifies that the current user has all specified
62+
permissions.
63+
"""
64+
permission_required = None
65+
66+
def get_permission_required(self):
67+
"""
68+
Override this method to override the permission_required attribute.
69+
Must return an iterable.
70+
"""
71+
if self.permission_required is None:
72+
raise ImproperlyConfigured(
73+
'{0} is missing the permission_required attribute. Define {0}.permission_required, or override '
74+
'{0}.get_permission_required().'.format(self.__class__.__name__)
75+
)
76+
if isinstance(self.permission_required, six.string_types):
77+
perms = (self.permission_required, )
78+
else:
79+
perms = self.permission_required
80+
return perms
81+
82+
def dispatch(self, request, *args, **kwargs):
83+
perms = self.get_permission_required()
84+
if not request.user.has_perms(perms):
85+
return self.handle_no_permission()
86+
return super(PermissionRequiredMixin, self).dispatch(request, *args, **kwargs)
87+
88+
89+
class UserPassesTestMixin(AccessMixin):
90+
"""
91+
CBV Mixin that allows you to define a test function which must return True
92+
if the current user can access the view.
93+
"""
94+
95+
def test_func(self):
96+
raise NotImplementedError(
97+
'{0} is missing the implementation of the test_func() method.'.format(self.__class__.__name__)
98+
)
99+
100+
def get_test_func(self):
101+
"""
102+
Override this method to use a different test_func method.
103+
"""
104+
return self.test_func
105+
106+
def dispatch(self, request, *args, **kwargs):
107+
user_test_result = self.get_test_func()()
108+
if not user_test_result:
109+
return self.handle_no_permission()
110+
return super(UserPassesTestMixin, self).dispatch(request, *args, **kwargs)

docs/releases/1.9.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,43 @@ the included auth forms for your project, you could set, for example::
6565

6666
See :ref:`password-validation` for more details.
6767

68+
Permission mixins for class-based views
69+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
70+
71+
Django now ships with the mixins
72+
:class:`~django.contrib.auth.mixins.AccessMixin`,
73+
:class:`~django.contrib.auth.mixins.LoginRequiredMixin`,
74+
:class:`~django.contrib.auth.mixins.PermissionRequiredMixin`, and
75+
:class:`~django.contrib.auth.mixins.UserPassesTestMixin` to provide the
76+
functionality of the ``django.contrib.auth.decorators`` for class-based views.
77+
These mixins have been taken from, or are at least inspired by, the
78+
`django-braces`_ project.
79+
80+
There are a few differences between Django's and django-braces' implementation,
81+
though:
82+
83+
* The :attr:`~django.contrib.auth.mixins.AccessMixin.raise_exception` attribute
84+
can only be ``True`` or ``False``. Custom exceptions or callables are not
85+
supported.
86+
87+
* The :meth:`~django.contrib.auth.mixins.AccessMixin.handle_no_permission`
88+
method does not take a ``request`` argument. The current request is available
89+
in ``self.request``.
90+
91+
* The custom ``test_func()`` of :class:`~django.contrib.auth.mixins.UserPassesTestMixin`
92+
does not take a ``user`` argument. The current user is available in
93+
``self.request.user``.
94+
95+
* The :attr:`permission_required <django.contrib.auth.mixins.PermissionRequiredMixin>`
96+
attribute supports a string (defining one permission) or a list/tuple of
97+
strings (defining multiple permissions) that need to be fulfilled to grant
98+
access.
99+
100+
* The new :attr:`~django.contrib.auth.mixins.AccessMixin.permission_denied_message`
101+
attribute allows passing a message to the ``PermissionDenied`` exception.
102+
103+
.. _django-braces: http://django-braces.readthedocs.org/en/latest/index.html
104+
68105
Minor features
69106
~~~~~~~~~~~~~~
70107

docs/topics/auth/default.txt

Lines changed: 149 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -425,8 +425,8 @@ login page::
425425

426426
.. currentmodule:: django.contrib.auth.decorators
427427

428-
The login_required decorator
429-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
428+
The ``login_required`` decorator
429+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
430430

431431
.. function:: login_required([redirect_field_name=REDIRECT_FIELD_NAME, login_url=None])
432432

@@ -500,6 +500,43 @@ The login_required decorator
500500
:func:`django.contrib.admin.views.decorators.staff_member_required`
501501
decorator a useful alternative to ``login_required()``.
502502

503+
.. currentmodule:: django.contrib.auth.mixins
504+
505+
The ``LoginRequired`` mixin
506+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
507+
508+
When using :doc:`class-based views </topics/class-based-views/index>`, you can
509+
achieve the same behavior as with ``login_required`` by using the
510+
``LoginRequiredMixin``. This mixin should be at the leftmost position in the
511+
inheritance list.
512+
513+
.. class:: LoginRequiredMixin
514+
515+
.. versionadded:: 1.9
516+
517+
If a view is using this mixin, all requests by non-authenticated users will
518+
be redirected to the login page or shown an HTTP 403 Forbidden error,
519+
depending on the
520+
:attr:`~django.contrib.auth.mixins.AccessMixin.raise_exception` parameter.
521+
522+
You can set any of the parameters of
523+
:class:`~django.contrib.auth.mixins.AccessMixin` to customize the handling
524+
of unauthorized users::
525+
526+
527+
from django.contrib.auth.mixins import LoginRequiredMixin
528+
529+
class MyView(LoginRequiredMixin, View):
530+
login_url = '/login/'
531+
redirect_field_name = 'redirect_to'
532+
533+
.. note::
534+
535+
Just as the ``login_required`` decorator, this mixin does NOT check the
536+
``is_active`` flag on a user.
537+
538+
.. currentmodule:: django.contrib.auth.decorators
539+
503540
Limiting access to logged-in users that pass a test
504541
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
505542

@@ -560,8 +597,50 @@ redirects to the login page::
560597
def my_view(request):
561598
...
562599

563-
The permission_required decorator
564-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
600+
.. currentmodule:: django.contrib.auth.mixins
601+
602+
.. class:: UserPassesTestMixin
603+
604+
.. versionadded:: 1.9
605+
606+
When using :doc:`class-based views </topics/class-based-views/index>`, you
607+
can use the ``UserPassesTestMixin`` to do this.
608+
609+
You have to override the ``test_func()`` method of the class to provide
610+
the test that is performed. Furthermore, you can set any of the parameters
611+
of :class:`~django.contrib.auth.mixins.AccessMixin` to customize the
612+
handling of unauthorized users::
613+
614+
from django.contrib.auth.mixins import UserPassesTestMixin
615+
616+
class MyView(UserPassesTestMixin, View):
617+
618+
def test_func(self):
619+
return self.request.user.email.endswith('@example.com')
620+
621+
.. admonition: Stacking UserPassesTestMixin
622+
623+
Due to the way ``UserPassesTestMixin`` is implemented, you cannot stack
624+
them in your inheritance list. The following does NOT work::
625+
626+
class TestMixin1(UserPassesTestMixin):
627+
def test_func(self):
628+
return self.request.user.email.endswith('@example.com')
629+
630+
class TestMixin2(UserPassesTestMixin):
631+
def test_func(self):
632+
return self.request.user.username.startswith('django')
633+
634+
class MyView(TestMixin1, TestMixin2, View):
635+
...
636+
637+
If ``TestMixin1`` would call ``super()`` and take that result into
638+
account, ``TestMixin1`` wouldn't work standalone anymore.
639+
640+
.. currentmodule:: django.contrib.auth.decorators
641+
642+
The ``permission_required`` decorator
643+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
565644

566645
.. function:: permission_required(perm, [login_url=None, raise_exception=False])
567646

@@ -583,7 +662,7 @@ The permission_required decorator
583662
The decorator may also take an iterable of permissions.
584663

585664
Note that :func:`~django.contrib.auth.decorators.permission_required()`
586-
also takes an optional ``login_url`` parameter. Example::
665+
also takes an optional ``login_url`` parameter::
587666

588667
from django.contrib.auth.decorators import permission_required
589668

@@ -604,16 +683,74 @@ The permission_required decorator
604683
In older versions, the ``permission`` parameter only worked with
605684
strings, lists, and tuples instead of strings and any iterable.
606685

607-
.. _applying-permissions-to-generic-views:
686+
.. currentmodule:: django.contrib.auth.mixins
608687

609-
Applying permissions to generic views
688+
The ``PermissionRequiredMixin`` mixin
610689
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
611690

612-
To apply a permission to a :doc:`class-based generic view
613-
</ref/class-based-views/index>`, decorate the :meth:`View.dispatch
614-
<django.views.generic.base.View.dispatch>` method on the class. See
615-
:ref:`decorating-class-based-views` for details. Another approach is to
616-
:ref:`write a mixin that wraps as_view() <mixins_that_wrap_as_view>`.
691+
To apply permission checks to :doc:`class-based views
692+
</ref/class-based-views/index>`, you can use the ``PermissionRequiredMixin``:
693+
694+
.. class:: PermissionRequiredMixin
695+
696+
.. versionadded:: 1.9
697+
698+
This mixin, just like the ``permisison_required``
699+
decorator, checks whether the user accessing a view has all given
700+
permissions. You should specify the permission (or an iterable of
701+
permissions) using the ``permission_required`` parameter::
702+
703+
from django.contrib.auth.mixins import PermissionRequiredMixin
704+
705+
class MyView(PermissionRequiredMixin, View):
706+
permission_required = 'polls.can_vote'
707+
# Or multiple of permissions:
708+
permission_required = ('polls.can_open', 'polls.can_edit')
709+
710+
You can set any of the parameters of
711+
:class:`~django.contrib.auth.mixins.AccessMixin` to customize the handling
712+
of unauthorized users.
713+
714+
Redirecting unauthorized requests in class-based views
715+
------------------------------------------------------
716+
717+
To ease the handling of access restrictions in :doc:`class-based views
718+
</ref/class-based-views/index>`, the ``AccessMixin`` can be used to redirect a
719+
user to the login page or issue an HTTP 403 Forbidden response.
720+
721+
.. class:: AccessMixin
722+
723+
.. versionadded:: 1.9
724+
725+
.. attribute:: login_url
726+
727+
The URL that users who don't pass the test will be redirected to.
728+
Defaults to :setting:`settings.LOGIN_URL <LOGIN_URL>`.
729+
730+
.. attribute:: permission_denied_message
731+
732+
When ``raise_exception`` is ``True``, this attribute can be used to
733+
control the error message passed to the error handler for display to
734+
the user. Defaults to an empty string.
735+
736+
.. attribute:: redirect_field_name
737+
738+
The name of the query parameter that will contain the URL the user
739+
should be redirected to after a successful login. If you set this to
740+
``None``, a query parameter won't be added. Defaults to ``"next"``.
741+
742+
.. attribute:: raise_exception
743+
744+
If this attribute is set to ``True``, a
745+
:class:`~django.core.exceptions.PermissionDenied` exception will be
746+
raised instead of the redirect. Defaults to ``False``.
747+
748+
.. method:: handle_no_permission()
749+
750+
Depending on the value of ``raise_exception``, the method either raises
751+
a :exc:`~django.core.exceptions.PermissionDenied` exception or
752+
redirects the user to the ``login_url``, optionally including the
753+
``redirect_field_name`` if it is set.
617754

618755
.. _session-invalidation-on-password-change:
619756

docs/topics/class-based-views/intro.txt

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -173,29 +173,6 @@ that inherits from ``View`` - for example, trying to use a form at the top of a
173173
list and combining :class:`~django.views.generic.edit.ProcessFormView` and
174174
:class:`~django.views.generic.list.ListView` - won't work as expected.
175175

176-
.. _mixins_that_wrap_as_view:
177-
178-
Mixins that wrap ``as_view()``
179-
------------------------------
180-
181-
One way to apply common behavior to many classes is to write a mixin that wraps
182-
the :meth:`~django.views.generic.base.View.as_view()` method.
183-
184-
For example, if you have many generic views that should be decorated with
185-
:func:`~django.contrib.auth.decorators.login_required` you could implement a
186-
mixin like this::
187-
188-
from django.contrib.auth.decorators import login_required
189-
190-
class LoginRequiredMixin(object):
191-
@classmethod
192-
def as_view(cls, **initkwargs):
193-
view = super(LoginRequiredMixin, cls).as_view(**initkwargs)
194-
return login_required(view)
195-
196-
class MyView(LoginRequiredMixin, ...):
197-
# this is a generic view
198-
...
199176

200177
Handling forms with class-based views
201178
=====================================

0 commit comments

Comments
 (0)