Index: django/contrib/auth/contrib/ldap/__init__.py
===================================================================
Index: django/contrib/auth/contrib/ldap/config.py
===================================================================
--- django/contrib/auth/contrib/ldap/config.py	(revision 0)
+++ django/contrib/auth/contrib/ldap/config.py	(revision 0)
@@ -0,0 +1,400 @@
+"""
+This module contains classes that will be needed for configuration of LDAP
+authentication. Unlike backend.py, this is safe to import into settings.py.
+Please see the docstring on the backend module for more information, including
+notes on naming conventions.
+"""
+
+try:
+    set
+except NameError:
+    from sets import Set as set     # Python 2.3 fallback
+
+import logging
+import pprint
+
+
+class _LDAPConfig(object):
+    """
+    A private class that loads and caches some global objects.
+    """
+    ldap = None
+    logger = None
+    
+    def get_ldap(cls):
+        """
+        Returns the ldap module. The unit test harness will assign a mock object
+        to _LDAPConfig.ldap. It is imperative that the ldap module not be
+        imported anywhere else so that the unit tests will pass in the absence
+        of python-ldap.
+        """
+        if cls.ldap is None:
+            import ldap
+            import ldap.filter
+            import ldap.dn
+            
+            cls.ldap = ldap
+        
+        return cls.ldap
+    get_ldap = classmethod(get_ldap)
+
+    def get_logger(cls):
+        """
+        Initializes and returns our logger instance.
+        """
+        if cls.logger is None:
+            class NullHandler(logging.Handler):
+                def emit(self, record):
+                    pass
+    
+            cls.logger = logging.getLogger('django.contrib.auth.contrib.ldap')
+            cls.logger.addHandler(NullHandler())
+            cls.logger.setLevel(logging.DEBUG)
+
+        return cls.logger
+    get_logger = classmethod(get_logger)
+
+
+# Our global logger
+logger = _LDAPConfig.get_logger()
+
+
+class LDAPSearch(object):
+    """
+    Public class that holds a set of LDAP search parameters. Objects of this
+    class should be considered immutable. Only the initialization method is
+    documented for configuration purposes. Internal clients may use the other
+    methods to refine and execute the search.
+    """
+    def __init__(self, base_dn, scope, filterstr=u'(objectClass=*)'):
+        """
+        These parameters are the same as the first three parameters to
+        ldap.search_s.
+        """
+        self.base_dn = base_dn
+        self.scope = scope
+        self.filterstr = filterstr
+        self.ldap = _LDAPConfig.get_ldap()
+    
+    def search_with_additional_terms(self, term_dict, escape=True):
+        """
+        Returns a new search object with additional search terms and-ed to the
+        filter string. term_dict maps attribute names to assertion values. If
+        you don't want the values escaped, pass escape=False.
+        """
+        term_strings = [self.filterstr]
+        
+        for name, value in term_dict.iteritems():
+            if escape:
+                value = self.ldap.filter.escape_filter_chars(value)
+            term_strings.append(u'(%s=%s)' % (name, value))
+        
+        filterstr = u'(&%s)' % ''.join(term_strings)
+        
+        return self.__class__(self.base_dn, self.scope, filterstr)
+    
+    def search_with_additional_term_string(self, filterstr):
+        """
+        Returns a new search object with filterstr and-ed to the original filter
+        string. The caller is responsible for passing in a properly escaped
+        string.
+        """
+        filterstr = u'(&%s%s)' % (self.filterstr, filterstr)
+        
+        return self.__class__(self.base_dn, self.scope, filterstr)
+    
+    def execute(self, connection, filterargs=()):
+        """
+        Executes the search on the given connection (an LDAPObject). filterargs
+        is an object that will be used for expansion of the filter string.
+        
+        The python-ldap library returns utf8-encoded strings. For the sake of
+        sanity, this method will decode all result strings and return them as
+        Unicode.
+        """
+        try:
+            filterstr = self.filterstr % filterargs
+            results = connection.search_s(self.base_dn.encode('utf-8'),
+                self.scope, filterstr.encode('utf-8'))
+            results = _DeepStringCoder('utf-8').decode(results)
+
+            result_dns = [result[0] for result in results]
+            logger.debug(u"search_s('%s', %d, '%s') returned %d objects: %s" %
+                (self.base_dn, self.scope, filterstr, len(result_dns), "; ".join(result_dns)))
+        except self.ldap.LDAPError, e:
+            results = []
+            logger.error(u"search_s('%s', %d, '%s') raised %s" %
+                (self.base_dn, self.scope, filterstr, pprint.pformat(e)))
+        
+        return results
+
+
+class _DeepStringCoder(object):
+    """
+    Encodes and decodes strings in a nested structure of lists, tuples, and
+    dicts. This is helpful when interacting with the Unicode-unaware
+    python-ldap.
+    """
+    def __init__(self, encoding):
+        self.encoding = encoding
+    
+    def decode(self, value):
+        try:
+            if isinstance(value, str):
+                value = value.decode(self.encoding)
+            elif isinstance(value, list):
+                value = self._decode_list(value)
+            elif isinstance(value, tuple):
+                value = tuple(self._decode_list(value))
+            elif isinstance(value, dict):
+                value = self._decode_dict(value)
+        except UnicodeDecodeError:
+            pass
+        
+        return value
+    
+    def _decode_list(self, value):
+        return [self.decode(v) for v in value]
+    
+    def _decode_dict(self, value):
+        return dict([(self.decode(k), self.decode(v)) for k,v in value.iteritems()])
+
+
+class LDAPGroupType(object):
+    """
+    This is an abstract base class for classes that determine LDAP group
+    membership. A group can mean many different things in LDAP, so we will need
+    a concrete subclass for each grouping mechanism. Clients may subclass this
+    if they have a group mechanism that is not handled by a built-in
+    implementation.
+    
+    name_attr is the name of the LDAP attribute from which we will take the
+    Django group name.
+    
+    Subclasses in this file must use self.ldap to access the python-ldap module.
+    This will be a mock object during unit tests.
+    """
+    def __init__(self, name_attr="cn"):
+        self.name_attr = name_attr
+        self.ldap = _LDAPConfig.get_ldap()
+
+    def user_groups(self, ldap_user, group_search):
+        """
+        Returns a list of group_info structures, each one a group to which
+        ldap_user belongs. group_search is an LDAPSearch object that returns all
+        of the groups that the user might belong to. Typical implementations
+        will apply additional filters to group_search and return the results of
+        the search. ldap_user represents the user and has the following three
+        properties:
+        
+        dn: the distinguished name
+        attrs: a dictionary of LDAP attributes (with lists of values)
+        connection: an LDAPObject that has been bound with credentials
+        
+        This is the primitive method in the API and must be implemented.
+        """
+        return []
+    
+    def is_member(self, ldap_user, group_dn):
+        """
+        This method is an optimization for determining group membership without
+        loading all of the user's groups. Subclasses that are able to do this
+        may return True or False. ldap_user is as above. group_dn is the
+        distinguished name of the group in question.
+        
+        The base implementation returns None, which means we don't have enough
+        information. The caller will have to call user_groups() instead and look
+        for group_dn in the results.
+        """
+        return None
+
+    def group_name_from_info(self, group_info):
+        """
+        Given the (DN, attrs) 2-tuple of an LDAP group, this returns the name of
+        the Django group. This may return None to indicate that a particular
+        LDAP group has no corresponding Django group.
+        
+        The base implementation returns the value of the cn attribute, or
+        whichever attribute was given to __init__ in the name_attr
+        parameter.
+        """
+        try:
+            name = group_info[1][self.name_attr][0]
+        except (KeyError, IndexError):
+            name = None
+        
+        return name
+
+
+class PosixGroupType(LDAPGroupType):
+    """
+    An LDAPGroupType subclass that handles groups of class posixGroup.
+    """
+    def user_groups(self, ldap_user, group_search):
+        """
+        Searches for any group that is either the user's primary or contains the
+        user as a member.
+        """
+        groups = []
+        
+        try:
+            user_uid = ldap_user.attrs['uid'][0]
+            user_gid = ldap_user.attrs['gidNumber'][0]
+            
+            filterstr = u'(|(gidNumber=%s)(memberUid=%s))' % (
+                self.ldap.filter.escape_filter_chars(user_gid),
+                self.ldap.filter.escape_filter_chars(user_uid)
+            )
+            
+            search = group_search.search_with_additional_term_string(filterstr)
+            groups = search.execute(ldap_user.connection)
+        except (KeyError, IndexError):
+            pass
+        
+        return groups
+
+    def is_member(self, ldap_user, group_dn):
+        """
+        Returns True if the group is the user's primary group or if the user is
+        listed in the group's memberUid attribute.
+        """
+        try:
+            user_uid = ldap_user.attrs['uid'][0]
+            user_gid = ldap_user.attrs['gidNumber'][0]
+
+            is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'memberUid', user_uid.encode('utf-8'))
+            if not is_member:
+                is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'gidNumber', user_gid.encode('utf-8'))
+        except (KeyError, IndexError):
+            is_member = False
+        
+        return is_member
+
+
+class MemberDNGroupType(LDAPGroupType):
+    """
+    An abstract base class for group types that store lists of members as
+    distinguished names. Subclasses should set member_attr to define the member
+    attribute name.
+    """
+    def __init__(self, member_attr, name_attr='cn'):
+        """
+        member_attr is the attribute on the group object that holds the list of
+        member DNs.
+        """
+        self.member_attr = member_attr
+        
+        super(MemberDNGroupType, self).__init__(name_attr)
+    
+    def user_groups(self, ldap_user, group_search):
+        search = group_search.search_with_additional_terms(
+            {self.member_attr: ldap_user.dn})
+        groups = search.execute(ldap_user.connection)
+        
+        return groups
+
+    def is_member(self, ldap_user, group_dn):
+        return ldap_user.connection.compare_s(group_dn.encode('utf-8'),
+            self.member_attr.encode('utf-8'), ldap_user.dn.encode('utf-8'))
+
+
+class NestedMemberDNGroupType(LDAPGroupType):
+    """
+    An abstract base class for group types that store lists of members as
+    distinguished names and support nested groups. There is no shortcut for
+    is_member in this case, so it's left unimplemented.
+    """
+    def __init__(self, member_attr, name_attr='cn'):
+        """
+        member_attr is the attribute on the group object that holds the list of
+        member DNs.
+        """
+        self.member_attr = member_attr
+        
+        super(NestedMemberDNGroupType, self).__init__(name_attr)
+        
+    def user_groups(self, ldap_user, group_search):
+        """
+        This searches for all of a user's groups from the bottom up. In other
+        words, it returns the groups that the user belongs to, the groups that
+        those groups belong to, etc. Circular references will be detected and
+        pruned.
+        """
+        group_info_map = {} # Maps group_dn to group_info of groups we've found
+        member_dn_set = set([ldap_user.dn]) # Member DNs to search with next
+        handled_dn_set = set() # Member DNs that we've already searched with
+        
+        while len(member_dn_set) > 0:
+            group_infos = self.find_groups_with_any_member(member_dn_set,
+                group_search, ldap_user.connection)
+            new_group_info_map = dict([(info[0], info) for info in group_infos])
+            group_info_map.update(new_group_info_map)
+            handled_dn_set.update(member_dn_set)
+
+            # Get ready for the next iteration. To avoid cycles, we make sure
+            # never to search with the same member DN twice.
+            member_dn_set = set(new_group_info_map.keys()) - handled_dn_set
+        
+        return group_info_map.values()
+        
+    def find_groups_with_any_member(self, member_dn_set, group_search, connection):
+        terms = [
+            u"(%s=%s)" % (self.member_attr, self.ldap.filter.escape_filter_chars(dn))
+            for dn in member_dn_set
+        ]
+        
+        filterstr = u"(|%s)" % "".join(terms)
+        search = group_search.search_with_additional_term_string(filterstr)
+        
+        return search.execute(connection)
+
+
+class GroupOfNamesType(MemberDNGroupType):
+    """
+    An LDAPGroupType subclass that handles groups of class groupOfNames.
+    """
+    def __init__(self, name_attr='cn'):
+        super(GroupOfNamesType, self).__init__('member', name_attr)
+
+
+class NestedGroupOfNamesType(NestedMemberDNGroupType):
+    """
+    An LDAPGroupType subclass that handles groups of class groupOfNames with
+    nested group references.
+    """
+    def __init__(self, name_attr='cn'):
+        super(NestedGroupOfNamesType, self).__init__('member', name_attr)
+
+
+class GroupOfUniqueNamesType(MemberDNGroupType):
+    """
+    An LDAPGroupType subclass that handles groups of class groupOfUniqueNames.
+    """
+    def __init__(self, name_attr='cn'):
+        super(GroupOfUniqueNamesType, self).__init__('uniqueMember', name_attr)
+
+
+class NestedGroupOfUniqueNamesType(NestedMemberDNGroupType):
+    """
+    An LDAPGroupType subclass that handles groups of class groupOfUniqueNames
+    with nested group references.
+    """
+    def __init__(self, name_attr='cn'):
+        super(NestedGroupOfUniqueNamesType, self).__init__('uniqueMember', name_attr)
+
+
+class ActiveDirectoryGroupType(MemberDNGroupType):
+    """
+    An LDAPGroupType subclass that handles Active Directory groups.
+    """
+    def __init__(self, name_attr='cn'):
+        super(ActiveDirectoryGroupType, self).__init__('member', name_attr)
+
+
+class NestedActiveDirectoryGroupType(NestedMemberDNGroupType):
+    """
+    An LDAPGroupType subclass that handles Active Directory groups with nested
+    group references.
+    """
+    def __init__(self, name_attr='cn'):
+        super(NestedActiveDirectoryGroupType, self).__init__('member', name_attr)
Index: django/contrib/auth/contrib/ldap/backend.py
===================================================================
--- django/contrib/auth/contrib/ldap/backend.py	(revision 0)
+++ django/contrib/auth/contrib/ldap/backend.py	(revision 0)
@@ -0,0 +1,629 @@
+"""
+LDAP authentication backend
+
+Complete documentation can be found in docs/howto/auth-ldap.txt (or the thing it
+compiles to).
+
+Use of this backend requires the python-ldap module. To support unit tests, we
+import ldap in a single centralized place (config._LDAPConfig) so that the test
+harness can insert a mock object.
+
+A few notes on naming conventions. If an identifier ends in _dn, it is a string
+representation of a distinguished name. If it ends in _info, it is a 2-tuple
+containing a DN and a dictionary of lists of attributes. ldap.search_s returns a
+list of such structures. An identifier that ends in _attrs is the dictionary of
+attributes from the _info structure.
+
+A connection is an LDAPObject that has been successfully bound with a DN and
+password. The identifier 'user' always refers to a User model object; LDAP user
+information will be user_dn or user_info.
+
+Additional classes can be found in the config module next to this one.
+"""
+
+try:
+    set
+except NameError:
+    from sets import Set as set     # Python 2.3 fallback
+
+import pprint
+
+import django.db
+from django.contrib.auth.models import User, Group, SiteProfileNotAvailable
+from django.contrib.auth.contrib.ldap.config import _LDAPConfig, LDAPSearch
+from django.core.cache import cache
+from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
+
+
+logger = _LDAPConfig.get_logger()
+
+
+class LDAPBackend(object):
+    """
+    The main backend class. This implements the auth backend API, although it
+    actually delegates most of its work to _LDAPUser, which is defined next.
+    """
+    ldap = None # The cached ldap module (or mock object)
+    
+    def __init__(self):
+        self.ldap = self.ldap_module()
+    
+    def ldap_module(cls):
+        """
+        Requests the ldap module from _LDAPConfig. Under a test harness, this
+        will be a mock object. We only do this once because this is where we
+        apply AUTH_LDAP_GLOBAL_OPTIONS.
+        """
+        if cls.ldap is None:
+            cls.ldap = _LDAPConfig.get_ldap()
+            
+            for opt, value in ldap_settings.AUTH_LDAP_GLOBAL_OPTIONS.iteritems():
+                cls.ldap.set_option(opt, value)
+        
+        return cls.ldap
+    ldap_module = classmethod(ldap_module)
+        
+    
+    #
+    # The Django auth backend API
+    #
+
+    def authenticate(self, username, password):
+        ldap_user = _LDAPUser(self, username=username)
+        user = ldap_user.authenticate(password)
+        
+        return user
+    
+    def get_user(self, user_id):
+        user = None
+        
+        try:
+            user = User.objects.get(pk=user_id)
+            _LDAPUser(self, user=user) # This sets user.ldap_user
+        except User.DoesNotExist:
+            pass
+        
+        return user
+    
+    def has_perm(self, user, perm):
+        return perm in self.get_all_permissions(user)
+
+    def has_module_perms(self, user, app_label):
+        for perm in self.get_all_permissions(user):
+            if perm[:perm.index('.')] == app_label:
+                return True
+
+        return False
+
+    def get_all_permissions(self, user):
+        return self.get_group_permissions(user)
+
+    def get_group_permissions(self, user):
+        if hasattr(user, 'ldap_user'):
+            return user.ldap_user.get_group_permissions()
+        else:
+            return set()
+    
+    #
+    # Hooks for subclasses
+    #
+    
+    def ldap_to_django_username(self, username):
+        return username
+
+    def django_to_ldap_username(self, username):
+        return username
+
+
+class _LDAPUser(object):
+    """
+    Represents an LDAP user and ultimately fields all requests that the
+    backend receives. This class exists for two reasons. First, it's
+    convenient to have a separate object for each request so that we can use
+    object attributes without running into threading problems. Second, these
+    objects get attached to the User objects, which allows us to cache
+    expensive LDAP information, especially around groups and permissions.
+    
+    self.backend is a reference back to the LDAPBackend instance, which we need
+    to access the ldap module and any hooks that a subclass has overridden.
+    """
+    class AuthenticationFailed(Exception):
+        pass
+    
+    #
+    # Initialization
+    #
+    
+    def __init__(self, backend, username=None, user=None):
+        """
+        A new LDAPUser must be initialized with either a username or an
+        authenticated User object. If a user is given, the username will be
+        ignored.
+        """
+        self.backend = backend
+        self.ldap = backend.ldap_module()
+        self._username = username
+        self._password = None
+        self._user_dn = None
+        self._user_attrs = None
+        self._user = None
+        self._groups = None
+        self._group_permissions = None
+        self._connection = None
+        self._connection_bound = False
+        
+        if user is not None:
+            self._set_authenticated_user(user)
+        
+        if username is None and user is None:
+            raise Exception("Internal error: _LDAPUser improperly initialized.")
+    
+    def _set_authenticated_user(self, user):
+        self._user = user
+        self._username = self.backend.django_to_ldap_username(user.username)
+
+        user.ldap_user = self
+        user.ldap_username = self._username
+    
+    #
+    # Entry points
+    #
+    
+    def authenticate(self, password):
+        """
+        Authenticates against the LDAP directory and returns the corresponding
+        User object if successful. Returns None on failure.
+        """
+        user = None
+        
+        self._password = password
+        
+        try:
+            self._authenticate_user_dn()
+            self._check_requirements()
+            self._get_or_create_user()
+
+            user = self._user
+        except self.AuthenticationFailed, e:
+            logger.debug(u"Authentication failed for %s" % self._username)
+        except self.ldap.LDAPError, e:
+            logger.warning(u"Caught LDAPError while authenticating %s: %s",
+                self._username, pprint.pformat(e))
+        except Exception, e:
+            logger.warning(u"Caught Exception while authenticating %s: %s",
+                self._username, pprint.pformat(e))
+            raise
+        
+        return user
+    
+    def get_group_permissions(self):
+        """
+        If allowed by the configuration, this returns the set of permissions
+        defined by the user's LDAP group memberships.
+        """
+        if self._group_permissions is None:
+            self._group_permissions = set()
+
+            if ldap_settings.AUTH_LDAP_FIND_GROUP_PERMS:
+                try:
+                    self._load_group_permissions()
+                except self.ldap.LDAPError, e:
+                    logger.warning("Caught LDAPError loading group permissions: %s",
+                        pprint.pformat(e))
+        
+        return self._group_permissions
+
+    #
+    # Public properties (callbacks). These are all lazy for performance reasons.
+    #
+
+    def _get_user_dn(self):
+        if self._user_dn is None:
+            self._load_user_dn()
+        
+        return self._user_dn
+    dn = property(_get_user_dn)
+
+    def _get_user_attrs(self):
+        if self._user_attrs is None:
+            self._load_user_attrs()
+        
+        return self._user_attrs
+    attrs = property(_get_user_attrs)
+
+    def _get_bound_connection(self):
+        if not self._connection_bound:
+            self._bind()
+        
+        return self._get_connection()
+    connection = property(_get_bound_connection)
+
+    #
+    # Authentication
+    #
+
+    def _authenticate_user_dn(self):
+        """
+        Binds to the LDAP server with the user's DN and password. Raises
+        AuthenticationFailed on failure.
+        """
+        try:
+            self._bind_as(self.dn, self._password)
+        except self.ldap.INVALID_CREDENTIALS:
+            raise self.AuthenticationFailed("User DN/password rejected by LDAP server.")
+    
+    def _load_user_attrs(self):
+        search = LDAPSearch(self.dn, self.ldap.SCOPE_BASE)
+        results = search.execute(self.connection)
+        
+        self._user_attrs = results[0][1]
+    
+    def _load_user_dn(self):
+        """
+        Returns the (cached) distinguished name of our user. This will
+        ultimately either construct the DN from a template in
+        AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it.
+        This may result in an AuthenticationFailed exception if we do not get
+        satisfactory results searching for the user's DN.
+        """
+        if self._using_simple_bind_mode():
+            self._construct_simple_user_dn()
+        else:
+            self._search_for_user_dn()
+
+    def _using_simple_bind_mode(self):
+        return (ldap_settings.AUTH_LDAP_USER_DN_TEMPLATE is not None)
+
+    def _construct_simple_user_dn(self):
+        template = ldap_settings.AUTH_LDAP_USER_DN_TEMPLATE
+        username = self.ldap.dn.escape_dn_chars(self._username)
+        
+        self._user_dn = template % {'user': username}
+
+    def _search_for_user_dn(self):
+        """
+        Searches the directory for a user matching AUTH_LDAP_USER_SEARCH.
+        Populates self._user_dn and self._user_attrs.
+        """
+        search = ldap_settings.AUTH_LDAP_USER_SEARCH
+        if search is None:
+            raise ImproperlyConfigured('AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.')
+        
+        results = search.execute(self.connection, {'user': self._username})
+        if results is None or len(results) != 1:
+            raise self.AuthenticationFailed("AUTH_LDAP_USER_SEARCH failed to return exactly one result.")
+
+        (self._user_dn, self._user_attrs) = results[0]
+
+    def _check_requirements(self):
+        """
+        Checks all authentication requirements beyond credentials. Raises
+        AuthenticationFailed on failure.
+        """
+        self._check_required_group()
+    
+    def _check_required_group(self):
+        """
+        Returns True if the group requirement (AUTH_LDAP_REQUIRE_GROUP) is
+        met. Always returns True if AUTH_LDAP_REQUIRE_GROUP is None.
+        """
+        required_group_dn = ldap_settings.AUTH_LDAP_REQUIRE_GROUP
+        
+        if required_group_dn is not None:
+            is_member = self._get_groups().is_member_of(required_group_dn)
+            if not is_member:
+                raise self.AuthenticationFailed("User is not a member of AUTH_LDAP_REQUIRE_GROUP")
+
+    #
+    # User management
+    #
+
+    def _get_or_create_user(self):
+        """
+        Loads the User model object from the database or creates it if it
+        doesn't exist. Also populates the fields, subject to
+        AUTH_LDAP_ALWAYS_UPDATE_USER.
+        """
+        save_user = False
+        
+        username = self.backend.ldap_to_django_username(self._username)
+
+        (self._user, created) = User.objects.get_or_create(username=username)
+
+        if created:
+            logger.debug("Created Django user %s", username)
+            self._user.set_unusable_password()
+            save_user = True
+
+        if(ldap_settings.AUTH_LDAP_ALWAYS_UPDATE_USER or created):
+            logger.debug("Populating Django user %s", username)
+            self._populate_user()
+            self._populate_and_save_user_profile()
+            save_user = True
+
+        if ldap_settings.AUTH_LDAP_MIRROR_GROUPS:
+            self._mirror_groups()
+
+        if save_user:
+            self._user.save()
+
+        self._user.ldap_user = self
+        self._user.ldap_username = self._username
+
+    def _populate_user(self):
+        """
+        Populates our User object with information from the LDAP directory.
+        """
+        self._populate_user_from_attributes()
+        self._populate_user_from_group_memberships()
+    
+    def _populate_user_from_attributes(self):
+        for field, attr in ldap_settings.AUTH_LDAP_USER_ATTR_MAP.iteritems():
+            try:
+                setattr(self._user, field, self.attrs[attr][0])
+            except (KeyError, IndexError):
+                pass
+    
+    def _populate_user_from_group_memberships(self):
+        for field, group_dn in ldap_settings.AUTH_LDAP_USER_FLAGS_BY_GROUP.iteritems():
+            value = self._get_groups().is_member_of(group_dn)
+            setattr(self._user, field, value)
+
+    def _populate_and_save_user_profile(self):
+        """
+        Populates a User profile object with fields from the LDAP directory.
+        """
+        try:
+            profile = self._user.get_profile()
+
+            for field, attr in ldap_settings.AUTH_LDAP_PROFILE_ATTR_MAP.iteritems():
+                try:
+                    # user_attrs is a hash of lists of attribute values
+                    setattr(profile, field, self.attrs[attr][0])
+                except (KeyError, IndexError):
+                    pass
+
+            if len(ldap_settings.AUTH_LDAP_PROFILE_ATTR_MAP) > 0:
+                profile.save()
+        except (SiteProfileNotAvailable, ObjectDoesNotExist):
+            pass
+    
+    def _mirror_groups(self):
+        """
+        Mirrors the user's LDAP groups in the Django database and updates the
+        user's membership.
+        """
+        group_names = self._get_groups().get_group_names()
+        groups = [Group.objects.get_or_create(name=group_name)[0] for group_name
+            in group_names]
+        
+        self._user.groups = groups
+    
+    #
+    # Group information
+    #
+    
+    def _load_group_permissions(self):
+        """
+        Populates self._group_permissions based on LDAP group membership and
+        Django group permissions.
+        
+        The SQL is lifted (with modifications) from ModelBackend.
+        """
+        group_names = self._get_groups().get_group_names()
+        placeholders = ', '.join(['%s'] * len(group_names))
+        
+        cursor = django.db.connection.cursor()
+        # The SQL below works out to the following, after DB quoting:
+        # cursor.execute("""
+        #     SELECT ct."app_label", p."codename"
+        #     FROM "auth_permission" p, "auth_group_permissions" gp, "auth_group" g, "django_content_type" ct
+        #     WHERE p."id" = gp."permission_id"
+        #         AND gp."group_id" = g."id"
+        #         AND ct."id" = p."content_type_id"
+        #         AND g."name" IN (%s, %s, ...)""", ['group1', 'group2', ...])
+        qn = django.db.connection.ops.quote_name
+        sql = u"""
+            SELECT ct.%s, p.%s
+            FROM %s p, %s gp, %s g, %s ct
+            WHERE p.%s = gp.%s
+                AND gp.%s = g.%s
+                AND ct.%s = p.%s
+                AND g.%s IN (%s)""" % (
+            qn('app_label'), qn('codename'),
+            qn('auth_permission'), qn('auth_group_permissions'),
+            qn('auth_group'), qn('django_content_type'),
+            qn('id'), qn('permission_id'),
+            qn('group_id'), qn('id'),
+            qn('id'), qn('content_type_id'),
+            qn('name'), placeholders)
+        
+        cursor.execute(sql, group_names)
+        self._group_permissions = \
+            set([u"%s.%s" % (row[0], row[1]) for row in cursor.fetchall()])
+
+    def _get_groups(self):
+        """
+        Returns an _LDAPUserGroups object, which can determine group
+        membership.
+        """
+        if self._groups is None:
+            self._groups = _LDAPUserGroups(self)
+        
+        return self._groups
+
+    #
+    # LDAP connection
+    #
+
+    def _bind(self):
+        """
+        Binds to the LDAP server with AUTH_LDAP_BIND_DN and
+        AUTH_LDAP_BIND_PASSWORD.
+        """
+        self._bind_as(ldap_settings.AUTH_LDAP_BIND_DN,
+            ldap_settings.AUTH_LDAP_BIND_PASSWORD)
+    
+    def _bind_as(self, bind_dn, bind_password):
+        """
+        Binds to the LDAP server with the given credentials. This does not trap
+        exceptions.
+        """
+        self._get_connection().simple_bind_s(bind_dn.encode('utf-8'),
+            bind_password.encode('utf-8'))
+        self._connection_bound = True
+
+    def _get_connection(self):
+        """
+        Returns our cached LDAPObject, which may or may not be bound.
+        """
+        if self._connection is None:
+            self._connection = self.ldap.initialize(ldap_settings.AUTH_LDAP_SERVER_URI)
+            
+            for opt, value in ldap_settings.AUTH_LDAP_CONNECTION_OPTIONS.iteritems():
+                self._connection.set_option(opt, value)
+        
+        return self._connection
+
+
+
+class _LDAPUserGroups(object):
+    """
+    Represents the set of groups that a user belongs to.
+    """
+    def __init__(self, ldap_user):
+        self._ldap_user = ldap_user
+        self._group_type = None
+        self._group_search = None
+        self._group_infos = None
+        self._group_dns = None
+        self._group_names = None
+        
+        self._init_group_settings()
+    
+    def _init_group_settings(self):
+        """
+        Loads the settings we need to deal with groups. Raises
+        ImproperlyConfigured if anything's not right.
+        """
+        self._group_type = ldap_settings.AUTH_LDAP_GROUP_TYPE
+        if self._group_type is None:
+            raise ImproperlyConfigured("AUTH_LDAP_GROUP_TYPE must be an LDAPGroupType instance.")
+        
+        self._group_search = ldap_settings.AUTH_LDAP_GROUP_SEARCH
+        if self._group_search is None:
+            raise ImproperlyConfigured("AUTH_LDAP_GROUP_SEARCH must be an LDAPSearch instance.")
+    
+    def get_group_names(self):
+        """
+        Returns the list of Django group names that this user belongs to by
+        virtue of LDAP group memberships.
+        """
+        if self._group_names is None:
+            self._load_cached_attr("_group_names")
+        
+        if self._group_names is None:
+            group_infos = self._get_group_infos()
+            self._group_names = [self._group_type.group_name_from_info(group_info)
+                for group_info in group_infos]
+            self._cache_attr("_group_names")
+        
+        return self._group_names
+    
+    def is_member_of(self, group_dn):
+        """
+        Returns true if our user is a member of the given group.
+        """
+        is_member = None
+        
+        # If we have self._group_dns, we'll use it. Otherwise, we'll try to
+        # avoid the cost of loading it.
+        if self._group_dns is None:
+            is_member = self._group_type.is_member(self._ldap_user, group_dn)
+        
+        if is_member is None:
+            is_member = (group_dn in self._get_group_dns())
+        
+        logger.debug("%s is%sa member of %s", self._ldap_user.dn,
+                     is_member and " " or " not ", group_dn)
+
+        return is_member
+    
+    def _get_group_dns(self):
+        """
+        Returns a (cached) set of the distinguished names in self._group_infos.
+        """
+        if self._group_dns is None:
+            group_infos = self._get_group_infos()
+            self._group_dns = set([group_info[0] for group_info in group_infos])
+        
+        return self._group_dns
+    
+    def _get_group_infos(self):
+        """
+        Returns a (cached) list of group_info structures for the groups that our
+        user is a member of.
+        """
+        if self._group_infos is None:
+            self._group_infos = self._group_type.user_groups(self._ldap_user,
+                self._group_search)
+        
+        return self._group_infos
+
+    def _load_cached_attr(self, attr_name):
+        if ldap_settings.AUTH_LDAP_CACHE_GROUPS:
+            key = self._cache_key(attr_name)
+            value = cache.get(key)
+            setattr(self, attr_name, value)
+    
+    def _cache_attr(self, attr_name):
+        if ldap_settings.AUTH_LDAP_CACHE_GROUPS:
+            key = self._cache_key(attr_name)
+            value = getattr(self, attr_name, None)
+            cache.set(key, value, ldap_settings.AUTH_LDAP_GROUP_CACHE_TIMEOUT)
+    
+    def _cache_key(self, attr_name):
+        return u'auth_ldap.%s.%s.%s' % (self.__class__.__name__, attr_name, self._ldap_user.dn)
+
+
+class LDAPSettings(object):
+    """
+    This is a simple class to take the place of the global settings object. An
+    instance will contain all of our settings as attributes, with default values
+    if they are not specified by the configuration.
+    """
+    defaults = {
+        'AUTH_LDAP_ALWAYS_UPDATE_USER': True,
+        'AUTH_LDAP_BIND_DN': '',
+        'AUTH_LDAP_BIND_PASSWORD': '',
+        'AUTH_LDAP_CACHE_GROUPS': False,
+        'AUTH_LDAP_CONNECTION_OPTIONS': {},
+        'AUTH_LDAP_FIND_GROUP_PERMS': False,
+        'AUTH_LDAP_GLOBAL_OPTIONS': {},
+        'AUTH_LDAP_GROUP_CACHE_TIMEOUT': None,
+        'AUTH_LDAP_GROUP_SEARCH': None,
+        'AUTH_LDAP_GROUP_TYPE': None,
+        'AUTH_LDAP_MIRROR_GROUPS': False,
+        'AUTH_LDAP_PROFILE_ATTR_MAP': {},
+        'AUTH_LDAP_REQUIRE_GROUP': None,
+        'AUTH_LDAP_SERVER_URI': 'ldap://localhost',
+        'AUTH_LDAP_USER_ATTR_MAP': {},
+        'AUTH_LDAP_USER_DN_TEMPLATE': None,
+        'AUTH_LDAP_USER_FLAGS_BY_GROUP': {},
+        'AUTH_LDAP_USER_SEARCH': None,
+    }
+    
+    def __init__(self):
+        """
+        Loads our settings from django.conf.settings, applying defaults for any
+        that are omitted.
+        """
+        from django.conf import settings
+        
+        for name, default in self.defaults.iteritems():
+            value = getattr(settings, name, default)
+            setattr(self, name, value)
+
+
+# Our global settings object
+ldap_settings = LDAPSettings()
Index: django/contrib/auth/contrib/__init__.py
===================================================================
Index: django/contrib/auth/tests/__init__.py
===================================================================
--- django/contrib/auth/tests/__init__.py	(revision 11638)
+++ django/contrib/auth/tests/__init__.py	(working copy)
@@ -4,6 +4,7 @@
 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.ldap import LDAPTest
 from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
 
 # The password for the fixture data users is 'password'
Index: django/contrib/auth/tests/ldap.py
===================================================================
--- django/contrib/auth/tests/ldap.py	(revision 0)
+++ django/contrib/auth/tests/ldap.py	(revision 0)
@@ -0,0 +1,919 @@
+# coding: utf-8
+
+try:
+    set
+except NameError:
+    from sets import Set as set     # Python 2.3 fallback
+
+import logging
+import sys
+
+from django.contrib.auth.contrib.ldap import backend
+from django.contrib.auth.contrib.ldap.config import _LDAPConfig, LDAPSearch
+from django.contrib.auth.contrib.ldap.config import PosixGroupType, MemberDNGroupType, NestedMemberDNGroupType
+from django.contrib.auth.contrib.ldap.config import GroupOfNamesType, NestedGroupOfNamesType
+from django.contrib.auth.contrib.ldap.config import GroupOfUniqueNamesType, NestedGroupOfUniqueNamesType
+from django.contrib.auth.contrib.ldap.config import ActiveDirectoryGroupType, NestedActiveDirectoryGroupType
+from django.contrib.auth.models import User, Permission, Group
+from django.test import TestCase
+
+
+class TestSettings(backend.LDAPSettings):
+    """
+    A replacement for backend.LDAPSettings that does not load settings
+    from django.conf.
+    """
+    def __init__(self, **kwargs):
+        for name, default in self.defaults.iteritems():
+            value = kwargs.get(name, default)
+            setattr(self, name, value)
+
+
+class MockLDAP(object):
+    """
+    This is a stand-in for the python-ldap module; it serves as both the ldap
+    module and the LDAPObject class. While it's temping to add some real LDAP
+    capabilities here, this is designed to remain as simple as possible, so as
+    to minimize the risk of creating bogus unit tests through a buggy test
+    harness.
+    
+    Simple operations can be simulated, but for nontrivial searches, the client
+    will have to seed the mock object with return values for expected API calls.
+    This may sound like cheating, but it's really no more so than a simulated
+    LDAP server. The fact is we can not require python-ldap to be installed in
+    order to run the unit tests, so all we can do is verify that LDAPBackend is
+    calling the APIs that we expect.
+
+    set_return_value takes the name of an API, a tuple of arguments, and a
+    return value. Every time an API is called, it looks for a predetermined
+    return value based on the arguments received. If it finds one, then it
+    returns it, or raises it if it's an Exception. If it doesn't find one, then
+    it tries to satisfy the request internally. If it can't, it raises a
+    PresetReturnRequiredError.
+    
+    At any time, the client may call ldap_methods_called_with_arguments() or
+    ldap_methods_called() to get a record of all of the LDAP API calls that have
+    been made, with or without arguments.
+    """
+    
+    class PresetReturnRequiredError(Exception): pass
+    
+    SCOPE_BASE = 0
+    SCOPE_ONELEVEL = 1
+    SCOPE_SUBTREE = 2
+    
+    class LDAPError(Exception): pass
+    class INVALID_CREDENTIALS(LDAPError): pass
+    class NO_SUCH_OBJECT(LDAPError): pass
+    
+    #
+    # Submodules
+    #
+    class dn(object):
+        def escape_dn_chars(s):
+            return s
+        escape_dn_chars = staticmethod(escape_dn_chars)
+
+    class filter(object):
+        def escape_filter_chars(s):
+            return s
+        escape_filter_chars = staticmethod(escape_filter_chars)
+
+
+    def __init__(self, directory):
+        """
+        directory is a complex structure with the entire contents of the
+        mock LDAP directory. directory must be a dictionary mapping
+        distinguished names to dictionaries of attributes. Each attribute
+        dictionary maps attribute names to lists of values. e.g.:
+        
+        {
+            "uid=alice,ou=users,dc=example,dc=com":
+            { 
+                "uid": ["alice"],
+                "userPassword": ["secret"],
+            },
+        }
+        """
+        self.directory = directory
+
+        self.reset()
+    
+    def reset(self):
+        """
+        Resets our recorded API calls and queued return values as well as
+        miscellaneous configuration options.
+        """
+        self.calls = []
+        self.return_value_maps = {}
+        self.options = {}
+    
+    def set_return_value(self, api_name, arguments, value):
+        """
+        Stores a preset return value for a given API with a given set of
+        arguments.
+        """
+        self.return_value_maps.setdefault(api_name, {})[arguments] = value
+    
+    def ldap_methods_called_with_arguments(self):
+        """
+        Returns a list of 2-tuples, one for each API call made since the last
+        reset. Each tuple contains the name of the API and a dictionary of
+        arguments. Argument defaults are included.
+        """
+        return self.calls
+    
+    def ldap_methods_called(self):
+        """
+        Returns the list of API names called.
+        """
+        return [call[0] for call in self.calls]
+    
+    #
+    # Begin LDAP methods
+    #
+    
+    def set_option(self, option, invalue):
+        self._record_call('set_option', {
+            'option': option,
+            'invalue': invalue
+        })
+        
+        self.options[option] = invalue
+    
+    def initialize(self, uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None):
+        self._record_call('initialize', {
+            'uri': uri,
+            'trace_level': trace_level,
+            'trace_file': trace_file,
+            'trace_stack_limit': trace_stack_limit
+        })
+        
+        value = self._get_return_value('initialize',
+            (uri, trace_level, trace_file, trace_stack_limit))
+        if value is None:
+            value = self
+        
+        return value
+
+    def simple_bind_s(self, who='', cred=''):
+        self._record_call('simple_bind_s', {
+            'who': who,
+            'cred': cred
+        })
+        
+        value = self._get_return_value('simple_bind_s', (who, cred))
+        if value is None:
+            value = self._simple_bind_s(who, cred)
+        
+        return value
+
+    def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
+        self._record_call('search_s', {
+            'base': base,
+            'scope': scope,
+            'filterstr':filterstr,
+            'attrlist':attrlist,
+            'attrsonly':attrsonly
+        })
+        
+        value = self._get_return_value('search_s',
+            (base, scope, filterstr, attrlist, attrsonly))
+        if value is None:
+            value = self._search_s(base, scope, filterstr, attrlist, attrsonly)
+        
+        return value
+    
+    def compare_s(self, dn, attr, value):
+        self._record_call('compare_s', {
+            'dn': dn,
+            'attr': attr,
+            'value': value
+        })
+        
+        result = self._get_return_value('compare_s', (dn, attr, value))
+        if result is None:
+            result = self._compare_s(dn, attr, value)
+        
+        # print "compare_s('%s', '%s', '%s'): %d" % (dn, attr, value, result)
+        
+        return result
+
+    #
+    # Internal implementations
+    #
+
+    def _simple_bind_s(self, who='', cred=''):
+        success = False
+        
+        if(who == '' and cred == ''):
+            success = True 
+        elif self._compare_s(who, 'userPassword', cred):
+            success = True
+
+        if success:
+            return (97, []) # python-ldap returns this; I don't know what it means
+        else:
+            raise self.INVALID_CREDENTIALS('%s:%s' % (who, cred))
+    
+    def _compare_s(self, dn, attr, value):
+        try:
+            found = (value in self.directory[dn][attr])
+        except KeyError:
+            found = False
+        
+        return found and 1 or 0
+    
+    def _search_s(self, base, scope, filterstr, attrlist, attrsonly):
+        """
+        We can do a SCOPE_BASE search with the default filter. Beyond that,
+        you're on your own.
+        """
+        if scope != self.SCOPE_BASE:
+            raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' %
+                (base, scope, filterstr, attrlist, attrsonly))
+        
+        if filterstr != '(objectClass=*)':
+            raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' %
+                (base, scope, filterstr, attrlist, attrsonly))
+        
+        attrs = self.directory.get(base)
+        if attrs is None:
+            raise self.NO_SUCH_OBJECT()
+        
+        return [(base, attrs)]
+    
+    #
+    # Utils
+    #
+
+    def _record_call(self, api_name, arguments):
+        self.calls.append((api_name, arguments))
+
+    def _get_return_value(self, api_name, arguments):
+        try:
+            value = self.return_value_maps[api_name][arguments]
+        except KeyError:
+            value = None
+        
+        if isinstance(value, Exception):
+            raise value
+        
+        return value
+
+
+class LDAPTest(TestCase):
+    
+    # Following are the objecgs in our mock LDAP directory
+    alice = ("uid=alice,ou=people,o=test", {
+        "uid": ["alice"],
+        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
+        "userPassword": ["password"],
+        "uidNumber": ["1000"],
+        "gidNumber": ["1000"],
+        "givenName": ["Alice"],
+        "sn": ["Adams"]
+    })
+    bob = ("uid=bob,ou=people,o=test", {
+        "uid": ["bob"],
+        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
+        "userPassword": ["password"],
+        "uidNumber": ["1001"],
+        "gidNumber": ["50"],
+        "givenName": ["Robert"],
+        "sn": ["Barker"]
+    })
+    dressler = (u"uid=dreßler,ou=people,o=test".encode('utf-8'), {
+        "uid": [u"dreßler".encode('utf-8')],
+        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
+        "userPassword": ["password"],
+        "uidNumber": ["1002"],
+        "gidNumber": ["50"],
+        "givenName": ["Wolfgang"],
+        "sn": [u"Dreßler".encode('utf-8')]
+    })
+    nobody = ("uid=nobody,ou=people,o=test", {
+        "uid": ["nobody"],
+        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
+        "userPassword": ["password"],
+        "binaryAttr": ["\xb2"]  # Invalid UTF-8
+    })
+
+    # posixGroup objects
+    active_px = ("cn=active_px,ou=groups,o=test", {
+        "cn": ["active_px"],
+        "objectClass": ["posixGroup"],
+        "gidNumber": ["1000"],
+    })
+    staff_px = ("cn=staff_px,ou=groups,o=test", {
+        "cn": ["staff_px"],
+        "objectClass": ["posixGroup"],
+        "gidNumber": ["1001"],
+        "memberUid": ["alice"],
+    })
+    superuser_px = ("cn=superuser_px,ou=groups,o=test", {
+        "cn": ["superuser_px"],
+        "objectClass": ["posixGroup"],
+        "gidNumber": ["1002"],
+        "memberUid": ["alice"],
+    })
+
+    # groupOfUniqueName groups
+    active_gon = ("cn=active_gon,ou=groups,o=test", {
+        "cn": ["active_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": ["uid=alice,ou=people,o=test"]
+    })
+    staff_gon = ("cn=staff_gon,ou=groups,o=test", {
+        "cn": ["staff_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": ["uid=alice,ou=people,o=test"]
+    })
+    superuser_gon = ("cn=superuser_gon,ou=groups,o=test", {
+        "cn": ["superuser_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": ["uid=alice,ou=people,o=test"]
+    })
+    
+    # Nested groups with a circular reference
+    parent_gon = ("cn=parent_gon,ou=groups,o=test", {
+        "cn": ["parent_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": ["cn=nested_gon,ou=groups,o=test"]
+    })
+    nested_gon = ("cn=nested_gon,ou=groups,o=test", {
+        "cn": ["nested_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": [
+            "uid=alice,ou=people,o=test",
+            "cn=circular_gon,ou=groups,o=test"
+        ]
+    })
+    circular_gon = ("cn=circular_gon,ou=groups,o=test", {
+        "cn": ["circular_gon"],
+        "objectClass": ["groupOfNames"],
+        "member": ["cn=parent_gon,ou=groups,o=test"]
+    })
+    
+
+    mock_ldap = MockLDAP({
+        alice[0]: alice[1],
+        bob[0]: bob[1],
+        dressler[0]: dressler[1],
+        nobody[0]: nobody[1],
+        active_px[0]: active_px[1],
+        staff_px[0]: staff_px[1],
+        superuser_px[0]: superuser_px[1],
+        active_gon[0]: active_gon[1],
+        staff_gon[0]: staff_gon[1],
+        superuser_gon[0]: superuser_gon[1],
+        parent_gon[0]: parent_gon[1],
+        nested_gon[0]: nested_gon[1],
+        circular_gon[0]: circular_gon[1],
+    })
+    
+
+    logging_configured = False
+    def configure_logger(cls):
+        if not cls.logging_configured:
+            logger = logging.getLogger('django.contrib.auth.contrib.ldap')
+            formatter = logging.Formatter("LDAP auth - %(levelname)s - %(message)s")
+            handler = logging.StreamHandler()
+        
+            handler.setLevel(logging.DEBUG)
+            handler.setFormatter(formatter)
+            logger.addHandler(handler)
+        
+            logger.setLevel(logging.CRITICAL)
+            
+            cls.logging_configured = True
+    configure_logger = classmethod(configure_logger)
+    
+
+    def setUp(self):
+        self.configure_logger()
+        self.mock_ldap.reset()
+
+        _LDAPConfig.ldap = self.mock_ldap
+        self.backend = backend.LDAPBackend()
+    
+    
+    def tearDown(self):
+        pass
+
+    
+    def test_options(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_CONNECTION_OPTIONS={'opt1': 'value1'}
+        )
+        
+        user = self.backend.authenticate(username='alice', password='password')
+        
+        self.assertEqual(self.mock_ldap.options, {'opt1': 'value1'})
+    
+    
+    def test_simple_bind(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
+        )
+        user_count = User.objects.count()
+        
+        user = self.backend.authenticate(username='alice', password='password')
+        
+        self.assert_(not user.has_usable_password())
+        self.assertEqual(user.username, 'alice')
+        self.assertEqual(User.objects.count(), user_count + 1)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s'])
+
+
+    def test_simple_bind_bad_user(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
+        )
+        user_count = User.objects.count()
+
+        user = self.backend.authenticate(username='evil_alice', password='password')
+
+        self.assert_(user is None)
+        self.assertEqual(User.objects.count(), user_count)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s'])
+
+
+    def test_simple_bind_bad_password(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
+        )
+        user_count = User.objects.count()
+
+        user = self.backend.authenticate(username='alice', password='bogus')
+
+        self.assert_(user is None)
+        self.assertEqual(User.objects.count(), user_count)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s'])
+    
+    
+    def test_existing_user(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
+        )
+        User.objects.create(username='alice')
+        user_count = User.objects.count()
+        
+        user = self.backend.authenticate(username='alice', password='password')
+        
+        # Make sure we only created one user
+        self.assert_(user is not None)
+        self.assertEqual(User.objects.count(), user_count)
+
+
+    def test_convert_username(self):
+        class MyBackend(backend.LDAPBackend):
+            def ldap_to_django_username(self, username):
+                return 'ldap_%s' % username
+            def django_to_ldap_username(self, username):
+                return username[5:]
+        
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
+        )
+        user_count = User.objects.count()
+        self.backend = MyBackend()
+        
+        user1 = self.backend.authenticate(username='alice', password='password')
+        user2 = self.backend.get_user(user1.pk)
+        
+        self.assertEqual(User.objects.count(), user_count + 1)
+        self.assertEqual(user1.username, 'ldap_alice')
+        self.assertEqual(user1.ldap_user._username, 'alice')
+        self.assertEqual(user1.ldap_username, 'alice')
+        self.assertEqual(user2.username, 'ldap_alice')
+        self.assertEqual(user2.ldap_user._username, 'alice')
+        self.assertEqual(user2.ldap_username, 'alice')
+
+
+    def test_search_bind(self):
+        self._init_settings(
+            AUTH_LDAP_USER_SEARCH=LDAPSearch(
+                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+                )
+            )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice])
+        user_count = User.objects.count()
+        
+        user = self.backend.authenticate(username='alice', password='password')
+        
+        self.assert_(user is not None)
+        self.assertEqual(User.objects.count(), user_count + 1)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'])
+
+
+    def test_search_bind_no_user(self):
+        self._init_settings(
+            AUTH_LDAP_USER_SEARCH=LDAPSearch(
+                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(cn=%(user)s)'
+                )
+            )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=people,o=test", 2, "(cn=alice)", None, 0), [])
+
+        user = self.backend.authenticate(username='alice', password='password')
+
+        self.assert_(user is None)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s', 'search_s'])
+    
+
+    def test_search_bind_multiple_users(self):
+        self._init_settings(
+            AUTH_LDAP_USER_SEARCH=LDAPSearch(
+                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=*)'
+                )
+            )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=people,o=test", 2, "(uid=*)", None, 0), [self.alice, self.bob])
+
+        user = self.backend.authenticate(username='alice', password='password')
+
+        self.assert_(user is None)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s', 'search_s'])
+
+
+    def test_search_bind_bad_password(self):
+        self._init_settings(
+            AUTH_LDAP_USER_SEARCH=LDAPSearch(
+                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+                )
+            )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice])
+
+        user = self.backend.authenticate(username='alice', password='bogus')
+
+        self.assert_(user is None)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'])
+    
+
+    def test_search_bind_with_credentials(self):
+        self._init_settings(
+            AUTH_LDAP_BIND_DN='uid=bob,ou=people,o=test',
+            AUTH_LDAP_BIND_PASSWORD='password',
+            AUTH_LDAP_USER_SEARCH=LDAPSearch(
+                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+                )
+            )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice])
+
+        user = self.backend.authenticate(username='alice', password='password')
+
+        self.assert_(user is not None)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'])
+
+
+    def test_search_bind_with_bad_credentials(self):
+        self._init_settings(
+            AUTH_LDAP_BIND_DN='uid=bob,ou=people,o=test',
+            AUTH_LDAP_BIND_PASSWORD='bogus',
+            AUTH_LDAP_USER_SEARCH=LDAPSearch(
+                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
+                )
+            )
+
+        user = self.backend.authenticate(username='alice', password='password')
+        
+        self.assert_(user is None)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s'])
+    
+    
+    def test_unicode_user(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'}
+        )
+        
+        user = self.backend.authenticate(username=u'dreßler', password='password')
+        
+        self.assert_(user is not None)
+        self.assertEqual(user.username, u'dreßler')
+        self.assertEqual(user.last_name, u'Dreßler')
+    
+    
+    def test_populate_user(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'}
+        )
+
+        user = self.backend.authenticate(username='alice', password='password')
+
+        self.assertEqual(user.username, 'alice')
+        self.assertEqual(user.first_name, 'Alice')
+        self.assertEqual(user.last_name, 'Adams')
+
+
+    def test_no_update_existing(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'},
+            AUTH_LDAP_ALWAYS_UPDATE_USER=False
+        )
+        User.objects.create(username='alice', first_name='Alicia', last_name='Astro')
+
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+
+        self.assertEqual(alice.first_name, 'Alicia')
+        self.assertEqual(alice.last_name, 'Astro')
+        self.assertEqual(bob.first_name, 'Robert')
+        self.assertEqual(bob.last_name, 'Barker')
+
+
+    def test_require_group(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+            AUTH_LDAP_REQUIRE_GROUP="cn=active_gon,ou=groups,o=test"
+        )
+        
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+        
+        self.assert_(alice is not None)
+        self.assert_(bob is None)
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s', 'compare_s', 'initialize', 'simple_bind_s', 'compare_s'])
+
+
+    def test_dn_group_membership(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+            AUTH_LDAP_USER_FLAGS_BY_GROUP={
+                'is_active': "cn=active_gon,ou=groups,o=test",
+                'is_staff': "cn=staff_gon,ou=groups,o=test",
+                'is_superuser': "cn=superuser_gon,ou=groups,o=test"
+            }
+        )
+        
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+        
+        self.assert_(alice.is_active)
+        self.assert_(alice.is_staff)
+        self.assert_(alice.is_superuser)
+        self.assert_(not bob.is_active)
+        self.assert_(not bob.is_staff)
+        self.assert_(not bob.is_superuser)
+
+
+    def test_posix_membership(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=PosixGroupType(),
+            AUTH_LDAP_USER_FLAGS_BY_GROUP={
+                'is_active': "cn=active_px,ou=groups,o=test",
+                'is_staff': "cn=staff_px,ou=groups,o=test",
+                'is_superuser': "cn=superuser_px,ou=groups,o=test"
+            }
+        )
+        
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+        
+        self.assert_(alice.is_active)
+        self.assert_(alice.is_staff)
+        self.assert_(alice.is_superuser)
+        self.assert_(not bob.is_active)
+        self.assert_(not bob.is_staff)
+        self.assert_(not bob.is_superuser)
+    
+    
+    def test_nested_dn_group_membership(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'),
+            AUTH_LDAP_USER_FLAGS_BY_GROUP={
+                'is_active': "cn=parent_gon,ou=groups,o=test",
+                'is_staff': "cn=parent_gon,ou=groups,o=test",
+            }
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=alice,ou=people,o=test)))", None, 0),
+            [self.active_gon, self.nested_gon]
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=active_gon,ou=groups,o=test)(member=cn=nested_gon,ou=groups,o=test)))", None, 0),
+            [self.parent_gon]
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=parent_gon,ou=groups,o=test)))", None, 0),
+            [self.circular_gon]
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=circular_gon,ou=groups,o=test)))", None, 0),
+            [self.nested_gon]
+        )
+        
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=bob,ou=people,o=test)))", None, 0),
+            []
+        )
+        
+        alice = self.backend.authenticate(username='alice', password='password')
+        bob = self.backend.authenticate(username='bob', password='password')
+        
+        self.assert_(alice.is_active)
+        self.assert_(alice.is_staff)
+        self.assert_(not bob.is_active)
+        self.assert_(not bob.is_staff)
+    
+    
+    def test_posix_missing_attributes(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=PosixGroupType(),
+            AUTH_LDAP_USER_FLAGS_BY_GROUP={
+                'is_active': "cn=active_px,ou=groups,o=test"
+            }
+        )
+        
+        nobody = self.backend.authenticate(username='nobody', password='password')
+
+        self.assert_(not nobody.is_active)
+    
+    
+    def test_dn_group_permissions(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+            AUTH_LDAP_FIND_GROUP_PERMS=True
+        )
+        self._init_groups()
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0),
+            [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon]
+        )
+        
+        alice = User.objects.create(username='alice')
+        alice = self.backend.get_user(alice.pk)
+        
+        self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"]))
+        self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"]))
+        self.assert_(self.backend.has_perm(alice, "auth.add_user"))
+        self.assert_(self.backend.has_module_perms(alice, "auth"))
+
+    
+    def test_posix_group_permissions(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test',
+                self.mock_ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)"
+            ),
+            AUTH_LDAP_GROUP_TYPE=PosixGroupType(),
+            AUTH_LDAP_FIND_GROUP_PERMS=True
+        )
+        self._init_groups()
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=posixGroup)(|(gidNumber=1000)(memberUid=alice)))", None, 0),
+            [self.active_px, self.staff_px, self.superuser_px]
+        )
+        
+        alice = User.objects.create(username='alice')
+        alice = self.backend.get_user(alice.pk)
+        
+        self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"]))
+        self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"]))
+        self.assert_(self.backend.has_perm(alice, "auth.add_user"))
+        self.assert_(self.backend.has_module_perms(alice, "auth"))
+    
+    def test_foreign_user_permissions(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+            AUTH_LDAP_FIND_GROUP_PERMS=True
+        )
+        self._init_groups()
+        
+        alice = User.objects.create(username='alice')
+
+        self.assertEqual(self.backend.get_group_permissions(alice), set())
+    
+    
+    def test_group_cache(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
+            AUTH_LDAP_FIND_GROUP_PERMS=True,
+            AUTH_LDAP_CACHE_GROUPS=True
+        )
+        self._init_groups()
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0),
+            [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon]
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=bob,ou=people,o=test))", None, 0),
+            []
+        )
+        
+        alice_id = User.objects.create(username='alice').pk
+        bob_id = User.objects.create(username='bob').pk
+
+        # Check permissions twice for each user
+        for i in range(2):
+            alice = self.backend.get_user(alice_id)
+            self.assertEqual(self.backend.get_group_permissions(alice),
+                set(["auth.add_user", "auth.change_user"]))
+
+            bob = self.backend.get_user(bob_id)
+            self.assertEqual(self.backend.get_group_permissions(bob), set())
+        
+        # Should have executed one LDAP search per user
+        self.assertEqual(self.mock_ldap.ldap_methods_called(),
+            ['initialize', 'simple_bind_s', 'search_s', 'initialize', 'simple_bind_s', 'search_s'])
+    
+    def test_group_mirroring(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test',
+                self.mock_ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)"
+            ),
+            AUTH_LDAP_GROUP_TYPE=PosixGroupType(),
+            AUTH_LDAP_MIRROR_GROUPS=True,
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=posixGroup)(|(gidNumber=1000)(memberUid=alice)))", None, 0),
+            [self.active_px, self.staff_px, self.superuser_px]
+        )
+    
+        self.assertEqual(Group.objects.count(), 0)
+
+        alice = self.backend.authenticate(username='alice', password='password')
+        
+        self.assertEqual(Group.objects.count(), 3)
+        self.assertEqual(set(alice.groups.all()), set(Group.objects.all()))
+
+    def test_nested_group_mirroring(self):
+        self._init_settings(
+            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
+            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
+            AUTH_LDAP_GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'),
+            AUTH_LDAP_MIRROR_GROUPS=True,
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=alice,ou=people,o=test)))", None, 0),
+            [self.active_gon, self.nested_gon]
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=active_gon,ou=groups,o=test)(member=cn=nested_gon,ou=groups,o=test)))", None, 0),
+            [self.parent_gon]
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=parent_gon,ou=groups,o=test)))", None, 0),
+            [self.circular_gon]
+        )
+        self.mock_ldap.set_return_value('search_s',
+            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=circular_gon,ou=groups,o=test)))", None, 0),
+            [self.nested_gon]
+        )
+        
+        alice = self.backend.authenticate(username='alice', password='password')
+        
+        self.assertEqual(Group.objects.count(), 4)
+        self.assertEqual(set(Group.objects.all().values_list('name', flat=True)),
+            set(['active_gon', 'nested_gon', 'parent_gon', 'circular_gon']))
+        self.assertEqual(set(alice.groups.all()), set(Group.objects.all()))
+
+
+    def _init_settings(self, **kwargs):
+        backend.ldap_settings = TestSettings(**kwargs)
+    
+    def _init_groups(self):
+        permissions = [
+            Permission.objects.get(codename="add_user"),
+            Permission.objects.get(codename="change_user")
+        ]
+
+        active_gon = Group.objects.create(name='active_gon')
+        active_gon.permissions.add(*permissions)
+
+        active_px = Group.objects.create(name='active_px')
+        active_px.permissions.add(*permissions)
Index: docs/howto/index.txt
===================================================================
--- docs/howto/index.txt	(revision 11638)
+++ docs/howto/index.txt	(working copy)
@@ -12,6 +12,7 @@
    :maxdepth: 1
 
    apache-auth
+   auth-ldap
    auth-remote-user
    custom-management-commands
    custom-model-fields
Index: docs/howto/auth-ldap.txt
===================================================================
--- docs/howto/auth-ldap.txt	(revision 0)
+++ docs/howto/auth-ldap.txt	(revision 0)
@@ -0,0 +1,572 @@
+.. _howto-auth-ldap:
+
+=========================
+Authentication using LDAP
+=========================
+
+.. versionadded:: 1.2
+
+Django includes an LDAP authentication backend to authenticate against any LDAP
+server. To enable, add
+:class:`django.contrib.auth.contrib.ldap.backend.LDAPBackend` to
+:setting:`AUTHENTICATION_BACKENDS`. LDAP configuration can be as simple as a
+single distinguished name template, but there are many rich options for working
+with :class:`~django.contrib.auth.models.User` objects, groups, and permissions.
+This backend depends on the `Python ldap <http://www.python-ldap.org/>`_ module.
+
+.. note::
+
+    :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` does not
+    inherit from :class:`~django.contrib.auth.backends.ModelBackend`. It is
+    possible to use
+    :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` exclusively
+    by configuring it to draw group membership from the LDAP server. However, if
+    you would like to assign permissions to individual users or add users to
+    groups within Django, you'll need to have both backends installed:
+
+    .. code-block:: python
+
+        AUTHENTICATION_BACKENDS = (
+            'django.contrib.auth.contrib.ldap.backend.LDAPBackend',
+            'django.contrib.auth.backends.ModelBackend',
+        )
+
+
+.. _howto-auth-ldap-basic-authentication:
+
+Configuring basic authentication
+================================
+
+If your LDAP server isn't running locally on the default port, you'll want to
+start by setting :setting:`AUTH_LDAP_SERVER_URI` to point to your server.
+
+.. code-block:: python
+
+    AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com"
+
+That done, the first step is to authenticate a username and password against the
+LDAP service. There are two ways to do this, called search/bind and simply bind.
+The first one involves connecting to the LDAP server either anonymously or with
+a fixed account and searching for the distinguished name of the authenticating
+user. Then we can attempt to bind again with the user's password. The second
+method is to derive the user's DN from his username and attempt to bind as the
+user directly.
+
+Because LDAP searches appear elsewhere in the configuration, the
+:class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` class is provided
+to encapsulate search information. In this case, the filter parameter should
+contain the placeholder ``%(user)s``. A simple configuration for the search/bind
+approach looks like this (some defaults included for completeness)::
+
+    import ldap
+    from django.contrib.auth.contrib.ldap.config import LDAPSearch
+
+    AUTH_LDAP_BIND_DN = ""
+    AUTH_LDAP_BIND_PASSWORD = ""
+    AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com",
+        ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
+
+This will perform an anonymous bind, search under
+``"ou=users,dc=example,dc=com"`` for an object with a uid matching the user's
+name, and try to bind using that DN and the user's password. The search must
+return exactly one result or authentication will fail. If you can't search
+anonymously, you can set :setting:`AUTH_LDAP_BIND_DN` to the distinguished name
+of an authorized user and :setting:`AUTH_LDAP_BIND_PASSWORD` to the password.
+
+To skip the search phase, set :setting:`AUTH_LDAP_USER_DN_TEMPLATE` to a
+template that will produce the authenticating user's DN directly. This template
+should have one placeholder, ``%(user)s``. If the previous example had used
+``ldap.SCOPE_ONELEVEL``, the following would be a more straightforward (and
+efficient) equivalent::
+
+    AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
+
+
+.. _howto-auth-ldap-groups:
+
+Working with groups
+===================
+
+Working with groups in LDAP can be a tricky business, as there isn't a single
+standard grouping mechanism. This module includes an extensible API for working
+with any kind of group and includes implementations for the most common ones.
+:class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` is a base class
+whose concrete subclasses can determine group membership for particular grouping
+mechanisms. Three built-in subclasses cover most grouping mechanisms:
+
+    * :class:`~django.contrib.auth.contrib.ldap.config.PosixGroupType`
+    * :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType`
+    * :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType`
+
+posixGroup objects are somewhat specialized, so they get their own class. The
+other two cover mechanisms whereby a group object stores a list of its members
+as distinguished names. This includes groupOfNames, groupOfUniqueNames, and
+Active Directory groups, among others. The nested variant allows groups to
+contain other groups, to as many levels as you like. For convenience and
+readability, several trivial subclasses of the above are provided:
+
+    * :class:`~django.contrib.auth.contrib.ldap.config.GroupOfNamesType`
+    * :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfNamesType`
+    * :class:`~django.contrib.auth.contrib.ldap.config.GroupOfUniqueNamesType`
+    * :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfUniqueNamesType`
+    * :class:`~django.contrib.auth.contrib.ldap.config.ActiveDirectoryGroupType`
+    * :class:`~django.contrib.auth.contrib.ldap.config.NestedActiveDirectoryGroupType`
+
+To get started, you'll need to provide some basic information about your LDAP
+groups. :setting:`AUTH_LDAP_GROUP_SEARCH` is an
+:class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that
+identifies the set of relevant group objects. That is, all groups that users
+might belong to as well as any others that we might need to know about (in the
+case of nested groups, for example). :setting:`AUTH_LDAP_GROUP_TYPE` is an
+instance of the class corresponding to the type of group that will be returned
+by :setting:`AUTH_LDAP_GROUP_SEARCH`. All groups referenced elsewhere in the
+configuration must be of this type and part of the search results.
+
+.. code-block:: python
+
+    import ldap
+    from django.contrib.auth.contrib.ldap.config import LDAPSearch, GroupOfNamesType
+    
+    AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=groups,dc=example,dc=com",
+        ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)"
+    )
+    AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
+
+The simplest use of groups is to limit the users who are allowed to log in. If
+:setting:`AUTH_LDAP_REQUIRE_GROUP` is set, then only users who are members of
+that group will successfully authenticate::
+
+    AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=groups,dc=example,dc=com"
+
+More advanced uses of groups are covered in the next two sections.
+
+
+.. _howto-auth-ldap-user-objects:
+
+User objects
+============
+
+Authenticating against an external source is swell, but Django's auth module is
+tightly bound to the :class:`django.contrib.auth.models.User` model. Thus, when
+a user logs in, we have to create a :class:`~django.contrib.auth.models.User`
+object to represent him in the database.
+
+The only required field for a user is the username, which we obviously have. The
+:class:`~django.contrib.auth.models.User` model is picky about the characters
+allowed in usernames, so
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` includes a pair
+of hooks,
+:meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username`
+and
+:meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.django_to_ldap_username`,
+to translate between LDAP usernames and Django usernames. You'll need this, for
+example, if your LDAP names have periods in them. You can subclass
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` to implement
+these hooks; by default the username is not modified.
+:class:`~django.contrib.auth.models.User` objects that are authenticated by
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will have an
+:attr:`~django.contrib.auth.models.User.ldap_username` attribute with the
+original (LDAP) username. :attr:`~django.contrib.auth.models.User.username`
+will, of course, be the Django username.
+
+LDAP directories tend to contain much more information about users that you may
+wish to propagate. A pair of settings, :setting:`AUTH_LDAP_USER_ATTR_MAP` and
+:setting:`AUTH_LDAP_PROFILE_ATTR_MAP`, serve to copy directory information into
+:class:`~django.contrib.auth.models.User` and profile objects. These are
+dictionaries that map user and profile model keys, respectively, to LDAP
+attribute names::
+
+    AUTH_LDAP_USER_ATTR_MAP = {"first_name": "givenName", "last_name": "sn"}
+    AUTH_LDAP_PROFILE_ATTR_MAP = {"home_directory": "homeDirectory"}
+
+Only string fields can be mapped to attributes. Boolean fields can be defined by
+group membership::
+
+    AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+        "is_active": "cn=active,ou=groups,dc=example,dc=com",
+        "is_staff": "cn=staff,ou=groups,dc=example,dc=com",
+        "is_superuser": "cn=superuser,ou=groups,dc=example,dc=com"
+    }
+
+By default, all mapped user fields will be updated each time the user logs in.
+To disable this, set :setting:`AUTH_LDAP_ALWAYS_UPDATE_USER` to ``False``.
+
+If you need to access multi-value attributes or there is some other reason that
+the above is inadequate, you can also access the user's raw LDAP attributes.
+``user.ldap_user`` is an object with two public properties:
+
+    * ``dn``: The user's distinguished name.
+    * ``attrs``: The user's LDAP attributes as a dictionary of lists of string
+      values.
+
+Python-ldap returns all attribute values as utf8-encoded strings. For
+convenience, this module will try to decode all values into Unicode strings. Any
+string that can not be successfully decoded will be left as-is; this may apply
+to binary values such as Active Directory's objectSid.
+
+.. note::
+
+    Users created by
+    :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will have an
+    unusable password set. This will only happen when the user is created, so if
+    you set a valid password in Django, the user will be able to log in through
+    :class:`~django.contrib.auth.backends.ModelBackend` (if configured) even if
+    he is rejected by LDAP. This is not generally recommended, but could be
+    useful as a fail-safe for selected users in case the LDAP server is
+    unavailable.
+
+
+.. _howto-auth-ldap-permissions:
+
+Permissions
+===========
+
+Groups are useful for more than just populating the user's ``is_*`` fields.
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` would not be
+complete without some way to turn a user's LDAP group memberships into Django
+model permissions. In fact, there are two ways to do this.
+
+Ultimately, both mechanisms need some way to map LDAP groups to Django groups.
+Implementations of
+:class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` will have an
+algorithm for deriving the Django group name from the LDAP group. Clients that
+need to modify this behavior can subclass the
+:class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` class. All of
+the built-in implementations take a ``name_attr`` argument to ``__init__``,
+which specifies the LDAP attribute from which to take the Django group name. By
+default, the ``cn`` attribute is used.
+
+The least invasive way to map group permissions is to set
+:setting:`AUTH_LDAP_FIND_GROUP_PERMS` to ``True``
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will then find
+all of the LDAP groups that a user belongs to, map them to Django groups, and
+load the permissions for those groups. You will need to create the Django groups
+yourself, generally through the admin interface.
+
+.. note::
+
+    After the user logs in, subsequent requests will have to determine group
+    membership based solely on the :class:`~django.contrib.auth.models.User`
+    object of the logged-in user. We will not have the user's password at this
+    point. This means that if :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``,
+    we must have access to the LDAP directory through
+    :setting:`AUTH_LDAP_BIND_DN` and :setting:`AUTH_LDAP_BIND_PASSWORD`, even if
+    you're using :setting:`AUTH_LDAP_USER_DN_TEMPLATE` to authenticate the user.
+
+To minimize traffic to the LDAP server,
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` can make use of
+Django's :ref:`cache framework <topics-cache>` to keep a copy of a user's LDAP
+group memberships. To enable this feature, set :setting:`AUTH_LDAP_CACHE_GROUPS`
+to ``True``. You can also set :setting:`AUTH_LDAP_GROUP_CACHE_TIMEOUT` to
+override the timeout of cache entries (in seconds).
+
+.. code-block:: python
+
+    AUTH_LDAP_CACHE_GROUPS = True
+    AUTH_LDAP_GROUP_CACHE_TIMEOUT = 300
+
+The second way to turn LDAP group memberships into permissions is to mirror the
+groups themselves. If :setting:`AUTH_LDAP_MIRROR_GROUPS` is ``True``, then every
+time a user logs in,
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will update the
+database with the user's LDAP groups. Any group that doesn't exist will be
+created and the user's Django group membership will be updated to exactly match
+his LDAP group membership. Note that if the LDAP server has nested groups, the
+Django database will end up with a flattened representation.
+
+This approach has two main differences from
+:setting:`AUTH_LDAP_FIND_GROUP_PERMS`. First,
+:setting:`AUTH_LDAP_FIND_GROUP_PERMS` will query for LDAP group membership
+either for every request or according to the cache timeout. With group
+mirroring, membership will be updated when the user authenticates. This may not
+be appropriate for sites with long session timeouts. The second difference is
+that with :setting:`AUTH_LDAP_FIND_GROUP_PERMS`, there is no way for clients to
+determine a user's group memberships, only their permissions. If you want to
+make decisions based directly on group membership, you'll have to mirror the
+groups.
+
+
+.. _howto-auth-ldap-logging:
+
+Logging
+=======
+
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` uses the standard
+logging module to log debug and warning messages to the logger named
+``'django.contrib.auth.contrib.ldap'``. If you need debug messages to
+help with configuration issues, you should add a handler to this logger.
+
+
+.. _howto-auth-ldap-options:
+
+More options
+============
+
+Miscellaneous settings for
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend`:
+
+    * :setting:`AUTH_LDAP_GLOBAL_OPTIONS`: A dictionary of options to pass to
+      python-ldap via ``ldap.set_option()``.
+    * :setting:`AUTH_LDAP_CONNECTION_OPTIONS`: A dictionary of options to pass
+      to each LDAPObject instance via ``LDAPObject.set_option()``.
+
+
+.. _howto-auth-ldap-performance:
+
+Performance
+===========
+
+:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` is carefully
+designed not to require a connection to the LDAP service for every request. Of
+course, this depends heavily on how it is configured. If LDAP traffic or latency
+is a concern for your deployment, this section has a few tips on minimizing it,
+in decreasing order of impact.
+
+    #. **Cache groups**. If :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``,
+       the default behavior is to reload a user's group memberships on every
+       request. This is the safest behavior, as any membership change takes
+       effect immediately, but it is expensive. If possible, set
+       :setting:`AUTH_LDAP_CACHE_GROUPS` to ``True`` to remove most of this
+       traffic. Alternatively, you might consider using
+       :setting:`AUTH_LDAP_MIRROR_GROUPS` and relying on
+       :class:`~django.contrib.auth.backends.ModelBackend` to supply group
+       permissions.
+    #. **Don't access user.ldap_user.***. These properties are only cached
+       on a per-request basis. If you can propagate LDAP attributes to a
+       :class:`~django.contrib.auth.models.User` or profile object, they will
+       only be updated at login. ``user.ldap_user.attrs`` triggers an LDAP
+       connection for every request in which it's accessed. If you're not using
+       :setting:`AUTH_LDAP_USER_DN_TEMPLATE`, then accessing
+       ``user.ldap_user.dn`` will also trigger an LDAP connection.
+    #. **Use simpler group types**. Some grouping mechanisms are more expensive
+       than others. This will often be outside your control, but it's important
+       to note that the extra functionality of more complex group types like
+       :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfNamesType`
+       is not free and will generally require a greater number and complexity of
+       LDAP queries.
+    #. **Use direct binding**. Binding with
+       :setting:`AUTH_LDAP_USER_DN_TEMPLATE` is a little bit more efficient than
+       relying on :setting:`AUTH_LDAP_USER_SEARCH`. Specifically, it saves two
+       LDAP operations (one bind and one search) per login.
+
+
+.. _howto-auth-ldap-example:
+
+Example configuration
+=====================
+
+Here is a complete example configuration from :file:`settings.py` that
+exercises nearly all of the features. In this example, we're authenticating
+against a global pool of users in the directory, but we have a special area set
+aside for Django groups (ou=django,ou=groups,dc=example,dc=com). Remember that
+most of this is optional if you just need simple authentication. Some default
+settings and arguments are included for completeness.
+
+.. code-block:: python
+
+    import ldap
+    from django.contrib.auth.contrib.ldap.config import LDAPSearch, GroupOfNamesType
+
+
+    # Baseline configuration.
+    AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com"
+
+    AUTH_LDAP_BIND_DN = "cn=django-agent,dc=example,dc=com"
+    AUTH_LDAP_BIND_PASSWORD = "phlebotinum"
+    AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com",
+        ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
+    # or perhaps:
+    # AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
+
+    # Set up the basic group parameters.
+    AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=django,ou=groups,dc=example,dc=com",
+        ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)"
+    )
+    AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn")
+
+    # Only users in this group can log in.
+    AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=django,ou=groups,dc=example,dc=com"
+
+    # Populate the Django user from the LDAP directory.
+    AUTH_LDAP_USER_ATTR_MAP = {
+        "first_name": "givenName",
+        "last_name": "sn",
+        "email": "mail"
+    }
+
+    AUTH_LDAP_PROFILE_ATTR_MAP = {
+        "employee_number": "employeeNumber"
+    }
+
+    AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+        "is_active": "cn=active,ou=django,ou=groups,dc=example,dc=com",
+        "is_staff": "cn=staff,ou=django,ou=groups,dc=example,dc=com",
+        "is_superuser": "cn=superuser,ou=django,ou=groups,dc=example,dc=com"
+    }
+    
+    # This is the default, but I like to be explicit.
+    AUTH_LDAP_ALWAYS_UPDATE_USER = True
+    
+    # Use LDAP group membership to calculate group permissions.
+    AUTH_LDAP_FIND_GROUP_PERMS = True
+
+    # Cache group memberships for an hour to minimize LDAP traffic
+    AUTH_LDAP_CACHE_GROUPS = True
+    AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
+
+
+    # Keep ModelBackend around for per-user permissions and maybe a local
+    # superuser.
+    AUTHENTICATION_BACKENDS = (
+        'django.contrib.auth.contrib.ldap.backend.LDAPBackend',
+        'django.contrib.auth.backends.ModelBackend',
+    )
+
+
+.. _howto-auth-ldap-api-reference:
+
+API reference
+=============
+
+Configuration
+-------------
+
+.. module:: django.contrib.auth.contrib.ldap.config
+
+.. class:: LDAPSearch
+
+    .. method:: __init__(base_dn, scope, filterstr='(objectClass=*)')
+
+        * ``base_dn``: The distinguished name of the search base.
+        * ``scope``: One of ``ldap.SCOPE_*``.
+        * ``filterstr``: An optional filter string (e.g. '(objectClass=person)').
+          In order to be valid, ``filterstr`` must be enclosed in parentheses.
+
+
+.. class:: LDAPGroupType
+
+    The base class for objects that will determine group membership for various
+    LDAP grouping mechanisms. Implementations are provided for common group
+    types or you can write your own. See the source code for subclassing notes.
+    
+    .. method:: __init__(name_attr='cn')
+        
+        By default, LDAP groups will be mapped to Django groups by taking the
+        first value of the cn attribute. You can specify a different attribute
+        with ``name_attr``.
+
+
+.. class:: PosixGroupType
+
+    A concrete subclass of
+    :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` that handles
+    the ``posixGroup`` object class. This checks for both primary group and
+    group membership.
+
+    .. method:: __init__(name_attr='cn')
+
+.. class:: MemberDNGroupType
+
+    A concrete subclass of
+    :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` that handles
+    grouping mechanisms wherein the group object contains a list of its member
+    DNs.
+    
+    .. method:: __init__(member_attr, name_attr='cn')
+    
+        * ``member_attr``: The attribute on the group object that contains a
+          list of member DNs. 'member' and 'uniqueMember' are common examples.
+
+
+.. class:: NestedMemberDNGroupType
+
+    Similar to
+    :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType`, except
+    this allows groups to contain other groups as members. Group hierarchies
+    will be traversed to determine membership.
+
+    .. method:: __init__(member_attr, name_attr='cn')
+    
+        As above.
+
+
+.. class:: GroupOfNamesType
+
+    A concrete subclass of
+    :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that
+    handles the ``groupOfNames`` object class. Equivalent to
+    ``MemberDNGroupType('member')``.
+
+    .. method:: __init__(name_attr='cn')
+
+
+.. class:: NestedGroupOfNamesType
+
+    A concrete subclass of
+    :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType`
+    that handles the ``groupOfNames`` object class. Equivalent to
+    ``NestedMemberDNGroupType('member')``.
+
+    .. method:: __init__(name_attr='cn')
+
+
+.. class:: GroupOfUniqueNamesType
+
+    A concrete subclass of
+    :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that
+    handles the ``groupOfUniqueNames`` object class. Equivalent to
+    ``MemberDNGroupType('uniqueMember')``.
+
+    .. method:: __init__(name_attr='cn')
+
+
+.. class:: NestedGroupOfUniqueNamesType
+
+    A concrete subclass of
+    :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType`
+    that handles the ``groupOfUniqueNames`` object class. Equivalent to
+    ``NestedMemberDNGroupType('uniqueMember')``.
+
+    .. method:: __init__(name_attr='cn')
+
+
+.. class:: ActiveDirectoryGroupType
+
+    A concrete subclass of
+    :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that
+    handles Active Directory groups. Equivalent to
+    ``MemberDNGroupType('member')``.
+
+    .. method:: __init__(name_attr='cn')
+
+
+.. class:: NestedActiveDirectoryGroupType
+
+    A concrete subclass of
+    :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType`
+    that handles Active Directory groups. Equivalent to
+    ``NestedMemberDNGroupType('member')``.
+
+    .. method:: __init__(name_attr='cn')
+
+
+Backend
+-------
+
+.. module:: django.contrib.auth.contrib.ldap.backend
+
+.. class:: LDAPBackend
+
+    .. method:: ldap_to_django_username(username)
+    
+        Returns a valid Django username based on the given LDAP username (which
+        is what the user enters). By default, ``username`` is returned
+        unchanged. This can be overriden by subclasses.
+
+    .. method:: django_to_ldap_username(username)
+
+        The inverse of
+        :meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username`.
+        If this is not symmetrical to
+        :meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username`,
+        the behavior is undefined.
Index: docs/ref/authbackends.txt
===================================================================
--- docs/ref/authbackends.txt	(revision 11638)
+++ docs/ref/authbackends.txt	(working copy)
@@ -35,3 +35,14 @@
     :attr:`request.META['REMOTE_USER'] <django.http.HttpRequest.META>`.  See
     the :ref:`Authenticating against REMOTE_USER <howto-auth-remote-user>`
     documentation.
+
+
+.. class:: LDAPBackend
+
+    .. versionadded:: 1.2
+    
+    This backend authenticates against an existing LDAP directory. You must have
+    python-ldap installed in order to use it. In addition to authenticating
+    individual users, it can optionally query the LDAP server for group
+    membership and thus group permissions. See the :ref:`howto-auth-ldap`
+    documentation.
Index: docs/ref/settings.txt
===================================================================
--- docs/ref/settings.txt	(revision 11638)
+++ docs/ref/settings.txt	(working copy)
@@ -105,6 +105,237 @@
 authenticate a user. See the :ref:`authentication backends documentation
 <authentication-backends>` for details.
 
+
+.. setting:: AUTH_LDAP_ALWAYS_UPDATE_USER
+
+AUTH_LDAP_ALWAYS_UPDATE_USER
+----------------------------
+
+Default: ``True``
+
+If ``True``, the fields of a :class:`~django.contrib.auth.models.User` object
+will be updated with the latest values from the LDAP directory every time the
+user logs in. Otherwise the :class:`~django.contrib.auth.models.User` object
+will only be populated when it is automatically created. See
+:ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`.
+
+
+.. setting:: AUTH_LDAP_BIND_DN
+
+AUTH_LDAP_BIND_DN
+-----------------
+
+Default: ``''`` (Empty string)
+
+The distinguished name to use for general access to the LDAP server. Use the
+empty string (the default) for an anonymous bind. If
+:setting:`AUTH_LDAP_USER_DN_TEMPLATE` is not set, we'll need this to search for
+the user. If :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``, we'll also need
+it to determine group membership on subsequent requests. See
+:ref:`Authentication using LDAP <howto-auth-ldap-basic-authentication>`.
+
+
+.. setting:: AUTH_LDAP_BIND_PASSWORD
+
+AUTH_LDAP_BIND_PASSWORD
+-----------------------
+
+Default: ``''`` (Empty string)
+
+The password to use with :setting:`AUTH_LDAP_BIND_DN`. See :ref:`Authentication
+using LDAP <howto-auth-ldap-basic-authentication>`.
+
+
+.. setting:: AUTH_LDAP_CACHE_GROUPS
+
+AUTH_LDAP_CACHE_GROUPS
+----------------------
+
+Default: ``False``
+
+If ``True``, LDAP group membership will be cached using Django's :ref:`cache
+framework <topics-cache>`. The cache timeout can be customized with
+:setting:`AUTH_LDAP_GROUP_CACHE_TIMEOUT`. See :ref:`Authentication using LDAP
+<howto-auth-ldap-permissions>`.
+
+
+.. setting:: AUTH_LDAP_CONNECTION_OPTIONS
+
+AUTH_LDAP_CONNECTION_OPTIONS
+----------------------------
+
+Default: ``{}``
+
+A hash of options to pass to each connection to the LDAP server via
+``LDAPObject.set_option()``. Keys are ``ldap.OPT_*`` constants. See
+:ref:`Authentication using LDAP <howto-auth-ldap-options>`.
+
+
+.. setting:: AUTH_LDAP_GROUP_CACHE_TIMEOUT
+
+AUTH_LDAP_GROUP_CACHE_TIMEOUT
+-----------------------------
+
+Default: ``None``
+
+If :setting:`AUTH_LDAP_CACHE_GROUPS` is ``True``, this overrides the default
+cache timeout for the group cache. See :ref:`Authentication using LDAP
+<howto-auth-ldap-permissions>`.
+
+
+.. setting:: AUTH_LDAP_FIND_GROUP_PERMS
+
+AUTH_LDAP_FIND_GROUP_PERMS
+--------------------------
+
+Default: ``False``
+
+If ``True``, :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will
+furnish group permissions based on the LDAP groups the authenticated user
+belongs to. :setting:`AUTH_LDAP_GROUP_SEARCH` and
+:setting:`AUTH_LDAP_GROUP_TYPE` must also be set. See :ref:`Authentication using
+LDAP <howto-auth-ldap-permissions>`.
+
+
+.. setting:: AUTH_LDAP_GLOBAL_OPTIONS
+
+AUTH_LDAP_GLOBAL_OPTIONS
+------------------------
+
+Default: ``{}``
+
+A hash of options to pass to ``ldap.set_option()``. Keys are ``ldap.OPT_*``
+constants. See :ref:`Authentication using LDAP <howto-auth-ldap-options>`.
+
+
+.. setting:: AUTH_LDAP_GROUP_SEARCH
+
+AUTH_LDAP_GROUP_SEARCH
+----------------------
+
+Default: ``None``
+
+An :class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that
+finds all LDAP groups that users might belong to. If your configuration makes
+any references to LDAP groups, this and :setting:`AUTH_LDAP_GROUP_TYPE` must be
+set. See :ref:`Authentication using LDAP <howto-auth-ldap-groups>`.
+
+
+.. setting:: AUTH_LDAP_GROUP_TYPE
+
+AUTH_LDAP_GROUP_TYPE
+--------------------
+
+Default: ``None``
+
+An :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType`
+instance describing the type of group returned by
+:setting:`AUTH_LDAP_GROUP_SEARCH`. See :ref:`Authentication using LDAP
+<howto-auth-ldap-groups>`.
+
+
+.. setting:: AUTH_LDAP_MIRROR_GROUPS
+
+AUTH_LDAP_MIRROR_GROUPS
+-----------------------
+
+Default: ``False``
+
+If ``True``, :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will
+mirror a user's LDAP group membership in the Django database. Any time a user
+authenticates, we will create all of his LDAP groups as Django groups and update
+his Django group membership to exactly match his LDAP group membership. If the
+LDAP server has nested groups, the Django database will end up with a flattened
+representation. See :ref:`Authentication using LDAP
+<howto-auth-ldap-permissions>`.
+
+
+.. setting:: AUTH_LDAP_PROFILE_ATTR_MAP
+
+AUTH_LDAP_PROFILE_ATTR_MAP
+--------------------------
+
+Default: ``{}``
+
+A mapping from user profile field names to LDAP attribute names. See
+:ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`.
+
+
+.. setting:: AUTH_LDAP_REQUIRE_GROUP
+
+AUTH_LDAP_REQUIRE_GROUP
+-----------------------
+
+Default: ``None``
+
+The distinguished name of a group that a user must belong to in order to
+successfully authenticate. See :ref:`Authentication using LDAP
+<howto-auth-ldap-groups>`.
+
+
+.. setting:: AUTH_LDAP_SERVER_URI
+
+AUTH_LDAP_SERVER_URI
+--------------------
+
+Default: ``ldap://localhost``
+
+The URI of the LDAP server. This can be any URI that is supported by your
+underlying LDAP libraries. See :ref:`Authentication using LDAP
+<howto-auth-ldap>`.
+
+
+.. setting:: AUTH_LDAP_USER_ATTR_MAP
+
+AUTH_LDAP_USER_ATTR_MAP
+-----------------------
+
+Default: ``{}``
+
+A mapping from :class:`~django.contrib.auth.models.User` field names to LDAP
+attribute names. See :ref:`Authentication using LDAP
+<howto-auth-ldap-user-objects>`.
+
+
+.. setting:: AUTH_LDAP_USER_DN_TEMPLATE
+
+AUTH_LDAP_USER_DN_TEMPLATE
+--------------------------
+
+Default: ``None``
+
+A string template that describes any user's distinguished name based on the
+username. This must contain the placeholder ``%(user)s``. See
+:ref:`Authentication using LDAP <howto-auth-ldap-basic-authentication>`.
+
+
+.. setting:: AUTH_LDAP_USER_FLAGS_BY_GROUP
+
+AUTH_LDAP_USER_FLAGS_BY_GROUP
+------------------------------
+
+Default: ``{}``
+
+A mapping from boolean :class:`~django.contrib.auth.models.User` field names to
+distinguished names of LDAP groups. The corresponding field is set to ``True``
+or ``False`` according to whether the user is a member of the group. See
+:ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`.
+
+
+.. setting:: AUTH_LDAP_USER_SEARCH
+
+AUTH_LDAP_USER_SEARCH
+---------------------
+
+Default: ``None``
+
+An :class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that will
+locate a user in the directory. The filter parameter should contain the
+placeholder ``%(user)s`` for the username. It must return exactly one result for
+authentication to succeed. See :ref:`Authentication using LDAP
+<howto-auth-ldap>`.
+
+
 .. setting:: AUTH_PROFILE_MODULE
 
 AUTH_PROFILE_MODULE
