Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-auth/plain/auth/views.py: 87%
52 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
1from urllib.parse import urlparse, urlunparse
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.views import View
14from .sessions import logout
15from .utils import resolve_url
18class LoginRequired(Exception):
19 def __init__(self, login_url=None, redirect_field_name="next"):
20 self.login_url = login_url or settings.AUTH_LOGIN_URL
21 self.redirect_field_name = redirect_field_name
24class AuthViewMixin:
25 login_required = True
26 staff_required = False
27 login_url = None
29 def check_auth(self) -> None:
30 """
31 Raises either LoginRequired or PermissionDenied.
32 - LoginRequired can specify a login_url and redirect_field_name
33 - PermissionDenied can specify a message
34 """
36 if not hasattr(self, "request"):
37 raise AttributeError(
38 "AuthViewMixin requires the request attribute to be set."
39 )
41 if self.login_required and not self.request.user:
42 raise LoginRequired(login_url=self.login_url)
44 if impersonator := getattr(self.request, "impersonator", None):
45 # Impersonators should be able to view staff pages while impersonating.
46 # There's probably never a case where an impersonator isn't staff, but it can be configured.
47 if self.staff_required and not impersonator.is_staff:
48 raise PermissionDenied(
49 "You do not have permission to access this page."
50 )
51 elif self.staff_required and not self.request.user.is_staff:
52 # Show a 404 so we don't expose staff urls to non-staff users
53 raise Http404()
55 def get_response(self) -> Response:
56 if not hasattr(self, "request"):
57 raise AttributeError(
58 "AuthViewMixin requires the request attribute to be set."
59 )
61 try:
62 self.check_auth()
63 except LoginRequired as e:
64 # Ideally this could be handled elsewhere... like PermissionDenied
65 # also seems like this code is used multiple places anyway...
66 # could be easier to get redirect query param
67 path = self.request.build_absolute_uri()
68 resolved_login_url = reverse(e.login_url)
69 # If the login url is the same scheme and net location then use the
70 # path as the "next" url.
71 login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
72 current_scheme, current_netloc = urlparse(path)[:2]
73 if (not login_scheme or login_scheme == current_scheme) and (
74 not login_netloc or login_netloc == current_netloc
75 ):
76 path = self.request.get_full_path()
77 return redirect_to_login(
78 path,
79 resolved_login_url,
80 e.redirect_field_name,
81 )
83 return super().get_response() # type: ignore
86class LogoutView(View):
87 def post(self):
88 logout(self.request)
89 return ResponseRedirect("/")
92def redirect_to_login(next, login_url=None, redirect_field_name="next"):
93 """
94 Redirect the user to the login page, passing the given 'next' page.
95 """
96 resolved_url = resolve_url(login_url or settings.AUTH_LOGIN_URL)
98 login_url_parts = list(urlparse(resolved_url))
99 if redirect_field_name:
100 querystring = QueryDict(login_url_parts[4], mutable=True)
101 querystring[redirect_field_name] = next
102 login_url_parts[4] = querystring.urlencode(safe="/")
104 return ResponseRedirect(urlunparse(login_url_parts))