# Imports
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponseRedirect
import logging
import re
from superdjango.conf import SUPERDJANGO
log = logging.getLogger(__name__)
# Exports
__all__ = (
"AccessMixin",
"LoginRequiredMixin",
)
# Mixins
[docs]class AccessMixin(object):
"""Provides attributes and methods for checking user authentication and permissions in a view."""
group_required = None
"""The group, if any, that a user must belong to in order to access the current view. Multiple groups may be
specified as a list.
"""
logging_enabled = False
"""Indicates access logging is enabled."""
login_required = False
"""Indicates that the user must be logged in to access the current view."""
permission_required = None
"""The permission name required to access the current view in the form of ``app_label.permission_name``. Multiple
permissions may be specified as a list.
"""
raise_access_exception = False
"""Indicates whether exceptions should be raised during dispatch or mitigated, if possible."""
ssl_required = SUPERDJANGO.SSL_REQUIRED
"""Indicates whether child views must be served via a secure connection."""
staff_required = False
"""Indicates ``is_staff`` must be set to ``True`` on the user's account."""
superuser_required = False
"""Indicates ``is_superuser`` must be set to ``True`` on the user's account."""
# noinspection PyUnusedLocal
[docs] def check_login(self, request, *args, **kwargs):
"""Check that the user is authenticated if authentication is required. Also checks ``staff_required`` and
``superuser_required``.
:param request: The current request object.
:rtype: bool
:returns: ``True`` is the user can access the current view.
"""
if self.login_required and not request.user.is_authenticated:
return False
if self.staff_required and not request.user.is_staff:
return False
if self.superuser_required and not request.user.is_superuser:
return False
return True
# noinspection PyUnusedLocal
[docs] def check_membership(self, request, *args, **kwargs):
"""Check group membership to see if the user can access the view.
:param request: The current request object.
:rtype: bool
:returns: ``True`` is the user can access the current view.
"""
if self.group_required is None:
return True
if request.user.is_superuser:
return True
if isinstance(self.group_required, (list, tuple)):
groups_required = self.group_required
else:
groups_required = (self.group_required,)
user_groups = request.user.groups.values_list("name", flat=True)
return len(set(groups_required).intersection(set(user_groups))) > 0
# noinspection PyUnusedLocal,PyMethodMayBeStatic
[docs] def check_other(self, request, *args, **kwargs):
"""Customize this method as needed to perform any additional access checks.
:rtype: bool
This example requires that a user has logged in within the last 90 days.
.. code-block:: python
# views.py
from django.conf import settings
from django.contrib.auth.views import logout_then_login
from datetime import timedelta
from django.utils.timezone import now
class MyView(AccessMixin):
def check_other(self, request, *args, **kwargs):
ninety_days = 60 * 60 * 24 * 90
current_dt = now()
delta = timedelta(seconds=ninety_days)
if current_dt > (request.user.last_login + delta):
return False
return True
def dispatch_other(reason, request, redirect_url=None):
return logout_then_login(request, redirect_url or settings.LOGIN_URL)
The ``AccessMixin`` need not be restricted to authentication and authorization. This example requires that the
user be anonymous (not logged in) and redirects to another location. Note that all other checks (except SSL)
must be ``False`` or ``None``.
.. code-block:: python
# views.py
class MyPublicView(AccessMixin):
def check_other(self, request, *args, **kwargs):
if request.user.is_authenticated:
return False
return True
def dispatch_other(reason, request, redirect_url=None):
return reverse("accounts_profile")
"""
return True
# noinspection PyUnusedLocal
[docs] def check_permission(self, request, *args, **kwargs):
"""Check that the current user has permission to access the view.
:param request: The current request object.
:rtype: bool
:returns: ``True`` is the user can access the current view.
"""
if self.permission_required is None:
return True
if type(self.permission_required) == str:
permissions = [self.permission_required]
else:
permissions = self.permission_required
for perm in permissions:
if request.user.has_perm(perm):
return True
return False
# noinspection PyUnusedLocal
[docs] def check_ssl(self, request, *args, **kwargs):
"""Check that the connection is secure as requested.
:param request: The current request object.
:rtype: bool
:returns: ``True`` is the check passes. This means that the connection is secure when ``ssl_required`` is
``True`` or that no check (one way or the other) is necessary because ``ssl_required`` is ``False``.
.. note::
``ssl_required`` is ignored when ``DEBUG`` is ``True``.
"""
if self.ssl_required:
if settings.DEBUG:
return True
if not request.is_secure():
return False
return True
[docs] def dispatch(self, request, *args, **kwargs):
"""Dispatch the request, checking for authentication and permission as needed.
Access is checked in the following order:
1. Check for SSL.
2. Check for an authenticated user.
3. Check for group membership.
4. Check for specific permissions.
5. Call ``check_other()``. See ``dispatch_other()``.
"""
if not self.check_ssl(request, *args, **kwargs):
return self.dispatch_insecure(request, message="This page may only be access using a secure connection.")
if not self.check_login(request, *args, **kwargs):
return self.dispatch_access_denied(request, message="A login is required to access this page.")
if not self.check_membership(request, *args, **kwargs):
return self.dispatch_access_denied(request, message="Group membership is required to access this page.")
if not self.check_permission(request, *args, **kwargs):
return self.dispatch_access_denied(request, message="Special permission is required to access this page.")
if not self.check_other(request, *args, **kwargs):
message = "Access denied for %s by %s.check_other()." % (request.user, self.__class__.__name__)
return self.dispatch_other(request, message=message)
# noinspection PyUnresolvedReferences
return super().dispatch(request, *args, **kwargs)
[docs] def dispatch_access_denied(self, request, message=None, redirect_url=None):
"""Handle authentication or permission issues during dispatch.
:param request: The current request object.
:param message: A reason for the permission failure.
:type message: str
:param redirect_url: The URL to which the user should be directed.
:type redirect_url: str
"""
_message = message or "Access to this page is not allowed."
if self.raise_access_exception:
raise PermissionDenied(_message)
self.log_access(_message, request)
if redirect_url is not None:
return HttpResponseRedirect(redirect_url)
else:
return self.redirect_to_login(request.get_full_path())
[docs] def dispatch_other(self, request, message=None, redirect_url=None):
"""Responds to failed ``check_other()``. Override this method to provide your own handling.
:param request: The current request object.
:param message: A reason for the permission failure.
:type message: str
:param redirect_url: The URL to which the user should be directed.
:type redirect_url: str
.. note::
By default, this method simply calls ``dispatch_access_denied()``.
"""
return self.dispatch_access_denied(request, message=message, redirect_url=redirect_url)
[docs] def dispatch_insecure(self, request, message=None):
"""Dispatch insecure requests.
:param request: The current request object.
:param message: A reason for the permission failure.
:type message: str
"""
_message = message or "An insecure version of this page is not available."
if self.raise_access_exception:
raise Http404(_message)
self.log_access(_message, request, level=logging.INFO)
return HttpResponseRedirect(self.get_https_url(request))
# noinspection PyMethodMayBeStatic
[docs] def get_https_url(self, request):
"""Get the current URL with the HTTPS protocol."""
url = request.build_absolute_uri(request.get_full_path())
return re.sub(r'^http', 'https', url)
def log_access(self, message, request, level=logging.WARNING):
if self.logging_enabled:
_message = "%s (user %s)" % (message, request.user)
if level == logging.ERROR:
log.error(_message)
elif level == logging.INFO:
log.info(_message)
else:
log.warning(_message)
# noinspection PyMethodMayBeStatic
[docs] def redirect_to_login(self, path):
"""Redirect to the login view using the given path as the next URL.
:param path: The next URL after logging in.
:type path: str
"""
# Import Django's utility here to avoid AppRegistryNotReady error.
from django.contrib.auth.views import redirect_to_login
return redirect_to_login(path)
[docs]class LoginRequiredMixin(AccessMixin):
"""Extend the standard ``AccessMixin`` to set ``login_required = True``."""
login_required = True