Merge 3.27
authorPhilippe Pepiot <philippe.pepiot@logilab.fr>
Thu, 05 Mar 2020 10:15:38 +0100
changeset 12900 2cc3f481ecd0
parent 12892 0df0db725f07 (current diff)
parent 12899 af59ca8b23d0 (diff)
child 12913 ebf4806e4ab7
Merge 3.27
cubicweb/__pkginfo__.py
cubicweb/server/hook.py
cubicweb/web/action.py
doc/tutorials/advanced/part02_security.rst
--- a/.hgtags	Fri Feb 28 17:11:01 2020 +0100
+++ b/.hgtags	Thu Mar 05 10:15:38 2020 +0100
@@ -647,3 +647,7 @@
 172f683a84f6dbc069298bba811f590afb5e5a43 3.26.14
 e77900f19390fdf38515afdd212d21ac2592693d 3.27.0
 e77900f19390fdf38515afdd212d21ac2592693d debian/3.27.0-1
+917601bb5b1ba13eb92296f5bd82eaa89e99bdad 3.27.1
+917601bb5b1ba13eb92296f5bd82eaa89e99bdad debian/3.27.1-1
+e731c31eaed06ac0a781db4d9a36d8b3732a4852 3.27.2
+e731c31eaed06ac0a781db4d9a36d8b3732a4852 debian/3.27.2-1
--- a/cubicweb/server/sources/ldapfeed.py	Fri Feb 28 17:11:01 2020 +0100
+++ b/cubicweb/server/sources/ldapfeed.py	Thu Mar 05 10:15:38 2020 +0100
@@ -17,10 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb ldap feed source"""
 
-from __future__ import division  # XXX why?
-
-from datetime import datetime
-
 import ldap3
 
 from logilab.common.configuration import merge_options
@@ -32,12 +28,11 @@
 from cubicweb import _
 
 # search scopes
-BASE = ldap3.SEARCH_SCOPE_BASE_OBJECT
-ONELEVEL = ldap3.SEARCH_SCOPE_SINGLE_LEVEL
-SUBTREE = ldap3.SEARCH_SCOPE_WHOLE_SUBTREE
-LDAP_SCOPES = {'BASE': BASE,
-               'ONELEVEL': ONELEVEL,
-               'SUBTREE': SUBTREE}
+LDAP_SCOPES = {
+    'BASE': ldap3.BASE,
+    'ONELEVEL': ldap3.LEVEL,
+    'SUBTREE': ldap3.SUBTREE,
+}
 
 # map ldap protocol to their standard port
 PROTO_PORT = {'ldap': 389,
@@ -117,6 +112,13 @@
           'help': 'additional filters to be set in the ldap query to find valid users',
           'group': 'ldap-source', 'level': 2,
           }),
+        ('start-tls',
+         {'type': 'choice',
+          'choices': ('true', 'false'),
+          'default': 'false',
+          'help': 'Start tls on connection (before bind)',
+          'group': 'ldap-source', 'level': 1,
+          }),
         ('user-login-attr',
          {'type': 'string',
           'default': 'uid',
@@ -196,8 +198,9 @@
         self._authenticate = getattr(self, '_auth_%s' % self.authmode)
         self.cnx_dn = typedconfig['data-cnx-dn']
         self.cnx_pwd = typedconfig['data-cnx-password']
+        self.start_tls = typedconfig['start-tls'] == "true"
         self.user_base_dn = str(typedconfig['user-base-dn'])
-        self.user_base_scope = globals()[typedconfig['user-scope']]
+        self.user_base_scope = LDAP_SCOPES[typedconfig['user-scope']]
         self.user_login_attr = typedconfig['user-login-attr']
         self.user_default_groups = typedconfig['user-default-group']
         self.user_attrs = {'dn': 'eid', 'modifyTimestamp': 'modification_date'}
@@ -254,7 +257,7 @@
         # check password by establishing a (unused) connection
         try:
             self._connect(user, password)
-        except ldap3.LDAPException as ex:
+        except ldap3.core.exceptions.LDAPException as ex:
             # Something went wrong, most likely bad credentials
             self.info('while trying to authenticate %s: %s', user, ex)
             raise AuthenticationError()
@@ -270,18 +273,29 @@
 
     def _connect(self, user=None, userpwd=None):
         protocol, host, port = self.connection_info()
+        kwargs = {}
+        if user:
+            kwargs['user'] = user['dn']
+        elif self.cnx_dn:
+            kwargs['user'] = self.cnx_dn
+            if self.cnx_pwd:
+                kwargs['password'] = self.cnx_pwd
         self.info('connecting %s://%s:%s as %s', protocol, host, port,
-                  user and user['dn'] or 'anonymous')
+                  kwargs.get('user', 'anonymous'))
         server = ldap3.Server(host, port=int(port))
         conn = ldap3.Connection(
-            server, user=user and user['dn'],
-            client_strategy=ldap3.STRATEGY_SYNC_RESTARTABLE,
-            auto_referrals=False)
+            server, client_strategy=ldap3.RESTARTABLE, auto_referrals=False,
+            raise_exceptions=True,
+            **kwargs)
+        if self.start_tls:
+            conn.start_tls()
+
         # Now bind with the credentials given. Let exceptions propagate out.
         if user is None:
-            # XXX always use simple bind for data connection
+            # anonymous bind
             if not self.cnx_dn:
-                conn.bind()
+                if not conn.bind():
+                    raise AuthenticationError(conn.result["message"])
             else:
                 self._authenticate(conn, {'dn': self.cnx_dn}, self.cnx_pwd)
         else:
@@ -292,7 +306,6 @@
         return conn
 
     def _auth_simple(self, conn, user, userpwd):
-        conn.authentication = ldap3.AUTH_SIMPLE
         conn.user = user['dn']
         conn.password = userpwd
         return conn.bind()
@@ -317,7 +330,10 @@
         if self._conn is None:
             self._conn = self._connect()
         ldapcnx = self._conn
-        if not ldapcnx.search(base, searchstr, search_scope=scope, attributes=attrs):
+        if self.start_tls:
+            ldapcnx.start_tls()
+            self.info("ldap start_tls started for %s", self.uri)
+        if not ldapcnx.search(base, searchstr, search_scope=scope, attributes=set(attrs) - {'dn'}):
             return []
         result = []
         for rec in ldapcnx.response:
@@ -334,13 +350,13 @@
         itemdict = {'dn': dn}
         for key, value in iterator:
             if self.user_attrs.get(key) == 'upassword':  # XXx better password detection
-                value = value[0].encode('utf-8')
+                value = value[0]
                 # we only support ldap_salted_sha1 for ldap sources, see: server/utils.py
                 if not value.startswith(b'{SSHA}'):
                     value = utils.crypt_password(value)
                 itemdict[key] = Binary(value)
             elif self.user_attrs.get(key) == 'modification_date':
-                itemdict[key] = datetime.strptime(value[0], '%Y%m%d%H%M%SZ')
+                itemdict[key] = value
             else:
                 if len(value) == 1:
                     itemdict[key] = value = value[0]
--- a/cubicweb/server/test/unittest_ldapsource.py	Fri Feb 28 17:11:01 2020 +0100
+++ b/cubicweb/server/test/unittest_ldapsource.py	Thu Mar 05 10:15:38 2020 +0100
@@ -143,12 +143,8 @@
 
     @classmethod
     def pre_setup_database(cls, cnx, config):
-        if sys.version_info[:2] >= (3, 7):
-            raise unittest.SkipTest(
-                'ldapfeed is not currently compatible with Python 3.7')
         cnx.create_entity('CWSource', name=u'ldap', type=u'ldapfeed', parser=u'ldapfeed',
                           url=URL, config=CONFIG_LDAPFEED)
-
         cnx.commit()
         return cls.pull(cnx)
 
--- a/debian/changelog	Fri Feb 28 17:11:01 2020 +0100
+++ b/debian/changelog	Thu Mar 05 10:15:38 2020 +0100
@@ -1,3 +1,15 @@
+cubicweb (3.27.2-1) unstable; urgency=medium
+
+  * New upstream release
+
+ -- Philippe Pepiot <philippe.pepiot@logilab.fr>  Thu, 05 Mar 2020 09:54:26 +0100
+
+cubicweb (3.27.1-1) unstable; urgency=medium
+
+  * New upstream release
+
+ -- Katia Saurfelt <katia.saurfelt@logilab.fr>  Tue, 11 Feb 2020 10:51:08 +0100
+
 cubicweb (3.27.0-1) unstable; urgency=medium
 
   * New upstream release
--- a/doc/book/admin/ldap.rst	Fri Feb 28 17:11:01 2020 +0100
+++ b/doc/book/admin/ldap.rst	Thu Mar 05 10:15:38 2020 +0100
@@ -83,6 +83,8 @@
 * `data-cnx-password`, password to use to open data connection to the
   ldap (eg used to respond to rql queries)
 
+* `start-tls`, starting TLS before bind (valid values: "true", "false")
+
 If the LDAP server accepts anonymous binds, then it is possible to
 leave data-cnx-dn and data-cnx-password empty. This is, however, quite
 unlikely in practice. Beware that the LDAP server might hide attributes
--- a/requirements/test-server.txt	Fri Feb 28 17:11:01 2020 +0100
+++ b/requirements/test-server.txt	Thu Mar 05 10:15:38 2020 +0100
@@ -1,2 +1,2 @@
 psycopg2-binary
-ldap3 < 2
+ldap3<3,>2