Merge lp:~gnarea/repoze.who.plugins.ldap/1.1proposal into lp:repoze.who.plugins.ldap

Proposed by Gustavo Narea
Status: Merged
Approved by: Gustavo Narea
Approved revision: 70
Merge reported by: Gustavo Narea
Merged at revision: not available
Proposed branch: lp:~gnarea/repoze.who.plugins.ldap/1.1proposal
Merge into: lp:repoze.who.plugins.ldap
Diff against target: 1071 lines (+589/-105)
10 files modified
CHANGELOG (+20/-8)
LICENSE (+2/-1)
demo/ldapauth/controllers/root.py (+3/-3)
demo/ldapauth/model/__init__.py (+1/-1)
demo/ldapauth/model/identity.py (+5/-5)
docs/source/index.rst (+5/-3)
repoze/who/plugins/ldap/__init__.py (+6/-3)
repoze/who/plugins/ldap/plugins.py (+288/-36)
repoze/who/plugins/ldap/tests.py (+255/-43)
setup.py (+4/-2)
To merge this branch: bzr merge lp:~gnarea/repoze.who.plugins.ldap/1.1proposal
Reviewer Review Type Date Requested Status
Gustavo Narea Pending
Review via email: mp+26455@code.launchpad.net

Commit message

Merged from the branch with the changes proposed by Lorenzo Catucci.

Description of the change

 - Provided ``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

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CHANGELOG'
2--- CHANGELOG 2010-01-28 23:42:16 +0000
3+++ CHANGELOG 2010-05-31 22:51:23 +0000
4@@ -1,14 +1,26 @@
5 repoze.who.plugins.ldap Changelog
6 =================================
7
8-2010-01-28
9-----------
10-
11-Changed the license to the `Repoze license <http://repoze.org/license.html>`_.
12-
13-
14-1.0 (2008-09-09)
15--------------------------------
16+1.1 Alpha 1 (unreleased)
17+------------------------
18+
19+
20+ - Changed the license to the `Repoze license <http://repoze.org/license.html>`_.
21+ - Provided ``start_tls`` option both for the authenticator and the metadata
22+ provider.
23+ - Enable both pattern-replacement and subtree searches for the naming
24+ attribute in ``_get_dn()``.
25+ - Enable configuration of the naming attribute
26+ - Enable the option to bind to the server with privileged credential before
27+ doing searches
28+ - Add a restrict pattern to pre-authentication DN searches
29+ - Let the user choose whether to return the full DN or the supplied login as
30+ the user identifier
31+
32+
33+1.0 (2008-09-11)
34+----------------
35+
36 The initial release.
37
38 - Provided the LDAP authenticator, which is compatible with identifiers that
39
40=== modified file 'LICENSE'
41--- LICENSE 2010-05-19 22:31:15 +0000
42+++ LICENSE 2010-05-31 22:51:23 +0000
43@@ -1,4 +1,5 @@
44-Copyright (c) 2008-2010, Gustavo Narea.
45+Copyright (c) 2008, Gustavo Narea.
46+Copyright (c) 2010, Gustavo Narea and Lorenzo M. Catucci.
47 All rights reserved.
48
49 Redistribution and use in source and binary forms, with or without modification,
50
51=== modified file 'demo/ldapauth/controllers/root.py'
52--- demo/ldapauth/controllers/root.py 2010-01-28 23:42:16 +0000
53+++ demo/ldapauth/controllers/root.py 2010-05-31 22:51:23 +0000
54@@ -14,10 +14,10 @@
55
56 @expose('ldapauth.templates.about')
57 def about(self):
58- if request.environ.get('repoze.who.auth') == None:
59+ if request.environ.get('repoze.who.identity') == None:
60 raise HTTPUnauthorized()
61- user = request.environ['repoze.who.auth']['repoze.who.userid']
62+ user = request.environ['repoze.who.identity']['repoze.who.userid']
63 flash('Your Distinguished Name (DN) is "%s"' % user)
64 # Passing the metadata
65- metadata = request.environ['repoze.who.auth']
66+ metadata = request.environ['repoze.who.identity']
67 return dict(metadata=metadata.items())
68
69=== modified file 'demo/ldapauth/model/__init__.py'
70--- demo/ldapauth/model/__init__.py 2010-01-28 23:42:16 +0000
71+++ demo/ldapauth/model/__init__.py 2010-05-31 22:51:23 +0000
72@@ -53,4 +53,4 @@
73 #mapper(Reflected, t_reflected)
74
75 # Import your model modules here.
76-from ldapauth.model.auth import User, Group, Permission
77+from ldapauth.model.identity import User, Group, Permission
78
79=== modified file 'demo/ldapauth/model/identity.py'
80--- demo/ldapauth/model/identity.py 2010-01-28 23:42:16 +0000
81+++ demo/ldapauth/model/identity.py 2010-05-31 22:51:23 +0000
82@@ -29,7 +29,7 @@
83 onupdate="CASCADE", ondelete="CASCADE"))
84 )
85
86-# auth model
87+# identity model
88
89 class Group(DeclarativeBase):
90 """An ultra-simple group definition.
91@@ -132,7 +132,7 @@
92
93 #elif "custom" == algorithm:
94 # custom_encryption_path = turbogears.config.get(
95- # "auth.custom_encryption", None )
96+ # "identity.custom_encryption", None )
97 #
98 # if custom_encryption_path:
99 # custom_encryption = turbogears.util.load_class(
100@@ -151,10 +151,10 @@
101 def validate_password(self, password):
102 """Check the password against existing credentials.
103 """
104- auth = config.get('auth', None)
105- if auth is None:
106+ identity = config.get('identity', None)
107+ if identity is None:
108 return password
109- algorithm = auth.get('password_encryption_method', None)
110+ algorithm = identity.get('password_encryption_method', None)
111 return self.password == self.__encrypt_password(algorithm, password)
112
113
114
115=== modified file 'docs/source/index.rst'
116--- docs/source/index.rst 2008-09-08 23:20:55 +0000
117+++ docs/source/index.rst 2010-05-31 22:51:23 +0000
118@@ -86,14 +86,16 @@
119
120 This plugin was made possible thanks to the people below:
121
122- - **Chris McDonough**, for his guidance throughout the development of the
123- plugin.
124+ - **Lorenzo M. Catucci**, of the `University of Rome "Tor Vergata"
125+ <http://www.uniroma2.it/>`_, for implementing all of the new features in v1.1.
126+ - **Chris McDonough**, for his guidance throughout the initial development of
127+ the plugin.
128
129
130 Copyright notice for this documentation
131 =======================================
132
133-Copyright (c) 2008, by Gustavo Narea.
134+Copyright (c) 2008-2010, by Gustavo Narea.
135
136 Permission is granted to copy, distribute and/or modify this document under the
137 terms of the `GNU Free Documentation License <http://www.gnu.org/copyleft/fdl.html>`_,
138
139=== modified file 'repoze/who/plugins/ldap/__init__.py'
140--- repoze/who/plugins/ldap/__init__.py 2010-05-19 22:31:15 +0000
141+++ repoze/who/plugins/ldap/__init__.py 2010-05-31 22:51:23 +0000
142@@ -1,7 +1,9 @@
143 # -*- coding: utf-8 -*-
144 #
145 # repoze.who.plugins.ldap, LDAP authentication for WSGI applications.
146-# Copyright (C) 2008-2010 by Gustavo Narea <http://gustavonarea.net/>
147+# Copyright (C) 2010 by Gustavo Narea <http://gustavonarea.net/> and
148+# Lorenzo M. Catucci <http://www.uniroma2.it/>.
149+# Copyright (C) 2008 by Gustavo Narea <http://gustavonarea.net/>.
150 #
151 # This file is part of repoze.who.plugins.ldap
152 # <http://code.gustavonarea.net/repoze.who.plugins.ldap/>
153@@ -34,6 +36,7 @@
154 import ldap
155
156 from repoze.who.plugins.ldap.plugins import LDAPAuthenticatorPlugin, \
157- LDAPAttributesPlugin
158+ LDAPAttributesPlugin, \
159+ LDAPSearchAuthenticatorPlugin
160
161-__all__ = ['LDAPAuthenticatorPlugin', 'LDAPAttributesPlugin']
162+__all__ = ['LDAPAuthenticatorPlugin', 'LDAPSearchAuthenticatorPlugin', 'LDAPAttributesPlugin']
163
164=== modified file 'repoze/who/plugins/ldap/plugins.py'
165--- repoze/who/plugins/ldap/plugins.py 2010-05-19 22:40:52 +0000
166+++ repoze/who/plugins/ldap/plugins.py 2010-05-31 22:51:23 +0000
167@@ -1,7 +1,9 @@
168 # -*- coding: utf-8 -*-
169 #
170 # repoze.who.plugins.ldap, LDAP authentication for WSGI applications.
171-# Copyright (C) 2008-2010 by Gustavo Narea <http://gustavonarea.net/>
172+# Copyright (C) 2010 by Gustavo Narea <http://gustavonarea.net/> and
173+# Lorenzo M. Catucci <http://www.uniroma2.it/>.
174+# Copyright (C) 2008 by Gustavo Narea <http://gustavonarea.net/>.
175 #
176 # This file is part of repoze.who.plugins.ldap
177 # <http://code.gustavonarea.net/repoze.who.plugins.ldap/>
178@@ -12,64 +14,114 @@
179 # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO,
180 # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
181 # FITNESS FOR A PARTICULAR PURPOSE.
182-
183 """LDAP plugins for repoze.who."""
184
185-__all__ = ['LDAPAuthenticatorPlugin', 'LDAPAttributesPlugin']
186+__all__ = ['LDAPAuthenticatorPlugin', 'LDAPSearchAuthenticatorPlugin',
187+ 'LDAPAttributesPlugin']
188
189 from zope.interface import implements
190 import ldap
191
192 from repoze.who.interfaces import IAuthenticator, IMetadataProvider
193
194+from base64 import b64encode, b64decode
195+
196+import re
197+
198
199 #{ Authenticators
200
201
202-class LDAPAuthenticatorPlugin(object):
203+class LDAPBaseAuthenticatorPlugin(object):
204
205 implements(IAuthenticator)
206
207- def __init__(self, ldap_connection, base_dn):
208+ def __init__(self, ldap_connection, base_dn, returned_id='dn',
209+ start_tls=False, bind_dn='', bind_pass='', **kwargs):
210 """Create an LDAP authentication plugin.
211
212 By passing an existing LDAPObject, you're free to use the LDAP
213 authentication method you want, the way you want.
214
215- If the default way to find the DN is not suitable for you, you may want
216- to override L{_get_dn}.
217+ If the default way to find the DN is not suitable for you, you must
218+ override L{_get_dn}.
219
220 This plugin is compatible with any identifier plugin that defines the
221- C{login} and C{password} items in the I{auth} dictionary.
222+ C{login} and C{password} items in the I{identity} dictionary.
223
224 @param ldap_connection: An initialized LDAP connection.
225 @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject}
226+
227 @param base_dn: The base for the I{Distinguished Name}. Something like
228 C{ou=employees,dc=example,dc=org}, to which will be prepended the
229 user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
230 @type base_dn: C{unicode}
231+ @param returned_id: Should we return the full DN or just the
232+ bare naming identifier value on successful authentication?
233+ @type returned_id: C{str}, 'dn' or 'login'
234+ @attention: While the DN is always unique, if you configure the
235+ authenticator plugin to return the bare naming attribute,
236+ you have to ensure its uniqueness in the DIT.
237+ @param start_tls: Should we negotiate a TLS upgrade on the connection with
238+ the directory server?
239+ @type start_tls: C{bool}
240+ @param bind_dn: Operate as the bind_dn directory entry
241+ @type bind_dn: C{str}
242+ @param bind_pass: The password for bind_dn directory entry
243+ @type bind_pass: C{str}
244 @raise ValueError: If at least one of the parameters is not defined.
245
246 """
247 if base_dn is None:
248 raise ValueError('A base Distinguished Name must be specified')
249 self.ldap_connection = make_ldap_connection(ldap_connection)
250+
251+ if start_tls:
252+ try:
253+ self.ldap_connection.start_tls_s()
254+ except:
255+ raise ValueError('Cannot upgrade the connection')
256+
257+ self.bind_dn = bind_dn
258+ self.bind_pass = bind_pass
259+
260 self.base_dn = base_dn
261
262+ if returned_id.lower() == 'dn':
263+ self.ret_style = 'd'
264+ elif returned_id.lower() == 'login':
265+ self.ret_style = 'l'
266+ else:
267+ raise ValueError("The return style should be 'dn' or 'login'")
268+
269+ def _get_dn(self, environ, identity):
270+ """
271+ Return the user DN based on the environment and the identity.
272+
273+ Must be implemented in a subclass
274+
275+ @param environ: The WSGI environment.
276+ @param identity: The identity dictionary.
277+ @return: The Distinguished Name (DN)
278+ @rtype: C{unicode}
279+ @raise ValueError: If the C{login} key is not in the I{identity} dict.
280+
281+ """
282+ raise ValueError('Unimplemented')
283+
284+
285 # IAuthenticatorPlugin
286- def authenticate(self, environ, auth):
287- """Return the Distinguished Name of the user to be authenticated.
288+ def authenticate(self, environ, identity):
289+ """Return the naming identifier of the user to be authenticated.
290
291- @attention: The uid is not returned because it may not be unique; the
292- DN, on the contrary, is always unique.
293- @return: The Distinguished Name (DN), if the credentials were valid.
294+ @return: The naming identifier, if the credentials were valid.
295 @rtype: C{unicode} or C{None}
296
297 """
298
299 try:
300- dn = self._get_dn(environ, auth)
301- password = auth['password']
302+ dn = self._get_dn(environ, identity)
303+ password = identity['password']
304 except (KeyError, TypeError, ValueError):
305 return None
306
307@@ -80,38 +132,196 @@
308
309 try:
310 self.ldap_connection.simple_bind_s(dn, password)
311+ userdata = identity.get('userdata', '')
312 # The credentials are valid!
313- return dn
314+ if self.ret_style == 'd':
315+ return dn
316+ else:
317+ identity['userdata'] = userdata + '<dn:%s>' % b64encode(dn)
318+ return identity['login']
319 except ldap.LDAPError:
320 return None
321+
322+ def __repr__(self):
323+ return '<%s %s>' % (self.__class__.__name__, id(self))
324+
325+
326+class LDAPAuthenticatorPlugin(LDAPBaseAuthenticatorPlugin):
327+
328+ def __init__(self, ldap_connection, base_dn, naming_attribute='uid',
329+ **kwargs):
330+ """Create an LDAP authentication plugin using pattern-determined DNs
331+
332+ By passing an existing LDAPObject, you're free to use the LDAP
333+ authentication method you want, the way you want.
334
335- def _get_dn(self, environ, auth):
336- """
337- Return the DN based on the environment and the auth.
338-
339- It prepends the user id to the base DN given in the constructor:
340-
341- If the C{login} item of the auth is C{rms} and the base DN is
342+ This plugin is compatible with any identifier plugin that defines the
343+ C{login} and C{password} items in the I{identity} dictionary.
344+
345+ @param ldap_connection: An initialized LDAP connection.
346+ @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject}
347+ @param base_dn: The base for the I{Distinguished Name}. Something like
348+ C{ou=employees,dc=example,dc=org}, to which will be prepended the
349+ user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
350+ @type base_dn: C{unicode}
351+ @param naming_attribute: The naming attribute for directory entries,
352+ C{uid} by default.
353+ @type naming_attribute: C{unicode}
354+
355+ @raise ValueError: If at least one of the parameters is not defined.
356+
357+ The following parameters are inherited from
358+ L{LDAPBaseAuthenticatorPlugin.__init__}
359+ @param base_dn: The base for the I{Distinguished Name}. Something like
360+ C{ou=employees,dc=example,dc=org}, to which will be prepended the
361+ user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
362+ @param returned_id: Should we return full Directory Names or just the
363+ bare naming identifier on successful authentication?
364+ @param start_tls: Should we negotiate a TLS upgrade on the connection with
365+ the directory server?
366+ @param bind_dn: Operate as the bind_dn directory entry
367+ @param bind_pass: The password for bind_dn directory entry
368+
369+
370+ """
371+ LDAPBaseAuthenticatorPlugin.__init__(self, ldap_connection, base_dn,
372+ **kwargs)
373+ self.naming_pattern = u'%s=%%s,%%s' % naming_attribute
374+
375+ def _get_dn(self, environ, identity):
376+ """
377+ Return the user naming identifier based on the environment and the
378+ identity.
379+
380+ If the C{login} item of the identity is C{rms} and the base DN is
381 C{ou=developers,dc=gnu,dc=org}, the resulting DN will be:
382- C{uid=rms,ou=developers,dc=gnu,dc=org}.
383+ C{uid=rms,ou=developers,dc=gnu,dc=org}
384
385- @attention: You may want to override this method if the DN generated by
386- default doesn't meet your requirements. If you do so, make sure to
387- raise a C{ValueError} exception if the operation is not successful.
388 @param environ: The WSGI environment.
389- @param auth: The auth dictionary.
390+ @param identity: The identity dictionary.
391 @return: The Distinguished Name (DN)
392 @rtype: C{unicode}
393- @raise ValueError: If the C{login} key is not in the I{auth} dict.
394+ @raise ValueError: If the C{login} key is not in the I{identity} dict.
395
396 """
397+
398+ if self.bind_dn:
399+ try:
400+ self.ldap_connection.bind_s(self.bind_dn, self.bind_password)
401+ except ldap.LDAPError:
402+ raise ValueError("Couldn't bind with supplied credentials")
403 try:
404- return u'uid=%s,%s' % (auth['login'], self.base_dn)
405+ return self.naming_pattern % ( identity['login'], self.base_dn)
406 except (KeyError, TypeError):
407 raise ValueError
408
409- def __repr__(self):
410- return '<%s %s>' % (self.__class__.__name__, id(self))
411+
412+class LDAPSearchAuthenticatorPlugin(LDAPBaseAuthenticatorPlugin):
413+
414+ def __init__(self, ldap_connection, base_dn, naming_attribute='uid',
415+ search_scope='subtree', restrict='', **kwargs):
416+ """Create an LDAP authentication plugin determining the DN via LDAP searches.
417+
418+ By passing an existing LDAPObject, you're free to use the LDAP
419+ authentication method you want, the way you want.
420+
421+ If the default way to find the DN is not suitable for you, you may want
422+ to override L{_get_dn}.
423+
424+ This plugin is compatible with any identifier plugin that defines the
425+ C{login} and C{password} items in the I{identity} dictionary.
426+
427+ @param ldap_connection: An initialized LDAP connection.
428+ @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject}
429+ @param base_dn: The base for the I{Distinguished Name}. Something like
430+ C{ou=employees,dc=example,dc=org}, to which will be prepended the
431+ user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
432+ @type base_dn: C{unicode}
433+ @param naming_attribute: The naming attribute for directory entries,
434+ C{uid} by default.
435+ @type naming_attribute: C{unicode}
436+ @param search_scope: Scope for ldap searches
437+ @type search_scope: C{str}, 'subtree' or 'onelevel', possibly
438+ abbreviated to at least the first three characters
439+ @param restrict: An ldap filter which will be ANDed to the search filter
440+ while searching for entries matching the naming attribute
441+ @type restrict: C{unicode}
442+ @attention: restrict will be interpolated into the search string as a
443+ bare string like in "(&%s(identifier=login))". It must be correctly
444+ parenthesised for such usage as in restrict = "(objectClass=*)".
445+
446+ @raise ValueError: If at least one of the parameters is not defined.
447+
448+ The following parameters are inherited from
449+ L{LDAPBaseAuthenticatorPlugin.__init__}
450+ @param base_dn: The base for the I{Distinguished Name}. Something like
451+ C{ou=employees,dc=example,dc=org}, to which will be prepended the
452+ user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
453+ @param returned_id: Should we return full Directory Names or just the
454+ bare naming identifier on successful authentication?
455+ @param start_tls: Should we negotiate a TLS upgrade on the connection
456+ with the directory server?
457+ @param bind_dn: Operate as the bind_dn directory entry
458+ @param bind_pass: The password for bind_dn directory entry
459+
460+ """
461+ LDAPBaseAuthenticatorPlugin.__init__(self, ldap_connection, base_dn,
462+ **kwargs)
463+
464+ if search_scope[:3].lower() == 'sub':
465+ self.search_scope = ldap.SCOPE_SUBTREE
466+ elif search_scope[:3].lower() == 'one':
467+ self.search_scope = ldap.SCOPE_ONELEVEL
468+ else:
469+ raise ValueError("The search scope should be 'one[level]' or 'sub[tree]'")
470+
471+ if restrict:
472+ self.search_pattern = u'(&%s(%s=%%s))' % (restrict,naming_attribute)
473+ else:
474+ self.search_pattern = u'(%s=%%s)' % naming_attribute
475+
476+ def _get_dn(self, environ, identity):
477+ """
478+ Return the DN based on the environment and the identity.
479+
480+ Searches the directory entry with naming attribute matching the
481+ C{login} item of the identity.
482+
483+ If the C{login} item of the identity is C{rms}, the naming attribute is
484+ C{uid} and the base DN is C{dc=gnu,dc=org}, we'll ask the server
485+ to search for C{uid = rms} beneath the search base, hopefully
486+ finding C{uid=rms,ou=developers,dc=gnu,dc=org}.
487+
488+ @param environ: The WSGI environment.
489+ @param identity: The identity dictionary.
490+ @return: The Distinguished Name (DN)
491+ @rtype: C{unicode}
492+ @raise ValueError: If the C{login} key is not in the I{identity} dict.
493+
494+ """
495+
496+ if self.bind_dn:
497+ try:
498+ self.ldap_connection.bind_s(self.bind_dn, self.bind_password)
499+ except ldap.LDAPError:
500+ raise ValueError("Couldn't bind with supplied credentials")
501+ try:
502+ login_name = identity['login'].replace('*',r'\*')
503+ srch = self.search_pattern % login_name
504+ dn_list = self.ldap_connection.search_s(
505+ self.base_dn,
506+ self.search_scope,
507+ srch,
508+ )
509+
510+ if len(dn_list) == 1:
511+ return dn_list[0][0]
512+ elif len(dn_list) > 1:
513+ raise ValueError('Too many entries found for %s' % srch)
514+ else:
515+ raise ValueError('No entry found for %s' %srch)
516+ except (KeyError, TypeError, ldap.LDAPError):
517+ raise # ValueError
518
519
520 #{ Metadata providers
521@@ -121,9 +331,12 @@
522 """Loads LDAP attributes of the authenticated user."""
523
524 implements(IMetadataProvider)
525+
526+ dnrx = re.compile('<dn:(?P<b64dn>[A-Za-z0-9+/]+=*)>')
527
528 def __init__(self, ldap_connection, attributes=None,
529- filterstr='(objectClass=*)'):
530+ filterstr='(objectClass=*)', start_tls = '',
531+ bind_dn = '', bind_pass =''):
532 """
533 Fetch LDAP attributes of the authenticated user.
534
535@@ -137,9 +350,28 @@
536 <http://www.faqs.org/rfcs/rfc4515.html>}; the results won't be
537 filtered unless you define this.
538 @type filterstr: C{str}
539+ @param start_tls: Should we negotiate a TLS upgrade on the connection with
540+ the directory server?
541+ @type start_tls: C{str}
542+ @param bind_dn: Operate as the bind_dn directory entry
543+ @type bind_dn: C{str}
544+ @param bind_pass: The password for bind_dn directory entry
545+ @type bind_pass: C{str}
546 @raise ValueError: If L{make_ldap_connection} could not create a
547 connection from C{ldap_connection}, or if C{attributes} is not an
548 iterable.
549+
550+ The following parameters are inherited from
551+ L{LDAPBaseAuthenticatorPlugin.__init__}
552+ @param base_dn: The base for the I{Distinguished Name}. Something like
553+ C{ou=employees,dc=example,dc=org}, to which will be prepended the
554+ user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
555+ @param returned_id: Should we return full Directory Names or just the
556+ naming attribute value on successful authentication?
557+ @param start_tls: Should we negotiate a TLS upgrade on the connection with
558+ the directory server?
559+ @param bind_dn: Operate as the bind_dn directory entry
560+ @param bind_pass: The password for bind_dn directory entry
561
562 """
563 if hasattr(attributes, 'split'):
564@@ -150,13 +382,21 @@
565 elif attributes is not None:
566 raise ValueError('The needed LDAP attributes are not valid')
567 self.ldap_connection = make_ldap_connection(ldap_connection)
568+ if start_tls:
569+ try:
570+ self.ldap_connection.start_tls_s()
571+ except:
572+ raise ValueError('Cannot upgrade the connection')
573+
574+ self.bind_dn = bind_dn
575+ self.bind_pass = bind_pass
576 self.attributes = attributes
577 self.filterstr = filterstr
578
579 # IMetadataProvider
580 def add_metadata(self, environ, identity):
581 """
582- Add metadata about the authenticated user to the auth.
583+ Add metadata about the authenticated user to the identity.
584
585 It modifies the C{identity} dictionary to add the metadata.
586
587@@ -165,17 +405,29 @@
588
589 """
590 # Search arguments:
591+ dnmatch = self.dnrx.match(identity.get('userdata',''))
592+ if dnmatch:
593+ dn = b64decode(dnmatch.group('b64dn'))
594+ else:
595+ dn = identity.get('repoze.who.userid')
596 args = (
597- identity.get('repoze.who.userid'),
598+ dn,
599 ldap.SCOPE_BASE,
600 self.filterstr,
601 self.attributes
602 )
603+ if self.bind_dn:
604+ try:
605+ self.ldap_connection.bind_s(self.bind_dn, self.bind_pass)
606+ except ldap.LDAPError:
607+ raise ValueError("Couldn't bind with supplied credentials")
608 try:
609 attributes = self.ldap_connection.search_s(*args)
610- identity.update(attributes)
611 except ldap.LDAPError, msg:
612 environ['repoze.who.logger'].warn('Cannot add metadata: %s' % msg)
613+ raise Exception(identity)
614+ else:
615+ identity.update(attributes[0][1])
616
617
618 #{ Utilities
619
620=== modified file 'repoze/who/plugins/ldap/tests.py'
621--- repoze/who/plugins/ldap/tests.py 2010-05-19 22:31:15 +0000
622+++ repoze/who/plugins/ldap/tests.py 2010-05-31 22:51:23 +0000
623@@ -1,50 +1,53 @@
624 # -*- coding: utf-8 -*-
625 #
626 # repoze.who.plugins.ldap, LDAP authentication for WSGI applications.
627-# Copyright (C) 2008-2010 by Gustavo Narea <http://gustavonarea.net/>
628+# Copyright (C) 2010 by Gustavo Narea <http://gustavonarea.net/> and
629+# Lorenzo M. Catucci <http://www.uniroma2.it/>.
630+# Copyright (C) 2008 by Gustavo Narea <http://gustavonarea.net/>.s
631 #
632 # This file is part of repoze.who.plugins.ldap
633 # <http://code.gustavonarea.net/repoze.who.plugins.ldap/>
634 #
635-# repoze.who.plugins.ldap is freedomware: you can redistribute it and/or modify
636-# it under the terms of the GNU General Public License as published by the
637-# Free Software Foundation, either version 3 of the License, or any later
638-# version.
639-#
640-# repoze.who.plugins.ldap is distributed in the hope that it will be useful,
641-# but WITHOUT ANY WARRANTY; without even the implied warranty of
642-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
643-# Public License for more details.
644-#
645-# You should have received a copy of the GNU General Public License along with
646-# repoze.who.plugins.ldap. If not, see <http://www.gnu.org/licenses/>.
647-
648+# This software is subject to the provisions of the BSD-like license at
649+# http://www.repoze.org/LICENSE.txt. A copy of the license should accompany
650+# this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL
651+# EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO,
652+# THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
653+# FITNESS FOR A PARTICULAR PURPOSE.
654 """Test suite for repoze.who.plugins.ldap"""
655
656 import unittest
657
658 from dataflake.ldapconnection.tests import fakeldap
659-from ldap import modlist
660+import ldap
661+from ldap import modlist, dn
662 from ldap.ldapobject import SimpleLDAPObject
663 from zope.interface.verify import verifyClass
664 from repoze.who.interfaces import IAuthenticator, IMetadataProvider
665
666 from repoze.who.plugins.ldap import LDAPAuthenticatorPlugin, \
667- LDAPAttributesPlugin
668+ LDAPAttributesPlugin, \
669+ LDAPSearchAuthenticatorPlugin
670 from repoze.who.plugins.ldap.plugins import make_ldap_connection
671
672+from base64 import b64encode
673+
674
675 class Base(unittest.TestCase):
676 """Base test case for the plugins"""
677
678 def setUp(self):
679 # Connecting to a fake server with a fake account:
680- conn = fakeldap.initialize('ldap://example.org')
681+ conn = fakeldap.FakeLDAPConnection()
682 conn.simple_bind_s('Manager', 'some password')
683+ # We must explicitly create the base_dn DIT components
684+ fakeldap.addTreeItems(base_dn)
685 # Adding a fake user, which is used in the tests
686 person_attr = {'cn': [fakeuser['cn']],
687 'uid': fakeuser['uid'],
688- 'userPassword': [fakeuser['hashedPassword']]}
689+ 'userPassword': [fakeuser['hashedPassword']],
690+ 'telephone': [fakeuser['telephone']],
691+ 'mail':[fakeuser['mail']]}
692 conn.add_s(fakeuser['dn'], modlist.addModlist(person_attr))
693 self.connection = conn
694 # Creating a fake environment:
695@@ -75,13 +78,14 @@
696 def test_without_connection(self):
697 self.assertRaises(ValueError, LDAPAuthenticatorPlugin, None,
698 'dc=example,dc=org')
699+
700 def test_without_base_dn(self):
701- conn = fakeldap.initialize('ldap://example.org')
702+ conn = fakeldap.FakeLDAPConnection()
703 self.assertRaises(TypeError, LDAPAuthenticatorPlugin, conn)
704 self.assertRaises(ValueError, LDAPAuthenticatorPlugin, conn, None)
705
706 def test_with_connection(self):
707- conn = fakeldap.initialize('ldap://example.org')
708+ conn = fakeldap.FakeLDAPConnection()
709 LDAPAuthenticatorPlugin(conn, 'dc=example,dc=org')
710
711 def test_connection_is_url(self):
712@@ -104,40 +108,167 @@
713 self.assertEqual(result, None)
714
715 def test_authenticate_incomplete_credentials(self):
716- auth1 = {'login': fakeuser['uid']}
717- auth2 = {'password': fakeuser['password']}
718- result1 = self.plugin.authenticate(self.env, auth1)
719- result2 = self.plugin.authenticate(self.env, auth2)
720+ identity1 = {'login': fakeuser['uid']}
721+ identity2 = {'password': fakeuser['password']}
722+ result1 = self.plugin.authenticate(self.env, identity1)
723+ result2 = self.plugin.authenticate(self.env, identity2)
724 self.assertEqual(result1, None)
725 self.assertEqual(result2, None)
726
727 def test_authenticate_noresults(self):
728- auth = {'login': 'i_dont_exist',
729+ identity = {'login': 'i_dont_exist',
730 'password': 'super secure password'}
731- result = self.plugin.authenticate(self.env, auth)
732+ result = self.plugin.authenticate(self.env, identity)
733 self.assertEqual(result, None)
734
735 def test_authenticate_comparefail(self):
736- auth = {'login': fakeuser['uid'],
737+ identity = {'login': fakeuser['uid'],
738 'password': 'wrong password'}
739- result = self.plugin.authenticate(self.env, auth)
740+ result = self.plugin.authenticate(self.env, identity)
741 self.assertEqual(result, None)
742
743 def test_authenticate_comparesuccess(self):
744- auth = {'login': fakeuser['uid'],
745+ identity = {'login': fakeuser['uid'],
746 'password': fakeuser['password']}
747- result = self.plugin.authenticate(self.env, auth)
748+ result = self.plugin.authenticate(self.env, identity)
749 self.assertEqual(result, fakeuser['dn'])
750
751 def test_custom_authenticator(self):
752 """L{LDAPAuthenticatorPlugin._get_dn} should be overriden with no
753 problems"""
754 plugin = CustomLDAPAuthenticatorPlugin(self.connection, base_dn)
755- auth = {'login': fakeuser['uid'],
756+ identity = {'login': fakeuser['uid'],
757 'password': fakeuser['password']}
758- result = plugin.authenticate(self.env, auth)
759- expected = 'uid=%s,ou=admins,%s' % (fakeuser['uid'], base_dn)
760+ result = plugin.authenticate(self.env, identity)
761+ expected = 'uid=%s,%s' % (fakeuser['uid'], base_dn)
762 self.assertEqual(result, expected)
763+ self.assertTrue(plugin.called)
764+
765+class TestLDAPSearchAuthenticatorPluginNaming(Base):
766+ """Tests for the L{LDAPSearchAuthenticatorPlugin} IAuthenticator plugin"""
767+
768+ def setUp(self):
769+ super(TestLDAPSearchAuthenticatorPluginNaming, self).setUp()
770+ # Loading the plugin:
771+ self.plugin = LDAPSearchAuthenticatorPlugin(
772+ self.connection,
773+ base_dn,
774+ naming_attribute='telephone',
775+ )
776+
777+ def test_authenticate_noresults(self):
778+ identity = {'login': 'i_dont_exist',
779+ 'password': 'super secure password'}
780+ result = self.plugin.authenticate(self.env, identity)
781+ self.assertEqual(result, None)
782+
783+ def test_authenticate_comparefail(self):
784+ identity = {'login': fakeuser['telephone'],
785+ 'password': 'wrong password'}
786+ result = self.plugin.authenticate(self.env, identity)
787+ self.assertEqual(result, None)
788+
789+ def test_authenticate_comparesuccess(self):
790+ identity = {'login': fakeuser['telephone'],
791+ 'password': fakeuser['password']}
792+ result = self.plugin.authenticate(self.env, identity)
793+ self.assertEqual(result, fakeuser['dn'])
794+
795+class TestLDAPAuthenticatorReturnLogin(Base):
796+ """
797+ Tests the L{LDAPAuthenticatorPlugin} IAuthenticator plugin returning
798+ login.
799+
800+ """
801+
802+ def setUp(self):
803+ super(TestLDAPAuthenticatorReturnLogin, self).setUp()
804+ # Loading the plugin:
805+ self.plugin = LDAPAuthenticatorPlugin(
806+ self.connection,
807+ base_dn,
808+ returned_id='login',
809+ )
810+
811+ def test_authenticate_noresults(self):
812+ identity = {'login': 'i_dont_exist',
813+ 'password': 'super secure password'}
814+ result = self.plugin.authenticate(self.env, identity)
815+ self.assertEqual(result, None)
816+
817+ def test_authenticate_comparefail(self):
818+ identity = {'login': fakeuser['uid'],
819+ 'password': 'wrong password'}
820+ result = self.plugin.authenticate(self.env, identity)
821+ self.assertEqual(result, None)
822+
823+ def test_authenticate_comparesuccess(self):
824+ identity = {'login': fakeuser['uid'],
825+ 'password': fakeuser['password']}
826+ result = self.plugin.authenticate(self.env, identity)
827+ self.assertEqual(result, fakeuser['uid'])
828+
829+ def test_authenticate_dn_in_userdata(self):
830+ identity = {'login': fakeuser['uid'],
831+ 'password': fakeuser['password']}
832+ expected_dn = '<dn:%s>' % b64encode(fakeuser['dn'])
833+ result = self.plugin.authenticate(self.env, identity)
834+ self.assertEqual(identity['userdata'], expected_dn)
835+
836+
837+class TestLDAPSearchAuthenticatorReturnLogin(Base):
838+ """
839+ Tests the L{LDAPSearchAuthenticatorPlugin} IAuthenticator plugin returning
840+ login.
841+
842+ """
843+
844+ def setUp(self):
845+ super(TestLDAPSearchAuthenticatorReturnLogin, self).setUp()
846+ # Loading the plugin:
847+ self.plugin = LDAPSearchAuthenticatorPlugin(
848+ self.connection,
849+ base_dn,
850+ returned_id='login',
851+ )
852+
853+ def test_authenticate_noresults(self):
854+ identity = {'login': 'i_dont_exist',
855+ 'password': 'super secure password'}
856+ result = self.plugin.authenticate(self.env, identity)
857+ self.assertEqual(result, None)
858+
859+ def test_authenticate_comparefail(self):
860+ identity = {'login': fakeuser['uid'],
861+ 'password': 'wrong password'}
862+ result = self.plugin.authenticate(self.env, identity)
863+ self.assertEqual(result, None)
864+
865+ def test_authenticate_comparesuccess(self):
866+ identity = {'login': fakeuser['uid'],
867+ 'password': fakeuser['password']}
868+ result = self.plugin.authenticate(self.env, identity)
869+ self.assertEqual(result, fakeuser['uid'])
870+
871+ def test_authenticate_dn_in_userdata(self):
872+ identity = {'login': fakeuser['uid'],
873+ 'password': fakeuser['password']}
874+ expected_dn = '<dn:%s>' % b64encode(fakeuser['dn'])
875+ result = self.plugin.authenticate(self.env, identity)
876+ self.assertEqual(identity['userdata'], expected_dn)
877+
878+
879+class TestLDAPAuthenticatorPluginStartTls(Base):
880+ """Tests for the L{LDAPAuthenticatorPlugin} IAuthenticator plugin"""
881+
882+ def setUp(self):
883+ super(TestLDAPAuthenticatorPluginStartTls, self).setUp()
884+ # Loading the plugin:
885+ self.plugin = LDAPAuthenticatorPlugin(self.connection, base_dn,
886+ start_tls=True)
887+
888+ def test_implements(self):
889+ verifyClass(IAuthenticator, LDAPAuthenticatorPlugin, tentative=True)
890
891
892 class TestMakeLDAPAttributesPlugin(unittest.TestCase):
893@@ -192,25 +323,26 @@
894 def test_add_metadata(self):
895 plugin = LDAPAttributesPlugin(self.connection)
896 environ = {}
897- auth = {'repoze.who.userid': fakeuser['dn']}
898- expected_auth = {
899+ identity = {'repoze.who.userid': fakeuser['dn']}
900+ expected_identity = {
901 'repoze.who.userid': fakeuser['dn'],
902 'cn': [fakeuser['cn']],
903 'userPassword': [fakeuser['hashedPassword']],
904- 'uid': fakeuser['uid']
905+ 'uid': fakeuser['uid'],
906+ 'telephone': [ fakeuser['telephone']],
907+ 'mail': [fakeuser['mail']]
908 }
909- plugin.add_metadata(environ, auth)
910- self.assertEqual(auth, expected_auth)
911+ plugin.add_metadata(environ, identity)
912+ self.assertEqual(identity, expected_identity)
913
914
915 # Test cases for plugin utilities
916
917-
918 class TestLDAPConnectionFactory(unittest.TestCase):
919 """Tests for L{make_ldap_connection}"""
920
921 def test_connection_is_object(self):
922- conn = fakeldap.initialize('ldap://example.org')
923+ conn = fakeldap.FakeLDAPConnection()
924 self.assertEqual(make_ldap_connection(conn), conn)
925
926 def test_connection_is_str(self):
927@@ -225,6 +357,74 @@
928 self.assertRaises(ValueError, make_ldap_connection, None)
929
930
931+# Test cases for the fakeldap connection itself
932+
933+class TestLDAPConnection(unittest.TestCase):
934+ """Connection use tests"""
935+
936+ def setUp(self):
937+ # Connecting to a fake server with a fake account:
938+ conn = fakeldap.FakeLDAPConnection()
939+ conn.simple_bind_s('Manager', 'some password')
940+ # We must explicitly create the base_dn DIT components
941+ fakeldap.addTreeItems(base_dn)
942+ # Adding a fake user, which is used in the tests
943+ self.person_attr = {'cn': [fakeuser['cn']],
944+ 'uid': fakeuser['uid'],
945+ 'userPassword': [fakeuser['hashedPassword']],
946+ 'telephone':[fakeuser['telephone']],
947+ 'objectClass': ['top']}
948+ conn.add_s(fakeuser['dn'], modlist.addModlist(self.person_attr))
949+ self.connection = conn
950+
951+ def tearDown(self):
952+ self.connection.delete_s(fakeuser['dn'])
953+
954+ def test_simple_search_result(self):
955+ rs = self.connection.search_s(
956+ base_dn,
957+ ldap.SCOPE_SUBTREE,
958+ '(uid=%s)' % fakeuser['uid'],
959+ )
960+ self.assertEqual(rs[0][0], fakeuser['dn'])
961+ self.assertEqual(rs[0][1], self.person_attr )
962+
963+ def unimplemented_test_and_search_result(self):
964+ rs = self.connection.search_s(
965+ base_dn,
966+ ldap.SCOPE_SUBTREE,
967+ '(&(objectclass=*)(uid=%s))' % fakeuser['uid'],
968+ )
969+ self.assertEqual(rs[0][0], fakeuser['dn'])
970+ self.assertEqual(rs[0][1], self.person_attr )
971+
972+ def unimplemented_test_bare_search_result(self):
973+ rs = self.connection.search_s(
974+ base_dn,
975+ ldap.SCOPE_SUBTREE,
976+ 'uid=%s' % fakeuser['uid'],
977+ )
978+ self.assertEqual(rs[0][0], fakeuser['dn'])
979+ self.assertEqual(rs[0][1], self.person_attr )
980+
981+ def error_test_email_address_search(self):
982+ rs = self.connection.search_s(
983+ base_dn,
984+ ldap.SCOPE_SUBTREE,
985+ '(mail=%s)' % fakeuser['mail'],
986+ )
987+ self.assertEqual(rs[0][0], fakeuser['dn'])
988+ self.assertEqual(rs[0][1], self.person_attr )
989+
990+ def error_test_plus_in_search(self):
991+ rs = self.connection.search_s(
992+ base_dn,
993+ ldap.SCOPE_SUBTREE,
994+ '(telephone=+%s)' % fakeuser['telephone'],
995+ )
996+ self.assertEqual(rs[0][0], fakeuser['dn'])
997+ self.assertEqual(rs[0][1], self.person_attr )
998+
999 #{ Fixtures
1000
1001 base_dn = 'ou=people,dc=example,dc=org'
1002@@ -234,18 +434,22 @@
1003 'uid': 'carla',
1004 'cn': 'Carla Paola',
1005 'mail': 'carla@example.org',
1006+ 'telephone': '39 123 456 789',
1007 'password': 'hello',
1008 'hashedPassword': '{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00='
1009 }
1010
1011+
1012 class CustomLDAPAuthenticatorPlugin(LDAPAuthenticatorPlugin):
1013 """Fake class to test that L{LDAPAuthenticatorPlugin._get_dn} can be
1014 overriden with no problems"""
1015- def _get_dn(self, environ, auth):
1016+
1017+ def _get_dn(self, environ, identity):
1018+ self.called = True
1019 try:
1020- return u'uid=%s,ou=admins,%s' % (auth['login'], self.base_dn)
1021+ return u'uid=%s,%s' % (identity['login'], self.base_dn)
1022 except (KeyError, TypeError):
1023- raise ValueError, ('Could not find the DN from the auth and '
1024+ raise ValueError, ('Could not find the DN from the identity and '
1025 'environment')
1026
1027
1028@@ -261,11 +465,19 @@
1029
1030 """
1031 suite = unittest.TestSuite()
1032+ suite.addTest(unittest.makeSuite(TestLDAPConnection, "test"))
1033 suite.addTest(unittest.makeSuite(TestMakeLDAPAuthenticatorPlugin, "test"))
1034 suite.addTest(unittest.makeSuite(TestLDAPAuthenticatorPlugin, "test"))
1035 suite.addTest(unittest.makeSuite(TestMakeLDAPAttributesPlugin, "test"))
1036 suite.addTest(unittest.makeSuite(TestLDAPAttributesPlugin, "test"))
1037 suite.addTest(unittest.makeSuite(TestLDAPConnectionFactory, "test"))
1038+ suite.addTest(unittest.makeSuite(TestLDAPSearchAuthenticatorPluginNaming,
1039+ "test"))
1040+ suite.addTest(unittest.makeSuite(TestLDAPAuthenticatorReturnLogin, "test"))
1041+ suite.addTest(unittest.makeSuite(TestLDAPSearchAuthenticatorReturnLogin,
1042+ "test"))
1043+ suite.addTest(unittest.makeSuite(TestLDAPAuthenticatorPluginStartTls,
1044+ "test"))
1045 return suite
1046
1047
1048
1049=== modified file 'setup.py'
1050--- setup.py 2010-05-19 22:31:15 +0000
1051+++ setup.py 2010-05-31 22:51:23 +0000
1052@@ -1,7 +1,9 @@
1053 # -*- coding: utf-8 -*-
1054 #
1055 # repoze.who.plugins.ldap, LDAP authentication for WSGI applications.
1056-# Copyright (C) 2008-2010 by Gustavo Narea <http://gustavonarea.net/>
1057+# Copyright (C) 2010 by Gustavo Narea <http://gustavonarea.net/> and
1058+# Lorenzo M. Catucci <http://www.uniroma2.it/>.
1059+# Copyright (C) 2008 by Gustavo Narea <http://gustavonarea.net/>.
1060 #
1061 # This file is part of repoze.who.plugins.ldap
1062 # <http://code.gustavonarea.net/repoze.who.plugins.ldap/>
1063@@ -46,7 +48,7 @@
1064 packages=find_packages(exclude=["*.tests", "demo", "demo.*"]),
1065 namespace_packages=['repoze', 'repoze.who', 'repoze.who.plugins'],
1066 zip_safe=False,
1067- tests_require = ['dataflake.ldapconnection==0.3'],
1068+ tests_require = ['dataflake.ldapconnection < 1.1dev'],
1069 install_requires=[
1070 'repoze.who >= 1.0.6, < 2.0dev',
1071 'python-ldap>=2.3.5',

Subscribers

People subscribed via source and target branches

to all changes: