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 +` 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 +` of the :ref:`User authentication guide +`. + + +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'] `. See + the :ref:`Authenticating against 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 ` +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