Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Imports
3from django.conf import settings
4from django.core.exceptions import PermissionDenied
5from django.http import Http404, HttpResponseRedirect
6import logging
7import re
8from superdjango.conf import SUPERDJANGO
10log = logging.getLogger(__name__)
12# Exports
14__all__ = (
15 "AccessMixin",
16 "LoginRequiredMixin",
17)
19# Mixins
22class AccessMixin(object):
23 """Provides attributes and methods for checking user authentication and permissions in a view."""
25 group_required = None
26 """The group, if any, that a user must belong to in order to access the current view. Multiple groups may be
27 specified as a list.
28 """
30 logging_enabled = False
31 """Indicates access logging is enabled."""
33 login_required = False
34 """Indicates that the user must be logged in to access the current view."""
36 permission_required = None
37 """The permission name required to access the current view in the form of ``app_label.permission_name``. Multiple
38 permissions may be specified as a list.
39 """
41 raise_access_exception = False
42 """Indicates whether exceptions should be raised during dispatch or mitigated, if possible."""
44 ssl_required = SUPERDJANGO.SSL_REQUIRED
45 """Indicates whether child views must be served via a secure connection."""
47 staff_required = False
48 """Indicates ``is_staff`` must be set to ``True`` on the user's account."""
50 superuser_required = False
51 """Indicates ``is_superuser`` must be set to ``True`` on the user's account."""
53 # noinspection PyUnusedLocal
54 def check_login(self, request, *args, **kwargs):
55 """Check that the user is authenticated if authentication is required. Also checks ``staff_required`` and
56 ``superuser_required``.
58 :param request: The current request object.
60 :rtype: bool
61 :returns: ``True`` is the user can access the current view.
63 """
64 if self.login_required and not request.user.is_authenticated:
65 return False
67 if self.staff_required and not request.user.is_staff:
68 return False
70 if self.superuser_required and not request.user.is_superuser:
71 return False
73 return True
75 # noinspection PyUnusedLocal
76 def check_membership(self, request, *args, **kwargs):
77 """Check group membership to see if the user can access the view.
79 :param request: The current request object.
81 :rtype: bool
82 :returns: ``True`` is the user can access the current view.
84 """
85 if self.group_required is None:
86 return True
88 if request.user.is_superuser:
89 return True
91 if isinstance(self.group_required, (list, tuple)):
92 groups_required = self.group_required
93 else:
94 groups_required = (self.group_required,)
96 user_groups = request.user.groups.values_list("name", flat=True)
98 return len(set(groups_required).intersection(set(user_groups))) > 0
100 # noinspection PyUnusedLocal,PyMethodMayBeStatic
101 def check_other(self, request, *args, **kwargs):
102 """Customize this method as needed to perform any additional access checks.
104 :rtype: bool
106 This example requires that a user has logged in within the last 90 days.
108 .. code-block:: python
110 # views.py
111 from django.conf import settings
112 from django.contrib.auth.views import logout_then_login
113 from datetime import timedelta
114 from django.utils.timezone import now
116 class MyView(AccessMixin):
117 def check_other(self, request, *args, **kwargs):
118 ninety_days = 60 * 60 * 24 * 90
120 current_dt = now()
121 delta = timedelta(seconds=ninety_days)
122 if current_dt > (request.user.last_login + delta):
123 return False
125 return True
127 def dispatch_other(reason, request, redirect_url=None):
128 return logout_then_login(request, redirect_url or settings.LOGIN_URL)
130 The ``AccessMixin`` need not be restricted to authentication and authorization. This example requires that the
131 user be anonymous (not logged in) and redirects to another location. Note that all other checks (except SSL)
132 must be ``False`` or ``None``.
134 .. code-block:: python
136 # views.py
137 class MyPublicView(AccessMixin):
139 def check_other(self, request, *args, **kwargs):
140 if request.user.is_authenticated:
141 return False
143 return True
145 def dispatch_other(reason, request, redirect_url=None):
146 return reverse("accounts_profile")
148 """
149 return True
151 # noinspection PyUnusedLocal
152 def check_permission(self, request, *args, **kwargs):
153 """Check that the current user has permission to access the view.
155 :param request: The current request object.
157 :rtype: bool
158 :returns: ``True`` is the user can access the current view.
160 """
161 if self.permission_required is None:
162 return True
164 if type(self.permission_required) == str:
165 permissions = [self.permission_required]
166 else:
167 permissions = self.permission_required
169 for perm in permissions:
170 if request.user.has_perm(perm):
171 return True
173 return False
175 # noinspection PyUnusedLocal
176 def check_ssl(self, request, *args, **kwargs):
177 """Check that the connection is secure as requested.
179 :param request: The current request object.
181 :rtype: bool
182 :returns: ``True`` is the check passes. This means that the connection is secure when ``ssl_required`` is
183 ``True`` or that no check (one way or the other) is necessary because ``ssl_required`` is ``False``.
185 .. note::
186 ``ssl_required`` is ignored when ``DEBUG`` is ``True``.
188 """
189 if self.ssl_required:
190 if settings.DEBUG:
191 return True
193 if not request.is_secure():
194 return False
196 return True
198 def dispatch(self, request, *args, **kwargs):
199 """Dispatch the request, checking for authentication and permission as needed.
201 Access is checked in the following order:
203 1. Check for SSL.
204 2. Check for an authenticated user.
205 3. Check for group membership.
206 4. Check for specific permissions.
207 5. Call ``check_other()``. See ``dispatch_other()``.
209 """
210 if not self.check_ssl(request, *args, **kwargs):
211 return self.dispatch_insecure(request, message="This page may only be access using a secure connection.")
213 if not self.check_login(request, *args, **kwargs):
214 return self.dispatch_access_denied(request, message="A login is required to access this page.")
216 if not self.check_membership(request, *args, **kwargs):
217 return self.dispatch_access_denied(request, message="Group membership is required to access this page.")
219 if not self.check_permission(request, *args, **kwargs):
220 return self.dispatch_access_denied(request, message="Special permission is required to access this page.")
222 if not self.check_other(request, *args, **kwargs):
223 message = "Access denied for %s by %s.check_other()." % (request.user, self.__class__.__name__)
224 return self.dispatch_other(request, message=message)
226 # noinspection PyUnresolvedReferences
227 return super().dispatch(request, *args, **kwargs)
229 def dispatch_access_denied(self, request, message=None, redirect_url=None):
230 """Handle authentication or permission issues during dispatch.
232 :param request: The current request object.
234 :param message: A reason for the permission failure.
235 :type message: str
237 :param redirect_url: The URL to which the user should be directed.
238 :type redirect_url: str
240 """
241 _message = message or "Access to this page is not allowed."
243 if self.raise_access_exception:
244 raise PermissionDenied(_message)
246 self.log_access(_message, request)
248 if redirect_url is not None:
249 return HttpResponseRedirect(redirect_url)
250 else:
251 return self.redirect_to_login(request.get_full_path())
253 def dispatch_other(self, request, message=None, redirect_url=None):
254 """Responds to failed ``check_other()``. Override this method to provide your own handling.
256 :param request: The current request object.
258 :param message: A reason for the permission failure.
259 :type message: str
261 :param redirect_url: The URL to which the user should be directed.
262 :type redirect_url: str
264 .. note::
265 By default, this method simply calls ``dispatch_access_denied()``.
267 """
268 return self.dispatch_access_denied(request, message=message, redirect_url=redirect_url)
270 def dispatch_insecure(self, request, message=None):
271 """Dispatch insecure requests.
273 :param request: The current request object.
275 :param message: A reason for the permission failure.
276 :type message: str
278 """
279 _message = message or "An insecure version of this page is not available."
281 if self.raise_access_exception:
282 raise Http404(_message)
284 self.log_access(_message, request, level=logging.INFO)
286 return HttpResponseRedirect(self.get_https_url(request))
288 # noinspection PyMethodMayBeStatic
289 def get_https_url(self, request):
290 """Get the current URL with the HTTPS protocol."""
291 url = request.build_absolute_uri(request.get_full_path())
292 return re.sub(r'^http', 'https', url)
294 def log_access(self, message, request, level=logging.WARNING):
295 if self.logging_enabled:
296 _message = "%s (user %s)" % (message, request.user)
297 if level == logging.ERROR:
298 log.error(_message)
299 elif level == logging.INFO:
300 log.info(_message)
301 else:
302 log.warning(_message)
304 # noinspection PyMethodMayBeStatic
305 def redirect_to_login(self, path):
306 """Redirect to the login view using the given path as the next URL.
308 :param path: The next URL after logging in.
309 :type path: str
311 """
312 # Import Django's utility here to avoid AppRegistryNotReady error.
313 from django.contrib.auth.views import redirect_to_login
314 return redirect_to_login(path)
317class LoginRequiredMixin(AccessMixin):
318 """Extend the standard ``AccessMixin`` to set ``login_required = True``."""
319 login_required = True