Index: django/contrib/auth/handlers/modpython.py
===================================================================
--- django/contrib/auth/handlers/modpython.py (revision 7403)
+++ django/contrib/auth/handlers/modpython.py (working copy)
@@ -1,56 +1,148 @@
from mod_python import apache
import os
+from urllib import quote
+from django.core import signals
+from django.dispatch import dispatcher
+from django.core.handlers.base import BaseHandler
+from django.core.handlers.modpython import ModPythonRequest
+from django.conf import settings
+from django.contrib.auth import authenticate, REDIRECT_FIELD_NAME
+from django.utils.encoding import iri_to_uri
-def authenhandler(req, **kwargs):
+_str_to_bool = lambda s: s.lower() in ('1', 'true', 'on', 'yes')
+
+class ModPythonAuthOptions:
+ def __init__(self, req):
+ options = req.get_options()
+ self.permission_name = options.get('DjangoPermissionName', None)
+ self.staff_only = _str_to_bool(options.get('DjangoRequireStaffStatus', "on"))
+ self.superuser_only = _str_to_bool(options.get('DjangoRequireSuperuserStatus', "off"))
+ self.raise_forbidden = _str_to_bool(options.get('DjangoRaiseForbidden', "off"))
+ self.settings_module = options.get('DJANGO_SETTINGS_MODULE', None)
+
+def setup_environment(req, options):
"""
- Authentication handler that checks against Django's auth database.
+ mod_python fakes the environ, and thus doesn't process SetEnv. This ensures
+ any future imports relying on settings will work.
"""
-
- # mod_python fakes the environ, and thus doesn't process SetEnv. This fixes
- # that so that the following import works
os.environ.update(req.subprocess_env)
+ if options.settings_module:
+ os.environ['DJANGO_SETTINGS_MODULE'] = options.settings_module
- # apache 2.2 requires a call to req.get_basic_auth_pw() before
- # req.user and friends are available.
- req.get_basic_auth_pw()
+def authenticate_user(user):
+ if user is None:
+ return False
+ if hasattr(user, 'is_authenticated') and not user.is_authenticated():
+ return False
+ return True
- # check for PythonOptions
- _str_to_bool = lambda s: s.lower() in ('1', 'true', 'on', 'yes')
+def validate_user(user, options):
+ if hasattr(user, 'is_active') and not user.is_active:
+ return False
+ if options.staff_only and not getattr(user, 'is_staff', None):
+ return False
+ if options.superuser_only and not getattr(user, 'is_superuser', None):
+ return False
+ # If a permission is required then user must have a has_perm function to
+ # validate.
+ if options.permission_name and (not hasattr(user, 'has_perm') or
+ not user.has_perm(options.permission_name)):
+ return False
+ return True
- options = req.get_options()
- permission_name = options.get('DjangoPermissionName', None)
- staff_only = _str_to_bool(options.get('DjangoRequireStaffStatus', "on"))
- superuser_only = _str_to_bool(options.get('DjangoRequireSuperuserStatus', "off"))
- settings_module = options.get('DJANGO_SETTINGS_MODULE', None)
- if settings_module:
- os.environ['DJANGO_SETTINGS_MODULE'] = settings_module
+def redirect_to_login(req):
+ path = quote(req.uri)
+ if req.args:
+ path = '%s?%s' % (path, req.args)
+ path = quote(path)
+ iri = '%s?%s=%s' % (settings.LOGIN_URL, REDIRECT_FIELD_NAME, path)
+ uri = iri_to_uri(iri)
+ req.err_headers_out.add('Location', uri)
+ if req.proto_num >= 1001:
+ # Use HTTP Error 303 (see other) for HTTP/1.1 browsers.
+ raise apache.SERVER_RETURN, apache.HTTP_SEE_OTHER
+ else:
+ # Otherwise use HTTP Error 302 (moved temporarily).
+ raise apache.SERVER_RETURN, apache.HTTP_MOVED_TEMPORARILY
- from django.contrib.auth.models import User
- from django import db
- db.reset_queries()
+def authenhandler(req, **kwargs):
+ """
+ mod_python authentication handler that checks against Django's auth
+ database.
+ """
+ options = ModPythonAuthOptions(req)
+ setup_environment(req, options)
- # check that the username is valid
- kwargs = {'username': req.user, 'is_active': True}
- if staff_only:
- kwargs['is_staff'] = True
- if superuser_only:
- kwargs['is_superuser'] = True
+ dispatcher.send(signal=signals.request_started)
try:
- try:
- user = User.objects.get(**kwargs)
- except User.DoesNotExist:
+ # This populates req.user too, so it's important to do first.
+ password = req.get_basic_auth_pw()
+
+ # Get the user from any of the installed backends.
+ user = authenticate(username=req.user, password=password)
+
+ if not authenticate_user(user):
+ # Raise unauthorized if the user doesn't authenticate to bring up a
+ # password dialog box to allow the user to authenticate.
return apache.HTTP_UNAUTHORIZED
-
- # check the password and any permission given
- if user.check_password(req.get_basic_auth_pw()):
- if permission_name:
- if user.has_perm(permission_name):
- return apache.OK
- else:
- return apache.HTTP_UNAUTHORIZED
- else:
- return apache.OK
+
+ # Validate the user
+ if validate_user(user, options):
+ return apache.OK
+
+ # mod_python docs say that HTTP_FORBIDDEN should be raised if the user
+ # authenticates but doesn't validate but Django provides it as an
+ # option, alternately raising HTTP_UNAUTHORIZED again to provide the
+ # option of logging in as an alternate user.
+ if options.raise_forbidden:
+ return apache.HTTP_FORBIDDEN
else:
- return apache.HTTP_UNAUTHORIZED
+ return apache.HTTP_UNAUTHORIZED
finally:
- db.connection.close()
+ dispatcher.send(signal=signals.request_finished)
+
+def accesshandler(req):
+ """
+ mod_python access handler that uses the contrib.auth framework (with
+ sessions, therefore requiring a session cookie).
+ """
+ options = ModPythonAuthOptions(req)
+ setup_environment(req, options)
+
+ # Set up middleware, now that settings works we can do it now.
+ base_handler = BaseHandler()
+ base_handler.load_middleware()
+
+ dispatcher.send(signal=signals.request_started)
+ try:
+ request = ModPythonRequest(req)
+
+ # Apply request middleware
+ for middleware_method in base_handler._request_middleware:
+ response = middleware_method(request)
+ if response:
+ # If we get a response then there's no need to keep processing
+ # any remaining request middleware.
+ break
+
+ user = getattr(request, 'user', None)
+ if not authenticate_user(user):
+ # Rather than raising HTTP_UNAUTHORIZED (which the browser won't be
+ # able to handle since this isn't basic HTTP authentication), write
+ # a response which redirects to settings.LOGIN_URL
+ redirect_to_login(req)
+
+ if validate_user(user, options):
+ return apache.OK
+
+ # mod_python docs say that HTTP_FORBIDDEN should be raised if the user
+ # authenticates but doesn't validate but Django provides it as an
+ # option, alternately redirecting to login to provide the option of
+ # logging in as an alternate user.
+ if options.raise_forbidden:
+ return apache.HTTP_FORBIDDEN
+ else:
+ redirect_to_login(req)
+
+ finally:
+ dispatcher.send(signal=signals.request_finished)
Index: docs/apache_auth.txt
===================================================================
--- docs/apache_auth.txt (revision 7403)
+++ docs/apache_auth.txt (working copy)
@@ -16,11 +16,23 @@
Configuring Apache
==================
-To check against Django's authorization database from a Apache configuration
-file, you'll need to use mod_python's ``PythonAuthenHandler`` directive along
-with the standard ``Auth*`` and ``Require`` directives::
+To check against Django's authorization database from an Apache configuration
+file, you can either use mod_python's ``PythonAccessHandler`` directive or
+the ``PythonAuthenHandler`` directive along with the standard ``Auth*`` and
+``Require`` directives.
+The ``PythonAccessHandler`` directive validates using the built-in
+``contrib.auth`` authentication, which uses the session cookie and redirects to
+the ``settings.LOGIN_URL`` if authentication is required::
+
+ SetEnv DJANGO_SETTINGS_MODULE mysite.settings
+ PythonAccessHandler django.contrib.auth.handlers.modpython
+
+
+The ``PythonAuthenHandler`` directive just uses basic HTTP authentication::
+
+
AuthType Basic
AuthName "example.com"
Require valid-user
@@ -72,8 +84,8 @@
PythonAuthenHandler django.contrib.auth.handlers.modpython
-By default, the authentication handler will limit access to the ``/example/``
-location to users marked as staff members. You can use a set of
+By default, the authentication handler examples above will limit access to the
+``/example/`` location to users marked as staff members. You can use a set of
``PythonOption`` directives to modify this behavior:
================================ =========================================
@@ -97,8 +109,25 @@
By default no specific permission will be
required.
+
+ ``DjangoRaiseForbidden`` If the user authenticates but does not
+ have the valid credentials, raise HTTP
+ Error 403 (forbidden) rather providing a
+ login prompt again.
+
+ Defaults to ``off``.
================================ =========================================
+You may also want to make Apache pass the correct headers to stop proxy servers
+(and local browser caches) from caching your protected resources. Assuming that
+the ``mod_expires`` and ``mod_headers`` modules are enabled on your Apache
+server, you can add the following directives to your ``Location`` block::
+
+ # Stop resources from being cached
+ ExpiresActive On
+ ExpiresDefault A0
+ Header append Cache-Control: "no-cache, no-store, must-revalidate, private"
+
Note that sometimes ``SetEnv`` doesn't play well in this mod_python
configuration, for reasons unknown. If you're having problems getting
mod_python to recognize your ``DJANGO_SETTINGS_MODULE``, you can set it using