Skip to content

Commit

Permalink
Fixed #24914 -- Added authentication mixins for CBVs
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
MarkusH committed Jun 17, 2015
1 parent 2f615b1 commit e5cb4e1
Show file tree
Hide file tree
Showing 6 changed files with 548 additions and 35 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ answer newbie questions, and generally made Django that much better:
Ram Rachum <ram@rachum.com>
Randy Barlow <randy@electronsweatshop.com>
Raphaël Barrois <raphael.barrois@m4x.org>
Raphael Michel <mail@raphaelmichel.de>
Raúl Cumplido <raulcumplido@gmail.com>
Remco Wendt <remco.wendt@gmail.com>
Renaud Parent <renaud.parent@gmail.com>
Expand Down
110 changes: 110 additions & 0 deletions django/contrib/auth/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.utils import six
from django.utils.encoding import force_text


class AccessMixin(object):
"""
Abstract CBV mixin that gives access mixins the same customizable
functionality.
"""
login_url = None
permission_denied_message = ''
raise_exception = False
redirect_field_name = REDIRECT_FIELD_NAME

def get_login_url(self):
"""
Override this method to override the login_url attribute.
"""
login_url = self.login_url or settings.LOGIN_URL
if not login_url:
raise ImproperlyConfigured(
'{0} is missing the login_url attribute. Define {0}.login_url, settings.LOGIN_URL, or override '
'{0}.get_login_url().'.format(self.__class__.__name__)
)
return force_text(login_url)

def get_permission_denied_message(self):
"""
Override this method to override the permission_denied_message attribute.
"""
return self.permission_denied_message

def get_redirect_field_name(self):
"""
Override this method to override the redirect_field_name attribute.
"""
return self.redirect_field_name

def handle_no_permission(self):
if self.raise_exception:
raise PermissionDenied(self.get_permission_denied_message())
return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name())


class LoginRequiredMixin(AccessMixin):
"""
CBV mixin which verifies that the current user is authenticated.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
return self.handle_no_permission()
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)


class PermissionRequiredMixin(AccessMixin):
"""
CBV mixin which verifies that the current user has all specified
permissions.
"""
permission_required = None

def get_permission_required(self):
"""
Override this method to override the permission_required attribute.
Must return an iterable.
"""
if self.permission_required is None:
raise ImproperlyConfigured(
'{0} is missing the permission_required attribute. Define {0}.permission_required, or override '
'{0}.get_permission_required().'.format(self.__class__.__name__)
)
if isinstance(self.permission_required, six.string_types):
perms = (self.permission_required, )
else:
perms = self.permission_required
return perms

def dispatch(self, request, *args, **kwargs):
perms = self.get_permission_required()
if not request.user.has_perms(perms):
return self.handle_no_permission()
return super(PermissionRequiredMixin, self).dispatch(request, *args, **kwargs)


class UserPassesTestMixin(AccessMixin):
"""
CBV Mixin that allows you to define a test function which must return True
if the current user can access the view.
"""

def test_func(self):
raise NotImplementedError(
'{0} is missing the implementation of the test_func() method.'.format(self.__class__.__name__)
)

def get_test_func(self):
"""
Override this method to use a different test_func method.
"""
return self.test_func

def dispatch(self, request, *args, **kwargs):
user_test_result = self.get_test_func()()
if not user_test_result:
return self.handle_no_permission()
return super(UserPassesTestMixin, self).dispatch(request, *args, **kwargs)
37 changes: 37 additions & 0 deletions docs/releases/1.9.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,43 @@ the included auth forms for your project, you could set, for example::

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

Permission mixins for class-based views
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Django now ships with the mixins
:class:`~django.contrib.auth.mixins.AccessMixin`,
:class:`~django.contrib.auth.mixins.LoginRequiredMixin`,
:class:`~django.contrib.auth.mixins.PermissionRequiredMixin`, and
:class:`~django.contrib.auth.mixins.UserPassesTestMixin` to provide the
functionality of the ``django.contrib.auth.decorators`` for class-based views.
These mixins have been taken from, or are at least inspired by, the
`django-braces`_ project.

There are a few differences between Django's and django-braces' implementation,
though:

* The :attr:`~django.contrib.auth.mixins.AccessMixin.raise_exception` attribute
can only be ``True`` or ``False``. Custom exceptions or callables are not
supported.

* The :meth:`~django.contrib.auth.mixins.AccessMixin.handle_no_permission`
method does not take a ``request`` argument. The current request is available
in ``self.request``.

* The custom ``test_func()`` of :class:`~django.contrib.auth.mixins.UserPassesTestMixin`
does not take a ``user`` argument. The current user is available in
``self.request.user``.

* The :attr:`permission_required <django.contrib.auth.mixins.PermissionRequiredMixin>`
attribute supports a string (defining one permission) or a list/tuple of
strings (defining multiple permissions) that need to be fulfilled to grant
access.

* The new :attr:`~django.contrib.auth.mixins.AccessMixin.permission_denied_message`
attribute allows passing a message to the ``PermissionDenied`` exception.

.. _django-braces: http://django-braces.readthedocs.org/en/latest/index.html

Minor features
~~~~~~~~~~~~~~

Expand Down
161 changes: 149 additions & 12 deletions docs/topics/auth/default.txt
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,8 @@ login page::

.. currentmodule:: django.contrib.auth.decorators

The login_required decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``login_required`` decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

.. currentmodule:: django.contrib.auth.mixins

The ``LoginRequired`` mixin
~~~~~~~~~~~~~~~~~~~~~~~~~~~

When using :doc:`class-based views </topics/class-based-views/index>`, you can
achieve the same behavior as with ``login_required`` by using the
``LoginRequiredMixin``. This mixin should be at the leftmost position in the
inheritance list.

.. class:: LoginRequiredMixin

.. versionadded:: 1.9

If a view is using this mixin, all requests by non-authenticated users will
be redirected to the login page or shown an HTTP 403 Forbidden error,
depending on the
:attr:`~django.contrib.auth.mixins.AccessMixin.raise_exception` parameter.

You can set any of the parameters of
:class:`~django.contrib.auth.mixins.AccessMixin` to customize the handling
of unauthorized users::


from django.contrib.auth.mixins import LoginRequiredMixin

class MyView(LoginRequiredMixin, View):
login_url = '/login/'
redirect_field_name = 'redirect_to'

.. note::

Just as the ``login_required`` decorator, this mixin does NOT check the
``is_active`` flag on a user.

.. currentmodule:: django.contrib.auth.decorators

Limiting access to logged-in users that pass a test
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -560,8 +597,50 @@ redirects to the login page::
def my_view(request):
...

The permission_required decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. currentmodule:: django.contrib.auth.mixins

.. class:: UserPassesTestMixin

.. versionadded:: 1.9

When using :doc:`class-based views </topics/class-based-views/index>`, you
can use the ``UserPassesTestMixin`` to do this.

You have to override the ``test_func()`` method of the class to provide
the test that is performed. Furthermore, you can set any of the parameters
of :class:`~django.contrib.auth.mixins.AccessMixin` to customize the
handling of unauthorized users::

from django.contrib.auth.mixins import UserPassesTestMixin

class MyView(UserPassesTestMixin, View):

def test_func(self):
return self.request.user.email.endswith('@example.com')

.. admonition: Stacking UserPassesTestMixin

Due to the way ``UserPassesTestMixin`` is implemented, you cannot stack
them in your inheritance list. The following does NOT work::

class TestMixin1(UserPassesTestMixin):
def test_func(self):
return self.request.user.email.endswith('@example.com')

class TestMixin2(UserPassesTestMixin):
def test_func(self):
return self.request.user.username.startswith('django')

class MyView(TestMixin1, TestMixin2, View):
...

If ``TestMixin1`` would call ``super()`` and take that result into
account, ``TestMixin1`` wouldn't work standalone anymore.

.. currentmodule:: django.contrib.auth.decorators

The ``permission_required`` decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

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

Note that :func:`~django.contrib.auth.decorators.permission_required()`
also takes an optional ``login_url`` parameter. Example::
also takes an optional ``login_url`` parameter::

from django.contrib.auth.decorators import permission_required

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

.. _applying-permissions-to-generic-views:
.. currentmodule:: django.contrib.auth.mixins

Applying permissions to generic views
The ``PermissionRequiredMixin`` mixin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To apply a permission to a :doc:`class-based generic view
</ref/class-based-views/index>`, decorate the :meth:`View.dispatch
<django.views.generic.base.View.dispatch>` method on the class. See
:ref:`decorating-class-based-views` for details. Another approach is to
:ref:`write a mixin that wraps as_view() <mixins_that_wrap_as_view>`.
To apply permission checks to :doc:`class-based views
</ref/class-based-views/index>`, you can use the ``PermissionRequiredMixin``:

.. class:: PermissionRequiredMixin

.. versionadded:: 1.9

This mixin, just like the ``permisison_required``
decorator, checks whether the user accessing a view has all given
permissions. You should specify the permission (or an iterable of
permissions) using the ``permission_required`` parameter::

from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
permission_required = 'polls.can_vote'
# Or multiple of permissions:
permission_required = ('polls.can_open', 'polls.can_edit')

You can set any of the parameters of
:class:`~django.contrib.auth.mixins.AccessMixin` to customize the handling
of unauthorized users.

Redirecting unauthorized requests in class-based views
------------------------------------------------------

To ease the handling of access restrictions in :doc:`class-based views
</ref/class-based-views/index>`, the ``AccessMixin`` can be used to redirect a
user to the login page or issue an HTTP 403 Forbidden response.

.. class:: AccessMixin

.. versionadded:: 1.9

.. attribute:: login_url

The URL that users who don't pass the test will be redirected to.
Defaults to :setting:`settings.LOGIN_URL <LOGIN_URL>`.

.. attribute:: permission_denied_message

When ``raise_exception`` is ``True``, this attribute can be used to
control the error message passed to the error handler for display to
the user. Defaults to an empty string.

.. attribute:: redirect_field_name

The name of the query parameter that will contain the URL the user
should be redirected to after a successful login. If you set this to
``None``, a query parameter won't be added. Defaults to ``"next"``.

.. attribute:: raise_exception

If this attribute is set to ``True``, a
:class:`~django.core.exceptions.PermissionDenied` exception will be
raised instead of the redirect. Defaults to ``False``.

.. method:: handle_no_permission()

Depending on the value of ``raise_exception``, the method either raises
a :exc:`~django.core.exceptions.PermissionDenied` exception or
redirects the user to the ``login_url``, optionally including the
``redirect_field_name`` if it is set.

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

Expand Down
23 changes: 0 additions & 23 deletions docs/topics/class-based-views/intro.txt
Original file line number Diff line number Diff line change
Expand Up @@ -173,29 +173,6 @@ that inherits from ``View`` - for example, trying to use a form at the top of a
list and combining :class:`~django.views.generic.edit.ProcessFormView` and
:class:`~django.views.generic.list.ListView` - won't work as expected.

.. _mixins_that_wrap_as_view:

Mixins that wrap ``as_view()``
------------------------------

One way to apply common behavior to many classes is to write a mixin that wraps
the :meth:`~django.views.generic.base.View.as_view()` method.

For example, if you have many generic views that should be decorated with
:func:`~django.contrib.auth.decorators.login_required` you could implement a
mixin like this::

from django.contrib.auth.decorators import login_required

class LoginRequiredMixin(object):
@classmethod
def as_view(cls, **initkwargs):
view = super(LoginRequiredMixin, cls).as_view(**initkwargs)
return login_required(view)

class MyView(LoginRequiredMixin, ...):
# this is a generic view
...

Handling forms with class-based views
=====================================
Expand Down
Loading

0 comments on commit e5cb4e1

Please sign in to comment.