Ticket #689: 689.3.diff

File 689.3.diff, 20.3 KB (added by Gary Wilson, 10 years ago)
  • django/contrib/auth/backends.py

    === modified file 'django/contrib/auth/backends.py'
     
    7878            return User.objects.get(pk=user_id)
    7979        except User.DoesNotExist:
    8080            return None
     81
     82
     83class RemoteUserBackend(ModelBackend):
     84    """
     85    This backend is to be used in conjunction with the ``RemoteUserMiddleware``
     86    found in the middleware module of this package, and is used when the server
     87    is handling authentication outside of Django.
     88
     89    By default, the ``authenticate`` method creates ``User`` objects for
     90    usernames that don't already exist in the database.  Subclasses can disable
     91    this behavior by setting the ``create_unknown_user`` attribute to
     92    ``False``.
     93    """
     94
     95    # Create a User object if not already in the database?
     96    create_unknown_user = True
     97
     98    def authenticate(self, remote_user):
     99        """
     100        The username passed as ``remote_user`` is considered trusted.  This
     101        method simply returns the ``User`` object with the given username,
     102        creating a new ``User`` object if ``create_unknown_user`` is ``True``.
     103
     104        Returns None if ``create_unknown_user`` is ``False`` and a ``User``
     105        object with the given username is not found in the database.
     106        """
     107        if not remote_user:
     108            return
     109        user = None
     110        username = self.clean_username(remote_user)
     111
     112        # Note that this could be accomplished in one try-except clause, but
     113        # instead we use get_or_create when creating unknown users since it has
     114        # built-in safeguards for multiple threads.
     115        if self.create_unknown_user:
     116            user, created = User.objects.get_or_create(username=username)
     117            if created:
     118                user = self.configure_user(user)
     119        else:
     120            try:
     121                user = User.objects.get(username=username)
     122            except User.DoesNotExist:
     123                pass
     124        return user
     125
     126    def clean_username(self, username):
     127        """
     128        Cleans the passed username to remove any extraneous text, and returns
     129        the cleaned username.
     130
     131        By default, this method returns the passed username unmodified.
     132        Override this method if you need to do special things with the
     133        username, like stripping @realm or cleaning something like
     134        cn=user,dc=domain.
     135        """
     136        return username
     137
     138    def configure_user(self, user):
     139        """
     140        Configures a user after creation and returns the updated user.
     141
     142        By default, this method returns the passed user unmodified.  Override
     143        this method if you would like to do additional setup on the user that
     144        was just created, such as set groups based on attributes in an LDAP
     145        directory.
     146        """
     147        return user
  • django/contrib/auth/middleware.py

    === modified file 'django/contrib/auth/middleware.py'
     
     1from django.contrib import auth
     2from django.core.exceptions import ImproperlyConfigured
     3
     4
    15class LazyUser(object):
    26    def __get__(self, request, obj_type=None):
    37        if not hasattr(request, '_cached_user'):
     
    59            request._cached_user = get_user(request)
    610        return request._cached_user
    711
     12
    813class AuthenticationMiddleware(object):
    914    def process_request(self, request):
    1015        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'."
    1116        request.__class__.user = LazyUser()
    1217        return None
     18
     19
     20class RemoteUserMiddleware(object):
     21    """
     22    Middleware for utilizing web-server-provided authentication.
     23
     24    If request.user is not authenticated, then this middleware attempts to
     25    authenticate the username passed in the ``REMOTE_USER`` request header.
     26    If authentication is successful, the user is automatically logged in to
     27    persist the user in the session.
     28
     29    The header used is configurable and defaults to ``REMOTE_USER``.  Subclass
     30    this class and change the ``header`` attribute if you need to use a
     31    different header.
     32    """
     33
     34    # Name of request header to grab username from.  This will be the key as
     35    # used in the request.META dictionary, i.e. the normalization of headers to
     36    # all uppercase and the addition of "HTTP_" prefix apply.
     37    header = "REMOTE_USER"
     38
     39    def process_request(self, request):
     40        # AuthenticationMiddleware is required so that request.user exists.
     41        if not hasattr(request, 'user'):
     42            raise ImproperlyConfigured(
     43                "The Django remote user auth middleware requires the"
     44                " authentication middleware to be installed.  Edit your"
     45                " MIDDLEWARE_CLASSES setting to insert"
     46                " 'django.contrib.auth.middleware.AuthenticationMiddleware'"
     47                " before the RemoteUserMiddleware class.")
     48        try:
     49            username = request.META[self.header]
     50        except KeyError:
     51            # If specified header doesn't exist then return (leaving
     52            # request.user set to AnonymousUser by the
     53            # AuthenticationMiddleware).
     54            return
     55        # If the user is already authenticated and that user is the user we are
     56        # getting passed in the headers, then the correct user is already
     57        # persisted in the session and we don't need to continue.
     58        if request.user.is_authenticated():
     59            if request.user.username == self.clean_username(username, request):
     60                return
     61        # We are seeing this user for the first time in this session, attempt
     62        # to authenticate the user.
     63        user = auth.authenticate(remote_user=username)
     64        if user:
     65            # User is valid.  Set request.user and persist user in the session
     66            # by logging the user in.
     67            request.user = user
     68            auth.login(request, user)
     69
     70    def clean_username(self, username, request):
     71        """
     72        Allows the backend to clean the username, if the backend defines a
     73        clean_username method.
     74        """
     75        backend_str = request.session[auth.BACKEND_SESSION_KEY]
     76        backend = auth.load_backend(backend_str)
     77        try:
     78            username = backend.clean_username(username)
     79        except AttributeError: # Backend has no clean_username method.
     80            pass
     81        return username
  • django/contrib/auth/tests/__init__.py

    === modified file 'django/contrib/auth/tests/__init__.py'
     
    11from django.contrib.auth.tests.basic import BASIC_TESTS
    2 from django.contrib.auth.tests.views import PasswordResetTest, ChangePasswordTest
     2from django.contrib.auth.tests.views \
     3        import PasswordResetTest, ChangePasswordTest
    34from django.contrib.auth.tests.forms import FORM_TESTS
     5from django.contrib.auth.tests.remote_user \
     6        import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest
    47from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
    58
    69# The password for the fixture data users is 'password'
  • django/contrib/auth/tests/remote_user.py

    === added file 'django/contrib/auth/tests/remote_user.py'
     
     1from datetime import datetime
     2
     3from django.conf import settings
     4from django.contrib.auth.backends import RemoteUserBackend
     5from django.contrib.auth.models import AnonymousUser, User
     6from django.test import TestCase
     7
     8
     9class RemoteUserTest(TestCase):
     10
     11    middleware = 'django.contrib.auth.middleware.RemoteUserMiddleware'
     12    backend = 'django.contrib.auth.backends.RemoteUserBackend'
     13    known_user = 'knownuser'
     14
     15    def setUp(self):
     16        self.curr_middleware = settings.MIDDLEWARE_CLASSES
     17        self.curr_auth = settings.AUTHENTICATION_BACKENDS
     18        settings.MIDDLEWARE_CLASSES += (self.middleware,)
     19        settings.AUTHENTICATION_BACKENDS = (self.backend,)
     20
     21    def test_no_remote_user(self):
     22        """
     23        Tests requests where no remote user is specified and insures that no
     24        users get created.
     25        """
     26        num_users = User.objects.count()
     27
     28        response = self.client.get('/')
     29        self.assert_(isinstance(response.context['user'], AnonymousUser))
     30        self.assertEqual(User.objects.count(), num_users)
     31
     32        response = self.client.get('/', REMOTE_USER=None)
     33        self.assert_(isinstance(response.context['user'], AnonymousUser))
     34        self.assertEqual(User.objects.count(), num_users)
     35
     36        response = self.client.get('/', REMOTE_USER='')
     37        self.assert_(isinstance(response.context['user'], AnonymousUser))
     38        self.assertEqual(User.objects.count(), num_users)
     39
     40    def test_unknown_user(self):
     41        """
     42        Tests the case where the username passed in the header does not exist
     43        as a User.
     44        """
     45        num_users = User.objects.count()
     46        response = self.client.get('/', REMOTE_USER='newuser')
     47        self.assertEqual(response.context['user'].username, 'newuser')
     48        self.assertEqual(User.objects.count(), num_users + 1)
     49        User.objects.get(username='newuser')
     50
     51        # Another request with same user should not create any new users.
     52        response = self.client.get('/', REMOTE_USER='newuser')
     53        self.assertEqual(User.objects.count(), num_users + 1)
     54
     55    def test_known_user(self):
     56        """
     57        Tests the case where the username passed in the header is a valid User.
     58        """
     59        User.objects.create(username='knownuser')
     60        User.objects.create(username='knownuser2')
     61        num_users = User.objects.count()
     62        response = self.client.get('/', REMOTE_USER='knownuser')
     63        self.assertEqual(response.context['user'].username, 'knownuser')
     64        self.assertEqual(User.objects.count(), num_users)
     65        # Test that a different user passed in the headers causes the new user
     66        # to be logged in.
     67        response = self.client.get('/', REMOTE_USER='knownuser2')
     68        self.assertEqual(response.context['user'].username, 'knownuser2')
     69        self.assertEqual(User.objects.count(), num_users)
     70
     71    def test_last_login(self):
     72        """
     73        Tests that a user's last_login is set the first time they make a
     74        request but not updated in subsequent requests with the same session.
     75        """
     76        user = User.objects.create(username='knownuser')
     77        # Set last_login to something so we can determine if it changes.
     78        default_login = datetime(2000, 1, 1)
     79        user.last_login = default_login
     80        user.save()
     81
     82        response = self.client.get('/', REMOTE_USER=self.known_user)
     83        self.assertNotEqual(default_login, response.context['user'].last_login)
     84
     85        user = User.objects.get(username='knownuser')
     86        user.last_login = default_login
     87        user.save()
     88        response = self.client.get('/', REMOTE_USER=self.known_user)
     89        self.assertEqual(default_login, response.context['user'].last_login)
     90
     91    def tearDown(self):
     92        """Restores settings to avoid breaking other tests."""
     93        settings.MIDDLEWARE_CLASSES = self.curr_middleware
     94        settings.AUTHENTICATION_BACKENDS = self.curr_auth
     95
     96
     97class RemoteUserNoCreateBackend(RemoteUserBackend):
     98    """Backend that doesn't create unknown users."""
     99    create_unknown_user = False
     100
     101
     102class RemoteUserNoCreateTest(RemoteUserTest):
     103    """
     104    Contains the same tests as RemoteUserTest, but using a custom auth backend
     105    class that doesn't create unknown users.
     106    """
     107
     108    backend =\
     109        'django.contrib.auth.tests.remote_user.RemoteUserNoCreateBackend'
     110
     111    def test_unknown_user(self):
     112        num_users = User.objects.count()
     113        response = self.client.get('/', REMOTE_USER='newuser')
     114        self.assert_(isinstance(response.context['user'], AnonymousUser))
     115        self.assertEqual(User.objects.count(), num_users)
     116
     117
     118class CustomRemoteUserBackend(RemoteUserBackend):
     119    """
     120    Backend that overrides RemoteUserBackend methods.
     121    """
     122
     123    def clean_username(self, username):
     124        """
     125        Grabs username before the @ character.
     126        """
     127        return username.split('@')[0]
     128
     129    def configure_user(self, user):
     130        """
     131        Sets user's email address.
     132        """
     133        user.email = 'user@example.com'
     134        user.save()
     135        return user
     136
     137
     138class RemoteUserCustomTest(RemoteUserTest):
     139
     140    backend =\
     141        'django.contrib.auth.tests.remote_user.CustomRemoteUserBackend'
     142    known_user = 'knownuser@example.com'
     143
     144    def test_clean_username(self):
     145        """
     146        The strings passed in REMOTE_USER should be cleaned and the known users
     147        should not have been configured with an email address.
     148        """
     149        User.objects.create(username='knownuser')
     150        User.objects.create(username='knownuser2')
     151        num_users = User.objects.count()
     152
     153        response = self.client.get('/', REMOTE_USER='knownuser@abc.com')
     154        self.assertEqual(response.context['user'].username, 'knownuser')
     155        self.assertEqual(response.context['user'].email, '')
     156        self.assertEqual(User.objects.count(), num_users)
     157
     158        response = self.client.get('/', REMOTE_USER='knownuser2@abc.com')
     159        self.assertEqual(response.context['user'].username, 'knownuser2')
     160        self.assertEqual(response.context['user'].email, '')
     161        self.assertEqual(User.objects.count(), num_users)
     162
     163    def test_unknown_user(self):
     164        """
     165        The unknown user created should be configured with an email address.
     166        """
     167        super(RemoteUserCustomTest, self).test_unknown_user()
     168        newuser = User.objects.get(username='newuser')
     169        self.assertEqual(newuser.email, 'user@example.com')
  • docs/ref/authbackends.txt

    === added file 'docs/ref/authbackends.txt'
     
     1.. _ref-authentication-backends:
     2
     3==========================================
     4Built-in authentication backends reference
     5==========================================
     6
     7.. module:: django.contrib.auth.backends
     8   :synopsis: Django's built-in authentication backend classes.
     9
     10This document details the authentication backends that come with Django. For
     11information on how how to use them and how to write your own authentication
     12backends, see the :ref:`Other authentication sources section
     13<authentication-backends>` of the :ref:`User authentication guide
     14<topics-auth>`.
     15
     16
     17Available authentication backends
     18=================================
     19
     20The following backends are available in :mod:`django.contrib.auth.backends`:
     21
     22.. class:: ModelBackend
     23
     24    This is the default authentication backend used by Django.  It
     25    authenticates using usernames and passwords stored in the the
     26    :class:`~django.contrib.auth.models.User` model.
     27
     28
     29.. class:: RemoteUserBackend
     30
     31    .. versionadded:: 1.1
     32
     33    Use this backend to take advantage of external-to-Django-handled
     34    authentication.  It authenticates using usernames passed in
     35    :attr:`request.META['REMOTE_USER'] <django.http.HttpRequest.META>`.  See
     36    the :ref:`Authenticating against REMOTE_USER <topics-auth-remote-user>`
     37    documentation.
  • docs/ref/index.txt

    === modified file 'docs/ref/index.txt'
     
    55
    66.. toctree::
    77   :maxdepth: 1
    8    
     8
     9   authbackends
    910   contrib/index
    1011   databases
    1112   django-admin
     
    1920   signals
    2021   templates/index
    2122   unicode
    22    
    23  No newline at end of file
  • docs/topics/auth-remote-user.txt

    === added file 'docs/topics/auth-remote-user.txt'
     
     1.. _topics-auth-remote-user:
     2
     3====================================
     4Authentication using ``REMOTE_USER``
     5====================================
     6
     7This document describes how to make use of external authentication sources
     8(where the Web server sets the ``REMOTE_USER`` environment variable) in your
     9Django applications.  This type of authentication solution is typically seen on
     10intranet sites, with single sign-on solutions such as IIS and Integrated
     11Windows Authentication or Apache and `mod_authnz_ldap`_, `CAS`_, `Cosign`_,
     12`WebAuth`_, `mod_auth_sspi`_, etc.
     13
     14.. _mod_authnz_ldap: http://httpd.apache.org/docs/2.2/mod/mod_authnz_ldap.html
     15.. _CAS: http://www.ja-sig.org/products/cas/
     16.. _Cosign: http://weblogin.org
     17.. _WebAuth: http://www.stanford.edu/services/webauth/
     18.. _mod_auth_sspi: http://sourceforge.net/projects/mod-auth-sspi
     19
     20When the Web server takes care of authentication it typically sets the
     21``REMOTE_USER`` environment variable for use in the underlying application.  In
     22Django, ``REMOTE_USER`` is made available in the :attr:`request.META
     23<django.http.HttpRequest.META>` attribute.  Django can be configured to make
     24use of the ``REMOTE_USER`` value using the ``RemoteUserMiddleware`` and
     25``RemoteUserBackend`` classes found in :mod:`django.contirb.auth`.
     26
     27Configuration
     28=============
     29
     30First, you must add the
     31:class:`django.contrib.auth.middleware.RemoteUserMiddleware` to the
     32:setting:`MIDDLEWARE_CLASSES` setting **after** the
     33:class:`django.contrib.auth.middleware.AuthenticationMiddleware`::
     34
     35    MIDDLEWARE_CLASSES = (
     36        ...
     37        'django.contrib.auth.middleware.AuthenticationMiddleware',
     38        'django.contrib.auth.middleware.RemoteUserMiddleware',
     39        ...
     40        )
     41
     42Next, you must replace the :class:`~django.contrib.auth.backends.ModelBackend`
     43with ``RemoteUserBackend`` in the :setting:`AUTHENTICATION_BACKENDS` setting::
     44
     45    AUTHENTICATION_BACKENDS = (
     46        'django.contrib.auth.backends.RemoteUserBackend',
     47    )
     48
     49With this setup, ``RemoteUserMiddleware`` will detect the username in
     50``request.META['REMOTE_USER']`` and will authenticate and auto-login that user
     51using the ``RemoteUserBackend``.
     52
     53.. note::
     54   Since the ``RemoteUserBackend`` inherits from ``ModelBackend``, you will
     55   still have all of the same permissions checking that is implemented in
     56   ``ModelBackend``.
     57
     58
     59``RemoteUserBackend``
     60=====================
     61
     62.. class:: django.contrib.backends.RemoteUserBackend
     63
     64If you need more control, you can create your own authentication backend
     65that inherits from ``RemoteUserBackend`` and overrides certain parts:
     66
     67Attributes
     68~~~~~~~~~~
     69
     70.. attribute:: RemoteUserBackend.create_unknown_user
     71
     72    ``True`` or ``False``.  Determines whether or not a
     73    :class:`~django.contrib.auth.models.User` object is created if not already
     74    in the database.  Defaults to ``True``.
     75
     76Methods
     77~~~~~~~
     78
     79.. method:: RemoteUserBackend.clean_username(username)
     80
     81   Clean unwanted text from the passed username.  This is useful if the
     82   username includes unwanted text, such extra LDAP DN information.  This
     83   method needs to return the cleaned username.
     84
     85.. method:: RemoteUserBackend.configure_user(user)
     86
     87   Configure a newly created user.  This method is called immediately after a
     88   new user is created, and can be used to perform custom setup actions for the
     89   user.  This methods needs to return the user object.
  • docs/topics/auth.txt

    === modified file 'docs/topics/auth.txt'
     
    12631263and the Django-based applications.
    12641264
    12651265So, to handle situations like this, the Django authentication system lets you
    1266 plug in another authentication sources. You can override Django's default
     1266plug in other authentication sources. You can override Django's default
    12671267database-based scheme, or you can use the default system in tandem with other
    12681268systems.
    12691269
     1270See the :ref:`authentication backend reference <ref-authentication-backends>`
     1271for information on the authentication backends included with Django.
     1272
    12701273Specifying authentication backends
    12711274----------------------------------
    12721275
  • docs/topics/index.txt

    === modified file 'docs/topics/index.txt'
     
    1717   files
    1818   testing
    1919   auth
     20   auth-remote-user
    2021   cache
    2122   email
    2223   i18n
Back to Top