Plain is headed towards 1.0! Subscribe for development updates →

  1from urllib.parse import urlparse, urlunparse
  2
  3from plain.exceptions import PermissionDenied
  4from plain.http import (
  5    Http404,
  6    QueryDict,
  7    Response,
  8    ResponseRedirect,
  9)
 10from plain.runtime import settings
 11from plain.urls import reverse
 12from plain.utils.cache import patch_cache_control
 13from plain.views import View
 14
 15from .sessions import logout
 16from .utils import resolve_url
 17
 18
 19class LoginRequired(Exception):
 20    def __init__(self, login_url=None, redirect_field_name="next"):
 21        self.login_url = login_url or settings.AUTH_LOGIN_URL
 22        self.redirect_field_name = redirect_field_name
 23
 24
 25class AuthViewMixin:
 26    login_required = True
 27    admin_required = False
 28    login_url = settings.AUTH_LOGIN_URL
 29
 30    def check_auth(self) -> None:
 31        """
 32        Raises either LoginRequired or PermissionDenied.
 33        - LoginRequired can specify a login_url and redirect_field_name
 34        - PermissionDenied can specify a message
 35        """
 36
 37        if not hasattr(self, "request"):
 38            raise AttributeError(
 39                "AuthViewMixin requires the request attribute to be set."
 40            )
 41
 42        if self.login_required and not self.request.user:
 43            raise LoginRequired(login_url=self.login_url)
 44
 45        if impersonator := getattr(self.request, "impersonator", None):
 46            # Impersonators should be able to view admin pages while impersonating.
 47            # There's probably never a case where an impersonator isn't admin, but it can be configured.
 48            if self.admin_required and not impersonator.is_admin:
 49                raise PermissionDenied(
 50                    "You do not have permission to access this page."
 51                )
 52        elif self.admin_required and not self.request.user.is_admin:
 53            # Show a 404 so we don't expose admin urls to non-admin users
 54            raise Http404()
 55
 56    def get_response(self) -> Response:
 57        if not hasattr(self, "request"):
 58            raise AttributeError(
 59                "AuthViewMixin requires the request attribute to be set."
 60            )
 61
 62        try:
 63            self.check_auth()
 64        except LoginRequired as e:
 65            if self.login_url:
 66                # Ideally this could be handled elsewhere... like PermissionDenied
 67                # also seems like this code is used multiple places anyway...
 68                # could be easier to get redirect query param
 69                path = self.request.build_absolute_uri()
 70                resolved_login_url = reverse(e.login_url)
 71                # If the login url is the same scheme and net location then use the
 72                # path as the "next" url.
 73                login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
 74                current_scheme, current_netloc = urlparse(path)[:2]
 75                if (not login_scheme or login_scheme == current_scheme) and (
 76                    not login_netloc or login_netloc == current_netloc
 77                ):
 78                    path = self.request.get_full_path()
 79                return redirect_to_login(
 80                    path,
 81                    resolved_login_url,
 82                    e.redirect_field_name,
 83                )
 84            else:
 85                raise PermissionDenied("Login required")
 86
 87        response = super().get_response()
 88        # Make sure it at least has private as a default
 89        patch_cache_control(response, private=True)
 90        return response
 91
 92
 93class LogoutView(View):
 94    def post(self):
 95        logout(self.request)
 96        return ResponseRedirect("/")
 97
 98
 99def redirect_to_login(next, login_url=None, redirect_field_name="next"):
100    """
101    Redirect the user to the login page, passing the given 'next' page.
102    """
103    resolved_url = resolve_url(login_url or settings.AUTH_LOGIN_URL)
104
105    login_url_parts = list(urlparse(resolved_url))
106    if redirect_field_name:
107        querystring = QueryDict(login_url_parts[4], mutable=True)
108        querystring[redirect_field_name] = next
109        login_url_parts[4] = querystring.urlencode(safe="/")
110
111    return ResponseRedirect(urlunparse(login_url_parts))