Ticket #11526: auth_ldap.diff

File auth_ldap.diff, 104.6 KB (added by Peter Sagerson, 14 years ago)

SVN revision 11638

  • django/contrib/auth/contrib/ldap/config.py

     
     1"""
     2This module contains classes that will be needed for configuration of LDAP
     3authentication. Unlike backend.py, this is safe to import into settings.py.
     4Please see the docstring on the backend module for more information, including
     5notes on naming conventions.
     6"""
     7
     8try:
     9    set
     10except NameError:
     11    from sets import Set as set     # Python 2.3 fallback
     12
     13import logging
     14import pprint
     15
     16
     17class _LDAPConfig(object):
     18    """
     19    A private class that loads and caches some global objects.
     20    """
     21    ldap = None
     22    logger = None
     23   
     24    def get_ldap(cls):
     25        """
     26        Returns the ldap module. The unit test harness will assign a mock object
     27        to _LDAPConfig.ldap. It is imperative that the ldap module not be
     28        imported anywhere else so that the unit tests will pass in the absence
     29        of python-ldap.
     30        """
     31        if cls.ldap is None:
     32            import ldap
     33            import ldap.filter
     34            import ldap.dn
     35           
     36            cls.ldap = ldap
     37       
     38        return cls.ldap
     39    get_ldap = classmethod(get_ldap)
     40
     41    def get_logger(cls):
     42        """
     43        Initializes and returns our logger instance.
     44        """
     45        if cls.logger is None:
     46            class NullHandler(logging.Handler):
     47                def emit(self, record):
     48                    pass
     49   
     50            cls.logger = logging.getLogger('django.contrib.auth.contrib.ldap')
     51            cls.logger.addHandler(NullHandler())
     52            cls.logger.setLevel(logging.DEBUG)
     53
     54        return cls.logger
     55    get_logger = classmethod(get_logger)
     56
     57
     58# Our global logger
     59logger = _LDAPConfig.get_logger()
     60
     61
     62class LDAPSearch(object):
     63    """
     64    Public class that holds a set of LDAP search parameters. Objects of this
     65    class should be considered immutable. Only the initialization method is
     66    documented for configuration purposes. Internal clients may use the other
     67    methods to refine and execute the search.
     68    """
     69    def __init__(self, base_dn, scope, filterstr=u'(objectClass=*)'):
     70        """
     71        These parameters are the same as the first three parameters to
     72        ldap.search_s.
     73        """
     74        self.base_dn = base_dn
     75        self.scope = scope
     76        self.filterstr = filterstr
     77        self.ldap = _LDAPConfig.get_ldap()
     78   
     79    def search_with_additional_terms(self, term_dict, escape=True):
     80        """
     81        Returns a new search object with additional search terms and-ed to the
     82        filter string. term_dict maps attribute names to assertion values. If
     83        you don't want the values escaped, pass escape=False.
     84        """
     85        term_strings = [self.filterstr]
     86       
     87        for name, value in term_dict.iteritems():
     88            if escape:
     89                value = self.ldap.filter.escape_filter_chars(value)
     90            term_strings.append(u'(%s=%s)' % (name, value))
     91       
     92        filterstr = u'(&%s)' % ''.join(term_strings)
     93       
     94        return self.__class__(self.base_dn, self.scope, filterstr)
     95   
     96    def search_with_additional_term_string(self, filterstr):
     97        """
     98        Returns a new search object with filterstr and-ed to the original filter
     99        string. The caller is responsible for passing in a properly escaped
     100        string.
     101        """
     102        filterstr = u'(&%s%s)' % (self.filterstr, filterstr)
     103       
     104        return self.__class__(self.base_dn, self.scope, filterstr)
     105   
     106    def execute(self, connection, filterargs=()):
     107        """
     108        Executes the search on the given connection (an LDAPObject). filterargs
     109        is an object that will be used for expansion of the filter string.
     110       
     111        The python-ldap library returns utf8-encoded strings. For the sake of
     112        sanity, this method will decode all result strings and return them as
     113        Unicode.
     114        """
     115        try:
     116            filterstr = self.filterstr % filterargs
     117            results = connection.search_s(self.base_dn.encode('utf-8'),
     118                self.scope, filterstr.encode('utf-8'))
     119            results = _DeepStringCoder('utf-8').decode(results)
     120
     121            result_dns = [result[0] for result in results]
     122            logger.debug(u"search_s('%s', %d, '%s') returned %d objects: %s" %
     123                (self.base_dn, self.scope, filterstr, len(result_dns), "; ".join(result_dns)))
     124        except self.ldap.LDAPError, e:
     125            results = []
     126            logger.error(u"search_s('%s', %d, '%s') raised %s" %
     127                (self.base_dn, self.scope, filterstr, pprint.pformat(e)))
     128       
     129        return results
     130
     131
     132class _DeepStringCoder(object):
     133    """
     134    Encodes and decodes strings in a nested structure of lists, tuples, and
     135    dicts. This is helpful when interacting with the Unicode-unaware
     136    python-ldap.
     137    """
     138    def __init__(self, encoding):
     139        self.encoding = encoding
     140   
     141    def decode(self, value):
     142        try:
     143            if isinstance(value, str):
     144                value = value.decode(self.encoding)
     145            elif isinstance(value, list):
     146                value = self._decode_list(value)
     147            elif isinstance(value, tuple):
     148                value = tuple(self._decode_list(value))
     149            elif isinstance(value, dict):
     150                value = self._decode_dict(value)
     151        except UnicodeDecodeError:
     152            pass
     153       
     154        return value
     155   
     156    def _decode_list(self, value):
     157        return [self.decode(v) for v in value]
     158   
     159    def _decode_dict(self, value):
     160        return dict([(self.decode(k), self.decode(v)) for k,v in value.iteritems()])
     161
     162
     163class LDAPGroupType(object):
     164    """
     165    This is an abstract base class for classes that determine LDAP group
     166    membership. A group can mean many different things in LDAP, so we will need
     167    a concrete subclass for each grouping mechanism. Clients may subclass this
     168    if they have a group mechanism that is not handled by a built-in
     169    implementation.
     170   
     171    name_attr is the name of the LDAP attribute from which we will take the
     172    Django group name.
     173   
     174    Subclasses in this file must use self.ldap to access the python-ldap module.
     175    This will be a mock object during unit tests.
     176    """
     177    def __init__(self, name_attr="cn"):
     178        self.name_attr = name_attr
     179        self.ldap = _LDAPConfig.get_ldap()
     180
     181    def user_groups(self, ldap_user, group_search):
     182        """
     183        Returns a list of group_info structures, each one a group to which
     184        ldap_user belongs. group_search is an LDAPSearch object that returns all
     185        of the groups that the user might belong to. Typical implementations
     186        will apply additional filters to group_search and return the results of
     187        the search. ldap_user represents the user and has the following three
     188        properties:
     189       
     190        dn: the distinguished name
     191        attrs: a dictionary of LDAP attributes (with lists of values)
     192        connection: an LDAPObject that has been bound with credentials
     193       
     194        This is the primitive method in the API and must be implemented.
     195        """
     196        return []
     197   
     198    def is_member(self, ldap_user, group_dn):
     199        """
     200        This method is an optimization for determining group membership without
     201        loading all of the user's groups. Subclasses that are able to do this
     202        may return True or False. ldap_user is as above. group_dn is the
     203        distinguished name of the group in question.
     204       
     205        The base implementation returns None, which means we don't have enough
     206        information. The caller will have to call user_groups() instead and look
     207        for group_dn in the results.
     208        """
     209        return None
     210
     211    def group_name_from_info(self, group_info):
     212        """
     213        Given the (DN, attrs) 2-tuple of an LDAP group, this returns the name of
     214        the Django group. This may return None to indicate that a particular
     215        LDAP group has no corresponding Django group.
     216       
     217        The base implementation returns the value of the cn attribute, or
     218        whichever attribute was given to __init__ in the name_attr
     219        parameter.
     220        """
     221        try:
     222            name = group_info[1][self.name_attr][0]
     223        except (KeyError, IndexError):
     224            name = None
     225       
     226        return name
     227
     228
     229class PosixGroupType(LDAPGroupType):
     230    """
     231    An LDAPGroupType subclass that handles groups of class posixGroup.
     232    """
     233    def user_groups(self, ldap_user, group_search):
     234        """
     235        Searches for any group that is either the user's primary or contains the
     236        user as a member.
     237        """
     238        groups = []
     239       
     240        try:
     241            user_uid = ldap_user.attrs['uid'][0]
     242            user_gid = ldap_user.attrs['gidNumber'][0]
     243           
     244            filterstr = u'(|(gidNumber=%s)(memberUid=%s))' % (
     245                self.ldap.filter.escape_filter_chars(user_gid),
     246                self.ldap.filter.escape_filter_chars(user_uid)
     247            )
     248           
     249            search = group_search.search_with_additional_term_string(filterstr)
     250            groups = search.execute(ldap_user.connection)
     251        except (KeyError, IndexError):
     252            pass
     253       
     254        return groups
     255
     256    def is_member(self, ldap_user, group_dn):
     257        """
     258        Returns True if the group is the user's primary group or if the user is
     259        listed in the group's memberUid attribute.
     260        """
     261        try:
     262            user_uid = ldap_user.attrs['uid'][0]
     263            user_gid = ldap_user.attrs['gidNumber'][0]
     264
     265            is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'memberUid', user_uid.encode('utf-8'))
     266            if not is_member:
     267                is_member = ldap_user.connection.compare_s(group_dn.encode('utf-8'), 'gidNumber', user_gid.encode('utf-8'))
     268        except (KeyError, IndexError):
     269            is_member = False
     270       
     271        return is_member
     272
     273
     274class MemberDNGroupType(LDAPGroupType):
     275    """
     276    An abstract base class for group types that store lists of members as
     277    distinguished names. Subclasses should set member_attr to define the member
     278    attribute name.
     279    """
     280    def __init__(self, member_attr, name_attr='cn'):
     281        """
     282        member_attr is the attribute on the group object that holds the list of
     283        member DNs.
     284        """
     285        self.member_attr = member_attr
     286       
     287        super(MemberDNGroupType, self).__init__(name_attr)
     288   
     289    def user_groups(self, ldap_user, group_search):
     290        search = group_search.search_with_additional_terms(
     291            {self.member_attr: ldap_user.dn})
     292        groups = search.execute(ldap_user.connection)
     293       
     294        return groups
     295
     296    def is_member(self, ldap_user, group_dn):
     297        return ldap_user.connection.compare_s(group_dn.encode('utf-8'),
     298            self.member_attr.encode('utf-8'), ldap_user.dn.encode('utf-8'))
     299
     300
     301class NestedMemberDNGroupType(LDAPGroupType):
     302    """
     303    An abstract base class for group types that store lists of members as
     304    distinguished names and support nested groups. There is no shortcut for
     305    is_member in this case, so it's left unimplemented.
     306    """
     307    def __init__(self, member_attr, name_attr='cn'):
     308        """
     309        member_attr is the attribute on the group object that holds the list of
     310        member DNs.
     311        """
     312        self.member_attr = member_attr
     313       
     314        super(NestedMemberDNGroupType, self).__init__(name_attr)
     315       
     316    def user_groups(self, ldap_user, group_search):
     317        """
     318        This searches for all of a user's groups from the bottom up. In other
     319        words, it returns the groups that the user belongs to, the groups that
     320        those groups belong to, etc. Circular references will be detected and
     321        pruned.
     322        """
     323        group_info_map = {} # Maps group_dn to group_info of groups we've found
     324        member_dn_set = set([ldap_user.dn]) # Member DNs to search with next
     325        handled_dn_set = set() # Member DNs that we've already searched with
     326       
     327        while len(member_dn_set) > 0:
     328            group_infos = self.find_groups_with_any_member(member_dn_set,
     329                group_search, ldap_user.connection)
     330            new_group_info_map = dict([(info[0], info) for info in group_infos])
     331            group_info_map.update(new_group_info_map)
     332            handled_dn_set.update(member_dn_set)
     333
     334            # Get ready for the next iteration. To avoid cycles, we make sure
     335            # never to search with the same member DN twice.
     336            member_dn_set = set(new_group_info_map.keys()) - handled_dn_set
     337       
     338        return group_info_map.values()
     339       
     340    def find_groups_with_any_member(self, member_dn_set, group_search, connection):
     341        terms = [
     342            u"(%s=%s)" % (self.member_attr, self.ldap.filter.escape_filter_chars(dn))
     343            for dn in member_dn_set
     344        ]
     345       
     346        filterstr = u"(|%s)" % "".join(terms)
     347        search = group_search.search_with_additional_term_string(filterstr)
     348       
     349        return search.execute(connection)
     350
     351
     352class GroupOfNamesType(MemberDNGroupType):
     353    """
     354    An LDAPGroupType subclass that handles groups of class groupOfNames.
     355    """
     356    def __init__(self, name_attr='cn'):
     357        super(GroupOfNamesType, self).__init__('member', name_attr)
     358
     359
     360class NestedGroupOfNamesType(NestedMemberDNGroupType):
     361    """
     362    An LDAPGroupType subclass that handles groups of class groupOfNames with
     363    nested group references.
     364    """
     365    def __init__(self, name_attr='cn'):
     366        super(NestedGroupOfNamesType, self).__init__('member', name_attr)
     367
     368
     369class GroupOfUniqueNamesType(MemberDNGroupType):
     370    """
     371    An LDAPGroupType subclass that handles groups of class groupOfUniqueNames.
     372    """
     373    def __init__(self, name_attr='cn'):
     374        super(GroupOfUniqueNamesType, self).__init__('uniqueMember', name_attr)
     375
     376
     377class NestedGroupOfUniqueNamesType(NestedMemberDNGroupType):
     378    """
     379    An LDAPGroupType subclass that handles groups of class groupOfUniqueNames
     380    with nested group references.
     381    """
     382    def __init__(self, name_attr='cn'):
     383        super(NestedGroupOfUniqueNamesType, self).__init__('uniqueMember', name_attr)
     384
     385
     386class ActiveDirectoryGroupType(MemberDNGroupType):
     387    """
     388    An LDAPGroupType subclass that handles Active Directory groups.
     389    """
     390    def __init__(self, name_attr='cn'):
     391        super(ActiveDirectoryGroupType, self).__init__('member', name_attr)
     392
     393
     394class NestedActiveDirectoryGroupType(NestedMemberDNGroupType):
     395    """
     396    An LDAPGroupType subclass that handles Active Directory groups with nested
     397    group references.
     398    """
     399    def __init__(self, name_attr='cn'):
     400        super(NestedActiveDirectoryGroupType, self).__init__('member', name_attr)
  • django/contrib/auth/contrib/ldap/backend.py

     
     1"""
     2LDAP authentication backend
     3
     4Complete documentation can be found in docs/howto/auth-ldap.txt (or the thing it
     5compiles to).
     6
     7Use of this backend requires the python-ldap module. To support unit tests, we
     8import ldap in a single centralized place (config._LDAPConfig) so that the test
     9harness can insert a mock object.
     10
     11A few notes on naming conventions. If an identifier ends in _dn, it is a string
     12representation of a distinguished name. If it ends in _info, it is a 2-tuple
     13containing a DN and a dictionary of lists of attributes. ldap.search_s returns a
     14list of such structures. An identifier that ends in _attrs is the dictionary of
     15attributes from the _info structure.
     16
     17A connection is an LDAPObject that has been successfully bound with a DN and
     18password. The identifier 'user' always refers to a User model object; LDAP user
     19information will be user_dn or user_info.
     20
     21Additional classes can be found in the config module next to this one.
     22"""
     23
     24try:
     25    set
     26except NameError:
     27    from sets import Set as set     # Python 2.3 fallback
     28
     29import pprint
     30
     31import django.db
     32from django.contrib.auth.models import User, Group, SiteProfileNotAvailable
     33from django.contrib.auth.contrib.ldap.config import _LDAPConfig, LDAPSearch
     34from django.core.cache import cache
     35from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
     36
     37
     38logger = _LDAPConfig.get_logger()
     39
     40
     41class LDAPBackend(object):
     42    """
     43    The main backend class. This implements the auth backend API, although it
     44    actually delegates most of its work to _LDAPUser, which is defined next.
     45    """
     46    ldap = None # The cached ldap module (or mock object)
     47   
     48    def __init__(self):
     49        self.ldap = self.ldap_module()
     50   
     51    def ldap_module(cls):
     52        """
     53        Requests the ldap module from _LDAPConfig. Under a test harness, this
     54        will be a mock object. We only do this once because this is where we
     55        apply AUTH_LDAP_GLOBAL_OPTIONS.
     56        """
     57        if cls.ldap is None:
     58            cls.ldap = _LDAPConfig.get_ldap()
     59           
     60            for opt, value in ldap_settings.AUTH_LDAP_GLOBAL_OPTIONS.iteritems():
     61                cls.ldap.set_option(opt, value)
     62       
     63        return cls.ldap
     64    ldap_module = classmethod(ldap_module)
     65       
     66   
     67    #
     68    # The Django auth backend API
     69    #
     70
     71    def authenticate(self, username, password):
     72        ldap_user = _LDAPUser(self, username=username)
     73        user = ldap_user.authenticate(password)
     74       
     75        return user
     76   
     77    def get_user(self, user_id):
     78        user = None
     79       
     80        try:
     81            user = User.objects.get(pk=user_id)
     82            _LDAPUser(self, user=user) # This sets user.ldap_user
     83        except User.DoesNotExist:
     84            pass
     85       
     86        return user
     87   
     88    def has_perm(self, user, perm):
     89        return perm in self.get_all_permissions(user)
     90
     91    def has_module_perms(self, user, app_label):
     92        for perm in self.get_all_permissions(user):
     93            if perm[:perm.index('.')] == app_label:
     94                return True
     95
     96        return False
     97
     98    def get_all_permissions(self, user):
     99        return self.get_group_permissions(user)
     100
     101    def get_group_permissions(self, user):
     102        if hasattr(user, 'ldap_user'):
     103            return user.ldap_user.get_group_permissions()
     104        else:
     105            return set()
     106   
     107    #
     108    # Hooks for subclasses
     109    #
     110   
     111    def ldap_to_django_username(self, username):
     112        return username
     113
     114    def django_to_ldap_username(self, username):
     115        return username
     116
     117
     118class _LDAPUser(object):
     119    """
     120    Represents an LDAP user and ultimately fields all requests that the
     121    backend receives. This class exists for two reasons. First, it's
     122    convenient to have a separate object for each request so that we can use
     123    object attributes without running into threading problems. Second, these
     124    objects get attached to the User objects, which allows us to cache
     125    expensive LDAP information, especially around groups and permissions.
     126   
     127    self.backend is a reference back to the LDAPBackend instance, which we need
     128    to access the ldap module and any hooks that a subclass has overridden.
     129    """
     130    class AuthenticationFailed(Exception):
     131        pass
     132   
     133    #
     134    # Initialization
     135    #
     136   
     137    def __init__(self, backend, username=None, user=None):
     138        """
     139        A new LDAPUser must be initialized with either a username or an
     140        authenticated User object. If a user is given, the username will be
     141        ignored.
     142        """
     143        self.backend = backend
     144        self.ldap = backend.ldap_module()
     145        self._username = username
     146        self._password = None
     147        self._user_dn = None
     148        self._user_attrs = None
     149        self._user = None
     150        self._groups = None
     151        self._group_permissions = None
     152        self._connection = None
     153        self._connection_bound = False
     154       
     155        if user is not None:
     156            self._set_authenticated_user(user)
     157       
     158        if username is None and user is None:
     159            raise Exception("Internal error: _LDAPUser improperly initialized.")
     160   
     161    def _set_authenticated_user(self, user):
     162        self._user = user
     163        self._username = self.backend.django_to_ldap_username(user.username)
     164
     165        user.ldap_user = self
     166        user.ldap_username = self._username
     167   
     168    #
     169    # Entry points
     170    #
     171   
     172    def authenticate(self, password):
     173        """
     174        Authenticates against the LDAP directory and returns the corresponding
     175        User object if successful. Returns None on failure.
     176        """
     177        user = None
     178       
     179        self._password = password
     180       
     181        try:
     182            self._authenticate_user_dn()
     183            self._check_requirements()
     184            self._get_or_create_user()
     185
     186            user = self._user
     187        except self.AuthenticationFailed, e:
     188            logger.debug(u"Authentication failed for %s" % self._username)
     189        except self.ldap.LDAPError, e:
     190            logger.warning(u"Caught LDAPError while authenticating %s: %s",
     191                self._username, pprint.pformat(e))
     192        except Exception, e:
     193            logger.warning(u"Caught Exception while authenticating %s: %s",
     194                self._username, pprint.pformat(e))
     195            raise
     196       
     197        return user
     198   
     199    def get_group_permissions(self):
     200        """
     201        If allowed by the configuration, this returns the set of permissions
     202        defined by the user's LDAP group memberships.
     203        """
     204        if self._group_permissions is None:
     205            self._group_permissions = set()
     206
     207            if ldap_settings.AUTH_LDAP_FIND_GROUP_PERMS:
     208                try:
     209                    self._load_group_permissions()
     210                except self.ldap.LDAPError, e:
     211                    logger.warning("Caught LDAPError loading group permissions: %s",
     212                        pprint.pformat(e))
     213       
     214        return self._group_permissions
     215
     216    #
     217    # Public properties (callbacks). These are all lazy for performance reasons.
     218    #
     219
     220    def _get_user_dn(self):
     221        if self._user_dn is None:
     222            self._load_user_dn()
     223       
     224        return self._user_dn
     225    dn = property(_get_user_dn)
     226
     227    def _get_user_attrs(self):
     228        if self._user_attrs is None:
     229            self._load_user_attrs()
     230       
     231        return self._user_attrs
     232    attrs = property(_get_user_attrs)
     233
     234    def _get_bound_connection(self):
     235        if not self._connection_bound:
     236            self._bind()
     237       
     238        return self._get_connection()
     239    connection = property(_get_bound_connection)
     240
     241    #
     242    # Authentication
     243    #
     244
     245    def _authenticate_user_dn(self):
     246        """
     247        Binds to the LDAP server with the user's DN and password. Raises
     248        AuthenticationFailed on failure.
     249        """
     250        try:
     251            self._bind_as(self.dn, self._password)
     252        except self.ldap.INVALID_CREDENTIALS:
     253            raise self.AuthenticationFailed("User DN/password rejected by LDAP server.")
     254   
     255    def _load_user_attrs(self):
     256        search = LDAPSearch(self.dn, self.ldap.SCOPE_BASE)
     257        results = search.execute(self.connection)
     258       
     259        self._user_attrs = results[0][1]
     260   
     261    def _load_user_dn(self):
     262        """
     263        Returns the (cached) distinguished name of our user. This will
     264        ultimately either construct the DN from a template in
     265        AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it.
     266        This may result in an AuthenticationFailed exception if we do not get
     267        satisfactory results searching for the user's DN.
     268        """
     269        if self._using_simple_bind_mode():
     270            self._construct_simple_user_dn()
     271        else:
     272            self._search_for_user_dn()
     273
     274    def _using_simple_bind_mode(self):
     275        return (ldap_settings.AUTH_LDAP_USER_DN_TEMPLATE is not None)
     276
     277    def _construct_simple_user_dn(self):
     278        template = ldap_settings.AUTH_LDAP_USER_DN_TEMPLATE
     279        username = self.ldap.dn.escape_dn_chars(self._username)
     280       
     281        self._user_dn = template % {'user': username}
     282
     283    def _search_for_user_dn(self):
     284        """
     285        Searches the directory for a user matching AUTH_LDAP_USER_SEARCH.
     286        Populates self._user_dn and self._user_attrs.
     287        """
     288        search = ldap_settings.AUTH_LDAP_USER_SEARCH
     289        if search is None:
     290            raise ImproperlyConfigured('AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance.')
     291       
     292        results = search.execute(self.connection, {'user': self._username})
     293        if results is None or len(results) != 1:
     294            raise self.AuthenticationFailed("AUTH_LDAP_USER_SEARCH failed to return exactly one result.")
     295
     296        (self._user_dn, self._user_attrs) = results[0]
     297
     298    def _check_requirements(self):
     299        """
     300        Checks all authentication requirements beyond credentials. Raises
     301        AuthenticationFailed on failure.
     302        """
     303        self._check_required_group()
     304   
     305    def _check_required_group(self):
     306        """
     307        Returns True if the group requirement (AUTH_LDAP_REQUIRE_GROUP) is
     308        met. Always returns True if AUTH_LDAP_REQUIRE_GROUP is None.
     309        """
     310        required_group_dn = ldap_settings.AUTH_LDAP_REQUIRE_GROUP
     311       
     312        if required_group_dn is not None:
     313            is_member = self._get_groups().is_member_of(required_group_dn)
     314            if not is_member:
     315                raise self.AuthenticationFailed("User is not a member of AUTH_LDAP_REQUIRE_GROUP")
     316
     317    #
     318    # User management
     319    #
     320
     321    def _get_or_create_user(self):
     322        """
     323        Loads the User model object from the database or creates it if it
     324        doesn't exist. Also populates the fields, subject to
     325        AUTH_LDAP_ALWAYS_UPDATE_USER.
     326        """
     327        save_user = False
     328       
     329        username = self.backend.ldap_to_django_username(self._username)
     330
     331        (self._user, created) = User.objects.get_or_create(username=username)
     332
     333        if created:
     334            logger.debug("Created Django user %s", username)
     335            self._user.set_unusable_password()
     336            save_user = True
     337
     338        if(ldap_settings.AUTH_LDAP_ALWAYS_UPDATE_USER or created):
     339            logger.debug("Populating Django user %s", username)
     340            self._populate_user()
     341            self._populate_and_save_user_profile()
     342            save_user = True
     343
     344        if ldap_settings.AUTH_LDAP_MIRROR_GROUPS:
     345            self._mirror_groups()
     346
     347        if save_user:
     348            self._user.save()
     349
     350        self._user.ldap_user = self
     351        self._user.ldap_username = self._username
     352
     353    def _populate_user(self):
     354        """
     355        Populates our User object with information from the LDAP directory.
     356        """
     357        self._populate_user_from_attributes()
     358        self._populate_user_from_group_memberships()
     359   
     360    def _populate_user_from_attributes(self):
     361        for field, attr in ldap_settings.AUTH_LDAP_USER_ATTR_MAP.iteritems():
     362            try:
     363                setattr(self._user, field, self.attrs[attr][0])
     364            except (KeyError, IndexError):
     365                pass
     366   
     367    def _populate_user_from_group_memberships(self):
     368        for field, group_dn in ldap_settings.AUTH_LDAP_USER_FLAGS_BY_GROUP.iteritems():
     369            value = self._get_groups().is_member_of(group_dn)
     370            setattr(self._user, field, value)
     371
     372    def _populate_and_save_user_profile(self):
     373        """
     374        Populates a User profile object with fields from the LDAP directory.
     375        """
     376        try:
     377            profile = self._user.get_profile()
     378
     379            for field, attr in ldap_settings.AUTH_LDAP_PROFILE_ATTR_MAP.iteritems():
     380                try:
     381                    # user_attrs is a hash of lists of attribute values
     382                    setattr(profile, field, self.attrs[attr][0])
     383                except (KeyError, IndexError):
     384                    pass
     385
     386            if len(ldap_settings.AUTH_LDAP_PROFILE_ATTR_MAP) > 0:
     387                profile.save()
     388        except (SiteProfileNotAvailable, ObjectDoesNotExist):
     389            pass
     390   
     391    def _mirror_groups(self):
     392        """
     393        Mirrors the user's LDAP groups in the Django database and updates the
     394        user's membership.
     395        """
     396        group_names = self._get_groups().get_group_names()
     397        groups = [Group.objects.get_or_create(name=group_name)[0] for group_name
     398            in group_names]
     399       
     400        self._user.groups = groups
     401   
     402    #
     403    # Group information
     404    #
     405   
     406    def _load_group_permissions(self):
     407        """
     408        Populates self._group_permissions based on LDAP group membership and
     409        Django group permissions.
     410       
     411        The SQL is lifted (with modifications) from ModelBackend.
     412        """
     413        group_names = self._get_groups().get_group_names()
     414        placeholders = ', '.join(['%s'] * len(group_names))
     415       
     416        cursor = django.db.connection.cursor()
     417        # The SQL below works out to the following, after DB quoting:
     418        # cursor.execute("""
     419        #     SELECT ct."app_label", p."codename"
     420        #     FROM "auth_permission" p, "auth_group_permissions" gp, "auth_group" g, "django_content_type" ct
     421        #     WHERE p."id" = gp."permission_id"
     422        #         AND gp."group_id" = g."id"
     423        #         AND ct."id" = p."content_type_id"
     424        #         AND g."name" IN (%s, %s, ...)""", ['group1', 'group2', ...])
     425        qn = django.db.connection.ops.quote_name
     426        sql = u"""
     427            SELECT ct.%s, p.%s
     428            FROM %s p, %s gp, %s g, %s ct
     429            WHERE p.%s = gp.%s
     430                AND gp.%s = g.%s
     431                AND ct.%s = p.%s
     432                AND g.%s IN (%s)""" % (
     433            qn('app_label'), qn('codename'),
     434            qn('auth_permission'), qn('auth_group_permissions'),
     435            qn('auth_group'), qn('django_content_type'),
     436            qn('id'), qn('permission_id'),
     437            qn('group_id'), qn('id'),
     438            qn('id'), qn('content_type_id'),
     439            qn('name'), placeholders)
     440       
     441        cursor.execute(sql, group_names)
     442        self._group_permissions = \
     443            set([u"%s.%s" % (row[0], row[1]) for row in cursor.fetchall()])
     444
     445    def _get_groups(self):
     446        """
     447        Returns an _LDAPUserGroups object, which can determine group
     448        membership.
     449        """
     450        if self._groups is None:
     451            self._groups = _LDAPUserGroups(self)
     452       
     453        return self._groups
     454
     455    #
     456    # LDAP connection
     457    #
     458
     459    def _bind(self):
     460        """
     461        Binds to the LDAP server with AUTH_LDAP_BIND_DN and
     462        AUTH_LDAP_BIND_PASSWORD.
     463        """
     464        self._bind_as(ldap_settings.AUTH_LDAP_BIND_DN,
     465            ldap_settings.AUTH_LDAP_BIND_PASSWORD)
     466   
     467    def _bind_as(self, bind_dn, bind_password):
     468        """
     469        Binds to the LDAP server with the given credentials. This does not trap
     470        exceptions.
     471        """
     472        self._get_connection().simple_bind_s(bind_dn.encode('utf-8'),
     473            bind_password.encode('utf-8'))
     474        self._connection_bound = True
     475
     476    def _get_connection(self):
     477        """
     478        Returns our cached LDAPObject, which may or may not be bound.
     479        """
     480        if self._connection is None:
     481            self._connection = self.ldap.initialize(ldap_settings.AUTH_LDAP_SERVER_URI)
     482           
     483            for opt, value in ldap_settings.AUTH_LDAP_CONNECTION_OPTIONS.iteritems():
     484                self._connection.set_option(opt, value)
     485       
     486        return self._connection
     487
     488
     489
     490class _LDAPUserGroups(object):
     491    """
     492    Represents the set of groups that a user belongs to.
     493    """
     494    def __init__(self, ldap_user):
     495        self._ldap_user = ldap_user
     496        self._group_type = None
     497        self._group_search = None
     498        self._group_infos = None
     499        self._group_dns = None
     500        self._group_names = None
     501       
     502        self._init_group_settings()
     503   
     504    def _init_group_settings(self):
     505        """
     506        Loads the settings we need to deal with groups. Raises
     507        ImproperlyConfigured if anything's not right.
     508        """
     509        self._group_type = ldap_settings.AUTH_LDAP_GROUP_TYPE
     510        if self._group_type is None:
     511            raise ImproperlyConfigured("AUTH_LDAP_GROUP_TYPE must be an LDAPGroupType instance.")
     512       
     513        self._group_search = ldap_settings.AUTH_LDAP_GROUP_SEARCH
     514        if self._group_search is None:
     515            raise ImproperlyConfigured("AUTH_LDAP_GROUP_SEARCH must be an LDAPSearch instance.")
     516   
     517    def get_group_names(self):
     518        """
     519        Returns the list of Django group names that this user belongs to by
     520        virtue of LDAP group memberships.
     521        """
     522        if self._group_names is None:
     523            self._load_cached_attr("_group_names")
     524       
     525        if self._group_names is None:
     526            group_infos = self._get_group_infos()
     527            self._group_names = [self._group_type.group_name_from_info(group_info)
     528                for group_info in group_infos]
     529            self._cache_attr("_group_names")
     530       
     531        return self._group_names
     532   
     533    def is_member_of(self, group_dn):
     534        """
     535        Returns true if our user is a member of the given group.
     536        """
     537        is_member = None
     538       
     539        # If we have self._group_dns, we'll use it. Otherwise, we'll try to
     540        # avoid the cost of loading it.
     541        if self._group_dns is None:
     542            is_member = self._group_type.is_member(self._ldap_user, group_dn)
     543       
     544        if is_member is None:
     545            is_member = (group_dn in self._get_group_dns())
     546       
     547        logger.debug("%s is%sa member of %s", self._ldap_user.dn,
     548                     is_member and " " or " not ", group_dn)
     549
     550        return is_member
     551   
     552    def _get_group_dns(self):
     553        """
     554        Returns a (cached) set of the distinguished names in self._group_infos.
     555        """
     556        if self._group_dns is None:
     557            group_infos = self._get_group_infos()
     558            self._group_dns = set([group_info[0] for group_info in group_infos])
     559       
     560        return self._group_dns
     561   
     562    def _get_group_infos(self):
     563        """
     564        Returns a (cached) list of group_info structures for the groups that our
     565        user is a member of.
     566        """
     567        if self._group_infos is None:
     568            self._group_infos = self._group_type.user_groups(self._ldap_user,
     569                self._group_search)
     570       
     571        return self._group_infos
     572
     573    def _load_cached_attr(self, attr_name):
     574        if ldap_settings.AUTH_LDAP_CACHE_GROUPS:
     575            key = self._cache_key(attr_name)
     576            value = cache.get(key)
     577            setattr(self, attr_name, value)
     578   
     579    def _cache_attr(self, attr_name):
     580        if ldap_settings.AUTH_LDAP_CACHE_GROUPS:
     581            key = self._cache_key(attr_name)
     582            value = getattr(self, attr_name, None)
     583            cache.set(key, value, ldap_settings.AUTH_LDAP_GROUP_CACHE_TIMEOUT)
     584   
     585    def _cache_key(self, attr_name):
     586        return u'auth_ldap.%s.%s.%s' % (self.__class__.__name__, attr_name, self._ldap_user.dn)
     587
     588
     589class LDAPSettings(object):
     590    """
     591    This is a simple class to take the place of the global settings object. An
     592    instance will contain all of our settings as attributes, with default values
     593    if they are not specified by the configuration.
     594    """
     595    defaults = {
     596        'AUTH_LDAP_ALWAYS_UPDATE_USER': True,
     597        'AUTH_LDAP_BIND_DN': '',
     598        'AUTH_LDAP_BIND_PASSWORD': '',
     599        'AUTH_LDAP_CACHE_GROUPS': False,
     600        'AUTH_LDAP_CONNECTION_OPTIONS': {},
     601        'AUTH_LDAP_FIND_GROUP_PERMS': False,
     602        'AUTH_LDAP_GLOBAL_OPTIONS': {},
     603        'AUTH_LDAP_GROUP_CACHE_TIMEOUT': None,
     604        'AUTH_LDAP_GROUP_SEARCH': None,
     605        'AUTH_LDAP_GROUP_TYPE': None,
     606        'AUTH_LDAP_MIRROR_GROUPS': False,
     607        'AUTH_LDAP_PROFILE_ATTR_MAP': {},
     608        'AUTH_LDAP_REQUIRE_GROUP': None,
     609        'AUTH_LDAP_SERVER_URI': 'ldap://localhost',
     610        'AUTH_LDAP_USER_ATTR_MAP': {},
     611        'AUTH_LDAP_USER_DN_TEMPLATE': None,
     612        'AUTH_LDAP_USER_FLAGS_BY_GROUP': {},
     613        'AUTH_LDAP_USER_SEARCH': None,
     614    }
     615   
     616    def __init__(self):
     617        """
     618        Loads our settings from django.conf.settings, applying defaults for any
     619        that are omitted.
     620        """
     621        from django.conf import settings
     622       
     623        for name, default in self.defaults.iteritems():
     624            value = getattr(settings, name, default)
     625            setattr(self, name, value)
     626
     627
     628# Our global settings object
     629ldap_settings = LDAPSettings()
  • django/contrib/auth/tests/__init__.py

     
    44from django.contrib.auth.tests.forms import FORM_TESTS
    55from django.contrib.auth.tests.remote_user \
    66        import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest
     7from django.contrib.auth.tests.ldap import LDAPTest
    78from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
    89
    910# The password for the fixture data users is 'password'
  • django/contrib/auth/tests/ldap.py

     
     1# coding: utf-8
     2
     3try:
     4    set
     5except NameError:
     6    from sets import Set as set     # Python 2.3 fallback
     7
     8import logging
     9import sys
     10
     11from django.contrib.auth.contrib.ldap import backend
     12from django.contrib.auth.contrib.ldap.config import _LDAPConfig, LDAPSearch
     13from django.contrib.auth.contrib.ldap.config import PosixGroupType, MemberDNGroupType, NestedMemberDNGroupType
     14from django.contrib.auth.contrib.ldap.config import GroupOfNamesType, NestedGroupOfNamesType
     15from django.contrib.auth.contrib.ldap.config import GroupOfUniqueNamesType, NestedGroupOfUniqueNamesType
     16from django.contrib.auth.contrib.ldap.config import ActiveDirectoryGroupType, NestedActiveDirectoryGroupType
     17from django.contrib.auth.models import User, Permission, Group
     18from django.test import TestCase
     19
     20
     21class TestSettings(backend.LDAPSettings):
     22    """
     23    A replacement for backend.LDAPSettings that does not load settings
     24    from django.conf.
     25    """
     26    def __init__(self, **kwargs):
     27        for name, default in self.defaults.iteritems():
     28            value = kwargs.get(name, default)
     29            setattr(self, name, value)
     30
     31
     32class MockLDAP(object):
     33    """
     34    This is a stand-in for the python-ldap module; it serves as both the ldap
     35    module and the LDAPObject class. While it's temping to add some real LDAP
     36    capabilities here, this is designed to remain as simple as possible, so as
     37    to minimize the risk of creating bogus unit tests through a buggy test
     38    harness.
     39   
     40    Simple operations can be simulated, but for nontrivial searches, the client
     41    will have to seed the mock object with return values for expected API calls.
     42    This may sound like cheating, but it's really no more so than a simulated
     43    LDAP server. The fact is we can not require python-ldap to be installed in
     44    order to run the unit tests, so all we can do is verify that LDAPBackend is
     45    calling the APIs that we expect.
     46
     47    set_return_value takes the name of an API, a tuple of arguments, and a
     48    return value. Every time an API is called, it looks for a predetermined
     49    return value based on the arguments received. If it finds one, then it
     50    returns it, or raises it if it's an Exception. If it doesn't find one, then
     51    it tries to satisfy the request internally. If it can't, it raises a
     52    PresetReturnRequiredError.
     53   
     54    At any time, the client may call ldap_methods_called_with_arguments() or
     55    ldap_methods_called() to get a record of all of the LDAP API calls that have
     56    been made, with or without arguments.
     57    """
     58   
     59    class PresetReturnRequiredError(Exception): pass
     60   
     61    SCOPE_BASE = 0
     62    SCOPE_ONELEVEL = 1
     63    SCOPE_SUBTREE = 2
     64   
     65    class LDAPError(Exception): pass
     66    class INVALID_CREDENTIALS(LDAPError): pass
     67    class NO_SUCH_OBJECT(LDAPError): pass
     68   
     69    #
     70    # Submodules
     71    #
     72    class dn(object):
     73        def escape_dn_chars(s):
     74            return s
     75        escape_dn_chars = staticmethod(escape_dn_chars)
     76
     77    class filter(object):
     78        def escape_filter_chars(s):
     79            return s
     80        escape_filter_chars = staticmethod(escape_filter_chars)
     81
     82
     83    def __init__(self, directory):
     84        """
     85        directory is a complex structure with the entire contents of the
     86        mock LDAP directory. directory must be a dictionary mapping
     87        distinguished names to dictionaries of attributes. Each attribute
     88        dictionary maps attribute names to lists of values. e.g.:
     89       
     90        {
     91            "uid=alice,ou=users,dc=example,dc=com":
     92            {
     93                "uid": ["alice"],
     94                "userPassword": ["secret"],
     95            },
     96        }
     97        """
     98        self.directory = directory
     99
     100        self.reset()
     101   
     102    def reset(self):
     103        """
     104        Resets our recorded API calls and queued return values as well as
     105        miscellaneous configuration options.
     106        """
     107        self.calls = []
     108        self.return_value_maps = {}
     109        self.options = {}
     110   
     111    def set_return_value(self, api_name, arguments, value):
     112        """
     113        Stores a preset return value for a given API with a given set of
     114        arguments.
     115        """
     116        self.return_value_maps.setdefault(api_name, {})[arguments] = value
     117   
     118    def ldap_methods_called_with_arguments(self):
     119        """
     120        Returns a list of 2-tuples, one for each API call made since the last
     121        reset. Each tuple contains the name of the API and a dictionary of
     122        arguments. Argument defaults are included.
     123        """
     124        return self.calls
     125   
     126    def ldap_methods_called(self):
     127        """
     128        Returns the list of API names called.
     129        """
     130        return [call[0] for call in self.calls]
     131   
     132    #
     133    # Begin LDAP methods
     134    #
     135   
     136    def set_option(self, option, invalue):
     137        self._record_call('set_option', {
     138            'option': option,
     139            'invalue': invalue
     140        })
     141       
     142        self.options[option] = invalue
     143   
     144    def initialize(self, uri, trace_level=0, trace_file=sys.stdout, trace_stack_limit=None):
     145        self._record_call('initialize', {
     146            'uri': uri,
     147            'trace_level': trace_level,
     148            'trace_file': trace_file,
     149            'trace_stack_limit': trace_stack_limit
     150        })
     151       
     152        value = self._get_return_value('initialize',
     153            (uri, trace_level, trace_file, trace_stack_limit))
     154        if value is None:
     155            value = self
     156       
     157        return value
     158
     159    def simple_bind_s(self, who='', cred=''):
     160        self._record_call('simple_bind_s', {
     161            'who': who,
     162            'cred': cred
     163        })
     164       
     165        value = self._get_return_value('simple_bind_s', (who, cred))
     166        if value is None:
     167            value = self._simple_bind_s(who, cred)
     168       
     169        return value
     170
     171    def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
     172        self._record_call('search_s', {
     173            'base': base,
     174            'scope': scope,
     175            'filterstr':filterstr,
     176            'attrlist':attrlist,
     177            'attrsonly':attrsonly
     178        })
     179       
     180        value = self._get_return_value('search_s',
     181            (base, scope, filterstr, attrlist, attrsonly))
     182        if value is None:
     183            value = self._search_s(base, scope, filterstr, attrlist, attrsonly)
     184       
     185        return value
     186   
     187    def compare_s(self, dn, attr, value):
     188        self._record_call('compare_s', {
     189            'dn': dn,
     190            'attr': attr,
     191            'value': value
     192        })
     193       
     194        result = self._get_return_value('compare_s', (dn, attr, value))
     195        if result is None:
     196            result = self._compare_s(dn, attr, value)
     197       
     198        # print "compare_s('%s', '%s', '%s'): %d" % (dn, attr, value, result)
     199       
     200        return result
     201
     202    #
     203    # Internal implementations
     204    #
     205
     206    def _simple_bind_s(self, who='', cred=''):
     207        success = False
     208       
     209        if(who == '' and cred == ''):
     210            success = True
     211        elif self._compare_s(who, 'userPassword', cred):
     212            success = True
     213
     214        if success:
     215            return (97, []) # python-ldap returns this; I don't know what it means
     216        else:
     217            raise self.INVALID_CREDENTIALS('%s:%s' % (who, cred))
     218   
     219    def _compare_s(self, dn, attr, value):
     220        try:
     221            found = (value in self.directory[dn][attr])
     222        except KeyError:
     223            found = False
     224       
     225        return found and 1 or 0
     226   
     227    def _search_s(self, base, scope, filterstr, attrlist, attrsonly):
     228        """
     229        We can do a SCOPE_BASE search with the default filter. Beyond that,
     230        you're on your own.
     231        """
     232        if scope != self.SCOPE_BASE:
     233            raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' %
     234                (base, scope, filterstr, attrlist, attrsonly))
     235       
     236        if filterstr != '(objectClass=*)':
     237            raise self.PresetReturnRequiredError('search_s("%s", %d, "%s", "%s", %d)' %
     238                (base, scope, filterstr, attrlist, attrsonly))
     239       
     240        attrs = self.directory.get(base)
     241        if attrs is None:
     242            raise self.NO_SUCH_OBJECT()
     243       
     244        return [(base, attrs)]
     245   
     246    #
     247    # Utils
     248    #
     249
     250    def _record_call(self, api_name, arguments):
     251        self.calls.append((api_name, arguments))
     252
     253    def _get_return_value(self, api_name, arguments):
     254        try:
     255            value = self.return_value_maps[api_name][arguments]
     256        except KeyError:
     257            value = None
     258       
     259        if isinstance(value, Exception):
     260            raise value
     261       
     262        return value
     263
     264
     265class LDAPTest(TestCase):
     266   
     267    # Following are the objecgs in our mock LDAP directory
     268    alice = ("uid=alice,ou=people,o=test", {
     269        "uid": ["alice"],
     270        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
     271        "userPassword": ["password"],
     272        "uidNumber": ["1000"],
     273        "gidNumber": ["1000"],
     274        "givenName": ["Alice"],
     275        "sn": ["Adams"]
     276    })
     277    bob = ("uid=bob,ou=people,o=test", {
     278        "uid": ["bob"],
     279        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
     280        "userPassword": ["password"],
     281        "uidNumber": ["1001"],
     282        "gidNumber": ["50"],
     283        "givenName": ["Robert"],
     284        "sn": ["Barker"]
     285    })
     286    dressler = (u"uid=dreßler,ou=people,o=test".encode('utf-8'), {
     287        "uid": [u"dreßler".encode('utf-8')],
     288        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
     289        "userPassword": ["password"],
     290        "uidNumber": ["1002"],
     291        "gidNumber": ["50"],
     292        "givenName": ["Wolfgang"],
     293        "sn": [u"Dreßler".encode('utf-8')]
     294    })
     295    nobody = ("uid=nobody,ou=people,o=test", {
     296        "uid": ["nobody"],
     297        "objectClass": ["person", "organizationalPerson", "inetOrgPerson", "posixAccount"],
     298        "userPassword": ["password"],
     299        "binaryAttr": ["\xb2"]  # Invalid UTF-8
     300    })
     301
     302    # posixGroup objects
     303    active_px = ("cn=active_px,ou=groups,o=test", {
     304        "cn": ["active_px"],
     305        "objectClass": ["posixGroup"],
     306        "gidNumber": ["1000"],
     307    })
     308    staff_px = ("cn=staff_px,ou=groups,o=test", {
     309        "cn": ["staff_px"],
     310        "objectClass": ["posixGroup"],
     311        "gidNumber": ["1001"],
     312        "memberUid": ["alice"],
     313    })
     314    superuser_px = ("cn=superuser_px,ou=groups,o=test", {
     315        "cn": ["superuser_px"],
     316        "objectClass": ["posixGroup"],
     317        "gidNumber": ["1002"],
     318        "memberUid": ["alice"],
     319    })
     320
     321    # groupOfUniqueName groups
     322    active_gon = ("cn=active_gon,ou=groups,o=test", {
     323        "cn": ["active_gon"],
     324        "objectClass": ["groupOfNames"],
     325        "member": ["uid=alice,ou=people,o=test"]
     326    })
     327    staff_gon = ("cn=staff_gon,ou=groups,o=test", {
     328        "cn": ["staff_gon"],
     329        "objectClass": ["groupOfNames"],
     330        "member": ["uid=alice,ou=people,o=test"]
     331    })
     332    superuser_gon = ("cn=superuser_gon,ou=groups,o=test", {
     333        "cn": ["superuser_gon"],
     334        "objectClass": ["groupOfNames"],
     335        "member": ["uid=alice,ou=people,o=test"]
     336    })
     337   
     338    # Nested groups with a circular reference
     339    parent_gon = ("cn=parent_gon,ou=groups,o=test", {
     340        "cn": ["parent_gon"],
     341        "objectClass": ["groupOfNames"],
     342        "member": ["cn=nested_gon,ou=groups,o=test"]
     343    })
     344    nested_gon = ("cn=nested_gon,ou=groups,o=test", {
     345        "cn": ["nested_gon"],
     346        "objectClass": ["groupOfNames"],
     347        "member": [
     348            "uid=alice,ou=people,o=test",
     349            "cn=circular_gon,ou=groups,o=test"
     350        ]
     351    })
     352    circular_gon = ("cn=circular_gon,ou=groups,o=test", {
     353        "cn": ["circular_gon"],
     354        "objectClass": ["groupOfNames"],
     355        "member": ["cn=parent_gon,ou=groups,o=test"]
     356    })
     357   
     358
     359    mock_ldap = MockLDAP({
     360        alice[0]: alice[1],
     361        bob[0]: bob[1],
     362        dressler[0]: dressler[1],
     363        nobody[0]: nobody[1],
     364        active_px[0]: active_px[1],
     365        staff_px[0]: staff_px[1],
     366        superuser_px[0]: superuser_px[1],
     367        active_gon[0]: active_gon[1],
     368        staff_gon[0]: staff_gon[1],
     369        superuser_gon[0]: superuser_gon[1],
     370        parent_gon[0]: parent_gon[1],
     371        nested_gon[0]: nested_gon[1],
     372        circular_gon[0]: circular_gon[1],
     373    })
     374   
     375
     376    logging_configured = False
     377    def configure_logger(cls):
     378        if not cls.logging_configured:
     379            logger = logging.getLogger('django.contrib.auth.contrib.ldap')
     380            formatter = logging.Formatter("LDAP auth - %(levelname)s - %(message)s")
     381            handler = logging.StreamHandler()
     382       
     383            handler.setLevel(logging.DEBUG)
     384            handler.setFormatter(formatter)
     385            logger.addHandler(handler)
     386       
     387            logger.setLevel(logging.CRITICAL)
     388           
     389            cls.logging_configured = True
     390    configure_logger = classmethod(configure_logger)
     391   
     392
     393    def setUp(self):
     394        self.configure_logger()
     395        self.mock_ldap.reset()
     396
     397        _LDAPConfig.ldap = self.mock_ldap
     398        self.backend = backend.LDAPBackend()
     399   
     400   
     401    def tearDown(self):
     402        pass
     403
     404   
     405    def test_options(self):
     406        self._init_settings(
     407            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     408            AUTH_LDAP_CONNECTION_OPTIONS={'opt1': 'value1'}
     409        )
     410       
     411        user = self.backend.authenticate(username='alice', password='password')
     412       
     413        self.assertEqual(self.mock_ldap.options, {'opt1': 'value1'})
     414   
     415   
     416    def test_simple_bind(self):
     417        self._init_settings(
     418            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
     419        )
     420        user_count = User.objects.count()
     421       
     422        user = self.backend.authenticate(username='alice', password='password')
     423       
     424        self.assert_(not user.has_usable_password())
     425        self.assertEqual(user.username, 'alice')
     426        self.assertEqual(User.objects.count(), user_count + 1)
     427        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     428            ['initialize', 'simple_bind_s'])
     429
     430
     431    def test_simple_bind_bad_user(self):
     432        self._init_settings(
     433            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
     434        )
     435        user_count = User.objects.count()
     436
     437        user = self.backend.authenticate(username='evil_alice', password='password')
     438
     439        self.assert_(user is None)
     440        self.assertEqual(User.objects.count(), user_count)
     441        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     442            ['initialize', 'simple_bind_s'])
     443
     444
     445    def test_simple_bind_bad_password(self):
     446        self._init_settings(
     447            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
     448        )
     449        user_count = User.objects.count()
     450
     451        user = self.backend.authenticate(username='alice', password='bogus')
     452
     453        self.assert_(user is None)
     454        self.assertEqual(User.objects.count(), user_count)
     455        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     456            ['initialize', 'simple_bind_s'])
     457   
     458   
     459    def test_existing_user(self):
     460        self._init_settings(
     461            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
     462        )
     463        User.objects.create(username='alice')
     464        user_count = User.objects.count()
     465       
     466        user = self.backend.authenticate(username='alice', password='password')
     467       
     468        # Make sure we only created one user
     469        self.assert_(user is not None)
     470        self.assertEqual(User.objects.count(), user_count)
     471
     472
     473    def test_convert_username(self):
     474        class MyBackend(backend.LDAPBackend):
     475            def ldap_to_django_username(self, username):
     476                return 'ldap_%s' % username
     477            def django_to_ldap_username(self, username):
     478                return username[5:]
     479       
     480        self._init_settings(
     481            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test'
     482        )
     483        user_count = User.objects.count()
     484        self.backend = MyBackend()
     485       
     486        user1 = self.backend.authenticate(username='alice', password='password')
     487        user2 = self.backend.get_user(user1.pk)
     488       
     489        self.assertEqual(User.objects.count(), user_count + 1)
     490        self.assertEqual(user1.username, 'ldap_alice')
     491        self.assertEqual(user1.ldap_user._username, 'alice')
     492        self.assertEqual(user1.ldap_username, 'alice')
     493        self.assertEqual(user2.username, 'ldap_alice')
     494        self.assertEqual(user2.ldap_user._username, 'alice')
     495        self.assertEqual(user2.ldap_username, 'alice')
     496
     497
     498    def test_search_bind(self):
     499        self._init_settings(
     500            AUTH_LDAP_USER_SEARCH=LDAPSearch(
     501                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
     502                )
     503            )
     504        self.mock_ldap.set_return_value('search_s',
     505            ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice])
     506        user_count = User.objects.count()
     507       
     508        user = self.backend.authenticate(username='alice', password='password')
     509       
     510        self.assert_(user is not None)
     511        self.assertEqual(User.objects.count(), user_count + 1)
     512        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     513            ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'])
     514
     515
     516    def test_search_bind_no_user(self):
     517        self._init_settings(
     518            AUTH_LDAP_USER_SEARCH=LDAPSearch(
     519                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(cn=%(user)s)'
     520                )
     521            )
     522        self.mock_ldap.set_return_value('search_s',
     523            ("ou=people,o=test", 2, "(cn=alice)", None, 0), [])
     524
     525        user = self.backend.authenticate(username='alice', password='password')
     526
     527        self.assert_(user is None)
     528        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     529            ['initialize', 'simple_bind_s', 'search_s'])
     530   
     531
     532    def test_search_bind_multiple_users(self):
     533        self._init_settings(
     534            AUTH_LDAP_USER_SEARCH=LDAPSearch(
     535                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=*)'
     536                )
     537            )
     538        self.mock_ldap.set_return_value('search_s',
     539            ("ou=people,o=test", 2, "(uid=*)", None, 0), [self.alice, self.bob])
     540
     541        user = self.backend.authenticate(username='alice', password='password')
     542
     543        self.assert_(user is None)
     544        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     545            ['initialize', 'simple_bind_s', 'search_s'])
     546
     547
     548    def test_search_bind_bad_password(self):
     549        self._init_settings(
     550            AUTH_LDAP_USER_SEARCH=LDAPSearch(
     551                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
     552                )
     553            )
     554        self.mock_ldap.set_return_value('search_s',
     555            ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice])
     556
     557        user = self.backend.authenticate(username='alice', password='bogus')
     558
     559        self.assert_(user is None)
     560        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     561            ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'])
     562   
     563
     564    def test_search_bind_with_credentials(self):
     565        self._init_settings(
     566            AUTH_LDAP_BIND_DN='uid=bob,ou=people,o=test',
     567            AUTH_LDAP_BIND_PASSWORD='password',
     568            AUTH_LDAP_USER_SEARCH=LDAPSearch(
     569                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
     570                )
     571            )
     572        self.mock_ldap.set_return_value('search_s',
     573            ("ou=people,o=test", 2, "(uid=alice)", None, 0), [self.alice])
     574
     575        user = self.backend.authenticate(username='alice', password='password')
     576
     577        self.assert_(user is not None)
     578        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     579            ['initialize', 'simple_bind_s', 'search_s', 'simple_bind_s'])
     580
     581
     582    def test_search_bind_with_bad_credentials(self):
     583        self._init_settings(
     584            AUTH_LDAP_BIND_DN='uid=bob,ou=people,o=test',
     585            AUTH_LDAP_BIND_PASSWORD='bogus',
     586            AUTH_LDAP_USER_SEARCH=LDAPSearch(
     587                "ou=people,o=test", self.mock_ldap.SCOPE_SUBTREE, '(uid=%(user)s)'
     588                )
     589            )
     590
     591        user = self.backend.authenticate(username='alice', password='password')
     592       
     593        self.assert_(user is None)
     594        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     595            ['initialize', 'simple_bind_s'])
     596   
     597   
     598    def test_unicode_user(self):
     599        self._init_settings(
     600            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     601            AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'}
     602        )
     603       
     604        user = self.backend.authenticate(username=u'dreßler', password='password')
     605       
     606        self.assert_(user is not None)
     607        self.assertEqual(user.username, u'dreßler')
     608        self.assertEqual(user.last_name, u'Dreßler')
     609   
     610   
     611    def test_populate_user(self):
     612        self._init_settings(
     613            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     614            AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'}
     615        )
     616
     617        user = self.backend.authenticate(username='alice', password='password')
     618
     619        self.assertEqual(user.username, 'alice')
     620        self.assertEqual(user.first_name, 'Alice')
     621        self.assertEqual(user.last_name, 'Adams')
     622
     623
     624    def test_no_update_existing(self):
     625        self._init_settings(
     626            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     627            AUTH_LDAP_USER_ATTR_MAP={'first_name': 'givenName', 'last_name': 'sn'},
     628            AUTH_LDAP_ALWAYS_UPDATE_USER=False
     629        )
     630        User.objects.create(username='alice', first_name='Alicia', last_name='Astro')
     631
     632        alice = self.backend.authenticate(username='alice', password='password')
     633        bob = self.backend.authenticate(username='bob', password='password')
     634
     635        self.assertEqual(alice.first_name, 'Alicia')
     636        self.assertEqual(alice.last_name, 'Astro')
     637        self.assertEqual(bob.first_name, 'Robert')
     638        self.assertEqual(bob.last_name, 'Barker')
     639
     640
     641    def test_require_group(self):
     642        self._init_settings(
     643            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     644            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     645            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
     646            AUTH_LDAP_REQUIRE_GROUP="cn=active_gon,ou=groups,o=test"
     647        )
     648       
     649        alice = self.backend.authenticate(username='alice', password='password')
     650        bob = self.backend.authenticate(username='bob', password='password')
     651       
     652        self.assert_(alice is not None)
     653        self.assert_(bob is None)
     654        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     655            ['initialize', 'simple_bind_s', 'compare_s', 'initialize', 'simple_bind_s', 'compare_s'])
     656
     657
     658    def test_dn_group_membership(self):
     659        self._init_settings(
     660            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     661            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     662            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
     663            AUTH_LDAP_USER_FLAGS_BY_GROUP={
     664                'is_active': "cn=active_gon,ou=groups,o=test",
     665                'is_staff': "cn=staff_gon,ou=groups,o=test",
     666                'is_superuser': "cn=superuser_gon,ou=groups,o=test"
     667            }
     668        )
     669       
     670        alice = self.backend.authenticate(username='alice', password='password')
     671        bob = self.backend.authenticate(username='bob', password='password')
     672       
     673        self.assert_(alice.is_active)
     674        self.assert_(alice.is_staff)
     675        self.assert_(alice.is_superuser)
     676        self.assert_(not bob.is_active)
     677        self.assert_(not bob.is_staff)
     678        self.assert_(not bob.is_superuser)
     679
     680
     681    def test_posix_membership(self):
     682        self._init_settings(
     683            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     684            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     685            AUTH_LDAP_GROUP_TYPE=PosixGroupType(),
     686            AUTH_LDAP_USER_FLAGS_BY_GROUP={
     687                'is_active': "cn=active_px,ou=groups,o=test",
     688                'is_staff': "cn=staff_px,ou=groups,o=test",
     689                'is_superuser': "cn=superuser_px,ou=groups,o=test"
     690            }
     691        )
     692       
     693        alice = self.backend.authenticate(username='alice', password='password')
     694        bob = self.backend.authenticate(username='bob', password='password')
     695       
     696        self.assert_(alice.is_active)
     697        self.assert_(alice.is_staff)
     698        self.assert_(alice.is_superuser)
     699        self.assert_(not bob.is_active)
     700        self.assert_(not bob.is_staff)
     701        self.assert_(not bob.is_superuser)
     702   
     703   
     704    def test_nested_dn_group_membership(self):
     705        self._init_settings(
     706            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     707            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     708            AUTH_LDAP_GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'),
     709            AUTH_LDAP_USER_FLAGS_BY_GROUP={
     710                'is_active': "cn=parent_gon,ou=groups,o=test",
     711                'is_staff': "cn=parent_gon,ou=groups,o=test",
     712            }
     713        )
     714        self.mock_ldap.set_return_value('search_s',
     715            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=alice,ou=people,o=test)))", None, 0),
     716            [self.active_gon, self.nested_gon]
     717        )
     718        self.mock_ldap.set_return_value('search_s',
     719            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=active_gon,ou=groups,o=test)(member=cn=nested_gon,ou=groups,o=test)))", None, 0),
     720            [self.parent_gon]
     721        )
     722        self.mock_ldap.set_return_value('search_s',
     723            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=parent_gon,ou=groups,o=test)))", None, 0),
     724            [self.circular_gon]
     725        )
     726        self.mock_ldap.set_return_value('search_s',
     727            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=circular_gon,ou=groups,o=test)))", None, 0),
     728            [self.nested_gon]
     729        )
     730       
     731        self.mock_ldap.set_return_value('search_s',
     732            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=bob,ou=people,o=test)))", None, 0),
     733            []
     734        )
     735       
     736        alice = self.backend.authenticate(username='alice', password='password')
     737        bob = self.backend.authenticate(username='bob', password='password')
     738       
     739        self.assert_(alice.is_active)
     740        self.assert_(alice.is_staff)
     741        self.assert_(not bob.is_active)
     742        self.assert_(not bob.is_staff)
     743   
     744   
     745    def test_posix_missing_attributes(self):
     746        self._init_settings(
     747            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     748            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     749            AUTH_LDAP_GROUP_TYPE=PosixGroupType(),
     750            AUTH_LDAP_USER_FLAGS_BY_GROUP={
     751                'is_active': "cn=active_px,ou=groups,o=test"
     752            }
     753        )
     754       
     755        nobody = self.backend.authenticate(username='nobody', password='password')
     756
     757        self.assert_(not nobody.is_active)
     758   
     759   
     760    def test_dn_group_permissions(self):
     761        self._init_settings(
     762            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     763            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     764            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
     765            AUTH_LDAP_FIND_GROUP_PERMS=True
     766        )
     767        self._init_groups()
     768        self.mock_ldap.set_return_value('search_s',
     769            ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0),
     770            [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon]
     771        )
     772       
     773        alice = User.objects.create(username='alice')
     774        alice = self.backend.get_user(alice.pk)
     775       
     776        self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"]))
     777        self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"]))
     778        self.assert_(self.backend.has_perm(alice, "auth.add_user"))
     779        self.assert_(self.backend.has_module_perms(alice, "auth"))
     780
     781   
     782    def test_posix_group_permissions(self):
     783        self._init_settings(
     784            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     785            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test',
     786                self.mock_ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)"
     787            ),
     788            AUTH_LDAP_GROUP_TYPE=PosixGroupType(),
     789            AUTH_LDAP_FIND_GROUP_PERMS=True
     790        )
     791        self._init_groups()
     792        self.mock_ldap.set_return_value('search_s',
     793            ("ou=groups,o=test", 2, "(&(objectClass=posixGroup)(|(gidNumber=1000)(memberUid=alice)))", None, 0),
     794            [self.active_px, self.staff_px, self.superuser_px]
     795        )
     796       
     797        alice = User.objects.create(username='alice')
     798        alice = self.backend.get_user(alice.pk)
     799       
     800        self.assertEqual(self.backend.get_group_permissions(alice), set(["auth.add_user", "auth.change_user"]))
     801        self.assertEqual(self.backend.get_all_permissions(alice), set(["auth.add_user", "auth.change_user"]))
     802        self.assert_(self.backend.has_perm(alice, "auth.add_user"))
     803        self.assert_(self.backend.has_module_perms(alice, "auth"))
     804   
     805    def test_foreign_user_permissions(self):
     806        self._init_settings(
     807            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     808            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     809            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
     810            AUTH_LDAP_FIND_GROUP_PERMS=True
     811        )
     812        self._init_groups()
     813       
     814        alice = User.objects.create(username='alice')
     815
     816        self.assertEqual(self.backend.get_group_permissions(alice), set())
     817   
     818   
     819    def test_group_cache(self):
     820        self._init_settings(
     821            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     822            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     823            AUTH_LDAP_GROUP_TYPE=MemberDNGroupType(member_attr='member'),
     824            AUTH_LDAP_FIND_GROUP_PERMS=True,
     825            AUTH_LDAP_CACHE_GROUPS=True
     826        )
     827        self._init_groups()
     828        self.mock_ldap.set_return_value('search_s',
     829            ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=alice,ou=people,o=test))", None, 0),
     830            [self.active_gon, self.staff_gon, self.superuser_gon, self.nested_gon]
     831        )
     832        self.mock_ldap.set_return_value('search_s',
     833            ("ou=groups,o=test", 2, "(&(objectClass=*)(member=uid=bob,ou=people,o=test))", None, 0),
     834            []
     835        )
     836       
     837        alice_id = User.objects.create(username='alice').pk
     838        bob_id = User.objects.create(username='bob').pk
     839
     840        # Check permissions twice for each user
     841        for i in range(2):
     842            alice = self.backend.get_user(alice_id)
     843            self.assertEqual(self.backend.get_group_permissions(alice),
     844                set(["auth.add_user", "auth.change_user"]))
     845
     846            bob = self.backend.get_user(bob_id)
     847            self.assertEqual(self.backend.get_group_permissions(bob), set())
     848       
     849        # Should have executed one LDAP search per user
     850        self.assertEqual(self.mock_ldap.ldap_methods_called(),
     851            ['initialize', 'simple_bind_s', 'search_s', 'initialize', 'simple_bind_s', 'search_s'])
     852   
     853    def test_group_mirroring(self):
     854        self._init_settings(
     855            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     856            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test',
     857                self.mock_ldap.SCOPE_SUBTREE, "(objectClass=posixGroup)"
     858            ),
     859            AUTH_LDAP_GROUP_TYPE=PosixGroupType(),
     860            AUTH_LDAP_MIRROR_GROUPS=True,
     861        )
     862        self.mock_ldap.set_return_value('search_s',
     863            ("ou=groups,o=test", 2, "(&(objectClass=posixGroup)(|(gidNumber=1000)(memberUid=alice)))", None, 0),
     864            [self.active_px, self.staff_px, self.superuser_px]
     865        )
     866   
     867        self.assertEqual(Group.objects.count(), 0)
     868
     869        alice = self.backend.authenticate(username='alice', password='password')
     870       
     871        self.assertEqual(Group.objects.count(), 3)
     872        self.assertEqual(set(alice.groups.all()), set(Group.objects.all()))
     873
     874    def test_nested_group_mirroring(self):
     875        self._init_settings(
     876            AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=people,o=test',
     877            AUTH_LDAP_GROUP_SEARCH=LDAPSearch('ou=groups,o=test', self.mock_ldap.SCOPE_SUBTREE),
     878            AUTH_LDAP_GROUP_TYPE=NestedMemberDNGroupType(member_attr='member'),
     879            AUTH_LDAP_MIRROR_GROUPS=True,
     880        )
     881        self.mock_ldap.set_return_value('search_s',
     882            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=uid=alice,ou=people,o=test)))", None, 0),
     883            [self.active_gon, self.nested_gon]
     884        )
     885        self.mock_ldap.set_return_value('search_s',
     886            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=active_gon,ou=groups,o=test)(member=cn=nested_gon,ou=groups,o=test)))", None, 0),
     887            [self.parent_gon]
     888        )
     889        self.mock_ldap.set_return_value('search_s',
     890            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=parent_gon,ou=groups,o=test)))", None, 0),
     891            [self.circular_gon]
     892        )
     893        self.mock_ldap.set_return_value('search_s',
     894            ("ou=groups,o=test", 2, "(&(objectClass=*)(|(member=cn=circular_gon,ou=groups,o=test)))", None, 0),
     895            [self.nested_gon]
     896        )
     897       
     898        alice = self.backend.authenticate(username='alice', password='password')
     899       
     900        self.assertEqual(Group.objects.count(), 4)
     901        self.assertEqual(set(Group.objects.all().values_list('name', flat=True)),
     902            set(['active_gon', 'nested_gon', 'parent_gon', 'circular_gon']))
     903        self.assertEqual(set(alice.groups.all()), set(Group.objects.all()))
     904
     905
     906    def _init_settings(self, **kwargs):
     907        backend.ldap_settings = TestSettings(**kwargs)
     908   
     909    def _init_groups(self):
     910        permissions = [
     911            Permission.objects.get(codename="add_user"),
     912            Permission.objects.get(codename="change_user")
     913        ]
     914
     915        active_gon = Group.objects.create(name='active_gon')
     916        active_gon.permissions.add(*permissions)
     917
     918        active_px = Group.objects.create(name='active_px')
     919        active_px.permissions.add(*permissions)
  • docs/howto/index.txt

     
    1212   :maxdepth: 1
    1313
    1414   apache-auth
     15   auth-ldap
    1516   auth-remote-user
    1617   custom-management-commands
    1718   custom-model-fields
  • docs/howto/auth-ldap.txt

     
     1.. _howto-auth-ldap:
     2
     3=========================
     4Authentication using LDAP
     5=========================
     6
     7.. versionadded:: 1.2
     8
     9Django includes an LDAP authentication backend to authenticate against any LDAP
     10server. To enable, add
     11:class:`django.contrib.auth.contrib.ldap.backend.LDAPBackend` to
     12:setting:`AUTHENTICATION_BACKENDS`. LDAP configuration can be as simple as a
     13single distinguished name template, but there are many rich options for working
     14with :class:`~django.contrib.auth.models.User` objects, groups, and permissions.
     15This backend depends on the `Python ldap <http://www.python-ldap.org/>`_ module.
     16
     17.. note::
     18
     19    :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` does not
     20    inherit from :class:`~django.contrib.auth.backends.ModelBackend`. It is
     21    possible to use
     22    :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` exclusively
     23    by configuring it to draw group membership from the LDAP server. However, if
     24    you would like to assign permissions to individual users or add users to
     25    groups within Django, you'll need to have both backends installed:
     26
     27    .. code-block:: python
     28
     29        AUTHENTICATION_BACKENDS = (
     30            'django.contrib.auth.contrib.ldap.backend.LDAPBackend',
     31            'django.contrib.auth.backends.ModelBackend',
     32        )
     33
     34
     35.. _howto-auth-ldap-basic-authentication:
     36
     37Configuring basic authentication
     38================================
     39
     40If your LDAP server isn't running locally on the default port, you'll want to
     41start by setting :setting:`AUTH_LDAP_SERVER_URI` to point to your server.
     42
     43.. code-block:: python
     44
     45    AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com"
     46
     47That done, the first step is to authenticate a username and password against the
     48LDAP service. There are two ways to do this, called search/bind and simply bind.
     49The first one involves connecting to the LDAP server either anonymously or with
     50a fixed account and searching for the distinguished name of the authenticating
     51user. Then we can attempt to bind again with the user's password. The second
     52method is to derive the user's DN from his username and attempt to bind as the
     53user directly.
     54
     55Because LDAP searches appear elsewhere in the configuration, the
     56:class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` class is provided
     57to encapsulate search information. In this case, the filter parameter should
     58contain the placeholder ``%(user)s``. A simple configuration for the search/bind
     59approach looks like this (some defaults included for completeness)::
     60
     61    import ldap
     62    from django.contrib.auth.contrib.ldap.config import LDAPSearch
     63
     64    AUTH_LDAP_BIND_DN = ""
     65    AUTH_LDAP_BIND_PASSWORD = ""
     66    AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com",
     67        ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
     68
     69This will perform an anonymous bind, search under
     70``"ou=users,dc=example,dc=com"`` for an object with a uid matching the user's
     71name, and try to bind using that DN and the user's password. The search must
     72return exactly one result or authentication will fail. If you can't search
     73anonymously, you can set :setting:`AUTH_LDAP_BIND_DN` to the distinguished name
     74of an authorized user and :setting:`AUTH_LDAP_BIND_PASSWORD` to the password.
     75
     76To skip the search phase, set :setting:`AUTH_LDAP_USER_DN_TEMPLATE` to a
     77template that will produce the authenticating user's DN directly. This template
     78should have one placeholder, ``%(user)s``. If the previous example had used
     79``ldap.SCOPE_ONELEVEL``, the following would be a more straightforward (and
     80efficient) equivalent::
     81
     82    AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
     83
     84
     85.. _howto-auth-ldap-groups:
     86
     87Working with groups
     88===================
     89
     90Working with groups in LDAP can be a tricky business, as there isn't a single
     91standard grouping mechanism. This module includes an extensible API for working
     92with any kind of group and includes implementations for the most common ones.
     93:class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` is a base class
     94whose concrete subclasses can determine group membership for particular grouping
     95mechanisms. Three built-in subclasses cover most grouping mechanisms:
     96
     97    * :class:`~django.contrib.auth.contrib.ldap.config.PosixGroupType`
     98    * :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType`
     99    * :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType`
     100
     101posixGroup objects are somewhat specialized, so they get their own class. The
     102other two cover mechanisms whereby a group object stores a list of its members
     103as distinguished names. This includes groupOfNames, groupOfUniqueNames, and
     104Active Directory groups, among others. The nested variant allows groups to
     105contain other groups, to as many levels as you like. For convenience and
     106readability, several trivial subclasses of the above are provided:
     107
     108    * :class:`~django.contrib.auth.contrib.ldap.config.GroupOfNamesType`
     109    * :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfNamesType`
     110    * :class:`~django.contrib.auth.contrib.ldap.config.GroupOfUniqueNamesType`
     111    * :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfUniqueNamesType`
     112    * :class:`~django.contrib.auth.contrib.ldap.config.ActiveDirectoryGroupType`
     113    * :class:`~django.contrib.auth.contrib.ldap.config.NestedActiveDirectoryGroupType`
     114
     115To get started, you'll need to provide some basic information about your LDAP
     116groups. :setting:`AUTH_LDAP_GROUP_SEARCH` is an
     117:class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that
     118identifies the set of relevant group objects. That is, all groups that users
     119might belong to as well as any others that we might need to know about (in the
     120case of nested groups, for example). :setting:`AUTH_LDAP_GROUP_TYPE` is an
     121instance of the class corresponding to the type of group that will be returned
     122by :setting:`AUTH_LDAP_GROUP_SEARCH`. All groups referenced elsewhere in the
     123configuration must be of this type and part of the search results.
     124
     125.. code-block:: python
     126
     127    import ldap
     128    from django.contrib.auth.contrib.ldap.config import LDAPSearch, GroupOfNamesType
     129   
     130    AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=groups,dc=example,dc=com",
     131        ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)"
     132    )
     133    AUTH_LDAP_GROUP_TYPE = GroupOfNamesType()
     134
     135The simplest use of groups is to limit the users who are allowed to log in. If
     136:setting:`AUTH_LDAP_REQUIRE_GROUP` is set, then only users who are members of
     137that group will successfully authenticate::
     138
     139    AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=groups,dc=example,dc=com"
     140
     141More advanced uses of groups are covered in the next two sections.
     142
     143
     144.. _howto-auth-ldap-user-objects:
     145
     146User objects
     147============
     148
     149Authenticating against an external source is swell, but Django's auth module is
     150tightly bound to the :class:`django.contrib.auth.models.User` model. Thus, when
     151a user logs in, we have to create a :class:`~django.contrib.auth.models.User`
     152object to represent him in the database.
     153
     154The only required field for a user is the username, which we obviously have. The
     155:class:`~django.contrib.auth.models.User` model is picky about the characters
     156allowed in usernames, so
     157:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` includes a pair
     158of hooks,
     159:meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username`
     160and
     161:meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.django_to_ldap_username`,
     162to translate between LDAP usernames and Django usernames. You'll need this, for
     163example, if your LDAP names have periods in them. You can subclass
     164:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` to implement
     165these hooks; by default the username is not modified.
     166:class:`~django.contrib.auth.models.User` objects that are authenticated by
     167:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will have an
     168:attr:`~django.contrib.auth.models.User.ldap_username` attribute with the
     169original (LDAP) username. :attr:`~django.contrib.auth.models.User.username`
     170will, of course, be the Django username.
     171
     172LDAP directories tend to contain much more information about users that you may
     173wish to propagate. A pair of settings, :setting:`AUTH_LDAP_USER_ATTR_MAP` and
     174:setting:`AUTH_LDAP_PROFILE_ATTR_MAP`, serve to copy directory information into
     175:class:`~django.contrib.auth.models.User` and profile objects. These are
     176dictionaries that map user and profile model keys, respectively, to LDAP
     177attribute names::
     178
     179    AUTH_LDAP_USER_ATTR_MAP = {"first_name": "givenName", "last_name": "sn"}
     180    AUTH_LDAP_PROFILE_ATTR_MAP = {"home_directory": "homeDirectory"}
     181
     182Only string fields can be mapped to attributes. Boolean fields can be defined by
     183group membership::
     184
     185    AUTH_LDAP_USER_FLAGS_BY_GROUP = {
     186        "is_active": "cn=active,ou=groups,dc=example,dc=com",
     187        "is_staff": "cn=staff,ou=groups,dc=example,dc=com",
     188        "is_superuser": "cn=superuser,ou=groups,dc=example,dc=com"
     189    }
     190
     191By default, all mapped user fields will be updated each time the user logs in.
     192To disable this, set :setting:`AUTH_LDAP_ALWAYS_UPDATE_USER` to ``False``.
     193
     194If you need to access multi-value attributes or there is some other reason that
     195the above is inadequate, you can also access the user's raw LDAP attributes.
     196``user.ldap_user`` is an object with two public properties:
     197
     198    * ``dn``: The user's distinguished name.
     199    * ``attrs``: The user's LDAP attributes as a dictionary of lists of string
     200      values.
     201
     202Python-ldap returns all attribute values as utf8-encoded strings. For
     203convenience, this module will try to decode all values into Unicode strings. Any
     204string that can not be successfully decoded will be left as-is; this may apply
     205to binary values such as Active Directory's objectSid.
     206
     207.. note::
     208
     209    Users created by
     210    :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will have an
     211    unusable password set. This will only happen when the user is created, so if
     212    you set a valid password in Django, the user will be able to log in through
     213    :class:`~django.contrib.auth.backends.ModelBackend` (if configured) even if
     214    he is rejected by LDAP. This is not generally recommended, but could be
     215    useful as a fail-safe for selected users in case the LDAP server is
     216    unavailable.
     217
     218
     219.. _howto-auth-ldap-permissions:
     220
     221Permissions
     222===========
     223
     224Groups are useful for more than just populating the user's ``is_*`` fields.
     225:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` would not be
     226complete without some way to turn a user's LDAP group memberships into Django
     227model permissions. In fact, there are two ways to do this.
     228
     229Ultimately, both mechanisms need some way to map LDAP groups to Django groups.
     230Implementations of
     231:class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` will have an
     232algorithm for deriving the Django group name from the LDAP group. Clients that
     233need to modify this behavior can subclass the
     234:class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` class. All of
     235the built-in implementations take a ``name_attr`` argument to ``__init__``,
     236which specifies the LDAP attribute from which to take the Django group name. By
     237default, the ``cn`` attribute is used.
     238
     239The least invasive way to map group permissions is to set
     240:setting:`AUTH_LDAP_FIND_GROUP_PERMS` to ``True``
     241:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will then find
     242all of the LDAP groups that a user belongs to, map them to Django groups, and
     243load the permissions for those groups. You will need to create the Django groups
     244yourself, generally through the admin interface.
     245
     246.. note::
     247
     248    After the user logs in, subsequent requests will have to determine group
     249    membership based solely on the :class:`~django.contrib.auth.models.User`
     250    object of the logged-in user. We will not have the user's password at this
     251    point. This means that if :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``,
     252    we must have access to the LDAP directory through
     253    :setting:`AUTH_LDAP_BIND_DN` and :setting:`AUTH_LDAP_BIND_PASSWORD`, even if
     254    you're using :setting:`AUTH_LDAP_USER_DN_TEMPLATE` to authenticate the user.
     255
     256To minimize traffic to the LDAP server,
     257:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` can make use of
     258Django's :ref:`cache framework <topics-cache>` to keep a copy of a user's LDAP
     259group memberships. To enable this feature, set :setting:`AUTH_LDAP_CACHE_GROUPS`
     260to ``True``. You can also set :setting:`AUTH_LDAP_GROUP_CACHE_TIMEOUT` to
     261override the timeout of cache entries (in seconds).
     262
     263.. code-block:: python
     264
     265    AUTH_LDAP_CACHE_GROUPS = True
     266    AUTH_LDAP_GROUP_CACHE_TIMEOUT = 300
     267
     268The second way to turn LDAP group memberships into permissions is to mirror the
     269groups themselves. If :setting:`AUTH_LDAP_MIRROR_GROUPS` is ``True``, then every
     270time a user logs in,
     271:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will update the
     272database with the user's LDAP groups. Any group that doesn't exist will be
     273created and the user's Django group membership will be updated to exactly match
     274his LDAP group membership. Note that if the LDAP server has nested groups, the
     275Django database will end up with a flattened representation.
     276
     277This approach has two main differences from
     278:setting:`AUTH_LDAP_FIND_GROUP_PERMS`. First,
     279:setting:`AUTH_LDAP_FIND_GROUP_PERMS` will query for LDAP group membership
     280either for every request or according to the cache timeout. With group
     281mirroring, membership will be updated when the user authenticates. This may not
     282be appropriate for sites with long session timeouts. The second difference is
     283that with :setting:`AUTH_LDAP_FIND_GROUP_PERMS`, there is no way for clients to
     284determine a user's group memberships, only their permissions. If you want to
     285make decisions based directly on group membership, you'll have to mirror the
     286groups.
     287
     288
     289.. _howto-auth-ldap-logging:
     290
     291Logging
     292=======
     293
     294:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` uses the standard
     295logging module to log debug and warning messages to the logger named
     296``'django.contrib.auth.contrib.ldap'``. If you need debug messages to
     297help with configuration issues, you should add a handler to this logger.
     298
     299
     300.. _howto-auth-ldap-options:
     301
     302More options
     303============
     304
     305Miscellaneous settings for
     306:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend`:
     307
     308    * :setting:`AUTH_LDAP_GLOBAL_OPTIONS`: A dictionary of options to pass to
     309      python-ldap via ``ldap.set_option()``.
     310    * :setting:`AUTH_LDAP_CONNECTION_OPTIONS`: A dictionary of options to pass
     311      to each LDAPObject instance via ``LDAPObject.set_option()``.
     312
     313
     314.. _howto-auth-ldap-performance:
     315
     316Performance
     317===========
     318
     319:class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` is carefully
     320designed not to require a connection to the LDAP service for every request. Of
     321course, this depends heavily on how it is configured. If LDAP traffic or latency
     322is a concern for your deployment, this section has a few tips on minimizing it,
     323in decreasing order of impact.
     324
     325    #. **Cache groups**. If :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``,
     326       the default behavior is to reload a user's group memberships on every
     327       request. This is the safest behavior, as any membership change takes
     328       effect immediately, but it is expensive. If possible, set
     329       :setting:`AUTH_LDAP_CACHE_GROUPS` to ``True`` to remove most of this
     330       traffic. Alternatively, you might consider using
     331       :setting:`AUTH_LDAP_MIRROR_GROUPS` and relying on
     332       :class:`~django.contrib.auth.backends.ModelBackend` to supply group
     333       permissions.
     334    #. **Don't access user.ldap_user.***. These properties are only cached
     335       on a per-request basis. If you can propagate LDAP attributes to a
     336       :class:`~django.contrib.auth.models.User` or profile object, they will
     337       only be updated at login. ``user.ldap_user.attrs`` triggers an LDAP
     338       connection for every request in which it's accessed. If you're not using
     339       :setting:`AUTH_LDAP_USER_DN_TEMPLATE`, then accessing
     340       ``user.ldap_user.dn`` will also trigger an LDAP connection.
     341    #. **Use simpler group types**. Some grouping mechanisms are more expensive
     342       than others. This will often be outside your control, but it's important
     343       to note that the extra functionality of more complex group types like
     344       :class:`~django.contrib.auth.contrib.ldap.config.NestedGroupOfNamesType`
     345       is not free and will generally require a greater number and complexity of
     346       LDAP queries.
     347    #. **Use direct binding**. Binding with
     348       :setting:`AUTH_LDAP_USER_DN_TEMPLATE` is a little bit more efficient than
     349       relying on :setting:`AUTH_LDAP_USER_SEARCH`. Specifically, it saves two
     350       LDAP operations (one bind and one search) per login.
     351
     352
     353.. _howto-auth-ldap-example:
     354
     355Example configuration
     356=====================
     357
     358Here is a complete example configuration from :file:`settings.py` that
     359exercises nearly all of the features. In this example, we're authenticating
     360against a global pool of users in the directory, but we have a special area set
     361aside for Django groups (ou=django,ou=groups,dc=example,dc=com). Remember that
     362most of this is optional if you just need simple authentication. Some default
     363settings and arguments are included for completeness.
     364
     365.. code-block:: python
     366
     367    import ldap
     368    from django.contrib.auth.contrib.ldap.config import LDAPSearch, GroupOfNamesType
     369
     370
     371    # Baseline configuration.
     372    AUTH_LDAP_SERVER_URI = "ldap://ldap.example.com"
     373
     374    AUTH_LDAP_BIND_DN = "cn=django-agent,dc=example,dc=com"
     375    AUTH_LDAP_BIND_PASSWORD = "phlebotinum"
     376    AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=users,dc=example,dc=com",
     377        ldap.SCOPE_SUBTREE, "(uid=%(user)s)")
     378    # or perhaps:
     379    # AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
     380
     381    # Set up the basic group parameters.
     382    AUTH_LDAP_GROUP_SEARCH = LDAPSearch("ou=django,ou=groups,dc=example,dc=com",
     383        ldap.SCOPE_SUBTREE, "(objectClass=groupOfNames)"
     384    )
     385    AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr="cn")
     386
     387    # Only users in this group can log in.
     388    AUTH_LDAP_REQUIRE_GROUP = "cn=enabled,ou=django,ou=groups,dc=example,dc=com"
     389
     390    # Populate the Django user from the LDAP directory.
     391    AUTH_LDAP_USER_ATTR_MAP = {
     392        "first_name": "givenName",
     393        "last_name": "sn",
     394        "email": "mail"
     395    }
     396
     397    AUTH_LDAP_PROFILE_ATTR_MAP = {
     398        "employee_number": "employeeNumber"
     399    }
     400
     401    AUTH_LDAP_USER_FLAGS_BY_GROUP = {
     402        "is_active": "cn=active,ou=django,ou=groups,dc=example,dc=com",
     403        "is_staff": "cn=staff,ou=django,ou=groups,dc=example,dc=com",
     404        "is_superuser": "cn=superuser,ou=django,ou=groups,dc=example,dc=com"
     405    }
     406   
     407    # This is the default, but I like to be explicit.
     408    AUTH_LDAP_ALWAYS_UPDATE_USER = True
     409   
     410    # Use LDAP group membership to calculate group permissions.
     411    AUTH_LDAP_FIND_GROUP_PERMS = True
     412
     413    # Cache group memberships for an hour to minimize LDAP traffic
     414    AUTH_LDAP_CACHE_GROUPS = True
     415    AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
     416
     417
     418    # Keep ModelBackend around for per-user permissions and maybe a local
     419    # superuser.
     420    AUTHENTICATION_BACKENDS = (
     421        'django.contrib.auth.contrib.ldap.backend.LDAPBackend',
     422        'django.contrib.auth.backends.ModelBackend',
     423    )
     424
     425
     426.. _howto-auth-ldap-api-reference:
     427
     428API reference
     429=============
     430
     431Configuration
     432-------------
     433
     434.. module:: django.contrib.auth.contrib.ldap.config
     435
     436.. class:: LDAPSearch
     437
     438    .. method:: __init__(base_dn, scope, filterstr='(objectClass=*)')
     439
     440        * ``base_dn``: The distinguished name of the search base.
     441        * ``scope``: One of ``ldap.SCOPE_*``.
     442        * ``filterstr``: An optional filter string (e.g. '(objectClass=person)').
     443          In order to be valid, ``filterstr`` must be enclosed in parentheses.
     444
     445
     446.. class:: LDAPGroupType
     447
     448    The base class for objects that will determine group membership for various
     449    LDAP grouping mechanisms. Implementations are provided for common group
     450    types or you can write your own. See the source code for subclassing notes.
     451   
     452    .. method:: __init__(name_attr='cn')
     453       
     454        By default, LDAP groups will be mapped to Django groups by taking the
     455        first value of the cn attribute. You can specify a different attribute
     456        with ``name_attr``.
     457
     458
     459.. class:: PosixGroupType
     460
     461    A concrete subclass of
     462    :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` that handles
     463    the ``posixGroup`` object class. This checks for both primary group and
     464    group membership.
     465
     466    .. method:: __init__(name_attr='cn')
     467
     468.. class:: MemberDNGroupType
     469
     470    A concrete subclass of
     471    :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType` that handles
     472    grouping mechanisms wherein the group object contains a list of its member
     473    DNs.
     474   
     475    .. method:: __init__(member_attr, name_attr='cn')
     476   
     477        * ``member_attr``: The attribute on the group object that contains a
     478          list of member DNs. 'member' and 'uniqueMember' are common examples.
     479
     480
     481.. class:: NestedMemberDNGroupType
     482
     483    Similar to
     484    :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType`, except
     485    this allows groups to contain other groups as members. Group hierarchies
     486    will be traversed to determine membership.
     487
     488    .. method:: __init__(member_attr, name_attr='cn')
     489   
     490        As above.
     491
     492
     493.. class:: GroupOfNamesType
     494
     495    A concrete subclass of
     496    :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that
     497    handles the ``groupOfNames`` object class. Equivalent to
     498    ``MemberDNGroupType('member')``.
     499
     500    .. method:: __init__(name_attr='cn')
     501
     502
     503.. class:: NestedGroupOfNamesType
     504
     505    A concrete subclass of
     506    :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType`
     507    that handles the ``groupOfNames`` object class. Equivalent to
     508    ``NestedMemberDNGroupType('member')``.
     509
     510    .. method:: __init__(name_attr='cn')
     511
     512
     513.. class:: GroupOfUniqueNamesType
     514
     515    A concrete subclass of
     516    :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that
     517    handles the ``groupOfUniqueNames`` object class. Equivalent to
     518    ``MemberDNGroupType('uniqueMember')``.
     519
     520    .. method:: __init__(name_attr='cn')
     521
     522
     523.. class:: NestedGroupOfUniqueNamesType
     524
     525    A concrete subclass of
     526    :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType`
     527    that handles the ``groupOfUniqueNames`` object class. Equivalent to
     528    ``NestedMemberDNGroupType('uniqueMember')``.
     529
     530    .. method:: __init__(name_attr='cn')
     531
     532
     533.. class:: ActiveDirectoryGroupType
     534
     535    A concrete subclass of
     536    :class:`~django.contrib.auth.contrib.ldap.config.MemberDNGroupType` that
     537    handles Active Directory groups. Equivalent to
     538    ``MemberDNGroupType('member')``.
     539
     540    .. method:: __init__(name_attr='cn')
     541
     542
     543.. class:: NestedActiveDirectoryGroupType
     544
     545    A concrete subclass of
     546    :class:`~django.contrib.auth.contrib.ldap.config.NestedMemberDNGroupType`
     547    that handles Active Directory groups. Equivalent to
     548    ``NestedMemberDNGroupType('member')``.
     549
     550    .. method:: __init__(name_attr='cn')
     551
     552
     553Backend
     554-------
     555
     556.. module:: django.contrib.auth.contrib.ldap.backend
     557
     558.. class:: LDAPBackend
     559
     560    .. method:: ldap_to_django_username(username)
     561   
     562        Returns a valid Django username based on the given LDAP username (which
     563        is what the user enters). By default, ``username`` is returned
     564        unchanged. This can be overriden by subclasses.
     565
     566    .. method:: django_to_ldap_username(username)
     567
     568        The inverse of
     569        :meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username`.
     570        If this is not symmetrical to
     571        :meth:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend.ldap_to_django_username`,
     572        the behavior is undefined.
  • docs/ref/authbackends.txt

     
    3535    :attr:`request.META['REMOTE_USER'] <django.http.HttpRequest.META>`.  See
    3636    the :ref:`Authenticating against REMOTE_USER <howto-auth-remote-user>`
    3737    documentation.
     38
     39
     40.. class:: LDAPBackend
     41
     42    .. versionadded:: 1.2
     43   
     44    This backend authenticates against an existing LDAP directory. You must have
     45    python-ldap installed in order to use it. In addition to authenticating
     46    individual users, it can optionally query the LDAP server for group
     47    membership and thus group permissions. See the :ref:`howto-auth-ldap`
     48    documentation.
  • docs/ref/settings.txt

     
    105105authenticate a user. See the :ref:`authentication backends documentation
    106106<authentication-backends>` for details.
    107107
     108
     109.. setting:: AUTH_LDAP_ALWAYS_UPDATE_USER
     110
     111AUTH_LDAP_ALWAYS_UPDATE_USER
     112----------------------------
     113
     114Default: ``True``
     115
     116If ``True``, the fields of a :class:`~django.contrib.auth.models.User` object
     117will be updated with the latest values from the LDAP directory every time the
     118user logs in. Otherwise the :class:`~django.contrib.auth.models.User` object
     119will only be populated when it is automatically created. See
     120:ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`.
     121
     122
     123.. setting:: AUTH_LDAP_BIND_DN
     124
     125AUTH_LDAP_BIND_DN
     126-----------------
     127
     128Default: ``''`` (Empty string)
     129
     130The distinguished name to use for general access to the LDAP server. Use the
     131empty string (the default) for an anonymous bind. If
     132:setting:`AUTH_LDAP_USER_DN_TEMPLATE` is not set, we'll need this to search for
     133the user. If :setting:`AUTH_LDAP_FIND_GROUP_PERMS` is ``True``, we'll also need
     134it to determine group membership on subsequent requests. See
     135:ref:`Authentication using LDAP <howto-auth-ldap-basic-authentication>`.
     136
     137
     138.. setting:: AUTH_LDAP_BIND_PASSWORD
     139
     140AUTH_LDAP_BIND_PASSWORD
     141-----------------------
     142
     143Default: ``''`` (Empty string)
     144
     145The password to use with :setting:`AUTH_LDAP_BIND_DN`. See :ref:`Authentication
     146using LDAP <howto-auth-ldap-basic-authentication>`.
     147
     148
     149.. setting:: AUTH_LDAP_CACHE_GROUPS
     150
     151AUTH_LDAP_CACHE_GROUPS
     152----------------------
     153
     154Default: ``False``
     155
     156If ``True``, LDAP group membership will be cached using Django's :ref:`cache
     157framework <topics-cache>`. The cache timeout can be customized with
     158:setting:`AUTH_LDAP_GROUP_CACHE_TIMEOUT`. See :ref:`Authentication using LDAP
     159<howto-auth-ldap-permissions>`.
     160
     161
     162.. setting:: AUTH_LDAP_CONNECTION_OPTIONS
     163
     164AUTH_LDAP_CONNECTION_OPTIONS
     165----------------------------
     166
     167Default: ``{}``
     168
     169A hash of options to pass to each connection to the LDAP server via
     170``LDAPObject.set_option()``. Keys are ``ldap.OPT_*`` constants. See
     171:ref:`Authentication using LDAP <howto-auth-ldap-options>`.
     172
     173
     174.. setting:: AUTH_LDAP_GROUP_CACHE_TIMEOUT
     175
     176AUTH_LDAP_GROUP_CACHE_TIMEOUT
     177-----------------------------
     178
     179Default: ``None``
     180
     181If :setting:`AUTH_LDAP_CACHE_GROUPS` is ``True``, this overrides the default
     182cache timeout for the group cache. See :ref:`Authentication using LDAP
     183<howto-auth-ldap-permissions>`.
     184
     185
     186.. setting:: AUTH_LDAP_FIND_GROUP_PERMS
     187
     188AUTH_LDAP_FIND_GROUP_PERMS
     189--------------------------
     190
     191Default: ``False``
     192
     193If ``True``, :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will
     194furnish group permissions based on the LDAP groups the authenticated user
     195belongs to. :setting:`AUTH_LDAP_GROUP_SEARCH` and
     196:setting:`AUTH_LDAP_GROUP_TYPE` must also be set. See :ref:`Authentication using
     197LDAP <howto-auth-ldap-permissions>`.
     198
     199
     200.. setting:: AUTH_LDAP_GLOBAL_OPTIONS
     201
     202AUTH_LDAP_GLOBAL_OPTIONS
     203------------------------
     204
     205Default: ``{}``
     206
     207A hash of options to pass to ``ldap.set_option()``. Keys are ``ldap.OPT_*``
     208constants. See :ref:`Authentication using LDAP <howto-auth-ldap-options>`.
     209
     210
     211.. setting:: AUTH_LDAP_GROUP_SEARCH
     212
     213AUTH_LDAP_GROUP_SEARCH
     214----------------------
     215
     216Default: ``None``
     217
     218An :class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that
     219finds all LDAP groups that users might belong to. If your configuration makes
     220any references to LDAP groups, this and :setting:`AUTH_LDAP_GROUP_TYPE` must be
     221set. See :ref:`Authentication using LDAP <howto-auth-ldap-groups>`.
     222
     223
     224.. setting:: AUTH_LDAP_GROUP_TYPE
     225
     226AUTH_LDAP_GROUP_TYPE
     227--------------------
     228
     229Default: ``None``
     230
     231An :class:`~django.contrib.auth.contrib.ldap.config.LDAPGroupType`
     232instance describing the type of group returned by
     233:setting:`AUTH_LDAP_GROUP_SEARCH`. See :ref:`Authentication using LDAP
     234<howto-auth-ldap-groups>`.
     235
     236
     237.. setting:: AUTH_LDAP_MIRROR_GROUPS
     238
     239AUTH_LDAP_MIRROR_GROUPS
     240-----------------------
     241
     242Default: ``False``
     243
     244If ``True``, :class:`~django.contrib.auth.contrib.ldap.backend.LDAPBackend` will
     245mirror a user's LDAP group membership in the Django database. Any time a user
     246authenticates, we will create all of his LDAP groups as Django groups and update
     247his Django group membership to exactly match his LDAP group membership. If the
     248LDAP server has nested groups, the Django database will end up with a flattened
     249representation. See :ref:`Authentication using LDAP
     250<howto-auth-ldap-permissions>`.
     251
     252
     253.. setting:: AUTH_LDAP_PROFILE_ATTR_MAP
     254
     255AUTH_LDAP_PROFILE_ATTR_MAP
     256--------------------------
     257
     258Default: ``{}``
     259
     260A mapping from user profile field names to LDAP attribute names. See
     261:ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`.
     262
     263
     264.. setting:: AUTH_LDAP_REQUIRE_GROUP
     265
     266AUTH_LDAP_REQUIRE_GROUP
     267-----------------------
     268
     269Default: ``None``
     270
     271The distinguished name of a group that a user must belong to in order to
     272successfully authenticate. See :ref:`Authentication using LDAP
     273<howto-auth-ldap-groups>`.
     274
     275
     276.. setting:: AUTH_LDAP_SERVER_URI
     277
     278AUTH_LDAP_SERVER_URI
     279--------------------
     280
     281Default: ``ldap://localhost``
     282
     283The URI of the LDAP server. This can be any URI that is supported by your
     284underlying LDAP libraries. See :ref:`Authentication using LDAP
     285<howto-auth-ldap>`.
     286
     287
     288.. setting:: AUTH_LDAP_USER_ATTR_MAP
     289
     290AUTH_LDAP_USER_ATTR_MAP
     291-----------------------
     292
     293Default: ``{}``
     294
     295A mapping from :class:`~django.contrib.auth.models.User` field names to LDAP
     296attribute names. See :ref:`Authentication using LDAP
     297<howto-auth-ldap-user-objects>`.
     298
     299
     300.. setting:: AUTH_LDAP_USER_DN_TEMPLATE
     301
     302AUTH_LDAP_USER_DN_TEMPLATE
     303--------------------------
     304
     305Default: ``None``
     306
     307A string template that describes any user's distinguished name based on the
     308username. This must contain the placeholder ``%(user)s``. See
     309:ref:`Authentication using LDAP <howto-auth-ldap-basic-authentication>`.
     310
     311
     312.. setting:: AUTH_LDAP_USER_FLAGS_BY_GROUP
     313
     314AUTH_LDAP_USER_FLAGS_BY_GROUP
     315------------------------------
     316
     317Default: ``{}``
     318
     319A mapping from boolean :class:`~django.contrib.auth.models.User` field names to
     320distinguished names of LDAP groups. The corresponding field is set to ``True``
     321or ``False`` according to whether the user is a member of the group. See
     322:ref:`Authentication using LDAP <howto-auth-ldap-user-objects>`.
     323
     324
     325.. setting:: AUTH_LDAP_USER_SEARCH
     326
     327AUTH_LDAP_USER_SEARCH
     328---------------------
     329
     330Default: ``None``
     331
     332An :class:`~django.contrib.auth.contrib.ldap.config.LDAPSearch` object that will
     333locate a user in the directory. The filter parameter should contain the
     334placeholder ``%(user)s`` for the username. It must return exactly one result for
     335authentication to succeed. See :ref:`Authentication using LDAP
     336<howto-auth-ldap>`.
     337
     338
    108339.. setting:: AUTH_PROFILE_MODULE
    109340
    110341AUTH_PROFILE_MODULE
Back to Top