From 74266e64ceb735be7e198425f12b8ca222ff973b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 4 May 2016 12:10:35 +0200 Subject: [PATCH] Increase LDAP coverage. --- modoboa/admin/tests/test_account.py | 18 ++++++ modoboa/core/tests.py | 93 +++++++++++++++++++++++------ modoboa/lib/ldap_utils.py | 34 +++++++---- 3 files changed, 114 insertions(+), 31 deletions(-) diff --git a/modoboa/admin/tests/test_account.py b/modoboa/admin/tests/test_account.py index 4ecd3ae5e..2b15f2d64 100644 --- a/modoboa/admin/tests/test_account.py +++ b/modoboa/admin/tests/test_account.py @@ -3,6 +3,8 @@ from django.core.urlresolvers import reverse from modoboa.core.models import User +from modoboa.core import tests as core_tests +from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase from .. import factories @@ -152,6 +154,22 @@ class AccountTestCase(ModoTestCase): ) +class LDAPAccountTestCase(core_tests.LDAPTestCaseMixin, ModoTestCase): + """Check LDAP related code.""" + + def test_autocreate_disabled(self): + """Check if objects are not created as expected.""" + self.activate_ldap_authentication() + self.searchbind_mode() + parameters.save_admin("AUTO_CREATE_DOMAIN_AND_MAILBOX", "no") + username = "testuser@example.com" + self.authenticate(username, "test") + self.assertFalse( + models.Domain.objects.filter(name="example.com").exists()) + self.assertFalse( + models.Mailbox.objects.filter(address="testuser").exists()) + + class PermissionsTestCase(ModoTestCase): @classmethod diff --git a/modoboa/core/tests.py b/modoboa/core/tests.py index ca0bfedc1..524dbf615 100644 --- a/modoboa/core/tests.py +++ b/modoboa/core/tests.py @@ -3,6 +3,7 @@ from django.core.urlresolvers import reverse from django.test import override_settings +from modoboa.lib import exceptions from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase @@ -39,20 +40,65 @@ class AuthenticationTestCase(ModoTestCase): self.assertTrue(response.url.endswith(reverse("admin:domain_list"))) +class LDAPTestCaseMixin(object): + """Set of methods used to test LDAP features.""" + + def activate_ldap_authentication(self): + """Modify settings.""" + parameters.save_admin( + "AUTHENTICATION_TYPE", "ldap") + parameters.save_admin("LDAP_SERVER_PORT", "3389") + + def restore_user_password(self, username, new_password): + """Restore user password to its initial state.""" + from modoboa.lib.ldap_utils import LDAPAuthBackend + + backend = LDAPAuthBackend() + for password in ["Toto1234", "test"]: + try: + backend.update_user_password( + username, password, new_password) + except exceptions.InternalError as exp: + print exp + pass + else: + return + raise RuntimeError("Can't restore user password.") + + def authenticate(self, user, password, restore_before=True): + """Restore password and authenticate user.""" + self.client.logout() + if restore_before: + self.restore_user_password(user, password) + self.assertTrue( + self.client.login(username=user, password=password)) + + def searchbind_mode(self): + """Apply settings required by the searchbind mode.""" + parameters.save_admin("LDAP_AUTH_METHOD", "searchbind") + parameters.save_admin("LDAP_BIND_DN", "cn=admin,dc=example,dc=com") + parameters.save_admin("LDAP_BIND_PASSWORD", "test") + parameters.save_admin("LDAP_SEARCH_BASE", "ou=users,dc=example,dc=com") + + def directbind_mode(self): + """Apply settings required by the directbind mode.""" + parameters.save_admin("LDAP_AUTH_METHOD", "directbind") + parameters.save_admin( + "LDAP_USER_DN_TEMPLATE", "cn=%(user)s,ou=users,dc=example,dc=com") + + @override_settings(AUTHENTICATION_BACKENDS=( 'modoboa.lib.authbackends.LDAPBackend', 'modoboa.lib.authbackends.SimpleBackend', )) -class LDAPAuthenticationTestCase(ModoTestCase): +class LDAPAuthenticationTestCase(LDAPTestCaseMixin, ModoTestCase): """Validate LDAP authentication scenarios.""" def setUp(self): """Create test data.""" super(LDAPAuthenticationTestCase, self).setUp() - parameters.save_admin( - "AUTHENTICATION_TYPE", "ldap") - parameters.save_admin("LDAP_SERVER_PORT", "3389") + self.activate_ldap_authentication() def check_created_user(self, username, group="SimpleUsers", with_mb=True): """Check that created user is valid.""" @@ -66,13 +112,9 @@ class LDAPAuthenticationTestCase(ModoTestCase): @override_settings(AUTH_LDAP_USER_DN_TEMPLATE=None) def test_searchbind_authentication(self): """Test the bind&search method.""" - self.client.logout() - parameters.save_admin("LDAP_AUTH_METHOD", "searchbind") - parameters.save_admin("LDAP_BIND_DN", "cn=admin,dc=example,dc=com") - parameters.save_admin("LDAP_BIND_PASSWORD", "test") - parameters.save_admin("LDAP_SEARCH_BASE", "ou=users,dc=example,dc=com") + self.searchbind_mode() username = "testuser@example.com" - self.assertTrue(self.client.login(username=username, password="test")) + self.authenticate(username, "test") self.check_created_user(username) self.client.logout() @@ -80,22 +122,19 @@ class LDAPAuthenticationTestCase(ModoTestCase): parameters.save_admin( "LDAP_GROUPS_SEARCH_BASE", "ou=groups,dc=example,dc=com") username = "mailadmin@example.com" - self.assertTrue(self.client.login(username=username, password="test")) + self.authenticate(username, "test", False) self.check_created_user(username, "DomainAdmins") def test_directbind_authentication(self): """Test the directbind method.""" self.client.logout() - parameters.save_admin("LDAP_AUTH_METHOD", "directbind") - parameters.save_admin( - "LDAP_USER_DN_TEMPLATE", "cn=%(user)s,ou=users,dc=example,dc=com") + self.directbind_mode() # 1: must fail because usernames of simple users must be email # addresses username = "testuser" with self.assertRaises(TypeError): - self.assertTrue(self.client.login( - username=username, password="test")) + self.client.login(username=username, password="test") # 1: must work because usernames of domain admins are not # always email addresses @@ -103,11 +142,11 @@ class LDAPAuthenticationTestCase(ModoTestCase): parameters.save_admin( "LDAP_GROUPS_SEARCH_BASE", "ou=groups,dc=example,dc=com") username = "mailadmin" - self.assertTrue(self.client.login(username=username, password="test")) + self.authenticate(username, "test", False) self.check_created_user(username, "DomainAdmins", False) -class ProfileTestCase(ModoTestCase): +class ProfileTestCase(LDAPTestCaseMixin, ModoTestCase): @classmethod def setUpTestData(cls): @@ -153,6 +192,24 @@ class ProfileTestCase(ModoTestCase): self.client.login(username="user@test.com", password="Toto1234") ) + @override_settings(AUTHENTICATION_BACKENDS=( + 'modoboa.lib.authbackends.LDAPBackend', + 'modoboa.lib.authbackends.SimpleBackend', + )) + def test_update_password_ldap(self): + """Update password for an LDAP user.""" + self.activate_ldap_authentication() + self.searchbind_mode() + + username = "testuser@example.com" + self.authenticate(username, "test") + self.ajax_post( + reverse("core:user_profile"), + {"language": "en", "oldpassword": "test", + "newpassword": "Toto1234", "confirmation": "Toto1234"} + ) + self.authenticate(username, "Toto1234", False) + class APIAccessFormTestCase(ModoTestCase): diff --git a/modoboa/lib/ldap_utils.py b/modoboa/lib/ldap_utils.py index 3ba247dac..42db29d44 100644 --- a/modoboa/lib/ldap_utils.py +++ b/modoboa/lib/ldap_utils.py @@ -46,6 +46,7 @@ from modoboa.lib.exceptions import InternalError class LDAPAuthBackend(object): def __init__(self): + parameters.apply_to_django_settings() self.server_uri = self._setting( "AUTH_LDAP_SERVER_URI", "ldap://localhost" ) @@ -53,7 +54,6 @@ class LDAPAuthBackend(object): self.ldap_ad = self._setting("LDAP_ACTIVE_DIRECTORY", False) self.conn = None self.user_filter = self._setting("LDAP_USER_FILTER", "") - self.user_dn = None def _setting(self, name, default): try: @@ -63,6 +63,7 @@ class LDAPAuthBackend(object): return value def _get_conn(self, dn, password): + """Get a connection from the server.""" conn = ldap.initialize(self.server_uri) conn.set_option(ldap.OPT_X_TLS_DEMAND, True) conn.set_option(ldap.OPT_DEBUG_LEVEL, 255) @@ -70,17 +71,21 @@ class LDAPAuthBackend(object): return conn def connect_to_server(self, user, password): - if parameters.get_admin("LDAP_AUTH_METHOD", app="core") == "searchbind": + """Connect to the server according to configuration.""" + if self.conn is not None: + return + mode = parameters.get_admin("LDAP_AUTH_METHOD", app="core") + if mode == "searchbind": bind_dn = self._setting("AUTH_LDAP_BIND_DN", "") bind_pwd = self._setting("AUTH_LDAP_BIND_PASSWORD", "") self.conn = self._get_conn(bind_dn, bind_pwd) - self.user_dn = self._find_user_dn(user) else: tpl = self._setting("AUTH_LDAP_USER_DN_TEMPLATE", "") self.user_dn = tpl % {"user": user} self.conn = self._get_conn(self.user_dn, password) def _find_user_dn(self, user): + """Find the DN of the given user.""" sbase = parameters.get_admin("LDAP_SEARCH_BASE", app="core") sfilter = parameters.get_admin("LDAP_SEARCH_FILTER", app="core") sfilter = sfilter % {"user": user} @@ -102,15 +107,18 @@ class LDAPAuthBackend(object): return hasher.encrypt(clearpassword) def update_user_password(self, user, password, newpassword): + """Update user password.""" + self.connect_to_server(user, password) + user_dn = self._find_user_dn(user) + if self.ldap_ad: + newpassword = ( + ('"%s"' % newpassword).encode('utf-16').lstrip('\377\376') + ) + ldif = [(ldap.MOD_REPLACE, + self.pwd_attr, + self._crypt_password(newpassword))] try: - self.connect_to_server(user, password) - if self.ldap_ad: - newpassword = ( - ('"%s"' % newpassword).encode('utf-16').lstrip('\377\376') - ) - ldif = [(ldap.MOD_REPLACE, - self.pwd_attr, - self._crypt_password(newpassword))] - self.conn.modify_s(self.user_dn, ldif) + self.conn.modify_s(user_dn, ldif) except ldap.LDAPError as e: - raise InternalError(_("Failed to update password: %s" % str(e))) + raise InternalError( + _("Failed to update password: {}").format(e))