Package repoze :: Package who :: Package plugins :: Package ldap :: Module plugins
[hide private]

Source Code for Module repoze.who.plugins.ldap.plugins

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # repoze.who.plugins.ldap, LDAP authentication for WSGI applications. 
  4  # Copyright (C) 2010 by Gustavo Narea  <http://gustavonarea.net/> and 
  5  #                       Lorenzo M. Catucci <http://www.uniroma2.it/>. 
  6  # Copyright (C) 2008 by Gustavo Narea <http://gustavonarea.net/>. 
  7  # 
  8  # This file is part of repoze.who.plugins.ldap 
  9  # <http://code.gustavonarea.net/repoze.who.plugins.ldap/> 
 10  # 
 11  # This software is subject to the provisions of the BSD-like license at 
 12  # http://www.repoze.org/LICENSE.txt.  A copy of the license should accompany 
 13  # this distribution.  THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL 
 14  # EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, 
 15  # THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND 
 16  # FITNESS FOR A PARTICULAR PURPOSE. 
 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  #{ Authenticators 
 33   
 34   
35 -class LDAPBaseAuthenticatorPlugin(object):
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 # IAuthenticatorPlugin
115 - def authenticate(self, environ, identity):
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 # The credentials are valid! 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
146 - def __repr__(self):
147 return '<%s %s>' % (self.__class__.__name__, id(self))
148 149
150 -class LDAPAuthenticatorPlugin(LDAPBaseAuthenticatorPlugin):
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
220 -class LDAPSearchAuthenticatorPlugin(LDAPBaseAuthenticatorPlugin):
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 # ValueError
323 324 325 #{ Metadata providers 326 327
328 -class LDAPAttributesPlugin(object):
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 # Converted to list, just in case... 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 # IMetadataProvider
395 - def add_metadata(self, environ, identity):
396 """ 397 Add metadata about the authenticated user to the identity. 398 399 It modifies the C{identity} dictionary to add the metadata. 400 401 @param environ: The WSGI environment. 402 @param identity: The repoze.who's identity dictionary. 403 404 """ 405 # Search arguments: 406 dnmatch = self.dnrx.match(identity.get('userdata','')) 407 if dnmatch: 408 dn = b64decode(dnmatch.group('b64dn')) 409 else: 410 dn = identity.get('repoze.who.userid') 411 args = ( 412 dn, 413 ldap.SCOPE_BASE, 414 self.filterstr, 415 self.attributes 416 ) 417 if self.bind_dn: 418 try: 419 self.ldap_connection.bind_s(self.bind_dn, self.bind_pass) 420 except ldap.LDAPError: 421 raise ValueError("Couldn't bind with supplied credentials") 422 try: 423 attributes = self.ldap_connection.search_s(*args) 424 except ldap.LDAPError, msg: 425 environ['repoze.who.logger'].warn('Cannot add metadata: %s' % msg) 426 raise Exception(identity) 427 else: 428 identity.update(attributes[0][1])
429 430 431 #{ Utilities 432 433
434 -def make_ldap_connection(ldap_connection):
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