15 # |
15 # |
16 # You should have received a copy of the GNU Lesser General Public License along |
16 # You should have received a copy of the GNU Lesser General Public License along |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """cubicweb ldap feed source""" |
18 """cubicweb ldap feed source""" |
19 |
19 |
20 from datetime import datetime |
|
21 |
|
22 import ldap3 |
20 import ldap3 |
23 |
21 |
24 from logilab.common.configuration import merge_options |
22 from logilab.common.configuration import merge_options |
25 |
23 |
26 from cubicweb import ValidationError, AuthenticationError, Binary |
24 from cubicweb import ValidationError, AuthenticationError, Binary |
28 from cubicweb.server.sources import datafeed |
26 from cubicweb.server.sources import datafeed |
29 |
27 |
30 from cubicweb import _ |
28 from cubicweb import _ |
31 |
29 |
32 # search scopes |
30 # search scopes |
33 LDAP_SCOPES = {'BASE': ldap3.SEARCH_SCOPE_BASE_OBJECT, |
31 LDAP_SCOPES = { |
34 'ONELEVEL': ldap3.SEARCH_SCOPE_SINGLE_LEVEL, |
32 'BASE': ldap3.BASE, |
35 'SUBTREE': ldap3.SEARCH_SCOPE_WHOLE_SUBTREE} |
33 'ONELEVEL': ldap3.LEVEL, |
36 |
34 'SUBTREE': ldap3.SUBTREE, |
|
35 } |
37 |
36 |
38 # map ldap protocol to their standard port |
37 # map ldap protocol to their standard port |
39 PROTO_PORT = {'ldap': 389, |
38 PROTO_PORT = {'ldap': 389, |
40 'ldaps': 636, |
39 'ldaps': 636, |
41 'ldapi': None, |
40 'ldapi': None, |
248 # no such user |
247 # no such user |
249 raise AuthenticationError() |
248 raise AuthenticationError() |
250 # check password by establishing a (unused) connection |
249 # check password by establishing a (unused) connection |
251 try: |
250 try: |
252 self._connect(user, password) |
251 self._connect(user, password) |
253 except ldap3.LDAPException as ex: |
252 except ldap3.core.exceptions.LDAPException as ex: |
254 # Something went wrong, most likely bad credentials |
253 # Something went wrong, most likely bad credentials |
255 self.info('while trying to authenticate %s: %s', user, ex) |
254 self.info('while trying to authenticate %s: %s', user, ex) |
256 raise AuthenticationError() |
255 raise AuthenticationError() |
257 except Exception: |
256 except Exception: |
258 self.error('while trying to authenticate %s', user, exc_info=True) |
257 self.error('while trying to authenticate %s', user, exc_info=True) |
264 raise AuthenticationError() |
263 raise AuthenticationError() |
265 return rset[0][0] |
264 return rset[0][0] |
266 |
265 |
267 def _connect(self, user=None, userpwd=None): |
266 def _connect(self, user=None, userpwd=None): |
268 protocol, host, port = self.connection_info() |
267 protocol, host, port = self.connection_info() |
|
268 kwargs = {} |
|
269 if user: |
|
270 kwargs['user'] = user['dn'] |
|
271 elif self.cnx_dn: |
|
272 kwargs['user'] = self.cnx_dn |
|
273 if self.cnx_pwd: |
|
274 kwargs['password'] = self.cnx_pwd |
269 self.info('connecting %s://%s:%s as %s', protocol, host, port, |
275 self.info('connecting %s://%s:%s as %s', protocol, host, port, |
270 user and user['dn'] or 'anonymous') |
276 kwargs.get('user', 'anonymous')) |
271 server = ldap3.Server(host, port=int(port)) |
277 server = ldap3.Server(host, port=int(port)) |
272 conn = ldap3.Connection( |
278 conn = ldap3.Connection( |
273 server, user=user and user['dn'], |
279 server, client_strategy=ldap3.RESTARTABLE, auto_referrals=False, |
274 client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE, |
280 raise_exceptions=True, |
275 auto_referrals=False) |
281 **kwargs) |
|
282 |
276 # Now bind with the credentials given. Let exceptions propagate out. |
283 # Now bind with the credentials given. Let exceptions propagate out. |
277 if user is None: |
284 if user is None: |
278 # XXX always use simple bind for data connection |
285 # anonymous bind |
279 if not self.cnx_dn: |
286 if not self.cnx_dn: |
280 conn.bind() |
287 if not conn.bind(): |
|
288 raise AuthenticationError(conn.result["message"]) |
281 else: |
289 else: |
282 self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd) |
290 self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd) |
283 else: |
291 else: |
284 # user specified, we want to check user/password, no need to return |
292 # user specified, we want to check user/password, no need to return |
285 # the connection which will be thrown out |
293 # the connection which will be thrown out |
286 if not self._authenticate(conn, user, userpwd): |
294 if not self._authenticate(conn, user, userpwd): |
287 raise AuthenticationError() |
295 raise AuthenticationError() |
288 return conn |
296 return conn |
289 |
297 |
290 def _auth_simple(self, conn, user, userpwd): |
298 def _auth_simple(self, conn, user, userpwd): |
291 conn.authentication = ldap3.AUTH_SIMPLE |
|
292 conn.user = user['dn'] |
299 conn.user = user['dn'] |
293 conn.password = userpwd |
300 conn.password = userpwd |
294 return conn.bind() |
301 return conn.bind() |
295 |
302 |
296 def _auth_digest_md5(self, conn, user, userpwd): |
303 def _auth_digest_md5(self, conn, user, userpwd): |
311 self.debug('ldap search %s %s %s %s %s', self.uri, base, scope, |
318 self.debug('ldap search %s %s %s %s %s', self.uri, base, scope, |
312 searchstr, list(attrs)) |
319 searchstr, list(attrs)) |
313 if self._conn is None: |
320 if self._conn is None: |
314 self._conn = self._connect() |
321 self._conn = self._connect() |
315 ldapcnx = self._conn |
322 ldapcnx = self._conn |
316 if not ldapcnx.search(base, searchstr, search_scope=scope, attributes=attrs): |
323 if not ldapcnx.search(base, searchstr, search_scope=scope, attributes=set(attrs) - {'dn'}): |
317 return [] |
324 return [] |
318 result = [] |
325 result = [] |
319 for rec in ldapcnx.response: |
326 for rec in ldapcnx.response: |
320 if rec['type'] != 'searchResEntry': |
327 if rec['type'] != 'searchResEntry': |
321 continue |
328 continue |
328 def _process_ldap_item(self, dn, iterator): |
335 def _process_ldap_item(self, dn, iterator): |
329 """Turn an ldap received item into a proper dict.""" |
336 """Turn an ldap received item into a proper dict.""" |
330 itemdict = {'dn': dn} |
337 itemdict = {'dn': dn} |
331 for key, value in iterator: |
338 for key, value in iterator: |
332 if self.user_attrs.get(key) == 'upassword': # XXx better password detection |
339 if self.user_attrs.get(key) == 'upassword': # XXx better password detection |
333 value = value[0].encode('utf-8') |
340 value = value[0] |
334 # we only support ldap_salted_sha1 for ldap sources, see: server/utils.py |
341 # we only support ldap_salted_sha1 for ldap sources, see: server/utils.py |
335 if not value.startswith(b'{SSHA}'): |
342 if not value.startswith(b'{SSHA}'): |
336 value = utils.crypt_password(value) |
343 value = utils.crypt_password(value) |
337 itemdict[key] = Binary(value) |
344 itemdict[key] = Binary(value) |
338 elif self.user_attrs.get(key) == 'modification_date': |
345 elif self.user_attrs.get(key) == 'modification_date': |
339 itemdict[key] = datetime.strptime(value[0], '%Y%m%d%H%M%SZ') |
346 itemdict[key] = value |
340 else: |
347 else: |
341 if len(value) == 1: |
348 if len(value) == 1: |
342 itemdict[key] = value = value[0] |
349 itemdict[key] = value = value[0] |
343 else: |
350 else: |
344 itemdict[key] = value |
351 itemdict[key] = value |