Ticket #2507: ldapauth.1.diff

File ldapauth.1.diff, 10.6 KB (added by Scott Paul Robertson <spr@…>, 17 years ago)

Patch with one bug fix and some documentation improvements.

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

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