summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/contrib/auth/backends.py61
-rw-r--r--django/contrib/auth/middleware.py69
-rw-r--r--django/contrib/auth/tests/__init__.py5
-rw-r--r--django/contrib/auth/tests/remote_user.py169
-rw-r--r--docs/howto/auth-remote-user.txt100
-rw-r--r--docs/howto/index.txt7
-rw-r--r--docs/ref/authbackends.txt37
-rw-r--r--docs/ref/index.txt4
-rw-r--r--docs/ref/request-response.txt9
-rw-r--r--docs/topics/auth.txt5
-rw-r--r--docs/topics/index.txt4
11 files changed, 457 insertions, 13 deletions
diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py
index bba883ba78..05f98358b7 100644
--- a/django/contrib/auth/backends.py
+++ b/django/contrib/auth/backends.py
@@ -78,3 +78,64 @@ class ModelBackend(object):
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
+
+
+class RemoteUserBackend(ModelBackend):
+ """
+ This backend is to be used in conjunction with the ``RemoteUserMiddleware``
+ found in the middleware module of this package, and is used when the server
+ is handling authentication outside of Django.
+
+ By default, the ``authenticate`` method creates ``User`` objects for
+ usernames that don't already exist in the database. Subclasses can disable
+ this behavior by setting the ``create_unknown_user`` attribute to
+ ``False``.
+ """
+
+ # Create a User object if not already in the database?
+ create_unknown_user = True
+
+ def authenticate(self, remote_user):
+ """
+ The username passed as ``remote_user`` is considered trusted. This
+ method simply returns the ``User`` object with the given username,
+ creating a new ``User`` object if ``create_unknown_user`` is ``True``.
+
+ Returns None if ``create_unknown_user`` is ``False`` and a ``User``
+ object with the given username is not found in the database.
+ """
+ if not remote_user:
+ return
+ user = None
+ username = self.clean_username(remote_user)
+
+ # Note that this could be accomplished in one try-except clause, but
+ # instead we use get_or_create when creating unknown users since it has
+ # built-in safeguards for multiple threads.
+ if self.create_unknown_user:
+ user, created = User.objects.get_or_create(username=username)
+ if created:
+ user = self.configure_user(user)
+ else:
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ pass
+ return user
+
+ def clean_username(self, username):
+ """
+ Performs any cleaning on the "username" prior to using it to get or
+ create the user object. Returns the cleaned username.
+
+ By default, returns the username unchanged.
+ """
+ return username
+
+ def configure_user(self, user):
+ """
+ Configures a user after creation and returns the updated user.
+
+ By default, returns the user unmodified.
+ """
+ return user
diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py
index 42dc15a366..11c61b28a3 100644
--- a/django/contrib/auth/middleware.py
+++ b/django/contrib/auth/middleware.py
@@ -1,3 +1,7 @@
+from django.contrib import auth
+from django.core.exceptions import ImproperlyConfigured
+
+
class LazyUser(object):
def __get__(self, request, obj_type=None):
if not hasattr(request, '_cached_user'):
@@ -5,8 +9,73 @@ class LazyUser(object):
request._cached_user = get_user(request)
return request._cached_user
+
class AuthenticationMiddleware(object):
def process_request(self, request):
assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
request.__class__.user = LazyUser()
return None
+
+
+class RemoteUserMiddleware(object):
+ """
+ Middleware for utilizing web-server-provided authentication.
+
+ If request.user is not authenticated, then this middleware attempts to
+ authenticate the username passed in the ``REMOTE_USER`` request header.
+ If authentication is successful, the user is automatically logged in to
+ persist the user in the session.
+
+ The header used is configurable and defaults to ``REMOTE_USER``. Subclass
+ this class and change the ``header`` attribute if you need to use a
+ different header.
+ """
+
+ # Name of request header to grab username from. This will be the key as
+ # used in the request.META dictionary, i.e. the normalization of headers to
+ # all uppercase and the addition of "HTTP_" prefix apply.
+ header = "REMOTE_USER"
+
+ def process_request(self, request):
+ # AuthenticationMiddleware is required so that request.user exists.
+ if not hasattr(request, 'user'):
+ raise ImproperlyConfigured(
+ "The Django remote user auth middleware requires the"
+ " authentication middleware to be installed. Edit your"
+ " MIDDLEWARE_CLASSES setting to insert"
+ " 'django.contrib.auth.middleware.AuthenticationMiddleware'"
+ " before the RemoteUserMiddleware class.")
+ try:
+ username = request.META[self.header]
+ except KeyError:
+ # If specified header doesn't exist then return (leaving
+ # request.user set to AnonymousUser by the
+ # AuthenticationMiddleware).
+ return
+ # If the user is already authenticated and that user is the user we are
+ # getting passed in the headers, then the correct user is already
+ # persisted in the session and we don't need to continue.
+ if request.user.is_authenticated():
+ if request.user.username == self.clean_username(username, request):
+ return
+ # We are seeing this user for the first time in this session, attempt
+ # to authenticate the user.
+ user = auth.authenticate(remote_user=username)
+ if user:
+ # User is valid. Set request.user and persist user in the session
+ # by logging the user in.
+ request.user = user
+ auth.login(request, user)
+
+ def clean_username(self, username, request):
+ """
+ Allows the backend to clean the username, if the backend defines a
+ clean_username method.
+ """
+ backend_str = request.session[auth.BACKEND_SESSION_KEY]
+ backend = auth.load_backend(backend_str)
+ try:
+ username = backend.clean_username(username)
+ except AttributeError: # Backend has no clean_username method.
+ pass
+ return username
diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py
index 23cfbafba7..e878d43a7b 100644
--- a/django/contrib/auth/tests/__init__.py
+++ b/django/contrib/auth/tests/__init__.py
@@ -1,6 +1,9 @@
from django.contrib.auth.tests.basic import BASIC_TESTS
-from django.contrib.auth.tests.views import PasswordResetTest, ChangePasswordTest
+from django.contrib.auth.tests.views \
+ import PasswordResetTest, ChangePasswordTest
from django.contrib.auth.tests.forms import FORM_TESTS
+from django.contrib.auth.tests.remote_user \
+ import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest
from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
# The password for the fixture data users is 'password'
diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py
new file mode 100644
index 0000000000..449216d1eb
--- /dev/null
+++ b/django/contrib/auth/tests/remote_user.py
@@ -0,0 +1,169 @@
+from datetime import datetime
+
+from django.conf import settings
+from django.contrib.auth.backends import RemoteUserBackend
+from django.contrib.auth.models import AnonymousUser, User
+from django.test import TestCase
+
+
+class RemoteUserTest(TestCase):
+
+ middleware = 'django.contrib.auth.middleware.RemoteUserMiddleware'
+ backend = 'django.contrib.auth.backends.RemoteUserBackend'
+
+ # Usernames to be passed in REMOTE_USER for the test_known_user test case.
+ known_user = 'knownuser'
+ known_user2 = 'knownuser2'
+
+ def setUp(self):
+ self.curr_middleware = settings.MIDDLEWARE_CLASSES
+ self.curr_auth = settings.AUTHENTICATION_BACKENDS
+ settings.MIDDLEWARE_CLASSES += (self.middleware,)
+ settings.AUTHENTICATION_BACKENDS = (self.backend,)
+
+ def test_no_remote_user(self):
+ """
+ Tests requests where no remote user is specified and insures that no
+ users get created.
+ """
+ num_users = User.objects.count()
+
+ response = self.client.get('/')
+ self.assert_(isinstance(response.context['user'], AnonymousUser))
+ self.assertEqual(User.objects.count(), num_users)
+
+ response = self.client.get('/', REMOTE_USER=None)
+ self.assert_(isinstance(response.context['user'], AnonymousUser))
+ self.assertEqual(User.objects.count(), num_users)
+
+ response = self.client.get('/', REMOTE_USER='')
+ self.assert_(isinstance(response.context['user'], AnonymousUser))
+ self.assertEqual(User.objects.count(), num_users)
+
+ def test_unknown_user(self):
+ """
+ Tests the case where the username passed in the header does not exist
+ as a User.
+ """
+ num_users = User.objects.count()
+ response = self.client.get('/', REMOTE_USER='newuser')
+ self.assertEqual(response.context['user'].username, 'newuser')
+ self.assertEqual(User.objects.count(), num_users + 1)
+ User.objects.get(username='newuser')
+
+ # Another request with same user should not create any new users.
+ response = self.client.get('/', REMOTE_USER='newuser')
+ self.assertEqual(User.objects.count(), num_users + 1)
+
+ def test_known_user(self):
+ """
+ Tests the case where the username passed in the header is a valid User.
+ """
+ User.objects.create(username='knownuser')
+ User.objects.create(username='knownuser2')
+ num_users = User.objects.count()
+ response = self.client.get('/', REMOTE_USER=self.known_user)
+ self.assertEqual(response.context['user'].username, 'knownuser')
+ self.assertEqual(User.objects.count(), num_users)
+ # Test that a different user passed in the headers causes the new user
+ # to be logged in.
+ response = self.client.get('/', REMOTE_USER=self.known_user2)
+ self.assertEqual(response.context['user'].username, 'knownuser2')
+ self.assertEqual(User.objects.count(), num_users)
+
+ def test_last_login(self):
+ """
+ Tests that a user's last_login is set the first time they make a
+ request but not updated in subsequent requests with the same session.
+ """
+ user = User.objects.create(username='knownuser')
+ # Set last_login to something so we can determine if it changes.
+ default_login = datetime(2000, 1, 1)
+ user.last_login = default_login
+ user.save()
+
+ response = self.client.get('/', REMOTE_USER=self.known_user)
+ self.assertNotEqual(default_login, response.context['user'].last_login)
+
+ user = User.objects.get(username='knownuser')
+ user.last_login = default_login
+ user.save()
+ response = self.client.get('/', REMOTE_USER=self.known_user)
+ self.assertEqual(default_login, response.context['user'].last_login)
+
+ def tearDown(self):
+ """Restores settings to avoid breaking other tests."""
+ settings.MIDDLEWARE_CLASSES = self.curr_middleware
+ settings.AUTHENTICATION_BACKENDS = self.curr_auth
+
+
+class RemoteUserNoCreateBackend(RemoteUserBackend):
+ """Backend that doesn't create unknown users."""
+ create_unknown_user = False
+
+
+class RemoteUserNoCreateTest(RemoteUserTest):
+ """
+ Contains the same tests as RemoteUserTest, but using a custom auth backend
+ class that doesn't create unknown users.
+ """
+
+ backend =\
+ 'django.contrib.auth.tests.remote_user.RemoteUserNoCreateBackend'
+
+ def test_unknown_user(self):
+ num_users = User.objects.count()
+ response = self.client.get('/', REMOTE_USER='newuser')
+ self.assert_(isinstance(response.context['user'], AnonymousUser))
+ self.assertEqual(User.objects.count(), num_users)
+
+
+class CustomRemoteUserBackend(RemoteUserBackend):
+ """
+ Backend that overrides RemoteUserBackend methods.
+ """
+
+ def clean_username(self, username):
+ """
+ Grabs username before the @ character.
+ """
+ return username.split('@')[0]
+
+ def configure_user(self, user):
+ """
+ Sets user's email address.
+ """
+ user.email = 'user@example.com'
+ user.save()
+ return user
+
+
+class RemoteUserCustomTest(RemoteUserTest):
+ """
+ Tests a custom RemoteUserBackend subclass that overrides the clean_username
+ and configure_user methods.
+ """
+
+ backend =\
+ 'django.contrib.auth.tests.remote_user.CustomRemoteUserBackend'
+ # REMOTE_USER strings with e-mail addresses for the custom backend to
+ # clean.
+ known_user = 'knownuser@example.com'
+ known_user2 = 'knownuser2@example.com'
+
+ def test_known_user(self):
+ """
+ The strings passed in REMOTE_USER should be cleaned and the known users
+ should not have been configured with an email address.
+ """
+ super(RemoteUserCustomTest, self).test_known_user()
+ self.assertEqual(User.objects.get(username='knownuser').email, '')
+ self.assertEqual(User.objects.get(username='knownuser2').email, '')
+
+ def test_unknown_user(self):
+ """
+ The unknown user created should be configured with an email address.
+ """
+ super(RemoteUserCustomTest, self).test_unknown_user()
+ newuser = User.objects.get(username='newuser')
+ self.assertEqual(newuser.email, 'user@example.com')
diff --git a/docs/howto/auth-remote-user.txt b/docs/howto/auth-remote-user.txt
new file mode 100644
index 0000000000..aa39b1fba4
--- /dev/null
+++ b/docs/howto/auth-remote-user.txt
@@ -0,0 +1,100 @@
+.. _howto-auth-remote-user:
+
+====================================
+Authentication using ``REMOTE_USER``
+====================================
+
+This document describes how to make use of external authentication sources
+(where the Web server sets the ``REMOTE_USER`` environment variable) in your
+Django applications. This type of authentication solution is typically seen on
+intranet sites, with single sign-on solutions such as IIS and Integrated
+Windows Authentication or Apache and `mod_authnz_ldap`_, `CAS`_, `Cosign`_,
+`WebAuth`_, `mod_auth_sspi`_, etc.
+
+.. _mod_authnz_ldap: http://httpd.apache.org/docs/2.2/mod/mod_authnz_ldap.html
+.. _CAS: http://www.ja-sig.org/products/cas/
+.. _Cosign: http://weblogin.org
+.. _WebAuth: http://www.stanford.edu/services/webauth/
+.. _mod_auth_sspi: http://sourceforge.net/projects/mod-auth-sspi
+
+When the Web server takes care of authentication it typically sets the
+``REMOTE_USER`` environment variable for use in the underlying application. In
+Django, ``REMOTE_USER`` is made available in the :attr:`request.META
+<django.http.HttpRequest.META>` attribute. Django can be configured to make
+use of the ``REMOTE_USER`` value using the ``RemoteUserMiddleware`` and
+``RemoteUserBackend`` classes found in :mod:`django.contirb.auth`.
+
+Configuration
+=============
+
+First, you must add the
+:class:`django.contrib.auth.middleware.RemoteUserMiddleware` to the
+:setting:`MIDDLEWARE_CLASSES` setting **after** the
+:class:`django.contrib.auth.middleware.AuthenticationMiddleware`::
+
+ MIDDLEWARE_CLASSES = (
+ ...
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.auth.middleware.RemoteUserMiddleware',
+ ...
+ )
+
+Next, you must replace the :class:`~django.contrib.auth.backends.ModelBackend`
+with ``RemoteUserBackend`` in the :setting:`AUTHENTICATION_BACKENDS` setting::
+
+ AUTHENTICATION_BACKENDS = (
+ 'django.contrib.auth.backends.RemoteUserBackend',
+ )
+
+With this setup, ``RemoteUserMiddleware`` will detect the username in
+``request.META['REMOTE_USER']`` and will authenticate and auto-login that user
+using the ``RemoteUserBackend``.
+
+.. note::
+ Since the ``RemoteUserBackend`` inherits from ``ModelBackend``, you will
+ still have all of the same permissions checking that is implemented in
+ ``ModelBackend``.
+
+If your authentication mechanism uses a custom HTTP header and not
+``REMOTE_USER``, you can subclass ``RemoteUserMiddleware`` and set the
+``header`` attribute to the desired ``request.META`` key. For example::
+
+ from django.contrib.auth.middleware import RemoteUserMiddleware
+
+ class CustomHeaderMiddleware(RemoteUserMiddleware):
+ header = 'HTTP_AUTHUSER'
+
+
+``RemoteUserBackend``
+=====================
+
+.. class:: django.contrib.backends.RemoteUserBackend
+
+If you need more control, you can create your own authentication backend
+that inherits from ``RemoteUserBackend`` and overrides certain parts:
+
+Attributes
+~~~~~~~~~~
+
+.. attribute:: RemoteUserBackend.create_unknown_user
+
+ ``True`` or ``False``. Determines whether or not a
+ :class:`~django.contrib.auth.models.User` object is created if not already
+ in the database. Defaults to ``True``.
+
+Methods
+~~~~~~~
+
+.. method:: RemoteUserBackend.clean_username(username)
+
+ Performs any cleaning on the ``username`` (e.g. stripping LDAP DN
+ information) prior to using it to get or create a
+ :class:`~django.contrib.auth.models.User` object. Returns the cleaned
+ username.
+
+.. method:: RemoteUserBackend.configure_user(user)
+
+ Configures a newly created user. This method is called immediately after a
+ new user is created, and can be used to perform custom setup actions, such
+ as setting the user's groups based on attributes in an LDAP directory.
+ Returns the user object.
diff --git a/docs/howto/index.txt b/docs/howto/index.txt
index 4fdae82750..1a27a2ebac 100644
--- a/docs/howto/index.txt
+++ b/docs/howto/index.txt
@@ -10,8 +10,9 @@ you quickly accomplish common tasks.
.. toctree::
:maxdepth: 1
-
+
apache-auth
+ auth-remote-user
custom-management-commands
custom-model-fields
custom-template-tags
@@ -30,5 +31,5 @@ you quickly accomplish common tasks.
The `Django community aggregator`_, where we aggregate content from the
global Django community. Many writers in the aggregator write this sort of
how-to material.
-
- .. _django community aggregator: http://www.djangoproject.com/community/ \ No newline at end of file
+
+ .. _django community aggregator: http://www.djangoproject.com/community/
diff --git a/docs/ref/authbackends.txt b/docs/ref/authbackends.txt
new file mode 100644
index 0000000000..941152733a
--- /dev/null
+++ b/docs/ref/authbackends.txt
@@ -0,0 +1,37 @@
+.. _ref-authentication-backends:
+
+==========================================
+Built-in authentication backends reference
+==========================================
+
+.. module:: django.contrib.auth.backends
+ :synopsis: Django's built-in authentication backend classes.
+
+This document details the authentication backends that come with Django. For
+information on how how to use them and how to write your own authentication
+backends, see the :ref:`Other authentication sources section
+<authentication-backends>` of the :ref:`User authentication guide
+<topics-auth>`.
+
+
+Available authentication backends
+=================================
+
+The following backends are available in :mod:`django.contrib.auth.backends`:
+
+.. class:: ModelBackend
+
+ This is the default authentication backend used by Django. It
+ authenticates using usernames and passwords stored in the the
+ :class:`~django.contrib.auth.models.User` model.
+
+
+.. class:: RemoteUserBackend
+
+ .. versionadded:: 1.1
+
+ Use this backend to take advantage of external-to-Django-handled
+ authentication. It authenticates using usernames passed in
+ :attr:`request.META['REMOTE_USER'] <django.http.HttpRequest.META>`. See
+ the :ref:`Authenticating against REMOTE_USER <howto-auth-remote-user>`
+ documentation.
diff --git a/docs/ref/index.txt b/docs/ref/index.txt
index c54cd20954..3ffa1fcce1 100644
--- a/docs/ref/index.txt
+++ b/docs/ref/index.txt
@@ -5,7 +5,8 @@ API Reference
.. toctree::
:maxdepth: 1
-
+
+ authbackends
contrib/index
databases
django-admin
@@ -19,4 +20,3 @@ API Reference
signals
templates/index
unicode
- \ No newline at end of file
diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt
index fcf4523218..cd0edc063c 100644
--- a/docs/ref/request-response.txt
+++ b/docs/ref/request-response.txt
@@ -138,6 +138,7 @@ All attributes except ``session`` should be considered read-only.
* ``QUERY_STRING`` -- The query string, as a single (unparsed) string.
* ``REMOTE_ADDR`` -- The IP address of the client.
* ``REMOTE_HOST`` -- The hostname of the client.
+ * ``REMOTE_USER`` -- The user authenticated by the web server, if any.
* ``REQUEST_METHOD`` -- A string such as ``"GET"`` or ``"POST"``.
* ``SERVER_NAME`` -- The hostname of the server.
* ``SERVER_PORT`` -- The port of the server.
@@ -294,7 +295,7 @@ a subclass of dictionary. Exceptions are outlined here:
Just like the standard dictionary ``setdefault()`` method, except it uses
``__setitem__`` internally.
-.. method:: QueryDict.update(other_dict)
+.. method:: QueryDict.update(other_dict)
Takes either a ``QueryDict`` or standard dictionary. Just like the standard
dictionary ``update()`` method, except it *appends* to the current
@@ -357,11 +358,11 @@ In addition, ``QueryDict`` has the following methods:
Like :meth:`items()`, except it includes all values, as a list, for each
member of the dictionary. For example::
-
+
>>> q = QueryDict('a=1&a=2&a=3')
>>> q.lists()
[('a', ['1', '2', '3'])]
-
+
.. method:: QueryDict.urlencode()
Returns a string of the data in query-string format.
@@ -452,7 +453,7 @@ Methods
-------
.. method:: HttpResponse.__init__(content='', mimetype=None, status=200, content_type=DEFAULT_CONTENT_TYPE)
-
+
Instantiates an ``HttpResponse`` object with the given page content (a
string) and MIME type. The ``DEFAULT_CONTENT_TYPE`` is ``'text/html'``.
diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt
index 9759ed821d..47405dd875 100644
--- a/docs/topics/auth.txt
+++ b/docs/topics/auth.txt
@@ -1263,10 +1263,13 @@ administrator and the users themselves if users had separate accounts in LDAP
and the Django-based applications.
So, to handle situations like this, the Django authentication system lets you
-plug in another authentication sources. You can override Django's default
+plug in other authentication sources. You can override Django's default
database-based scheme, or you can use the default system in tandem with other
systems.
+See the :ref:`authentication backend reference <ref-authentication-backends>`
+for information on the authentication backends included with Django.
+
Specifying authentication backends
----------------------------------
diff --git a/docs/topics/index.txt b/docs/topics/index.txt
index 5d83980837..d4a32ab6ce 100644
--- a/docs/topics/index.txt
+++ b/docs/topics/index.txt
@@ -7,7 +7,7 @@ Introductions to all the key parts of Django you'll need to know:
.. toctree::
:maxdepth: 1
-
+
install
db/index
http/index
@@ -23,4 +23,4 @@ Introductions to all the key parts of Django you'll need to know:
pagination
serialization
settings
- signals \ No newline at end of file
+ signals