Merge lp:~gnarea/repoze.who.plugins.ldap/1.1proposal into lp:repoze.who.plugins.ldap
- 1.1proposal
- Merge into trunk
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 |
Related bugs: | |
Related blueprints: |
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', |