Hide keyboard shortcuts

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 

2 

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 

9 

10log = logging.getLogger(__name__) 

11 

12# Exports 

13 

14__all__ = ( 

15 "AccessMixin", 

16 "LoginRequiredMixin", 

17) 

18 

19# Mixins 

20 

21 

22class AccessMixin(object): 

23 """Provides attributes and methods for checking user authentication and permissions in a view.""" 

24 

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

29 

30 logging_enabled = False 

31 """Indicates access logging is enabled.""" 

32 

33 login_required = False 

34 """Indicates that the user must be logged in to access the current view.""" 

35 

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

40 

41 raise_access_exception = False 

42 """Indicates whether exceptions should be raised during dispatch or mitigated, if possible.""" 

43 

44 ssl_required = SUPERDJANGO.SSL_REQUIRED 

45 """Indicates whether child views must be served via a secure connection.""" 

46 

47 staff_required = False 

48 """Indicates ``is_staff`` must be set to ``True`` on the user's account.""" 

49 

50 superuser_required = False 

51 """Indicates ``is_superuser`` must be set to ``True`` on the user's account.""" 

52 

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``. 

57 

58 :param request: The current request object. 

59 

60 :rtype: bool 

61 :returns: ``True`` is the user can access the current view. 

62 

63 """ 

64 if self.login_required and not request.user.is_authenticated: 

65 return False 

66 

67 if self.staff_required and not request.user.is_staff: 

68 return False 

69 

70 if self.superuser_required and not request.user.is_superuser: 

71 return False 

72 

73 return True 

74 

75 # noinspection PyUnusedLocal 

76 def check_membership(self, request, *args, **kwargs): 

77 """Check group membership to see if the user can access the view. 

78 

79 :param request: The current request object. 

80 

81 :rtype: bool 

82 :returns: ``True`` is the user can access the current view. 

83 

84 """ 

85 if self.group_required is None: 

86 return True 

87 

88 if request.user.is_superuser: 

89 return True 

90 

91 if isinstance(self.group_required, (list, tuple)): 

92 groups_required = self.group_required 

93 else: 

94 groups_required = (self.group_required,) 

95 

96 user_groups = request.user.groups.values_list("name", flat=True) 

97 

98 return len(set(groups_required).intersection(set(user_groups))) > 0 

99 

100 # noinspection PyUnusedLocal,PyMethodMayBeStatic 

101 def check_other(self, request, *args, **kwargs): 

102 """Customize this method as needed to perform any additional access checks. 

103 

104 :rtype: bool 

105 

106 This example requires that a user has logged in within the last 90 days. 

107 

108 .. code-block:: python 

109 

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 

115 

116 class MyView(AccessMixin): 

117 def check_other(self, request, *args, **kwargs): 

118 ninety_days = 60 * 60 * 24 * 90 

119 

120 current_dt = now() 

121 delta = timedelta(seconds=ninety_days) 

122 if current_dt > (request.user.last_login + delta): 

123 return False 

124 

125 return True 

126 

127 def dispatch_other(reason, request, redirect_url=None): 

128 return logout_then_login(request, redirect_url or settings.LOGIN_URL) 

129 

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``. 

133 

134 .. code-block:: python 

135 

136 # views.py 

137 class MyPublicView(AccessMixin): 

138 

139 def check_other(self, request, *args, **kwargs): 

140 if request.user.is_authenticated: 

141 return False 

142 

143 return True 

144 

145 def dispatch_other(reason, request, redirect_url=None): 

146 return reverse("accounts_profile") 

147 

148 """ 

149 return True 

150 

151 # noinspection PyUnusedLocal 

152 def check_permission(self, request, *args, **kwargs): 

153 """Check that the current user has permission to access the view. 

154 

155 :param request: The current request object. 

156 

157 :rtype: bool 

158 :returns: ``True`` is the user can access the current view. 

159 

160 """ 

161 if self.permission_required is None: 

162 return True 

163 

164 if type(self.permission_required) == str: 

165 permissions = [self.permission_required] 

166 else: 

167 permissions = self.permission_required 

168 

169 for perm in permissions: 

170 if request.user.has_perm(perm): 

171 return True 

172 

173 return False 

174 

175 # noinspection PyUnusedLocal 

176 def check_ssl(self, request, *args, **kwargs): 

177 """Check that the connection is secure as requested. 

178 

179 :param request: The current request object. 

180 

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``. 

184 

185 .. note:: 

186 ``ssl_required`` is ignored when ``DEBUG`` is ``True``. 

187 

188 """ 

189 if self.ssl_required: 

190 if settings.DEBUG: 

191 return True 

192 

193 if not request.is_secure(): 

194 return False 

195 

196 return True 

197 

198 def dispatch(self, request, *args, **kwargs): 

199 """Dispatch the request, checking for authentication and permission as needed. 

200 

201 Access is checked in the following order: 

202 

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

208 

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

212 

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

215 

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

218 

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

221 

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) 

225 

226 # noinspection PyUnresolvedReferences 

227 return super().dispatch(request, *args, **kwargs) 

228 

229 def dispatch_access_denied(self, request, message=None, redirect_url=None): 

230 """Handle authentication or permission issues during dispatch. 

231 

232 :param request: The current request object. 

233 

234 :param message: A reason for the permission failure. 

235 :type message: str 

236 

237 :param redirect_url: The URL to which the user should be directed. 

238 :type redirect_url: str 

239 

240 """ 

241 _message = message or "Access to this page is not allowed." 

242 

243 if self.raise_access_exception: 

244 raise PermissionDenied(_message) 

245 

246 self.log_access(_message, request) 

247 

248 if redirect_url is not None: 

249 return HttpResponseRedirect(redirect_url) 

250 else: 

251 return self.redirect_to_login(request.get_full_path()) 

252 

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. 

255 

256 :param request: The current request object. 

257 

258 :param message: A reason for the permission failure. 

259 :type message: str 

260 

261 :param redirect_url: The URL to which the user should be directed. 

262 :type redirect_url: str 

263 

264 .. note:: 

265 By default, this method simply calls ``dispatch_access_denied()``. 

266 

267 """ 

268 return self.dispatch_access_denied(request, message=message, redirect_url=redirect_url) 

269 

270 def dispatch_insecure(self, request, message=None): 

271 """Dispatch insecure requests. 

272 

273 :param request: The current request object. 

274 

275 :param message: A reason for the permission failure. 

276 :type message: str 

277 

278 """ 

279 _message = message or "An insecure version of this page is not available." 

280 

281 if self.raise_access_exception: 

282 raise Http404(_message) 

283 

284 self.log_access(_message, request, level=logging.INFO) 

285 

286 return HttpResponseRedirect(self.get_https_url(request)) 

287 

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) 

293 

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) 

303 

304 # noinspection PyMethodMayBeStatic 

305 def redirect_to_login(self, path): 

306 """Redirect to the login view using the given path as the next URL. 

307 

308 :param path: The next URL after logging in. 

309 :type path: str 

310 

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) 

315 

316 

317class LoginRequiredMixin(AccessMixin): 

318 """Extend the standard ``AccessMixin`` to set ``login_required = True``.""" 

319 login_required = True