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))