1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """LDAP plugins for repoze.who."""
18
19 __all__ = ['LDAPBaseAuthenticatorPlugin', 'LDAPAuthenticatorPlugin',
20 'LDAPSearchAuthenticatorPlugin', 'LDAPAttributesPlugin']
21
22 from zope.interface import implements
23 import ldap
24
25 from repoze.who.interfaces import IAuthenticator, IMetadataProvider
26
27 from base64 import b64encode, b64decode
28
29 import re
30
31
32
33
34
36
37 implements(IAuthenticator)
38
39 - def __init__(self, ldap_connection, base_dn, returned_id='dn',
40 start_tls=False, bind_dn='', bind_pass='', **kwargs):
41 """Create an LDAP authentication plugin.
42
43 By passing an existing LDAPObject, you're free to use the LDAP
44 authentication method you want, the way you want.
45
46 This is an *abstract* class, which means it's useless in itself. You
47 can only use subclasses of this class that implement the L{_get_dn}
48 method (e.g., the built-in authenticators).
49
50 This plugin is compatible with any identifier plugin that defines the
51 C{login} and C{password} items in the I{identity} dictionary.
52
53 @param ldap_connection: An initialized LDAP connection.
54 @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject}
55
56 @param base_dn: The base for the I{Distinguished Name}. Something like
57 C{ou=employees,dc=example,dc=org}, to which will be prepended the
58 user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
59 @type base_dn: C{unicode}
60 @param returned_id: Should we return the full DN or just the
61 bare naming identifier value on successful authentication?
62 @type returned_id: C{str}, 'dn' or 'login'
63 @attention: While the DN is always unique, if you configure the
64 authenticator plugin to return the bare naming attribute,
65 you have to ensure its uniqueness in the DIT.
66 @param start_tls: Should we negotiate a TLS upgrade on the connection with
67 the directory server?
68 @type start_tls: C{bool}
69 @param bind_dn: Operate as the bind_dn directory entry
70 @type bind_dn: C{str}
71 @param bind_pass: The password for bind_dn directory entry
72 @type bind_pass: C{str}
73 @raise ValueError: If at least one of the parameters is not defined.
74
75 """
76 if base_dn is None:
77 raise ValueError('A base Distinguished Name must be specified')
78 self.ldap_connection = make_ldap_connection(ldap_connection)
79
80 if start_tls:
81 try:
82 self.ldap_connection.start_tls_s()
83 except:
84 raise ValueError('Cannot upgrade the connection')
85
86 self.bind_dn = bind_dn
87 self.bind_pass = bind_pass
88
89 self.base_dn = base_dn
90
91 if returned_id.lower() == 'dn':
92 self.ret_style = 'd'
93 elif returned_id.lower() == 'login':
94 self.ret_style = 'l'
95 else:
96 raise ValueError("The return style should be 'dn' or 'login'")
97
98 - def _get_dn(self, environ, identity):
99 """
100 Return the user DN based on the environment and the identity.
101
102 Must be implemented in a subclass
103
104 @param environ: The WSGI environment.
105 @param identity: The identity dictionary.
106 @return: The Distinguished Name (DN)
107 @rtype: C{unicode}
108 @raise ValueError: If the C{login} key is not in the I{identity} dict.
109
110 """
111 raise ValueError('Unimplemented')
112
113
114
116 """Return the naming identifier of the user to be authenticated.
117
118 @return: The naming identifier, if the credentials were valid.
119 @rtype: C{unicode} or C{None}
120
121 """
122
123 try:
124 dn = self._get_dn(environ, identity)
125 password = identity['password']
126 except (KeyError, TypeError, ValueError):
127 return None
128
129 if not hasattr(self.ldap_connection, 'simple_bind_s'):
130 environ['repoze.who.logger'].warn('Cannot bind with the provided '
131 'LDAP connection object')
132 return None
133
134 try:
135 self.ldap_connection.simple_bind_s(dn, password)
136 userdata = identity.get('userdata', '')
137
138 if self.ret_style == 'd':
139 return dn
140 else:
141 identity['userdata'] = userdata + '<dn:%s>' % b64encode(dn)
142 return identity['login']
143 except ldap.LDAPError:
144 return None
145
147 return '<%s %s>' % (self.__class__.__name__, id(self))
148
149
151
152 - def __init__(self, ldap_connection, base_dn, naming_attribute='uid',
153 **kwargs):
154 """Create an LDAP authentication plugin using pattern-determined DNs
155
156 By passing an existing LDAPObject, you're free to use the LDAP
157 authentication method you want, the way you want.
158
159 This plugin is compatible with any identifier plugin that defines the
160 C{login} and C{password} items in the I{identity} dictionary.
161
162 @param ldap_connection: An initialized LDAP connection.
163 @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject}
164 @param base_dn: The base for the I{Distinguished Name}. Something like
165 C{ou=employees,dc=example,dc=org}, to which will be prepended the
166 user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
167 @type base_dn: C{unicode}
168 @param naming_attribute: The naming attribute for directory entries,
169 C{uid} by default.
170 @type naming_attribute: C{unicode}
171
172 @raise ValueError: If at least one of the parameters is not defined.
173
174 The following parameters are inherited from
175 L{LDAPBaseAuthenticatorPlugin.__init__}
176 @param base_dn: The base for the I{Distinguished Name}. Something like
177 C{ou=employees,dc=example,dc=org}, to which will be prepended the
178 user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
179 @param returned_id: Should we return full Directory Names or just the
180 bare naming identifier on successful authentication?
181 @param start_tls: Should we negotiate a TLS upgrade on the connection with
182 the directory server?
183 @param bind_dn: Operate as the bind_dn directory entry
184 @param bind_pass: The password for bind_dn directory entry
185
186
187 """
188 LDAPBaseAuthenticatorPlugin.__init__(self, ldap_connection, base_dn,
189 **kwargs)
190 self.naming_pattern = u'%s=%%s,%%s' % naming_attribute
191
192 - def _get_dn(self, environ, identity):
193 """
194 Return the user naming identifier based on the environment and the
195 identity.
196
197 If the C{login} item of the identity is C{rms} and the base DN is
198 C{ou=developers,dc=gnu,dc=org}, the resulting DN will be:
199 C{uid=rms,ou=developers,dc=gnu,dc=org}
200
201 @param environ: The WSGI environment.
202 @param identity: The identity dictionary.
203 @return: The Distinguished Name (DN)
204 @rtype: C{unicode}
205 @raise ValueError: If the C{login} key is not in the I{identity} dict.
206
207 """
208
209 if self.bind_dn:
210 try:
211 self.ldap_connection.bind_s(self.bind_dn, self.bind_password)
212 except ldap.LDAPError:
213 raise ValueError("Couldn't bind with supplied credentials")
214 try:
215 return self.naming_pattern % ( identity['login'], self.base_dn)
216 except (KeyError, TypeError):
217 raise ValueError
218
219
221
222 - def __init__(self, ldap_connection, base_dn, naming_attribute='uid',
223 search_scope='subtree', restrict='', **kwargs):
224 """Create an LDAP authentication plugin determining the DN via LDAP searches.
225
226 By passing an existing LDAPObject, you're free to use the LDAP
227 authentication method you want, the way you want.
228
229 This plugin is compatible with any identifier plugin that defines the
230 C{login} and C{password} items in the I{identity} dictionary.
231
232 @param ldap_connection: An initialized LDAP connection.
233 @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject}
234 @param base_dn: The base for the I{Distinguished Name}. Something like
235 C{ou=employees,dc=example,dc=org}, to which will be prepended the
236 user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
237 @type base_dn: C{unicode}
238 @param naming_attribute: The naming attribute for directory entries,
239 C{uid} by default.
240 @type naming_attribute: C{unicode}
241 @param search_scope: Scope for ldap searches
242 @type search_scope: C{str}, 'subtree' or 'onelevel', possibly
243 abbreviated to at least the first three characters
244 @param restrict: An ldap filter which will be ANDed to the search filter
245 while searching for entries matching the naming attribute
246 @type restrict: C{unicode}
247 @attention: restrict will be interpolated into the search string as a
248 bare string like in "(&%s(identifier=login))". It must be correctly
249 parenthesised for such usage as in restrict = "(objectClass=*)".
250
251 @raise ValueError: If at least one of the parameters is not defined.
252
253 The following parameters are inherited from
254 L{LDAPBaseAuthenticatorPlugin.__init__}
255 @param base_dn: The base for the I{Distinguished Name}. Something like
256 C{ou=employees,dc=example,dc=org}, to which will be prepended the
257 user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
258 @param returned_id: Should we return full Directory Names or just the
259 bare naming identifier on successful authentication?
260 @param start_tls: Should we negotiate a TLS upgrade on the connection
261 with the directory server?
262 @param bind_dn: Operate as the bind_dn directory entry
263 @param bind_pass: The password for bind_dn directory entry
264
265 """
266 LDAPBaseAuthenticatorPlugin.__init__(self, ldap_connection, base_dn,
267 **kwargs)
268
269 if search_scope[:3].lower() == 'sub':
270 self.search_scope = ldap.SCOPE_SUBTREE
271 elif search_scope[:3].lower() == 'one':
272 self.search_scope = ldap.SCOPE_ONELEVEL
273 else:
274 raise ValueError("The search scope should be 'one[level]' or 'sub[tree]'")
275
276 if restrict:
277 self.search_pattern = u'(&%s(%s=%%s))' % (restrict,naming_attribute)
278 else:
279 self.search_pattern = u'(%s=%%s)' % naming_attribute
280
281 - def _get_dn(self, environ, identity):
282 """
283 Return the DN based on the environment and the identity.
284
285 Searches the directory entry with naming attribute matching the
286 C{login} item of the identity.
287
288 If the C{login} item of the identity is C{rms}, the naming attribute is
289 C{uid} and the base DN is C{dc=gnu,dc=org}, we'll ask the server
290 to search for C{uid = rms} beneath the search base, hopefully
291 finding C{uid=rms,ou=developers,dc=gnu,dc=org}.
292
293 @param environ: The WSGI environment.
294 @param identity: The identity dictionary.
295 @return: The Distinguished Name (DN)
296 @rtype: C{unicode}
297 @raise ValueError: If the C{login} key is not in the I{identity} dict.
298
299 """
300
301 if self.bind_dn:
302 try:
303 self.ldap_connection.bind_s(self.bind_dn, self.bind_password)
304 except ldap.LDAPError:
305 raise ValueError("Couldn't bind with supplied credentials")
306 try:
307 login_name = identity['login'].replace('*',r'\*')
308 srch = self.search_pattern % login_name
309 dn_list = self.ldap_connection.search_s(
310 self.base_dn,
311 self.search_scope,
312 srch,
313 )
314
315 if len(dn_list) == 1:
316 return dn_list[0][0]
317 elif len(dn_list) > 1:
318 raise ValueError('Too many entries found for %s' % srch)
319 else:
320 raise ValueError('No entry found for %s' %srch)
321 except (KeyError, TypeError, ldap.LDAPError):
322 raise
323
324
325
326
327
329 """Loads LDAP attributes of the authenticated user."""
330
331 implements(IMetadataProvider)
332
333 dnrx = re.compile('<dn:(?P<b64dn>[A-Za-z0-9+/]+=*)>')
334
335 - def __init__(self, ldap_connection, attributes=None,
336 filterstr='(objectClass=*)', start_tls='',
337 bind_dn='', bind_pass=''):
338 """
339 Fetch LDAP attributes of the authenticated user.
340
341 @param ldap_connection: The LDAP connection to use to fetch this data.
342 @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject} or C{str}
343 @param attributes: The authenticated user's LDAP attributes you want to
344 use in your application; an interable or a comma-separate list of
345 attributes in a string, or C{None} to fetch them all.
346 @type attributes: C{iterable} or C{str}
347 @param filterstr: A filter for the search, as documented in U{RFC4515
348 <http://www.faqs.org/rfcs/rfc4515.html>}; the results won't be
349 filtered unless you define this.
350 @type filterstr: C{str}
351 @param start_tls: Should we negotiate a TLS upgrade on the connection with
352 the directory server?
353 @type start_tls: C{str}
354 @param bind_dn: Operate as the bind_dn directory entry
355 @type bind_dn: C{str}
356 @param bind_pass: The password for bind_dn directory entry
357 @type bind_pass: C{str}
358 @raise ValueError: If L{make_ldap_connection} could not create a
359 connection from C{ldap_connection}, or if C{attributes} is not an
360 iterable.
361
362 The following parameters are inherited from
363 L{LDAPBaseAuthenticatorPlugin.__init__}
364 @param base_dn: The base for the I{Distinguished Name}. Something like
365 C{ou=employees,dc=example,dc=org}, to which will be prepended the
366 user id: C{uid=jsmith,ou=employees,dc=example,dc=org}.
367 @param returned_id: Should we return full Directory Names or just the
368 naming attribute value on successful authentication?
369 @param start_tls: Should we negotiate a TLS upgrade on the connection with
370 the directory server?
371 @param bind_dn: Operate as the bind_dn directory entry
372 @param bind_pass: The password for bind_dn directory entry
373
374 """
375 if hasattr(attributes, 'split'):
376 attributes = attributes.split(',')
377 elif hasattr(attributes, '__iter__'):
378
379 attributes = list(attributes)
380 elif attributes is not None:
381 raise ValueError('The needed LDAP attributes are not valid')
382 self.ldap_connection = make_ldap_connection(ldap_connection)
383 if start_tls:
384 try:
385 self.ldap_connection.start_tls_s()
386 except:
387 raise ValueError('Cannot upgrade the connection')
388
389 self.bind_dn = bind_dn
390 self.bind_pass = bind_pass
391 self.attributes = attributes
392 self.filterstr = filterstr
393
394
429
430
431
432
433
435 """Return an LDAP connection object to the specified server.
436
437 If the C{ldap_connection} is already an LDAP connection object, it will
438 be returned as is. If it's an LDAP URL, it will return an LDAP connection
439 to the LDAP server specified in the URL.
440
441 @param ldap_connection: The LDAP connection object or the LDAP URL of the
442 server to be connected to.
443 @type ldap_connection: C{ldap.ldapobject.SimpleLDAPObject}, C{str} or
444 C{unicode}
445 @return: The LDAP connection object.
446 @rtype: C{ldap.ldapobject.SimpleLDAPObject}
447 @raise ValueError: If C{ldap_connection} is C{None}.
448
449 """
450 if isinstance(ldap_connection, str) or isinstance(ldap_connection, unicode):
451 return ldap.initialize(ldap_connection)
452 elif ldap_connection is None:
453 raise ValueError('An LDAP connection must be specified')
454 return ldap_connection
455
456
457
458