cubicweb/server/sources/ldapfeed.py
branch3.27
changeset 12895 5a9d1e64f505
parent 12894 ba528f08ddfa
child 12897 d0ade9350d0e
equal deleted inserted replaced
12894:ba528f08ddfa 12895:5a9d1e64f505
    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