changeset a87fed70983b in modules/ldap_authentication:default
details: 
https://hg.tryton.org/modules/ldap_authentication?cmd=changeset&node=a87fed70983b
description:
        Add support for STARTTLS with LDAP server

        issue11561
        review441181003
diffstat:

 CHANGELOG            |    1 +
 doc/index.rst        |   10 +++
 res.py               |   15 ++++-
 tests/test_module.py |  144 ++++++++++++++++++++++++++++++++++++++++++++++----
 4 files changed, 156 insertions(+), 14 deletions(-)

diffs (277 lines):

diff -r cc05d028e137 -r a87fed70983b CHANGELOG
--- a/CHANGELOG Sun Oct 02 00:23:53 2022 +0200
+++ b/CHANGELOG Mon Oct 10 21:18:17 2022 +0200
@@ -1,3 +1,4 @@
+* Add support for STARTTLS
 * Enforce certificate validation for LDAP connection (issue11564)
 
 Version 6.4.0 - 2022-05-02
diff -r cc05d028e137 -r a87fed70983b doc/index.rst
--- a/doc/index.rst     Sun Oct 02 00:23:53 2022 +0200
+++ b/doc/index.rst     Mon Oct 10 21:18:17 2022 +0200
@@ -16,6 +16,13 @@
 ---
 
 The LDAP URL to use to connect to the server following the :rfc:`2255`.
+It is extended to support SSL_ and STARTTLS_.
+The available protocols are:
+
+   - ``ldap``: simple LDAP
+   - ``ldap+tls``: LDAP with STARTTLS
+   - ``ldaps``: LDAP with SSL
+
 
 bind_pass
 ---------
@@ -31,3 +38,6 @@
 -----------
 
 A boolean to create user if not in the database.
+
++.. _SSL: https://en.wikipedia.org/wiki/Secure_Sockets_Layer
++.. _STARTTLS: https://en.wikipedia.org/wiki/STARTTLS
diff -r cc05d028e137 -r a87fed70983b res.py
--- a/res.py    Sun Oct 02 00:23:53 2022 +0200
+++ b/res.py    Mon Oct 10 21:18:17 2022 +0200
@@ -47,6 +47,8 @@
     else:
         scheme, port = 'ldap', 389
         tls = None
+        if 'tls' in uri.scheme:
+            tls = ldap3.Tls(validate=ssl.CERT_REQUIRED)
     return ldap3.Server('%s://%s:%s' % (
             scheme, uri.hostname, uri.port or port), tls=tls)
 
@@ -82,7 +84,12 @@
             # XXX find better way to get the password
             bindpass = config.get(section, 'bind_pass')
 
-        with ldap3.Connection(server, bindname, bindpass) as con:
+        bind_method = ldap3.AUTO_BIND_DEFAULT
+        if server.ssl is False and server.tls is not None:
+            bind_method = ldap3.AUTO_BIND_TLS_BEFORE_BIND
+
+        with ldap3.Connection(
+                server, bindname, bindpass, auto_bind=bind_method) as con:
             con.search(dn, filter_, search_scope=scope, attributes=attrs)
             result = con.entries
             if result and len(result) > 1:
@@ -141,7 +148,11 @@
                 users = cls.ldap_search_user(login, server, attrs=[uid])
                 if users and len(users) == 1:
                     [(dn, attrs)] = users
-                    with ldap3.Connection(server, dn, password) as con:
+                    with ldap3.Connection(
+                            server, dn, password,
+                            auto_bind=ldap3.AUTO_BIND_NONE) as con:
+                        if server.ssl is False and server.tls is not None:
+                            con.start_tls()
                         if (password and con.bind()):
                             # Use ldap uid so we always get the right case
                             login = attrs.get(uid, [login])[0]
diff -r cc05d028e137 -r a87fed70983b tests/test_module.py
--- a/tests/test_module.py      Sun Oct 02 00:23:53 2022 +0200
+++ b/tests/test_module.py      Mon Oct 10 21:18:17 2022 +0200
@@ -1,12 +1,12 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
 
-from unittest.mock import ANY, patch
+from unittest.mock import ANY, MagicMock, Mock, patch
 
 import ldap3
 
 from trytond.config import config
-from trytond.modules.ldap_authentication.res import parse_ldap_url
+from trytond.modules.ldap_authentication.res import ldap_server, parse_ldap_url
 from trytond.pool import Pool
 from trytond.tests.test_tryton import ModuleTestCase, with_transaction
 
@@ -23,19 +23,19 @@
         config.set('session', 'authentications', 'ldap')
         self.addCleanup(config.set, 'session', 'authentications', methods)
         config.add_section(section)
-        config.set(section, 'uri', 'ldap://localhost/dc=tryton,dc=org')
         self.addCleanup(config.remove_section, section)
 
-    @with_transaction()
-    def test_user_get_login(self):
-        'Test User.get_login'
+    def _get_login(
+            self, uri='ldap://localhost/dc=tryton,dc=org', start_tls=False):
         pool = Pool()
         User = pool.get('res.user')
+        config.set(section, 'uri', uri)
 
         @patch.object(ldap3, 'Connection')
         @patch.object(User, 'ldap_search_user')
         def get_login(login, password, find, ldap_search_user, Connection):
-            con = Connection.return_value
+            con = Connection.return_value = MagicMock()
+            con.__enter__.return_value = con
             con.bind.return_value = bool(find)
             if find:
                 ldap_search_user.return_value = [('dn', {'uid': [find]})]
@@ -45,31 +45,87 @@
                     'password': password,
                     })
             if find:
-                Connection.assert_called_with(ANY, ANY, password)
+                Connection.assert_called_with(
+                    ANY, ANY, password, auto_bind=ldap3.AUTO_BIND_NONE)
+                if start_tls:
+                    con.start_tls.assert_called()
+                else:
+                    con.start_tls.assert_not_called()
+                con.bind.assert_called()
             return user_id
+        return get_login
 
-        # Test existing user
+    @with_transaction()
+    def test_user_get_login_existing_user(self):
+        "Test User.get_login with existing user"
+        pool = Pool()
+        User = pool.get('res.user')
         user, = User.search([('login', '=', 'admin')])
+
+        get_login = self._get_login()
+
         self.assertEqual(get_login('admin', 'admin', 'admin'), user.id)
         self.assertEqual(get_login('AdMiN', 'admin', 'admin'), user.id)
 
-        # Test new user
+    @with_transaction()
+    def test_user_get_login_unknown_user(self):
+        "test User.get_login with unknown user"
+        get_login = self._get_login()
+
         self.assertFalse(get_login('foo', 'bar', None))
         self.assertFalse(get_login('foo', 'bar', 'foo'))
 
-        # Test create new user
+    @with_transaction()
+    def test_user_get_login_create_user(self):
+        "Test User.get_login with user to create"
+        pool = Pool()
+        User = pool.get('res.user')
         config.set(section, 'create_user', 'True')
+        get_login = self._get_login()
+
         user_id = get_login('foo', 'bar', 'foo')
         foo, = User.search([('login', '=', 'foo')])
+
         self.assertEqual(user_id, foo.id)
         self.assertEqual(foo.name, 'foo')
 
-        # Test create new user with different case
+    @with_transaction()
+    def test_user_get_login_create_user_case(self):
+        "Test User.get_login with user to create with different case"
+        pool = Pool()
+        User = pool.get('res.user')
+        config.set(section, 'create_user', 'True')
+        get_login = self._get_login()
+
         user_id = get_login('BaR', 'foo', 'bar')
         bar, = User.search([('login', '=', 'bar')])
+
         self.assertEqual(user_id, bar.id)
         self.assertEqual(bar.name, 'bar')
 
+    @with_transaction()
+    def test_user_get_login_with_tls(self):
+        "Test User.get_login with TLS"
+        pool = Pool()
+        User = pool.get('res.user')
+        user, = User.search([('login', '=', 'admin')])
+
+        get_login = self._get_login(
+            'ldap+tls://localhost/dc=tryton,dc=org', start_tls=True)
+
+        self.assertEqual(get_login('admin', 'admin', 'admin'), user.id)
+
+    @with_transaction()
+    def test_user_get_login_with_ssl(self):
+        "Test User.get_login with SSL"
+        pool = Pool()
+        User = pool.get('res.user')
+        user, = User.search([('login', '=', 'admin')])
+
+        get_login = self._get_login('ldaps://localhost/dc=tryton,dc=org')
+
+        self.assertEqual(get_login('admin', 'admin', 'admin'), user.id)
+
     def test_parse_ldap_url(self):
         'Test parse_ldap_url'
         self.assertEqual(
@@ -99,5 +155,69 @@
                 'ldap:///??sub??!bindname=cn=Manager%2co=Foo')[5],
             {'!bindname': ['cn=Manager,o=Foo']})
 
+    def test_ldap_server(self):
+        "Test ldap_server"
+        for uri, (host, tls) in [
+                ('ldap://localhost/dc=tryton,dc=org',
+                    ('ldap://localhost:389', None)),
+                ('ldaps://localhost/dc=tryton,dc=org',
+                    ('ldaps://localhost:636', True)),
+                ('ldap+tls://localhost/dc=tryton,dc=org',
+                    ('ldap://localhost:389', True)),
+                ]:
+            config.set(section, 'uri', uri)
+
+            with patch('ldap3.Server') as Server:
+                ldap_server()
+
+                self.assertEqual(Server.call_args.args, (host,))
+                if tls:
+                    self.assertTrue(Server.call_args.kwargs.get('tls'))
+                else:
+                    self.assertFalse(Server.call_args.kwargs.get('tls'))
+
+    def _ldap_search_user(
+            self, uri='ldap://localhost/dc=tryton,dc=org',
+            auto_bind=ldap3.AUTO_BIND_DEFAULT):
+        pool = Pool()
+        User = pool.get('res.user')
+        config.set(section, 'uri', uri)
+
+        @patch.object(ldap3, 'Connection')
+        def ldap_search_user(login, attrs, Connection):
+            con = Connection.return_value = MagicMock()
+            con.__enter__.return_value = con
+            con.entries = [Mock()]
+            server = ldap_server()
+            User.ldap_search_user(login, server, attrs=attrs)
+            Connection.assert_called_with(
+                ANY, ANY, ANY, auto_bind=auto_bind)
+            con.search.assert_called()
+        return ldap_search_user
+
+    @with_transaction()
+    def test_ldap_search_user(self):
+        "Test User.ldap_search_user"
+        ldap_search_user = self._ldap_search_user()
+
+        ldap_search_user('admin', None)
+
+    @with_transaction()
+    def test_ldap_search_user_with_tls(self):
+        "Test User.ldap_search_user with TSL"
+        ldap_search_user = self._ldap_search_user(
+            'ldap+tls://localhost/dc=tryton,dc=org',
+            ldap3.AUTO_BIND_TLS_BEFORE_BIND)
+
+        ldap_search_user('admin', None)
+
+    @with_transaction()
+    def test_ldap_search_user_with_ssl(self):
+        "Test User.ldap_search_user with SSL"
+        ldap_search_user = self._ldap_search_user(
+            'ldaps://localhost/dc=tryton,dc=org')
+
+        ldap_search_user('admin', None)
+
 
 del ModuleTestCase

Reply via email to