New submission from Lorenzo M. Catucci <lore...@sancho.ccd.uniroma2.it>:

While I know it's not standard practice, I'm sending a wholesale patch
since I needed all of the enhancements.

 - provide start_tls option both for the authenticator and the metadata
   provider
 - enable both pattern-replacement and subtree searches for the naming
   attribute in _get_dn
 - enable configuration of the naming attribute
 - enable the option to bind to the server with privileged credentials
   before doing searches
 - add a restrict pattern to pre-authentication DN searches
 - let the API user choose whether to return the full DN or the supplied
   login as the user identifier

As is, this patch does close two open lp: tickets

https://bugs.launchpad.net/repoze.who.plugins.ldap/+bug/363178 (assumed DN)
https://bugs.launchpad.net/repoze.who.plugins.ldap/+bug/489557 (start_tls)

and sidesteps a stated requirement on lm milestone 1.1:  "it should provide a
 identifier plugin that finds the DN from an email address" by letting the user
configure the authenticator to use a choosen naming attribute

----------
files: repoze_who_plugins_ldap.diff
messages: 335
nosy: catucci
priority: feature
status: unread
title: repoze.who.plugins.ldap enhancements
topic: repoze.who

__________________________________
Repoze Bugs <b...@bugs.repoze.org>
<http://bugs.repoze.org/issue111>
__________________________________
=== modified file 'CHANGELOG'
--- CHANGELOG	2008-09-09 17:41:31 +0000
+++ CHANGELOG	2009-12-04 00:39:29 +0000
@@ -1,6 +1,19 @@
 repoze.who.plugins.ldap Changelog
 =================================
 
+Future (20yy-mm-dd)
+-------------------------------
+
+ - provide start_tls option both for the authenticator and the metadata provider
+ - enable both pattern-replacement and subtree searches for the naming
+   attribute in _get_dn
+ - enable configuration of the naming attribute
+ - enable the option to bind to the server with privileged credential before
+   doing searches
+ - add a restrict pattern to pre-authentication DN searches
+ - let the user choose whether to return the full DN or the supplied login as
+   the user identifier
+
 1.0 (2008-09-09)
 -------------------------------
 The initial release.

=== modified file 'repoze/who/plugins/ldap/plugins.py'
--- repoze/who/plugins/ldap/plugins.py	2008-09-08 22:12:05 +0000
+++ repoze/who/plugins/ldap/plugins.py	2009-12-04 00:39:29 +0000
@@ -28,6 +28,10 @@
 
 from repoze.who.interfaces import IAuthenticator, IMetadataProvider
 
+from base64 import b64encode, b64decode
+
+import re
+
 
 #{ Authenticators
 
@@ -36,7 +40,11 @@
 
     implements(IAuthenticator)
 
-    def __init__(self, ldap_connection, base_dn):
+    def __init__(self, ldap_connection, base_dn,
+                       naming_mode = 'pattern', naming_attribute='uid',
+                       search_scope = 'subtree', restrict = '',
+                       returned_id = 'dn', start_tls = '',
+                       bind_dn = '', bind_pass =''):
         """Create an LDAP authentication plugin.
         
         By passing an existing LDAPObject, you're free to use the LDAP
@@ -54,14 +62,72 @@
             C{ou=employees,dc=example,dc=org}, to which will be prepended the
             user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
         @type base_dn: C{unicode}
+        @param naming_mode: How should we get the I{Distinguished Name} for
+            the incoming user. 
+        @type naming_mode: C{str}, 'pattern' or 'search'
+        @param naming_attribute: The naming attribute for directory entries,
+            C{uid} by default.
+        @type naming_attribute: C{unicode}
+        @param search_scope: Scope for ldap searches
+        @type search_scope: C{str}, 'subtree' or 'onelevel', possibly
+            abbreviated to at least the first three characters
+        @param restrict: An ldap filter which will be ANDed to the search filter
+            while searching for entries matching the naming attribute
+        @type restrict: C{unicode}
+        @param returned_id: Should we return full Directory Names or just the
+            naming attribute value on successfull authentication
+        @type returned_id: C{str}, 'dn' or 'userid'
+        @param start_tls: Should we negotiate a TLS upgrade on the connection with
+            the directory server?
+        @type start_tls: C{str}
+        @param bind_dn: Operate as the bind_dn directory entry
+        @type bind_dn: C{str}
+        @param bind_pass: The password for bind_dn directory entry
+        @type bind_pass: C{str}
         @raise ValueError: If at least one of the parameters is not defined.
         
         """
         if base_dn is None:
             raise ValueError('A base Distinguished Name must be specified')
         self.ldap_connection = make_ldap_connection(ldap_connection)
+
+        if start_tls:
+            try:
+                self.ldap_connection.start_tls_s()
+            except:
+                raise ValueError('Cannot upgrade the connection')
+
+        self.bind_dn   = bind_dn
+        self.bind_pass = bind_pass
+
         self.base_dn = base_dn
 
+        if returned_id.lower() == 'dn':
+            self.ret_style = 'd'
+        elif returned_id.lower() == 'userid':
+            self.ret_style = 'u'
+        else:
+            raise ValueError("The return style should be 'dn' or 'userid'")
+
+        if naming_mode == 'pattern':
+            self.naming_mode = 'p'
+            self.naming_pattern = u'%s=%%s,%%s' % naming_attribute
+
+        elif naming_mode == 'search':
+            self.naming_mode = 's'
+            if restrict:
+                self.search_pattern = u'(&%s(%s=%%s))' % (restrict,naming_attribute)
+            else:
+                self.search_pattern = u'%s=%%s' % naming_attribute
+            if search_scope[:3] == 'sub':
+                self.search_scope = ldap.SCOPE_SUBTREE
+            elif search_scope[:3] == 'one':
+                self.search_scope = ldap.SCOPE_ONELEVEL
+            else:
+                raise ValueError("The search scope should be 'one[level]' or 'sub[tree]'")
+        else:
+            raise ValueError("The naming mode should be 'pattern' or 'search'")
+
     # IAuthenticatorPlugin
     def authenticate(self, environ, identity):
         """Return the Distinguished Name of the user to be authenticated.
@@ -86,24 +152,54 @@
         
         try:
             self.ldap_connection.simple_bind_s(dn, password)
+            userdata = identity.get('userdata','')
             # The credentials are valid!
-            return dn
+            if self.ret_style == 'd':
+                return dn
+            else:
+                identity['userdata'] = userdata + '<dn:%s>' % b64encode(dn)
+                return identity['login']
         except ldap.LDAPError:
             return None
-    
+
+
     def _get_dn(self, environ, identity):
         """
         Return the DN based on the environment and the identity.
         
+        @attention: You may want to override this method if the DN generated by
+            default doesn't meet your requirements. If you do so, make sure to
+            raise a C{ValueError} exception if the operation is not successful.
+        @param environ: The WSGI environment.
+        @param identity: The identity dictionary.
+        @return: The Distinguished Name (DN)
+        @rtype: C{unicode}
+        @raise ValueError: If the C{login} key is not in the I{identity} dict.
+        
+        """
+
+        if self.bind_dn:
+            try:
+                self.ldap_connection.bind_s(self.bind_dn, self.bind_password)
+            except ldap.LDAPError:
+                raise ValueError("Couldn't bind with supplied credentials")
+        if self.naming_mode == 'p':
+            return self._get_dn_pattern(environ, identity)
+        elif self.naming_mode == 's':
+            return self._get_dn_search(environ, identity)
+        else:
+            raise ValueError
+
+    def _get_dn_pattern(self, environ, identity):
+        """
+        Return the DN based on the environment and the identity.
+        
         It prepends the user id to the base DN given in the constructor:
         
         If the C{login} item of the identity is C{rms} and the base DN is
         C{ou=developers,dc=gnu,dc=org}, the resulting DN will be:
         C{uid=rms,ou=developers,dc=gnu,dc=org}.
-        
-        @attention: You may want to override this method if the DN generated by
-            default doesn't meet your requirements. If you do so, make sure to
-            raise a C{ValueError} exception if the operation is not successful.
+
         @param environ: The WSGI environment.
         @param identity: The identity dictionary.
         @return: The Distinguished Name (DN)
@@ -112,10 +208,44 @@
         
         """
         try:
-            return u'uid=%s,%s' % (identity['login'], self.base_dn)
+            return self.naming_pattern % ( identity['login'], self.base_dn)
         except (KeyError, TypeError):
             raise ValueError
 
+    def _get_dn_search(self, environ, identity):
+        """
+        Return the DN based on the environment and the identity.
+        
+        It searches the directory entry with naming attribute matching the
+        C{login} item of the identity.
+
+        If the C{login} item of the identity is C{rms}, the naming attribute is
+        C{uid} and the base DN is C{dc=gnu,dc=org}, we'll ask the server
+        to search for C{uid = rms} beneath the search base, hopefully 
+        finding C{uid=rms,ou=developers,dc=gnu,dc=org}.
+        
+        @param environ: The WSGI environment.
+        @param identity: The identity dictionary.
+        @return: The Distinguished Name (DN)
+        @rtype: C{unicode}
+        @raise ValueError: If the C{login} key is not in the I{identity} dict.
+        
+        """
+        try:
+            login_name = identity['login'].replace('*',r'\*')
+            srch = self.search_pattern % login_name
+            dn_list = self.ldap_connection.search_s(self.base_dn, self.search_scope, 
+                                          srch ,[])
+            if len(dn_list) == 1:
+                return dn_list[0][0]
+            elif len(dn_list) > 1:
+                raise ValueError('Too many entries found for %s' % srch)
+            else:
+                raise ValueError('No entry found for %s' %srch)
+        except (KeyError, TypeError,ldap.LDAPError):
+            raise ValueError
+
+
     def __repr__(self):
         return '<%s %s>' % (self.__class__.__name__, id(self))
 
@@ -127,9 +257,12 @@
     """Loads LDAP attributes of the authenticated user."""
     
     implements(IMetadataProvider)
+
+    dnrx = re.compile('<dn:(?P<b64dn>[A-Za-z0-9+/]+=*)>')
     
     def __init__(self, ldap_connection, attributes=None,
-                 filterstr='(objectClass=*)'):
+                 filterstr='(objectClass=*)', start_tls = '',
+                 bind_dn = '', bind_pass =''):
         """
         Fetch LDAP attributes of the authenticated user.
         
@@ -143,6 +276,13 @@
             <http://www.faqs.org/rfcs/rfc4515.html>}; the results won't be
             filtered unless you define this.
         @type filterstr: C{str}
+        @param start_tls: Should we negotiate a TLS upgrade on the connection with
+            the directory server?
+        @type start_tls: C{str}
+        @param bind_dn: Operate as the bind_dn directory entry
+        @type bind_dn: C{str}
+        @param bind_pass: The password for bind_dn directory entry
+        @type bind_pass: C{str}
         @raise ValueError: If L{make_ldap_connection} could not create a
             connection from C{ldap_connection}, or if C{attributes} is not an
             iterable.
@@ -156,6 +296,14 @@
         elif attributes is not None:
             raise ValueError('The needed LDAP attributes are not valid')
         self.ldap_connection = make_ldap_connection(ldap_connection)
+        if start_tls:
+            try:
+                self.ldap_connection.start_tls_s()
+            except:
+                raise ValueError('Cannot upgrade the connection')
+
+        self.bind_dn   = bind_dn
+        self.bind_pass = bind_pass
         self.attributes = attributes
         self.filterstr = filterstr
     
@@ -171,18 +319,29 @@
         
         """
         # Search arguments:
+        dnmatch = self.dnrx.match(identity.get('userdata',''))
+        if dnmatch:
+            dn = b64decode(dnmatch.group('b64dn'))
+        else:
+            dn = identity.get('repoze.who.userid')
         args = (
-            identity.get('repoze.who.userid'),
+            dn,
             ldap.SCOPE_BASE,
             self.filterstr,
             self.attributes
         )
+        if self.bind_dn:
+            try:
+                self.ldap_connection.bind_s(self.bind_dn, self.bind_pass)
+            except ldap.LDAPError:
+                raise ValueError("Couldn't bind with supplied credentials")
         try:
             for (dn, attributes) in self.ldap_connection.search_s(*args):
                 identity.update(attributes)
         except ldap.LDAPError, msg:
             environ['repoze.who.logger'].warn('Cannot add metadata: %s' % \
                                               msg)
+            raise Exception(identity)
             return
 
 

_______________________________________________
Repoze-dev mailing list
Repoze-dev@lists.repoze.org
http://lists.repoze.org/listinfo/repoze-dev

Reply via email to