Ticket #2507: ldap.2.diff

File ldap.2.diff, 10.0 KB (added by spr@…, 18 years ago)

Update to allow inheritance of LDAPBackend, so you can override the User object and generation of the bind string. This is the full patch for LDAP auth support.

  • django/conf/global_settings.py

     
    296296##################
    297297
    298298AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
     299
     300# Defaults for LDAPBackend
     301LDAP_SERVER_URI = 'ldap://localhost'
     302LDAP_SEARCHDN = 'dc=localhost'
     303LDAP_SCOPE = 2  # ldap.SCOPE_SUBTREE
     304LDAP_SEARCH_FILTER = 'cn=%s'
     305LDAP_UPDATE_FIELDS = True
     306LDAP_BIND_DN = 'dc=localhost'
     307LDAP_BIND_ATTRIBUTE = 'dn'
  • django/contrib/auth/backends.py

     
    11from django.contrib.auth.models import User
     2from django.conf import settings
    23
    34class ModelBackend:
    45    """
     
    1920            return User.objects.get(pk=user_id)
    2021        except User.DoesNotExist:
    2122            return None
     23
     24class LDAPBackend(object):
     25    """
     26    Authenticate a user against LDAP.
     27    Requires python-ldap to be installed.
     28
     29    Requires the following things to be in settings.py:
     30    LDAP_SERVER_URI -- string, ldap uri.
     31        default: 'ldap://localhost'
     32    LDAP_SEARCHDN -- string of the LDAP dn to use for searching
     33        default: 'dc=localhost'
     34    LDAP_SCOPE -- one of: ldap.SCOPE_*, used for searching
     35        ldap.SCOPE_BASE = 0
     36        ldap.SCOPE_ONELEVEL = 1
     37        ldap.SCOPE_SUBTREE = 2
     38        see python-ldap docs for the search function
     39        default = ldap.SCOPE_SUBTREE
     40    LDAP_SEARCH_FILTER -- formated string, the filter to use for searching for a
     41        user. Used as: filterstr = LDAP_SEARCH_FILTER % username
     42        default = 'cn=%s'
     43    LDAP_UPDATE_FIELDS -- boolean, do we sync the db with ldap on each auth
     44        default = True
     45
     46    Bind Options (Required unless _pre_bind() is overridden):
     47    LDAP_BINDDN -- string of the LDAP dn to use for binding
     48    LDAP_BIND_ATTRIBUTE -- string of the LDAP attribute to use in binding.
     49
     50    Required unless LDAP_FULL_NAME is set:
     51    LDAP_FIRST_NAME -- string, LDAP attribute to get the given name from
     52    LDAP_LAST_NAME -- string, LDAP attribute to get the last name from
     53
     54    Optional Settings:
     55    LDAP_FULL_NAME -- string, LDAP attribute to get name from, splits on ' '
     56    LDAP_GID -- string, LDAP attribute to get group name/number from
     57    LDAP_SU_GIDS -- list of strings, group names/numbers that are superusers
     58    LDAP_STAFF_GIDS -- list of strings, group names/numbers that are staff
     59    LDAP_EMAIL -- string, LDAP attribute to get email from
     60    LDAP_DEFAULT_EMAIL_SUFFIX -- string, appened to username if no email found
     61    LDAP_OPTIONS -- hash, python-ldap global options and their values
     62        {ldap.OPT_X_TLS_CACERTDIR: '/etc/ldap/ca/'}
     63
     64    How binding is done:
     65    LDAP_BINDDN = 'ou=people,dc=example,dc=com'
     66    LDAP_BIND_ATTRIBUTE = 'uid'
     67    # The bind would be performed via:
     68    # uid=username,ou=people,dc=example,dc=com
     69
     70    By inheriting this class you can change:
     71    * How the dn to bind with is produced by overriding _pre_bind()
     72    * What type of user object to use by overriding: _get_user_by_name(),
     73      _create_user_object(), and _get_user()
     74    * Be aware of what
     75    See mk_pre_auth.__doc__ for more information on overriding pre_bind()
     76    """
     77    import ldap
     78
     79    def authenticate(self, username=None, password=None):
     80        if not username and password is not None: # we need a user/pass
     81            return None
     82
     83        if hasattr(settings, 'LDAP_OPTIONS'):
     84            for k in settings.LDAP_OPTIONS:
     85                ldap.set_option(k, settings.LDAP_OPTIONS[k])
     86
     87        l = ldap.initialize(settings.LDAP_SERVER_URI)
     88
     89        bind_string = self._pre_bind(l, username)
     90
     91        try:
     92            l.bind_s(bind_string, password)
     93        except ldap.INVALID_CREDENTIALS: # Failed user/pass
     94            l.unbind_s()
     95            return None
     96
     97        try:
     98            user = self._get_user_by_name(username)
     99        except User.DoesNotExist:
     100            user = None
     101
     102        if user is not None:
     103            if settings.LDAP_UPDATE_FIELDS:
     104                self._update_user(l, user)
     105        else:
     106            user = self._get_ldap_user(l, username)
     107
     108        l.unbind_s()
     109        return user
     110
     111# Functions provided to override to customize to your LDAP configuration.
     112    def _pre_bind(self, l, username):
     113        """
     114        Function that returns the dn to bind against ldap with.
     115        called as: self._pre_bind(ldapobject, username)
     116        """
     117        return "%s=%s,%s" % (settings.LDAP_BIND_ATTRIBUTE, username,
     118                settings.LDAP_BINDDN)
     119   
     120    def _get_user_by_name(self, username):
     121        """
     122        Returns an object of contrib.auth.models.User that has a matching
     123        username.
     124        called as: self._get_user_by_name(username)
     125        """
     126        return User.objects.get(username=username)
     127
     128    def _create_user_object(self, username, password):
     129        """
     130        Creates and returns an object of contrib.auth.models.User.
     131        called as: self._create_user_object(username, password)
     132        """
     133        return User(username=username, password=password)
     134
     135    # Required for an authentication backend
     136    def get_user(self, user_id):
     137        try:
     138            return User.objects.get(pk=user_id)
     139        except:
     140            return None
     141# End of functions to override
     142
     143    def _get_ldap_user(self, l, username):
     144        """
     145        Helper method, makes a user object and call update_user to populate
     146        """
     147
     148        # Generate a random password string.
     149        from random import choice
     150        import string
     151        password = ''.join([choice(string.printable) for i in range(10)])
     152
     153        user = self._create_user_object(username, password)
     154        self._update_user(l, user)
     155        return user
     156
     157    def _update_user(self, l, user):
     158        """
     159        Helper method, populates a user object with various attributes from
     160        LDAP.
     161        """
     162
     163        username = user.username
     164        filter = settings.LDAP_SEARCH_FILTER % username
     165        attrs = l.search_s(settings.LDAP_SEARCHDN, settings.LDAP_SCOPE,
     166                filter)[0][1]
     167
     168        if (hasattr(settings, 'LDAP_FIRST_NAME')
     169                and hasattr(settings, 'LDAP_LAST_NAME')):
     170            if (settings.LDAP_FIRST_NAME in attrs
     171                    and settings.LDAP_LAST_NAME in attrs):
     172                user.first_name = attrs[settings.LDAP_FIRST_NAME][0]
     173                user.last_name = attrs[settings.LDAP_LAST_NAME][0]
     174            else:
     175                raise NameError('Missing needed fields %s or %s in LDAP'
     176                        % (settings.LDAP_FIRST_NAME, settings.LDAP_LAST_NAME))
     177        elif hasattr(settings, 'LDAP_FULL_NAME'):
     178                if settings.LDAP_FULL_NAME in attrs:
     179                    tmp = attrs[settings.FULL_NAME_FIELD][0]
     180                    user.first_name = tmp.split(' ')[0]
     181                    user.last_name = ' '.join(tmp.split(' ')[1:])
     182                else:
     183                    raise NameError('Required field %s missing in LDAP'
     184                            % (settings.LDAP_FULL_NAME))
     185        else:
     186            raise NameError('Name fields not defined in settings.py')
     187
     188        if hasattr(settings, 'LDAP_EMAIL') and settings.LDAP_EMAIL in attrs:
     189            user.email = attrs[settings.EMAIL_FIELD][0]
     190        elif hasattr(settings, 'LDAP_DEFAULT_EMAIL_SUFFIX'):
     191            user.email = username + settings.LDAP_DEFAULT_EMAIL_SUFFIX
     192
     193        if (hasattr(settings, 'LDAP_GID')
     194                and settings.LDAP_GID in attrs
     195                and hasattr(settings, 'LDAP_SU_GIDS')
     196                and attrs[settings.LDAP_GID][0] in settings.LDAP_SU_GIDS):
     197            user.is_superuser = True
     198            user.is_staff = True
     199        elif (hasattr(settings, 'LDAP_GID')
     200                and settings.LDAP_GID in attrs
     201                and hasattr(settings, 'LDAP_STAFF_GIDS')
     202                and attrs[settings.LDAP_GID][0] in settings.LDAP_STAFF_GIDS):
     203            user.is_superuser = False
     204            user.is_staff = True
     205        else:
     206            user.is_superuser = False
     207            user.is_staff = False
     208
     209        user.save()
     210
     211    def mk_pre_auth_bind(auth_user, auth_pass,
     212            search_base=settings.LDAP_SEARCHDN,
     213            search_scope=settings.LDAP_SCOPE,
     214            search_filter=settings.LDAP_SEARCH_FILTER):
     215        """
     216        Returns a function, to be used to override pre_bind().
     217        auth_user -- string, ldap formatted user to bind with
     218        auth_pass -- string, password for auth_user
     219        search_base -- string, base for the ldap search
     220        search_filter -- string, ldap search filter to use, %'d with username
     221        search_scope -- ldap.SCOPE_* value, search scope
     222
     223        Used in a child class:
     224        class MyLDAPBackend(LDAPBackend):
     225            pre_bind = LDAPBackend.mk_pre_auth_bind('dn=Me,dc=example,dc=com',
     226                    'pass')
     227        which defaults to:
     228        class MyLDAPBackend(LDAPBackend):
     229            pre_bind = LDAPBackend.mk_pre_auth_bind('dn=Me,dc=example,dc=com',
     230                    'pass', LDAP_SEARCHDN, LDAP_SCOPE, LDAP_SEARCH_FILTER)
     231        or you can customize it:
     232        class MyLDAPBackend(LDAPBackend):
     233            pre_bind = LDAPBackend.mk_pre_auth_bind('dn=Me,dc=example,dc=com',
     234                    'pass', 'ou=people,dc=example,dc=com'',
     235                    ldap.SCOPE_SUBTREE, '(&(objectclass=person) (cn=%s))')
     236        """
     237        def pre_auth_bind(l, user):
     238            try:
     239                l.simple_bind_s(auth_user, auth_pass)
     240            except ldap.LDAPError:
     241                return None
     242
     243            filter = search_filter % user
     244
     245            result = l.search_s(search_base, search_scope, filter,
     246                    attrsonly=1)
     247
     248            if len(result) != 1:
     249                return None
     250
     251            return result[0][0]
     252        return pre_auth_bind
Back to Top