Ticket #2507: ldapauth.4.diff

File ldapauth.4.diff, 12.7 KB (added by Thomas Güttler, 16 years ago)

See comment for ldapauth.4.diff

  • django/contrib/auth/contrib/__init__.py

     
     1
  • django/contrib/auth/contrib/ldapauth.py

     
     1from django.conf import settings
     2from django.contrib.auth.models import User
     3
     4import logging
     5
     6class LDAPBackend(object):
     7    """
     8    Authenticate a user against LDAP.
     9    Requires python-ldap to be installed.
     10
     11    Requires the following things to be in settings.py:
     12    LDAP_DEBUG -- boolean
     13        Uses logging module for debugging messages.
     14    LDAP_SERVER_URI -- string, ldap uri.
     15        default: 'ldap://localhost'
     16    LDAP_SEARCHDN -- string of the LDAP dn to use for searching
     17        default: 'dc=localhost'
     18    LDAP_SCOPE -- one of: ldap.SCOPE_*, used for searching
     19        see python-ldap docs for the search function
     20        default = ldap.SCOPE_SUBTREE
     21    LDAP_SEARCH_FILTER -- formated string, the filter to use for searching for a
     22        user. Used as: filterstr = LDAP_SEARCH_FILTER % username
     23        default = 'cn=%s'
     24    LDAP_UPDATE_FIELDS -- boolean, do we sync the db with ldap on each auth
     25        default = True
     26
     27    Required unless LDAP_FULL_NAME is set:
     28    LDAP_FIRST_NAME -- string, LDAP attribute to get the given name from
     29    LDAP_LAST_NAME -- string, LDAP attribute to get the last name from
     30
     31    Optional Settings:
     32    LDAP_FULL_NAME -- string, LDAP attribute to get name from, splits on ' '
     33    LDAP_GID -- string, LDAP attribute to get group name/number from
     34    LDAP_SU_GIDS -- list of strings, group names/numbers that are superusers
     35    LDAP_STAFF_GIDS -- list of strings, group names/numbers that are staff
     36    LDAP_EMAIL -- string, LDAP attribute to get email from
     37    LDAP_DEFAULT_EMAIL_SUFFIX -- string, appened to username if no email found
     38    LDAP_OPTIONS -- hash, python-ldap global options and their values
     39        {ldap.OPT_X_TLS_CACERTDIR: '/etc/ldap/ca/'}
     40    LDAP_ACTIVE_FIELD -- list of strings, LDAP attribute to get active status
     41        from
     42    LDAP_ACTIVE -- list of strings, allowed for active from LDAP_ACTIVE_FIELD
     43
     44    You must pick a method for determining the DN of a user and set the needed
     45    settings:
     46        - You can set LDAP_BINDDN and LDAP_BIND_ATTRIBUTE like:
     47            LDAP_BINDDN = 'ou=people,dc=example,dc=com'
     48            LDAP_BIND_ATTRIBUTE = 'uid'
     49          and the user DN would be:
     50            'uid=%s,ou=people,dc=example,dc=com' % username
     51
     52        - Look for the DN on the directory, this is what will happen if you do
     53          not define the LDAP_BINDDN setting. In that case you may need to
     54          define LDAP_PREBINDDN and LDAP_PREBINDPW if your LDAP server does not
     55          allow anonymous queries. The search will be performed with the
     56          LDAP_SEARCH_FILTER setting.
     57
     58        - Override the _pre_bind() method, which receives the ldap object and
     59          the username as it's parameters and should return the DN of the user.
     60
     61    By inheriting this class you can change:
     62        - How the dn to bind with is produced by overriding _pre_bind()
     63        - What type of user object to use by overriding: _get_user_by_name(),
     64          _create_user_object(), and get_user()
     65    """
     66
     67    import ldap
     68    from django.conf import settings
     69    from django.contrib.auth.models import User
     70
     71    settings = {
     72            'LDAP_SERVER_URI': 'ldap://localhost',
     73            'LDAP_SEARCHDN': 'dc=localhost',
     74            'LDAP_SCOPE': ldap.SCOPE_SUBTREE,
     75            'LDAP_SEARCH_FILTER': 'cn=%s',
     76            'LDAP_UPDATE_FIELDS': True,
     77            'LDAP_BINDDN': None,
     78            'LDAP_BIND_ATTRIBUTE': None,
     79            'LDAP_FIRST_NAME': None,
     80            'LDAP_LAST_NAME': None,
     81            'LDAP_FULL_NAME': None,
     82            'LDAP_GID': None,
     83            'LDAP_SU_GIDS': None,
     84            'LDAP_STAFF_GIDS': None,
     85            'LDAP_ACTIVE_FIELD': None,
     86            'LDAP_ACTIVE': None,
     87            'LDAP_EMAIL': None,
     88            'LDAP_DEFAULT_EMAIL_SUFFIX': None,
     89            'LDAP_OPTIONS': None,
     90            'LDAP_BINDDN': None,
     91            'LDAP_BIND_ATTRIBUTE': None,
     92            'LDAP_PREBINDDN': None,
     93            'LDAP_PREBINDPW': None,
     94            'LDAP_DEBUG': True,
     95        }
     96
     97    def __init__(self):
     98        # Load settings from settings.py, put them on self.settings
     99        # overriding the defaults.
     100        for var in self.settings.iterkeys():
     101            if hasattr(settings, var):
     102                self.settings[var] = settings.__getattr__(var)
     103
     104    def authenticate(self, username=None, password=None):
     105        # Make sure we have a user and pass
     106        if not username and password is not None:
     107            if self.settings['LDAP_DEBUG']:
     108                assert False
     109                logging.info('LDAPBackend.authenticate failed: username or password empty: %s %s' % (
     110                    username, password))
     111            return None
     112
     113        if self.settings['LDAP_OPTIONS']:
     114            for k in self.settings['LDAP_OPTIONS']:
     115                self.ldap.set_option(k, self.settings.LDAP_OPTIONS[k])
     116
     117        l = self.ldap.initialize(self.settings['LDAP_SERVER_URI'])
     118
     119        bind_string = self._pre_bind(l, username)
     120        if not bind_string:
     121            if self.settings['LDAP_DEBUG']:
     122                logging.info('LDAPBackend.authenticate failed: _pre_bind return no bind_string (%s, %s)' % (
     123                    l, username))
     124            return None
     125       
     126        try:
     127            # Try to bind as the provided user. We leave the bind until
     128            # the end for other ldap.search_s call to work authenticated.
     129            l.bind_s(bind_string, password)
     130        except (self.ldap.INVALID_CREDENTIALS,
     131                self.ldap.UNWILLING_TO_PERFORM), exc:
     132            # Failed user/pass (or missing password)
     133            if self.settings['LDAP_DEBUG']:
     134                logging.info('LDAPBackend.authenticate failed: %s' % exc)
     135            l.unbind_s()
     136            return None
     137
     138        try:
     139            user = self._get_user_by_name(username)
     140        except User.DoesNotExist:
     141            user = self._get_ldap_user(l, username)
     142
     143        if user is not None:
     144            if self.settings['LDAP_UPDATE_FIELDS']:
     145                self._update_user(l, user)
     146
     147        l.unbind_s()
     148        if self.settings['LDAP_DEBUG']:
     149            if user is None:
     150                logging.info('LDAPBackend.authenticate failed: user is None')
     151            else:
     152                logging.info('LDAPBackend.authenticate ok: %s %s' % (user, user.__dict__))
     153        return user
     154
     155    # Functions provided to override to customize to your LDAP configuration.
     156    def _pre_bind(self, l, username):
     157        """
     158        Function that returns the dn to bind against ldap with.
     159        called as: self._pre_bind(ldapobject, username)
     160        """
     161        if not self.settings['LDAP_BINDDN']:
     162            # When the LDAP_BINDDN setting is blank we try to find the
     163            # dn binding anonymously or using LDAP_PREBINDDN
     164            if self.settings['LDAP_PREBINDDN']:
     165                try:
     166                    l.simple_bind_s(self.settings['LDAP_PREBINDDN'],
     167                            self.settings['LDAP_PREBINDPW'])
     168                except ldap.LDAPError, exc:
     169                    if self.settings['LDAP_DEBUG']:
     170                        logging.info('LDAPBackend _pre_bind: LDAPError: %s' % exc)
     171                    return None
     172
     173            # Now do the actual search
     174            filter = self.settings['LDAP_SEARCH_FILTER'] % username
     175            result = l.search_s(self.settings['LDAP_SEARCHDN'],
     176                        self.settings['LDAP_SCOPE'], filter, attrsonly=1)
     177
     178            if self.settings['LDAP_PREBINDDN']:
     179                l.unbind_s()
     180            if len(result) != 1:
     181                if self.settings['LDAP_DEBUG']:
     182                    logging.info('LDAPBackend _pre_bind: not exactly one result: %s (%s %s %s)' % (
     183                        result, self.settings['LDAP_SEARCHDN'], self.settings['LDAP_SCOPE'], filter))
     184                return None
     185            return result[0][0]
     186        else:
     187            # LDAP_BINDDN is set so we use it as a template.
     188            return "%s=%s,%s" % (self.settings['LDAP_BIND_ATTRIBUTE'], username,
     189                    self.settings['LDAP_BINDDN'])
     190   
     191    def _get_user_by_name(self, username):
     192        """
     193        Returns an object of contrib.auth.models.User that has a matching
     194        username.
     195        called as: self._get_user_by_name(username)
     196        """
     197        return User.objects.get(username=username)
     198
     199    def _create_user_object(self, username, password):
     200        """
     201        Creates and returns an object of contrib.auth.models.User.
     202        called as: self._create_user_object(username, password)
     203        """
     204        return User(username=username, password=password)
     205
     206    # Required for an authentication backend
     207    def get_user(self, user_id):
     208        try:
     209            return User.objects.get(pk=user_id)
     210        except:
     211            return None
     212    # End of functions to override
     213
     214    def _get_ldap_user(self, l, username):
     215        """
     216        Helper method, makes a user object and call update_user to populate
     217        """
     218
     219        # Generate a random password string.
     220        password = User.objects.make_random_password(10)
     221        user = self._create_user_object(username, password)
     222        return user
     223
     224    def _update_user(self, l, user):
     225        """
     226        Helper method, populates a user object with various attributes from
     227        LDAP.
     228        """
     229
     230        username = user.username
     231        filter = self.settings['LDAP_SEARCH_FILTER'] % username
     232
     233        # Get results of search and make sure something was found.
     234        # At this point this shouldn't fail.
     235        hold = l.search_s(self.settings['LDAP_SEARCHDN'],
     236                    self.settings['LDAP_SCOPE'], filter)
     237        if len(hold) < 1:
     238            raise AssertionError('No results found with: %s' % (filter))
     239
     240        dn = hold[0][0]
     241        attrs = hold[0][1]
     242        firstn = self.settings['LDAP_FIRST_NAME'] or None
     243        lastn = self.settings['LDAP_LAST_NAME'] or None
     244        emailf = self.settings['LDAP_EMAIL'] or None
     245
     246        if firstn:
     247            if firstn in attrs:
     248                user.first_name = attrs[firstn][0]
     249            else:
     250                raise NameError('Missing attribute: %s in result for %s'
     251                        % (firstn, dn))
     252        if lastn:
     253            if lastn in attrs:
     254                user.last_name = attrs[lastn][0]
     255            else:
     256                raise NameError('Missing attribute: %s in result for %s'
     257                        % (lastn, dn))
     258        if not firstn and not lastn and self.settings['LDAP_FULL_NAME']:
     259            fulln = self.settings['LDAP_FULL_NAME']
     260            if fulln in attrs:
     261                    tmp = attrs[fulln][0]
     262                    user.first_name = tmp.split(' ')[0]
     263                    user.last_name = ' '.join(tmp.split(' ')[1:])
     264            else:
     265                raise NameError('Missing attribute: %s in result for %s'
     266                        % (fulln, dn))
     267
     268        if emailf and emailf in attrs:
     269            user.email = attrs[emailf][0]
     270        elif self.settings['LDAP_DEFAULT_EMAIL_SUFFIX']:
     271            user.email = username + self.settings['LDAP_DEFAULT_EMAIL_SUFFIX']
     272
     273        if (hasattr(settings, 'LDAP_GID')
     274                and settings.LDAP_GID in attrs
     275                and hasattr(settings, 'LDAP_SU_GIDS')
     276                and attrs[settings.LDAP_GID][0] in settings.LDAP_SU_GIDS):
     277            user.is_superuser = True
     278            user.is_staff = True
     279        elif (hasattr(settings, 'LDAP_GID')
     280                and settings.LDAP_GID in attrs
     281                and hasattr(settings, 'LDAP_STAFF_GIDS')
     282                and attrs[settings.LDAP_GID][0] in settings.LDAP_STAFF_GIDS):
     283            user.is_superuser = False
     284            user.is_staff = True
     285        else:
     286            user.is_superuser = False
     287            user.is_staff = False
     288        if hasattr(settings, 'LDAP_ACTIVE_FIELD'):
     289            if (settings.LDAP_ACTIVE_FIELD in attrs
     290                and hasattr(settings, 'LDAP_ACTIVE')
     291                and attrs[settings.LDAP_ACTIVE_FIELD][0]
     292                in settings.LDAP_ACTIVE):
     293                user.is_active = True
     294            else:
     295                user.is_active = False
     296        else:
     297            # LDAP_ACTIVE_FIELD not defined, all users are active
     298            user.is_active = True
     299        user.save()
     300
Back to Top