Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-auth/plain/auth/views.py: 87%

1 from urllib.parse import urlparse, urlunparse

2

3 from plain.exceptions import PermissionDenied

4 from plain.http import (

5 Http404,

6 QueryDict,

7 Response,

8 ResponseRedirect,

9)

10 from plain.runtime import settings

11 from plain.urls import reverse

12 from plain.views import View

13

14 from .sessions import logout

15 from .utils import resolve_url

16

17

18 class 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

22

23

24 class AuthViewMixin:

25 login_required = True

26 staff_required = False

27 login_url = None

28

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

35

36 if not hasattr(self, "request"):

37 raise AttributeError(

38 "AuthViewMixin requires the request attribute to be set."

39 )

40

41 if self.login_required and not self.request.user:

42 raise LoginRequired(login_url=self.login_url)

43

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

54

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 )

60

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 )

82

83 return super().get_response() # type: ignore

84

85

86 class LogoutView(View):

87 def post(self):

88 logout(self.request)

89 return ResponseRedirect("/")

90

91

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

97

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="/")

103

104 return ResponseRedirect(urlunparse(login_url_parts))