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